290 lines
11 KiB
Vue
290 lines
11 KiB
Vue
<template>
|
||
<el-dialog v-model="visible" :title="dialogTitle" width="560px" draggable :close-on-click-modal="false">
|
||
<el-form v-if="mode === 'bind'" ref="bindFormRef" :model="bindForm" label-width="120px">
|
||
<el-form-item label="业务终端">
|
||
<el-input :model-value="sourceName" disabled />
|
||
</el-form-item>
|
||
<el-alert class="mb-5" type="info" :closable="false" show-icon>
|
||
<template #title
|
||
>此处建立“业务终端 → 尔印 USB 打印机”的一对一关系。打印机没有独立屏幕、账号和心跳;绑定的食堂或超市终端负责发现 USB
|
||
设备、接收任务、打印并回传结果。</template
|
||
>
|
||
</el-alert>
|
||
<el-form-item label="尔印打印机" prop="printerHardId" :rules="[{ required: true, message: '请选择打印机' }]">
|
||
<el-select
|
||
v-model="bindForm.printerHardId"
|
||
class="w-full"
|
||
filterable
|
||
:loading="loading"
|
||
placeholder="请选择同商户同部门的空闲打印机"
|
||
>
|
||
<el-option
|
||
v-for="printer in availablePrinters"
|
||
:key="printer.id"
|
||
:label="`${printer.name || '未命名打印机'}(ID:${printer.id})`"
|
||
:value="String(printer.id)"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<p v-if="!loading && availablePrinters.length === 0" class="text-sm text-[var(--el-text-color-secondary)]">
|
||
当前部门没有可绑定的空闲尔印打印机资产,请先新增并分配商户部门。
|
||
</p>
|
||
</el-form>
|
||
<el-form v-else ref="configFormRef" :model="configForm" :rules="configRules" label-width="150px">
|
||
<el-alert class="mb-5" type="info" :closable="false" show-icon>
|
||
<template #title
|
||
>推荐在已绑定的食堂或超市终端打开“打印机信息”,点击“上报当前设备”自动回填型号、VID 和
|
||
PID;管理端手填仅用于现场无法上报时。</template
|
||
>
|
||
</el-alert>
|
||
<el-form-item prop="printerModel">
|
||
<template #label>
|
||
<span class="inline-flex items-center gap-1">
|
||
打印机型号
|
||
<el-tooltip
|
||
content="用于运维人员识别实际 USB 打印机。优先由绑定的食堂或超市终端自动上报;手工填写示例:GEZHI / micro-printer。"
|
||
placement="top"
|
||
popper-class="max-w-[360px] leading-5"
|
||
>
|
||
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]" tabindex="0" aria-label="打印机型号填写说明"
|
||
><InfoFilled
|
||
/></el-icon>
|
||
</el-tooltip>
|
||
</span>
|
||
</template>
|
||
<el-input v-model="configForm.printerModel" placeholder="推荐由绑定的业务终端自动上报" />
|
||
</el-form-item>
|
||
<el-form-item prop="usbVendorId">
|
||
<template #label>
|
||
<span class="inline-flex items-center gap-1">
|
||
USB VID
|
||
<el-tooltip
|
||
content="USB 厂商编号,管理端填写十进制值,范围 0~65535。当前联调设备识别为 10473(十六进制 0x28E9);不确定时请勿猜测,使用终端自动上报。"
|
||
placement="top"
|
||
popper-class="max-w-[360px] leading-5"
|
||
>
|
||
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]" tabindex="0" aria-label="USB VID 填写说明"
|
||
><InfoFilled
|
||
/></el-icon>
|
||
</el-tooltip>
|
||
</span>
|
||
</template>
|
||
<el-input-number
|
||
v-model="configForm.usbVendorId"
|
||
:min="0"
|
||
:max="65535"
|
||
controls-position="right"
|
||
class="w-full"
|
||
placeholder="例如 10473"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item prop="usbProductId">
|
||
<template #label>
|
||
<span class="inline-flex items-center gap-1">
|
||
USB PID
|
||
<el-tooltip
|
||
content="USB 产品编号,管理端填写十进制值,必须与 VID 同时填写或同时留空。当前联调设备识别为 649(十六进制 0x0289);推荐使用终端自动上报。"
|
||
placement="top"
|
||
popper-class="max-w-[360px] leading-5"
|
||
>
|
||
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]" tabindex="0" aria-label="USB PID 填写说明"
|
||
><InfoFilled
|
||
/></el-icon>
|
||
</el-tooltip>
|
||
</span>
|
||
</template>
|
||
<el-input-number
|
||
v-model="configForm.usbProductId"
|
||
:min="0"
|
||
:max="65535"
|
||
controls-position="right"
|
||
class="w-full"
|
||
placeholder="例如 649"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item prop="paperWidth">
|
||
<template #label>
|
||
<span class="inline-flex items-center gap-1">
|
||
纸宽
|
||
<el-tooltip
|
||
content="首期只支持 80mm 热敏纸,对应后端图片宽度 576px。此项固定为 80,无需修改。"
|
||
placement="top"
|
||
popper-class="max-w-[360px] leading-5"
|
||
>
|
||
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]" tabindex="0" aria-label="纸宽配置说明"
|
||
><InfoFilled
|
||
/></el-icon>
|
||
</el-tooltip>
|
||
</span>
|
||
</template>
|
||
<el-input-number v-model="configForm.paperWidth" :min="80" :max="80" controls-position="right" class="w-full" />
|
||
<span class="ml-2">mm(首期固定)</span>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<template #label>
|
||
<span class="inline-flex items-center gap-1">
|
||
自动切刀
|
||
<el-tooltip
|
||
content="打印完成后发送切纸命令。打印机带自动切刀时保持开启;若型号没有切刀,或测试打印在切纸阶段报错,再关闭此项。"
|
||
placement="top"
|
||
popper-class="max-w-[360px] leading-5"
|
||
>
|
||
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]" tabindex="0" aria-label="自动切刀配置说明"
|
||
><InfoFilled
|
||
/></el-icon>
|
||
</el-tooltip>
|
||
</span>
|
||
</template>
|
||
<el-switch v-model="configForm.autoCut" active-value="1" inactive-value="0" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<template #label>
|
||
<span class="inline-flex items-center gap-1">
|
||
图片指令模式
|
||
<el-tooltip
|
||
content="控制 PNG 图片转换成哪种 ESC/POS 指令。优先使用 GS v 0;只有真机测试出现白纸、缺行或图片不完整时,才切换 ESC * 兼容模式并重新测试。"
|
||
placement="top"
|
||
popper-class="max-w-[360px] leading-5"
|
||
>
|
||
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]" tabindex="0" aria-label="图片指令模式说明"
|
||
><InfoFilled
|
||
/></el-icon>
|
||
</el-tooltip>
|
||
</span>
|
||
</template>
|
||
<el-radio-group v-model="configForm.imageMode"
|
||
><el-radio label="GS_V0">GS v 0(默认)</el-radio><el-radio label="ESC_STAR">ESC *(兼容回退)</el-radio></el-radio-group
|
||
>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="visible = false">取消</el-button>
|
||
<el-button type="primary" :loading="loading" :disabled="mode === 'bind' && availablePrinters.length === 0" @click="submit"
|
||
>保存</el-button
|
||
>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts" name="HardPrinterDialog">
|
||
import { InfoFilled } from "@element-plus/icons-vue"
|
||
import * as printerApi from "/@/api/hard/hard-printer"
|
||
import { useMessage } from "/@/hooks/message"
|
||
|
||
const emit = defineEmits(["refresh"])
|
||
const visible = ref(false)
|
||
const loading = ref(false)
|
||
const mode = ref<"bind" | "config">("bind")
|
||
const sourceName = ref("")
|
||
/** 来源业务终端雪花 ID,始终按字符串传递以保留完整精度。 */
|
||
const sourceHardId = ref<printerApi.HardwareId | null>(null)
|
||
/** 目标打印机资产雪花 ID,始终按字符串传递以保留完整精度。 */
|
||
const printerHardId = ref<printerApi.HardwareId | null>(null)
|
||
const availablePrinters = ref<any[]>([])
|
||
const bindFormRef = ref()
|
||
const configFormRef = ref()
|
||
/** 当前待提交的一对一打印机绑定。 */
|
||
const bindForm = reactive<{ printerHardId: printerApi.HardwareId | null }>({ printerHardId: null })
|
||
const configForm = reactive<printerApi.HardPrinterConfig>({
|
||
printerHardId: "",
|
||
printerModel: "",
|
||
usbVendorId: undefined,
|
||
usbProductId: undefined,
|
||
paperWidth: 80,
|
||
autoCut: "1",
|
||
imageMode: "GS_V0",
|
||
})
|
||
const validateUsbPair = (_rule: unknown, _value: unknown, callback: (error?: Error) => void) => {
|
||
const hasVid = configForm.usbVendorId !== undefined && configForm.usbVendorId !== null
|
||
const hasPid = configForm.usbProductId !== undefined && configForm.usbProductId !== null
|
||
callback(hasVid === hasPid ? undefined : new Error("USB VID 和 PID 必须同时填写或同时留空"))
|
||
}
|
||
const configRules = {
|
||
usbVendorId: [{ validator: validateUsbPair, trigger: "change" }],
|
||
usbProductId: [{ validator: validateUsbPair, trigger: "change" }],
|
||
paperWidth: [{ required: true, message: "纸宽必须为80mm", trigger: "change" }],
|
||
}
|
||
const dialogTitle = computed(() => (mode.value === "bind" ? "绑定尔印打印机" : "打印机配置"))
|
||
|
||
/** 恢复打印配置默认值,避免连续打开不同设备时残留上一台打印机的参数。 */
|
||
const resetConfig = (hardId: printerApi.HardwareId) => {
|
||
Object.assign(configForm, {
|
||
id: undefined,
|
||
printerHardId: hardId,
|
||
printerModel: "",
|
||
usbVendorId: undefined,
|
||
usbProductId: undefined,
|
||
paperWidth: 80,
|
||
autoCut: "1",
|
||
imageMode: "GS_V0",
|
||
})
|
||
}
|
||
|
||
/** 打开来源终端绑定弹窗,并使用未经过数值转换的雪花 ID 查询候选打印机。 */
|
||
const openBind = async (row: any) => {
|
||
mode.value = "bind"
|
||
const hardId = String(row.id)
|
||
sourceHardId.value = hardId
|
||
sourceName.value = row.name || `设备 ${row.id}`
|
||
bindForm.printerHardId = null
|
||
visible.value = true
|
||
loading.value = true
|
||
try {
|
||
const response: any = await printerApi.listAvailable(hardId)
|
||
availablePrinters.value = response.data || []
|
||
} catch (error: any) {
|
||
availablePrinters.value = []
|
||
useMessage().error(error?.msg || "读取可绑定打印机失败")
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 打开 EY_PRINT 参数弹窗,并保持目标设备雪花 ID 的字符串形态。 */
|
||
const openConfig = async (row: any) => {
|
||
mode.value = "config"
|
||
const hardId = String(row.id)
|
||
printerHardId.value = hardId
|
||
resetConfig(hardId)
|
||
visible.value = true
|
||
loading.value = true
|
||
try {
|
||
const response: any = await printerApi.getConfig(hardId)
|
||
Object.assign(configForm, response.data || {}, { printerHardId: hardId })
|
||
} catch (error: any) {
|
||
useMessage().error(error?.msg || "读取打印机配置失败")
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 校验并提交绑定或配置,设备主键保持字符串以便后端无损转换为 Long。 */
|
||
const submit = async () => {
|
||
const formRef = mode.value === "bind" ? bindFormRef.value : configFormRef.value
|
||
const valid = await formRef?.validate?.().catch(() => false)
|
||
if (valid === false) return
|
||
loading.value = true
|
||
try {
|
||
if (mode.value === "bind") {
|
||
if (!sourceHardId.value || !bindForm.printerHardId) {
|
||
useMessage().warning("请选择尔印打印机")
|
||
return
|
||
}
|
||
await printerApi.bind({ sourceHardId: sourceHardId.value, printerHardId: bindForm.printerHardId })
|
||
useMessage().success("绑定成功,请在该业务终端的“打印机信息”页面刷新并上报 USB 设备")
|
||
} else {
|
||
await printerApi.saveConfig({ ...configForm, printerHardId: printerHardId.value || configForm.printerHardId })
|
||
useMessage().success("配置保存成功")
|
||
}
|
||
visible.value = false
|
||
emit("refresh")
|
||
} catch (error: any) {
|
||
useMessage().error(error?.msg || "保存失败")
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
defineExpose({ openBind, openConfig })
|
||
</script>
|