Files
pj_admin_cloud/src/views/hard/hard-manager/printer-dialog.vue
T
2026-09-08 15:25:06 +08:00

290 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>