309 lines
10 KiB
Vue
309 lines
10 KiB
Vue
<template>
|
||
<div class="printer-manager">
|
||
<el-card class="printer-card">
|
||
<template #header>
|
||
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
|
||
<span>打印机管理</span>
|
||
<el-button type="primary" @click="refreshPrinters" :loading="loading">
|
||
<el-icon>
|
||
<Refresh />
|
||
</el-icon>
|
||
刷新打印机列表
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
|
||
<el-form label-width="140px" label-position="left" class="printer-form">
|
||
<el-form-item label="条码打印机">
|
||
<div class="printer-row">
|
||
<el-select v-model="barcodePrinter" placeholder="请选择条码打印机" style="width: 400px" clearable>
|
||
<el-option v-for="p in printers" :key="p.name" :label="p.name" :value="p.name" />
|
||
</el-select>
|
||
<el-button type="primary" @click="confirmBarcodePrinter">确定</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="快递单打印机">
|
||
<div class="printer-row">
|
||
<el-select v-model="expressPrinter" placeholder="请选择快递单打印机" style="width: 400px" clearable>
|
||
<el-option v-for="p in printers" :key="p.name" :label="p.name" :value="p.name" />
|
||
</el-select>
|
||
<el-button type="primary" @click="confirmExpressPrinter">确定</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="小票纸大小">
|
||
<div class="printer-row">
|
||
<el-select v-model="paperSize" placeholder="请选择小票纸大小" style="width: 400px" clearable>
|
||
<el-option label="60(长)*30(高)" value="60*30" />
|
||
<el-option label="60(长)*40(高)" value="60*40" />
|
||
<el-option label="40(长)*60(高)" value="40*60" />
|
||
</el-select>
|
||
<el-button type="primary" @click="confirmPaperSize">确定</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<!-- TODO -->
|
||
<el-form-item label="切换模式快捷键">
|
||
<div class="printer-row">
|
||
<el-input :model-value="'Alt+c'" disabled style="width: 200px" />
|
||
<img v-if="barcodeImages['Alt+c']" :src="barcodeImages['Alt+c']" />
|
||
<el-button type="primary" @click="handlePrintBarcode('Alt+c')">打印条码</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="拍照快捷键">
|
||
<div class="printer-row">
|
||
<el-input :model-value="'Alt+a'" disabled style="width: 200px" />
|
||
<img v-if="barcodeImages['Alt+a']" :src="barcodeImages['Alt+a']" />
|
||
<el-button type="primary" @click="handlePrintBarcode('Alt+a')">打印条码</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="创建波次快捷键">
|
||
<div class="printer-row">
|
||
<el-input :model-value="'Alt+x'" disabled style="width: 200px" />
|
||
<img v-if="barcodeImages['Alt+x']" :src="barcodeImages['Alt+x']" />
|
||
<el-button type="primary" @click="handlePrintBarcode('Alt+x')">打印条码</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="生成波次条形码快捷键">
|
||
<div class="printer-row">
|
||
<el-input :model-value="'Alt+b'" disabled style="width: 200px" />
|
||
<img v-if="barcodeImages['Alt+b']" :src="barcodeImages['Alt+b']" />
|
||
<el-button type="primary" @click="handlePrintBarcode('Alt+b')">打印条码</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
|
||
|
||
<el-empty v-if="!loading && printers.length === 0" description="未检测到可用打印机" />
|
||
</el-card>
|
||
|
||
<el-card class="printer-card">
|
||
<template #header>
|
||
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
|
||
<span>摄像头配置</span>
|
||
<el-button type="primary" @click="refreshCameras" :loading="cameraLoading">
|
||
<el-icon>
|
||
<Refresh />
|
||
</el-icon>
|
||
刷新摄像头列表
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
|
||
<el-form label-width="140px" label-position="left" class="printer-form">
|
||
<el-form-item label="摄像头">
|
||
<div class="printer-row">
|
||
<el-select v-model="selectedCamera" placeholder="请选择摄像头" style="width: 400px" clearable>
|
||
<el-option v-for="c in cameras" :key="c.deviceId" :label="c.label" :value="c.deviceId" />
|
||
</el-select>
|
||
<el-button type="primary" @click="confirmCamera">确定</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<el-empty v-if="!cameraLoading && cameras.length === 0" description="未检测到可用摄像头" />
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { Refresh } from '@element-plus/icons-vue'
|
||
import { getSimplePrinterList, initLodop, createPrintTask } from '@/api/print'
|
||
import JsBarcode from 'jsbarcode'
|
||
|
||
const STORAGE_KEY_BARCODE = 'printer_barcode'
|
||
const STORAGE_KEY_EXPRESS = 'printer_express'
|
||
const STORAGE_KEY_PAPER_SIZE = 'printer_paper_size'
|
||
const STORAGE_KEY_CAMERA = 'printer_camera'
|
||
|
||
const loading = ref(false)
|
||
const printers = ref([])
|
||
const barcodePrinter = ref('')
|
||
const expressPrinter = ref('')
|
||
const paperSize = ref('')
|
||
|
||
const cameraLoading = ref(false)
|
||
const cameras = ref([])
|
||
const selectedCamera = ref('')
|
||
|
||
const shortcutItems = ['Alt+c', 'Alt+a', 'Alt+x', 'Alt+b']
|
||
const barcodeImages = reactive({})
|
||
|
||
shortcutItems.forEach(key => {
|
||
barcodeImages[key] = ''
|
||
})
|
||
|
||
const fetchBarcodeImage = (text) => {
|
||
try {
|
||
const canvas = document.createElement('canvas')
|
||
JsBarcode(canvas, text, {
|
||
format: 'CODE128',
|
||
fontSize: 14,
|
||
margin: 5,
|
||
displayValue: true,
|
||
lineColor: '#000000',
|
||
width: 1.5,
|
||
height: 40
|
||
})
|
||
barcodeImages[text] = canvas.toDataURL('image/png')
|
||
} catch {
|
||
// ignore barcode load error
|
||
}
|
||
}
|
||
|
||
/** 使用 C-Lodop 和 localStorage 中保存的打印机打印条码 */
|
||
const handlePrintBarcode = async (key) => {
|
||
const imageUrl = barcodeImages[key]
|
||
if (!imageUrl) {
|
||
ElMessage.warning({ message: '条形码尚未生成,无法打印', customClass: 'scan-warning-message' })
|
||
return
|
||
}
|
||
|
||
const printerName = localStorage.getItem(STORAGE_KEY_BARCODE)
|
||
if (!printerName) {
|
||
ElMessage.error({ message: '条码打印机未选择', customClass: 'scan-error-message' })
|
||
return
|
||
}
|
||
|
||
try {
|
||
const LODOP = await createPrintTask('barcode', { content: key })
|
||
LODOP.SET_PRINTER_INDEX(printerName)
|
||
LODOP.PRINT()
|
||
ElMessage.success({ message: '打印任务已发送', duration: 1000, customClass: 'scan-success-message' })
|
||
} catch (error) {
|
||
ElMessage.error({ message: '打印失败:' + (error.message || '未知错误'), customClass: 'scan-error-message' })
|
||
}
|
||
}
|
||
|
||
const confirmBarcodePrinter = () => {
|
||
if (barcodePrinter.value) {
|
||
localStorage.setItem(STORAGE_KEY_BARCODE, barcodePrinter.value)
|
||
ElMessage.success({ message: '条码打印机已保存', duration: 1000, customClass: 'scan-success-message' })
|
||
} else {
|
||
localStorage.removeItem(STORAGE_KEY_BARCODE)
|
||
ElMessage.success({ message: '条码打印机已清除', duration: 1000, customClass: 'scan-success-message' })
|
||
}
|
||
}
|
||
|
||
const confirmPaperSize = () => {
|
||
if (paperSize.value) {
|
||
localStorage.setItem(STORAGE_KEY_PAPER_SIZE, paperSize.value)
|
||
ElMessage.success({ message: '小票纸大小已保存', duration: 1000, customClass: 'scan-success-message' })
|
||
}
|
||
}
|
||
|
||
const confirmExpressPrinter = () => {
|
||
if (expressPrinter.value) {
|
||
localStorage.setItem(STORAGE_KEY_EXPRESS, expressPrinter.value)
|
||
ElMessage.success({ message: '快递单打印机已保存', duration: 1000, customClass: 'scan-success-message' })
|
||
} else {
|
||
localStorage.removeItem(STORAGE_KEY_EXPRESS)
|
||
ElMessage.success({ message: '快递单打印机已清除', duration: 1000, customClass: 'scan-success-message' })
|
||
}
|
||
}
|
||
|
||
const fetchCameras = async () => {
|
||
cameraLoading.value = true
|
||
try {
|
||
const devices = await navigator.mediaDevices.enumerateDevices()
|
||
cameras.value = devices.filter((d) => d.kind === 'videoinput').map((d) => ({
|
||
deviceId: d.deviceId,
|
||
label: d.label || `摄像头 ${d.deviceId.slice(0, 8)}`
|
||
}))
|
||
} catch {
|
||
ElMessage.error({ message: '获取摄像头列表失败', customClass: 'scan-error-message' })
|
||
cameras.value = []
|
||
} finally {
|
||
cameraLoading.value = false
|
||
}
|
||
}
|
||
|
||
const refreshCameras = () => {
|
||
fetchCameras()
|
||
}
|
||
|
||
const confirmCamera = () => {
|
||
if (selectedCamera.value) {
|
||
localStorage.setItem(STORAGE_KEY_CAMERA, selectedCamera.value)
|
||
ElMessage.success({ message: '摄像头已保存', duration: 1000, customClass: 'scan-success-message' })
|
||
} else {
|
||
localStorage.removeItem(STORAGE_KEY_CAMERA)
|
||
ElMessage.success({ message: '摄像头已清除', duration: 1000, customClass: 'scan-success-message' })
|
||
}
|
||
}
|
||
|
||
const fetchPrinters = async () => {
|
||
loading.value = true
|
||
try {
|
||
const list = await getSimplePrinterList()
|
||
printers.value = list.map((name) => ({ name }))
|
||
} catch (error) {
|
||
ElMessage.error({ message: '获取打印机列表失败', customClass: 'scan-error-message' })
|
||
printers.value = []
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const refreshPrinters = () => {
|
||
fetchPrinters()
|
||
}
|
||
|
||
onMounted(() => {
|
||
barcodePrinter.value = localStorage.getItem(STORAGE_KEY_BARCODE) || ''
|
||
expressPrinter.value = localStorage.getItem(STORAGE_KEY_EXPRESS) || ''
|
||
paperSize.value = localStorage.getItem(STORAGE_KEY_PAPER_SIZE) || ''
|
||
selectedCamera.value = localStorage.getItem(STORAGE_KEY_CAMERA) || ''
|
||
fetchPrinters()
|
||
fetchCameras()
|
||
|
||
shortcutItems.forEach(key => fetchBarcodeImage(key))
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.printer-manager {
|
||
padding: 20px;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.page-header h2 {
|
||
margin: 0;
|
||
font-size: 22px;
|
||
color: #303133;
|
||
}
|
||
|
||
.printer-card {
|
||
min-height: 200px;
|
||
}
|
||
|
||
.printer-form {
|
||
max-width: 600px;
|
||
}
|
||
|
||
.printer-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.printer-form :deep(.el-form-item) {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.printer-form :deep(.el-form-item__label) {
|
||
align-self: center;
|
||
line-height: normal;
|
||
}
|
||
</style>
|