优化提交预检、废卡按钮置灰与等待超时提示
This commit is contained in:
@@ -2,6 +2,7 @@ import { resolve } from 'path'
|
|||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
|
||||||
|
|
||||||
const pkg = JSON.parse(readFileSync(resolve('package.json'), 'utf-8')) as { version: string }
|
const pkg = JSON.parse(readFileSync(resolve('package.json'), 'utf-8')) as { version: string }
|
||||||
|
|
||||||
@@ -25,6 +26,14 @@ export default defineConfig({
|
|||||||
define: {
|
define: {
|
||||||
__APP_VERSION__: JSON.stringify(pkg.version)
|
__APP_VERSION__: JSON.stringify(pkg.version)
|
||||||
},
|
},
|
||||||
plugins: [vue()]
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
// 构建时预编译多语言消息 + 使用 vue-i18n runtime 构建,
|
||||||
|
// 避免运行时 new Function() 编译消息(会被 CSP script-src 'self' 拦截导致白屏)
|
||||||
|
VueI18nPlugin({
|
||||||
|
include: [resolve('src/renderer/src/i18n/locales/**')],
|
||||||
|
runtimeOnly: true
|
||||||
|
})
|
||||||
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Generated
+938
-8
File diff suppressed because it is too large
Load Diff
@@ -90,6 +90,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@intlify/unplugin-vue-i18n": "^1.6.0",
|
||||||
"@vitejs/plugin-vue": "^4.6.2",
|
"@vitejs/plugin-vue": "^4.6.2",
|
||||||
"electron": "20.3.12",
|
"electron": "20.3.12",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "^24.13.3",
|
||||||
|
|||||||
@@ -106,6 +106,20 @@ function createWindow(): void {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 页面加载失败(dev server 未就绪 / 生产文件缺失)时记录并提示,避免静默白屏
|
||||||
|
mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
|
||||||
|
log.error('renderer did-fail-load', { errorCode, errorDescription, validatedURL })
|
||||||
|
if (errorCode !== -3) {
|
||||||
|
mainWindow?.webContents.executeJavaScript(
|
||||||
|
`document.getElementById('app').innerHTML = '<pre style="margin:16px;padding:16px;background:#fff5f5;border:1px solid #feb2b2;border-radius:8px;color:#c53030;font-size:13px;white-space:pre-wrap;">[main:did-fail-load] ${errorCode} ${errorDescription}\\n${validatedURL}</pre>'`
|
||||||
|
).catch(() => undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.webContents.on('preload-error', (_event, preloadPath, error) => {
|
||||||
|
log.error('preload-error', { preloadPath, error: String(error) })
|
||||||
|
})
|
||||||
|
|
||||||
if (process.env.ELECTRON_RENDERER_URL) {
|
if (process.env.ELECTRON_RENDERER_URL) {
|
||||||
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
|
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -136,7 +136,12 @@ export function registerIpcHandlers(): void {
|
|||||||
assertReady()
|
assertReady()
|
||||||
const dll = await loadDllModule()
|
const dll = await loadDllModule()
|
||||||
const code = dll.dllPrinterReset()
|
const code = dll.dllPrinterReset()
|
||||||
return code === CS_OK ? ok() : fail(code, '重置失败')
|
// 物理动作指令:负数为错误,0 或正数(状态/警告码)均视为指令已被打印机接受
|
||||||
|
if (code < 0) {
|
||||||
|
log.warn('SAPI_PrinterResetprinter returned non-OK code', code)
|
||||||
|
return fail(code, '重置失败')
|
||||||
|
}
|
||||||
|
return ok({ code })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return fail(CS_FAIL, String(err))
|
return fail(CS_FAIL, String(err))
|
||||||
}
|
}
|
||||||
@@ -148,7 +153,11 @@ export function registerIpcHandlers(): void {
|
|||||||
const dll = await loadDllModule()
|
const dll = await loadDllModule()
|
||||||
if (!dll.isRejectApiAvailable()) return fail(CS_FAIL, 'REJECT_API_UNAVAILABLE')
|
if (!dll.isRejectApiAvailable()) return fail(CS_FAIL, 'REJECT_API_UNAVAILABLE')
|
||||||
const code = dll.dllPrinterReject()
|
const code = dll.dllPrinterReject()
|
||||||
return code === CS_OK ? ok() : fail(code, '废卡失败')
|
if (code < 0) {
|
||||||
|
log.warn('SAPI_PrinterMovetoreject returned non-OK code', code)
|
||||||
|
return fail(code, '废卡失败')
|
||||||
|
}
|
||||||
|
return ok({ code })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return fail(CS_FAIL, String(err))
|
return fail(CS_FAIL, String(err))
|
||||||
}
|
}
|
||||||
@@ -161,7 +170,11 @@ export function registerIpcHandlers(): void {
|
|||||||
const dll = await loadDllModule()
|
const dll = await loadDllModule()
|
||||||
if (!dll.isUsbReaderApiAvailable()) return fail(CS_FAIL, 'USB_READER_API_UNAVAILABLE')
|
if (!dll.isUsbReaderApiAvailable()) return fail(CS_FAIL, 'USB_READER_API_UNAVAILABLE')
|
||||||
const code = dll.dllPrinterMoveToUsbReader()
|
const code = dll.dllPrinterMoveToUsbReader()
|
||||||
return code === CS_OK ? ok() : fail(code, '读卡失败')
|
if (code < 0) {
|
||||||
|
log.warn('SAPI_PrinterMovetousbreader returned non-OK code', code)
|
||||||
|
return fail(code, '读卡失败')
|
||||||
|
}
|
||||||
|
return ok({ code })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return fail(CS_FAIL, String(err))
|
return fail(CS_FAIL, String(err))
|
||||||
}
|
}
|
||||||
@@ -174,7 +187,12 @@ export function registerIpcHandlers(): void {
|
|||||||
const dll = await loadDllModule()
|
const dll = await loadDllModule()
|
||||||
if (!dll.isHopperApiAvailable()) return fail(CS_FAIL, 'HOPPER_API_UNAVAILABLE')
|
if (!dll.isHopperApiAvailable()) return fail(CS_FAIL, 'HOPPER_API_UNAVAILABLE')
|
||||||
const code = dll.dllPrinterMoveToHopper()
|
const code = dll.dllPrinterMoveToHopper()
|
||||||
return code === CS_OK ? ok() : fail(code, '退卡失败')
|
if (code < 0) {
|
||||||
|
log.warn('SAPI_PrinterMovetohopper returned non-OK code', code)
|
||||||
|
return fail(code, '退卡失败')
|
||||||
|
}
|
||||||
|
// 卡已物理退出但返回正数状态码时,不再误报失败
|
||||||
|
return ok({ code })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return fail(CS_FAIL, String(err))
|
return fail(CS_FAIL, String(err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,15 @@ export function startJobPoll(id: string): void {
|
|||||||
const cancelled = r.jobState === 6
|
const cancelled = r.jobState === 6
|
||||||
const finished = r.jobState === 100
|
const finished = r.jobState === 100
|
||||||
const terminal = failed || cancelled
|
const terminal = failed || cancelled
|
||||||
|
// 失败当刻立即取打印机错误串(与 USB 收集轮询对齐),避免渲染层事后取到通用/过期文案
|
||||||
|
let errorMessage = ''
|
||||||
|
if (failed) {
|
||||||
|
try {
|
||||||
|
errorMessage = dll.dllGetPrinterErrorStr(-1) || ''
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('GetPrinterErrorStr on job failed failed', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
const tick = {
|
const tick = {
|
||||||
jobId,
|
jobId,
|
||||||
queryErrorCode: r.queryErrorCode,
|
queryErrorCode: r.queryErrorCode,
|
||||||
@@ -88,7 +97,8 @@ export function startJobPoll(id: string): void {
|
|||||||
terminal,
|
terminal,
|
||||||
failed,
|
failed,
|
||||||
cancelled,
|
cancelled,
|
||||||
finished
|
finished,
|
||||||
|
errorMessage
|
||||||
}
|
}
|
||||||
emitTrace('[poll] job:poll-tick', tick)
|
emitTrace('[poll] job:poll-tick', tick)
|
||||||
send('job:poll-tick', tick)
|
send('job:poll-tick', tick)
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ let SAPI_PrinterMovetoreject: any = null
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let SAPI_PrinterMovetousbreader: any = null
|
let SAPI_PrinterMovetousbreader: any = null
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let SAPI_PrinterMovetohopper: any = null
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let SAPI_GetPrinterCardPosition: any = null
|
let SAPI_GetPrinterCardPosition: any = null
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let SAPI_PrinterCheckstatus: any = null
|
let SAPI_PrinterCheckstatus: any = null
|
||||||
@@ -55,6 +57,7 @@ let hasCheckstatusApi = false
|
|||||||
let hasCancelApi = false
|
let hasCancelApi = false
|
||||||
let hasUploadApi = false
|
let hasUploadApi = false
|
||||||
let hasUsbReaderApi = false
|
let hasUsbReaderApi = false
|
||||||
|
let hasHopperApi = false
|
||||||
let loggedCancelMissing = false
|
let loggedCancelMissing = false
|
||||||
let loggedRejectMissing = false
|
let loggedRejectMissing = false
|
||||||
|
|
||||||
@@ -206,6 +209,7 @@ function loadLibrary(): void {
|
|||||||
cancel: hasCancelApi,
|
cancel: hasCancelApi,
|
||||||
reject: hasRejectApi,
|
reject: hasRejectApi,
|
||||||
usbReader: hasUsbReaderApi,
|
usbReader: hasUsbReaderApi,
|
||||||
|
hopper: hasHopperApi,
|
||||||
cardPosition: hasCardPositionApi,
|
cardPosition: hasCardPositionApi,
|
||||||
checkstatus: hasCheckstatusApi
|
checkstatus: hasCheckstatusApi
|
||||||
})
|
})
|
||||||
@@ -231,6 +235,11 @@ export function isUsbReaderApiAvailable(): boolean {
|
|||||||
return hasUsbReaderApi
|
return hasUsbReaderApi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isHopperApiAvailable(): boolean {
|
||||||
|
loadLibrary()
|
||||||
|
return hasHopperApi
|
||||||
|
}
|
||||||
|
|
||||||
export function isCardPositionApiAvailable(): boolean {
|
export function isCardPositionApiAvailable(): boolean {
|
||||||
loadLibrary()
|
loadLibrary()
|
||||||
return hasCardPositionApi
|
return hasCardPositionApi
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const channels = {
|
|||||||
'dll:printer-status',
|
'dll:printer-status',
|
||||||
'dll:printer-reset',
|
'dll:printer-reset',
|
||||||
'dll:printer-reject',
|
'dll:printer-reject',
|
||||||
|
'dll:printer-read-card',
|
||||||
|
'dll:printer-eject-card',
|
||||||
'dll:printer-error-str',
|
'dll:printer-error-str',
|
||||||
'dll:job-create',
|
'dll:job-create',
|
||||||
'dll:job-cancel',
|
'dll:job-cancel',
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export const ICON_NAMES = [
|
|||||||
'stop',
|
'stop',
|
||||||
'warning',
|
'warning',
|
||||||
'id-card',
|
'id-card',
|
||||||
'eject'
|
'eject',
|
||||||
|
'eye',
|
||||||
|
'eye-slash'
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type IconName = (typeof ICON_NAMES)[number]
|
export type IconName = (typeof ICON_NAMES)[number]
|
||||||
@@ -38,5 +40,7 @@ export const ICON_FA_CLASS: Record<IconName, string> = {
|
|||||||
stop: 'fas fa-stop',
|
stop: 'fas fa-stop',
|
||||||
warning: 'fas fa-exclamation-triangle',
|
warning: 'fas fa-exclamation-triangle',
|
||||||
'id-card': 'fas fa-id-card',
|
'id-card': 'fas fa-id-card',
|
||||||
eject: 'fas fa-eject'
|
eject: 'fas fa-eject',
|
||||||
|
eye: 'fas fa-eye',
|
||||||
|
'eye-slash': 'fas fa-eye-slash'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,28 @@
|
|||||||
import { dllPrinterInfo, dllPrinterStatus, parsePrinterInfo } from '@/api/cardsoon'
|
import { dllPrinterInfo, dllPrinterStatus, parsePrinterInfo } from '@/api/cardsoon'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
|
|
||||||
|
/** 状态是否表示打印机未连接/不可用 */
|
||||||
|
function isDisconnectedStatus(text: string): boolean {
|
||||||
|
return !text || text === '—' || text.includes('未连接') || text.includes('Not connected')
|
||||||
|
}
|
||||||
|
|
||||||
|
let prevStatusText = ''
|
||||||
|
|
||||||
export async function refreshLiveStatus(): Promise<void> {
|
export async function refreshLiveStatus(): Promise<void> {
|
||||||
const store = useConfigStore()
|
const store = useConfigStore()
|
||||||
try {
|
try {
|
||||||
const r = await dllPrinterStatus()
|
const r = await dllPrinterStatus()
|
||||||
if (r.ok && r.data?.statusText) {
|
if (r.ok && r.data?.statusText) {
|
||||||
store.setPrinter({ ...store.printer, statusText: r.data.statusText })
|
const newStatus = r.data.statusText
|
||||||
|
// 检测状态变化:从未连接 → 已连接时,立即刷新打印机信息(型号/isSingleSide等)
|
||||||
|
const wasDisconnected = isDisconnectedStatus(prevStatusText)
|
||||||
|
const nowConnected = !isDisconnectedStatus(newStatus)
|
||||||
|
store.setPrinter({ ...store.printer, statusText: newStatus })
|
||||||
|
if (wasDisconnected && nowConnected) {
|
||||||
|
// 打印机刚连上,立即拉取型号/色带等信息
|
||||||
|
void refreshPrinterInfo()
|
||||||
|
}
|
||||||
|
prevStatusText = newStatus
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ export default {
|
|||||||
home: {
|
home: {
|
||||||
toolsTitle: 'Tools',
|
toolsTitle: 'Tools',
|
||||||
tasksTitle: 'Tasks',
|
tasksTitle: 'Tasks',
|
||||||
|
readCard: 'Read',
|
||||||
|
ejectCard: 'Eject',
|
||||||
dataDistribute: 'Data Distribution',
|
dataDistribute: 'Data Distribution',
|
||||||
dataDistributeDesc: 'Distribute data to print cards',
|
dataDistributeDesc: 'Distribute data onto cards',
|
||||||
dataCollect: 'Data Collection',
|
dataCollect: 'Data Collection',
|
||||||
dataCollectDesc: 'Collect data from cards'
|
dataCollectDesc: 'Collect data from cards'
|
||||||
},
|
},
|
||||||
@@ -53,8 +55,12 @@ export default {
|
|||||||
dongleHint: '(101 = unlimited)',
|
dongleHint: '(101 = unlimited)',
|
||||||
dongleAuthLabel: 'Auth Code',
|
dongleAuthLabel: 'Auth Code',
|
||||||
donglePassword: 'Enter authorization code',
|
donglePassword: 'Enter authorization code',
|
||||||
|
showAuth: 'Show auth code',
|
||||||
|
hideAuth: 'Hide auth code',
|
||||||
templatePreview: 'Label Preview',
|
templatePreview: 'Label Preview',
|
||||||
addLabel: 'Add Label',
|
addLabel: 'Add Label',
|
||||||
|
removeLabel: 'Remove',
|
||||||
|
templateRemoved: 'Label removed',
|
||||||
doubleSide: 'Double-sided',
|
doubleSide: 'Double-sided',
|
||||||
singleSideHint: 'Single-side printer detected, please select a side (front or back)',
|
singleSideHint: 'Single-side printer detected, please select a side (front or back)',
|
||||||
frontSide: 'Front',
|
frontSide: 'Front',
|
||||||
@@ -172,6 +178,9 @@ export default {
|
|||||||
templateNotExist: 'Template file does not exist',
|
templateNotExist: 'Template file does not exist',
|
||||||
printFlagMismatch: 'Print side mismatch, please reselect',
|
printFlagMismatch: 'Print side mismatch, please reselect',
|
||||||
singleSideMustPick: 'Single-side printer, please select front or back',
|
singleSideMustPick: 'Single-side printer, please select front or back',
|
||||||
|
pickSideRequired: 'Please select a print side (front or back) before submitting',
|
||||||
|
printerNotConnected: 'Printer not connected, please connect the printer first',
|
||||||
|
printerNotReady: 'Printer not connected or in fault (e.g. card jam). Please fix the device before submitting',
|
||||||
dongleAuthRequired: 'Please enter authorization code',
|
dongleAuthRequired: 'Please enter authorization code',
|
||||||
dongleCountInvalid: 'Dongle count must be 0 or 1-101',
|
dongleCountInvalid: 'Dongle count must be 0 or 1-101',
|
||||||
createJobFailed: 'Failed to create task',
|
createJobFailed: 'Failed to create task',
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export default {
|
|||||||
home: {
|
home: {
|
||||||
toolsTitle: '工具',
|
toolsTitle: '工具',
|
||||||
tasksTitle: '任务',
|
tasksTitle: '任务',
|
||||||
|
readCard: '读卡',
|
||||||
|
ejectCard: '退卡',
|
||||||
dataDistribute: '数据分发',
|
dataDistribute: '数据分发',
|
||||||
dataDistributeDesc: '分发数据到打印卡片',
|
dataDistributeDesc: '分发数据到打印卡片',
|
||||||
dataCollect: '数据收集',
|
dataCollect: '数据收集',
|
||||||
@@ -53,12 +55,16 @@ export default {
|
|||||||
dongleHint: '(101 为不限次数)',
|
dongleHint: '(101 为不限次数)',
|
||||||
dongleAuthLabel: '授权码',
|
dongleAuthLabel: '授权码',
|
||||||
donglePassword: '请输入授权码',
|
donglePassword: '请输入授权码',
|
||||||
|
showAuth: '显示授权码',
|
||||||
|
hideAuth: '隐藏授权码',
|
||||||
templatePreview: '标签预览',
|
templatePreview: '标签预览',
|
||||||
addLabel: '添加标签',
|
addLabel: '添加标签',
|
||||||
|
removeLabel: '移除标签',
|
||||||
|
templateRemoved: '已移除标签',
|
||||||
doubleSide: '双面',
|
doubleSide: '双面',
|
||||||
singleSideHint: '当前为单面打印机,请选择打印面(正面或背面)',
|
singleSideHint: '当前为单面打印机,请选择打印面(正面或背面)',
|
||||||
frontSide: 'FRONT',
|
frontSide: '正面',
|
||||||
backSide: 'BACK',
|
backSide: '背面',
|
||||||
imageFieldNotSelected: '未选择图片',
|
imageFieldNotSelected: '未选择图片',
|
||||||
templateLoaded: '已加载标签模板',
|
templateLoaded: '已加载标签模板',
|
||||||
templateNoPreview: '模板已打开,但未解析到可预览内容',
|
templateNoPreview: '模板已打开,但未解析到可预览内容',
|
||||||
@@ -160,7 +166,9 @@ export default {
|
|||||||
startUsbFailed: '启动 USB 收集失败',
|
startUsbFailed: '启动 USB 收集失败',
|
||||||
usbWaitComplete: '请先等待 USB 收集完成',
|
usbWaitComplete: '请先等待 USB 收集完成',
|
||||||
cannotEnterRunning: '无法进入运行页,已取消任务',
|
cannotEnterRunning: '无法进入运行页,已取消任务',
|
||||||
initFailedHint: '初始化失败,请检查任务目录权限'
|
initFailedHint: '初始化失败,请检查任务目录权限',
|
||||||
|
waitTimeoutCollect: '等待时间较长:请检查 U 盘数据线是否连接、卡片是否插好(可把卡插反后重试)',
|
||||||
|
waitTimeoutDistribute: '等待进卡时间较长:请检查卡片是否到位、打印机是否夹卡或报错'
|
||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
noTask: '请配置拷贝路径或打印模板',
|
noTask: '请配置拷贝路径或打印模板',
|
||||||
@@ -173,6 +181,8 @@ export default {
|
|||||||
printFlagMismatch: '打印面数不匹配,请重新选择',
|
printFlagMismatch: '打印面数不匹配,请重新选择',
|
||||||
singleSideMustPick: '当前为单面打印机,请选择正面或背面',
|
singleSideMustPick: '当前为单面打印机,请选择正面或背面',
|
||||||
pickSideRequired: '请选择打印面(正面或背面)后再提交',
|
pickSideRequired: '请选择打印面(正面或背面)后再提交',
|
||||||
|
printerNotConnected: '打印机未连接,请先连接打印机后再提交',
|
||||||
|
printerNotReady: '打印机未连接或设备异常(如夹卡),请排除故障后再提交',
|
||||||
dongleAuthRequired: '请输入授权码',
|
dongleAuthRequired: '请输入授权码',
|
||||||
dongleCountInvalid: '加密狗次数须为 0 或 1-101',
|
dongleCountInvalid: '加密狗次数须为 0 或 1-101',
|
||||||
createJobFailed: '创建任务失败',
|
createJobFailed: '创建任务失败',
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export default {
|
|||||||
home: {
|
home: {
|
||||||
toolsTitle: '工具',
|
toolsTitle: '工具',
|
||||||
tasksTitle: '任務',
|
tasksTitle: '任務',
|
||||||
|
readCard: '讀卡',
|
||||||
|
ejectCard: '退卡',
|
||||||
dataDistribute: '數據分發',
|
dataDistribute: '數據分發',
|
||||||
dataDistributeDesc: '分發數據到列印卡片',
|
dataDistributeDesc: '分發數據到列印卡片',
|
||||||
dataCollect: '數據收集',
|
dataCollect: '數據收集',
|
||||||
@@ -53,12 +55,16 @@ export default {
|
|||||||
dongleHint: '(101 為不限次數)',
|
dongleHint: '(101 為不限次數)',
|
||||||
dongleAuthLabel: '授權碼',
|
dongleAuthLabel: '授權碼',
|
||||||
donglePassword: '請輸入授權碼',
|
donglePassword: '請輸入授權碼',
|
||||||
|
showAuth: '顯示授權碼',
|
||||||
|
hideAuth: '隱藏授權碼',
|
||||||
templatePreview: '標籤預覽',
|
templatePreview: '標籤預覽',
|
||||||
addLabel: '新增標籤',
|
addLabel: '新增標籤',
|
||||||
|
removeLabel: '移除標籤',
|
||||||
|
templateRemoved: '已移除標籤',
|
||||||
doubleSide: '雙面',
|
doubleSide: '雙面',
|
||||||
singleSideHint: '當前為單面印表機,請選擇列印面(正面或背面)',
|
singleSideHint: '當前為單面印表機,請選擇列印面(正面或背面)',
|
||||||
frontSide: 'FRONT',
|
frontSide: '正面',
|
||||||
backSide: 'BACK',
|
backSide: '背面',
|
||||||
imageFieldNotSelected: '未選擇圖片',
|
imageFieldNotSelected: '未選擇圖片',
|
||||||
templateLoaded: '已載入標籤模板',
|
templateLoaded: '已載入標籤模板',
|
||||||
templateNoPreview: '模板已開啟,但未解析到可預覽內容',
|
templateNoPreview: '模板已開啟,但未解析到可預覽內容',
|
||||||
@@ -173,6 +179,8 @@ export default {
|
|||||||
printFlagMismatch: '列印面數不匹配,請重新選擇',
|
printFlagMismatch: '列印面數不匹配,請重新選擇',
|
||||||
singleSideMustPick: '當前為單面印表機,請選擇正面或背面',
|
singleSideMustPick: '當前為單面印表機,請選擇正面或背面',
|
||||||
pickSideRequired: '請選擇列印面(正面或背面)後再提交',
|
pickSideRequired: '請選擇列印面(正面或背面)後再提交',
|
||||||
|
printerNotConnected: '印表機未連接,請先連接印表機後再提交',
|
||||||
|
printerNotReady: '印表機未連接或設備異常(如夾卡),請排除故障後再提交',
|
||||||
dongleAuthRequired: '請輸入授權碼',
|
dongleAuthRequired: '請輸入授權碼',
|
||||||
dongleCountInvalid: '加密狗次數須為 0 或 1-101',
|
dongleCountInvalid: '加密狗次數須為 0 或 1-101',
|
||||||
createJobFailed: '建立任務失敗',
|
createJobFailed: '建立任務失敗',
|
||||||
|
|||||||
@@ -24,73 +24,92 @@ import './styles/shell.css'
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
window.cardsoonApi.on('app:trace', (payload) => {
|
// preload 未注入时(如普通浏览器调试/ preload 加载失败)不再白屏,直接跳过桌面侧能力
|
||||||
|
const hasApi = typeof window.cardsoonApi !== 'undefined'
|
||||||
const p = payload as { level: string; message: string; data?: Record<string, unknown> }
|
|
||||||
|
|
||||||
if (p.level === 'error') console.error(p.message, p.data ?? '')
|
|
||||||
|
|
||||||
else console.log(p.message, p.data ?? '')
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
if (hasApi) {
|
||||||
|
window.cardsoonApi.on('app:trace', (payload) => {
|
||||||
|
const p = payload as { level: string; message: string; data?: Record<string, unknown> }
|
||||||
|
if (p.level === 'error') console.error(p.message, p.data ?? '')
|
||||||
|
else console.log(p.message, p.data ?? '')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function setTrace(on: boolean): Promise<void> {
|
async function setTrace(on: boolean): Promise<void> {
|
||||||
|
|
||||||
await window.cardsoonApi.invoke('config:set', { traceEnabled: on })
|
await window.cardsoonApi.invoke('config:set', { traceEnabled: on })
|
||||||
|
|
||||||
console.info(`[trace] 控制台日志已${on ? '开启' : '关闭'}`)
|
console.info(`[trace] 控制台日志已${on ? '开启' : '关闭'}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = window as Window & { trace?: (on?: boolean) => Promise<void>; dllTrace?: (on?: boolean) => Promise<void> }
|
||||||
|
|
||||||
|
if (hasApi) {
|
||||||
|
w.trace = async (on = true) => setTrace(on)
|
||||||
|
w.dllTrace = w.trace
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
const cfg = await configGet()
|
||||||
|
const on = cfg.ok && cfg.data?.traceEnabled === true
|
||||||
|
console.info(`[trace] 控制台日志: ${on ? '已开启' : '已关闭'},执行 trace(false) 关闭`)
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const w = window as Window & { trace?: (on?: boolean) => Promise<void>; dllTrace?: (on?: boolean) => Promise<void> }
|
/**
|
||||||
|
* 渲染进程启动崩溃兜底:把白屏变成可见错误面板,
|
||||||
|
* 便于在无法打开 DevTools 的现场截图定位问题。
|
||||||
|
* 仅在 Vue 挂载前/挂载瞬间触发的错误会覆盖界面;
|
||||||
|
* 挂载完成后的运行时错误只进控制台,不破坏已有界面。
|
||||||
|
*/
|
||||||
|
let appMounted = false
|
||||||
|
|
||||||
w.trace = async (on = true) => setTrace(on)
|
function showFatalError(stage: string, err: unknown): void {
|
||||||
|
if (appMounted) {
|
||||||
|
console.error(`[renderer:${stage}]`, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const el = document.getElementById('app')
|
||||||
|
if (!el) return
|
||||||
|
const detail = err instanceof Error ? `${err.message}\n\n${err.stack ?? ''}` : String(err)
|
||||||
|
el.innerHTML = `<pre style="margin:16px;padding:16px;background:#fff5f5;border:1px solid #feb2b2;border-radius:8px;color:#c53030;font-size:13px;white-space:pre-wrap;word-break:break-all;">[renderer:${stage}]\n${detail}</pre>`
|
||||||
|
}
|
||||||
|
|
||||||
w.dllTrace = w.trace
|
window.addEventListener('error', (e) => {
|
||||||
|
showFatalError('window.onerror', e.error ?? e.message)
|
||||||
|
})
|
||||||
|
window.addEventListener('unhandledrejection', (e) => {
|
||||||
|
showFatalError('unhandledrejection', e.reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.config.errorHandler = (err, _instance, info) => {
|
||||||
|
console.error('[vue errorHandler]', err, info)
|
||||||
|
if (!appMounted) showFatalError(`vue:${info}`, err)
|
||||||
|
}
|
||||||
|
|
||||||
void (async () => {
|
const pinia = createPinia()
|
||||||
|
|
||||||
const cfg = await configGet()
|
app.use(pinia)
|
||||||
|
|
||||||
const on = cfg.ok && cfg.data?.traceEnabled === true
|
app.use(i18n)
|
||||||
|
|
||||||
console.info(`[trace] 控制台日志: ${on ? '已开启' : '已关闭'},执行 trace(false) 关闭`)
|
app.use(router)
|
||||||
|
|
||||||
})()
|
void (async () => {
|
||||||
|
await useNetworkAuthStore().loadFromStorage()
|
||||||
|
|
||||||
|
const dongleStore = useDongleAuthStore()
|
||||||
|
|
||||||
|
await dongleStore.loadFromSecrets()
|
||||||
|
|
||||||
const app = createApp(App)
|
useDistributeFormStore().dongleAuthCode = dongleStore.authCode
|
||||||
|
})()
|
||||||
|
|
||||||
const pinia = createPinia()
|
app.mount('#app')
|
||||||
|
appMounted = true
|
||||||
app.use(pinia)
|
} catch (err) {
|
||||||
|
showFatalError('bootstrap', err)
|
||||||
app.use(i18n)
|
}
|
||||||
|
|
||||||
app.use(router)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
|
|
||||||
await useNetworkAuthStore().loadFromStorage()
|
|
||||||
|
|
||||||
const dongleStore = useDongleAuthStore()
|
|
||||||
|
|
||||||
await dongleStore.loadFromSecrets()
|
|
||||||
|
|
||||||
useDistributeFormStore().dongleAuthCode = dongleStore.authCode
|
|
||||||
|
|
||||||
})()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.mount('#app')
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export const useJobStore = defineStore('job', {
|
|||||||
clearActiveJob() {
|
clearActiveJob() {
|
||||||
this.jobId = ''
|
this.jobId = ''
|
||||||
},
|
},
|
||||||
|
/** 重置次数计数器(不影响当前 jobId/lastJobJson) */
|
||||||
|
resetCounts() {
|
||||||
|
this.successCount = 0
|
||||||
|
this.failCount = 0
|
||||||
|
},
|
||||||
reset() {
|
reset() {
|
||||||
this.jobId = ''
|
this.jobId = ''
|
||||||
this.lastJobJson = ''
|
this.lastJobJson = ''
|
||||||
|
|||||||
@@ -65,3 +65,15 @@
|
|||||||
.fa-exclamation-triangle::before {
|
.fa-exclamation-triangle::before {
|
||||||
content: '\f071';
|
content: '\f071';
|
||||||
}
|
}
|
||||||
|
.fa-id-card::before {
|
||||||
|
content: '\f2c2';
|
||||||
|
}
|
||||||
|
.fa-eject::before {
|
||||||
|
content: '\f052';
|
||||||
|
}
|
||||||
|
.fa-eye::before {
|
||||||
|
content: '\f06e';
|
||||||
|
}
|
||||||
|
.fa-eye-slash::before {
|
||||||
|
content: '\f070';
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,22 +73,99 @@
|
|||||||
color: var(--cs-primary);
|
color: var(--cs-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 读卡 / 退卡 并排按钮行 */
|
/* 不支持的工具按钮(如当前 DLL 无废卡接口):置灰禁用 */
|
||||||
.m-tool-row {
|
.m-tool-btn:disabled {
|
||||||
|
background: #f1f3f5;
|
||||||
|
border-color: #e9ecef;
|
||||||
|
color: #adb5bd;
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-tool-btn:disabled:hover {
|
||||||
|
border-color: #e9ecef;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-tool-btn:disabled i,
|
||||||
|
.m-tool-btn:disabled:hover i {
|
||||||
|
color: #ced4da;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 读卡 / 退卡 PK 式斜切分隔按钮 ========== */
|
||||||
|
.m-tool-versus {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.m-tool-btn--half {
|
.m-tool-versus:hover {
|
||||||
flex: 1;
|
border-color: var(--cs-primary);
|
||||||
min-width: 0;
|
box-shadow: 0 4px 12px rgba(0, 128, 0, 0.10);
|
||||||
justify-content: center;
|
}
|
||||||
padding: 0 8px;
|
|
||||||
|
.m-versus-half {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #495057;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background 0.18s ease, color 0.18s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.m-tool-btn--half i {
|
.m-versus-half span {
|
||||||
width: 16px;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-versus-half i {
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
transition: color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左半:内容朝中间(右对齐),右边缘斜切为 \ 形 */
|
||||||
|
.m-versus-half--read {
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-right: 12px;
|
||||||
|
clip-path: polygon(0 0, calc(100% - 9px) 0, calc(100% + 9px) 100%, 0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右半:内容朝中间(左对齐),左边缘斜切为 \ 形 */
|
||||||
|
.m-versus-half--eject {
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-left: 12px;
|
||||||
|
clip-path: polygon(calc(0% - 9px) 0, 100% 0, 100% 100%, calc(0% + 9px) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-versus-half:hover {
|
||||||
|
background: rgba(0, 128, 0, 0.08);
|
||||||
|
color: var(--cs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-versus-half:hover i {
|
||||||
|
color: var(--cs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-versus-half:active {
|
||||||
|
background: rgba(0, 128, 0, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 垂直分隔线 ========== */
|
/* ========== 垂直分隔线 ========== */
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.m-panel-toolbar .c-checkbox-item .c-input.dog-auth {
|
.m-panel-toolbar .c-checkbox-item .c-input.dog-auth {
|
||||||
width: 110px;
|
width: 140px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
@@ -120,6 +120,36 @@
|
|||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 授权码输入框容器 + 小眼睛 */
|
||||||
|
.m-panel-toolbar .c-checkbox-item .dog-auth-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-panel-toolbar .c-checkbox-item .dog-auth-toggle {
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: #adb5bd;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-panel-toolbar .c-checkbox-item .dog-auth-toggle:hover {
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
/* 授权码标签 */
|
/* 授权码标签 */
|
||||||
.m-panel-toolbar .c-checkbox-item .dog-auth-label {
|
.m-panel-toolbar .c-checkbox-item .dog-auth-label {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export interface JobPollPayload {
|
|||||||
failed: boolean
|
failed: boolean
|
||||||
cancelled: boolean
|
cancelled: boolean
|
||||||
finished: boolean
|
finished: boolean
|
||||||
|
/** 任务失败当刻由主进程取到的打印机错误串 */
|
||||||
|
errorMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsbPollPayload {
|
export interface UsbPollPayload {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DistributeFormState } from '@/stores/distributeForm'
|
import type { DistributeFormState } from '@/stores/distributeForm'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import { fsPathExists } from '@/api/cardsoon'
|
import { fsPathExists, dllPrinterStatus } from '@/api/cardsoon'
|
||||||
import { resolveJobTasks, validateJobConfig } from '@/utils/validateJobConfig'
|
import { resolveJobTasks, validateJobConfig } from '@/utils/validateJobConfig'
|
||||||
import { resolveCopyNetworkHosts } from '@/utils/copyNetworkHosts'
|
import { resolveCopyNetworkHosts } from '@/utils/copyNetworkHosts'
|
||||||
import { useNetworkAuthStore } from '@/stores/networkAuth'
|
import { useNetworkAuthStore } from '@/stores/networkAuth'
|
||||||
@@ -22,11 +22,47 @@ export function printFlagMismatch(printFlag: number, templateFlag: number): bool
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交前打印机硬件状态预检(分发/收集共用)。
|
||||||
|
* - 返回非 null 字符串:拦截提交并提示(未连接 / 设备故障码,如夹卡)
|
||||||
|
* - 返回 null:放行(含 DLL 不支持状态查询、查询异常等无法判断的情况,交给后续流程)
|
||||||
|
* 注:夹卡等具体故障码表待 DLL 侧提供后,可在此按 statusCode 精确映射文案。
|
||||||
|
*/
|
||||||
|
export async function preflightPrinterStatus(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const r = await dllPrinterStatus()
|
||||||
|
if (!r.ok) {
|
||||||
|
const msg = r.message || ''
|
||||||
|
// DLL 不支持状态查询时无法判断,放行交给后续流程
|
||||||
|
if (msg.includes('不支持') || msg.toLowerCase().includes('not supported')) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return t('validation.printerNotReady')
|
||||||
|
}
|
||||||
|
const data = r.data
|
||||||
|
if (!data) return t('validation.printerNotReady')
|
||||||
|
// 健康:主进程返回了明确的非负状态码(0 空闲 / 忙碌 / 打印中)
|
||||||
|
if (typeof data.statusCode === 'number' && data.statusCode >= 0) return null
|
||||||
|
// 缓存兜底 + liveError:实时查询失败(断连 / 故障码)
|
||||||
|
if (data.fromCache && data.liveError) return t('validation.printerNotReady')
|
||||||
|
// 无状态码(未初始化 / 未知异常):保守拦截
|
||||||
|
return t('validation.printerNotReady')
|
||||||
|
} catch {
|
||||||
|
// 查询异常时不阻塞,让后续流程处理
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function validateJobPreflight(form: DistributeFormState): Promise<string | null> {
|
export async function validateJobPreflight(form: DistributeFormState): Promise<string | null> {
|
||||||
const err = validateJobConfig(form)
|
const err = validateJobConfig(form)
|
||||||
if (err) return err
|
if (err) return err
|
||||||
|
|
||||||
|
// 打印机硬件预检:打印/拷贝任务都需要设备,未连接或故障码(如夹卡)时拦截提交
|
||||||
const { hasCopy, hasPrint } = resolveJobTasks(form)
|
const { hasCopy, hasPrint } = resolveJobTasks(form)
|
||||||
|
if (hasPrint || hasCopy) {
|
||||||
|
const fault = await preflightPrinterStatus()
|
||||||
|
if (fault) return fault
|
||||||
|
}
|
||||||
|
|
||||||
if (hasCopy) {
|
if (hasCopy) {
|
||||||
const paths = form.pathList.map((x) => x.path)
|
const paths = form.pathList.map((x) => x.path)
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import AppIcon from '@/components/AppIcon.vue'
|
|||||||
import { useCollectStore } from '@/stores/collect'
|
import { useCollectStore } from '@/stores/collect'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { dialogOpenDirectory, dllUsbCopy } from '@/api/cardsoon'
|
import { dialogOpenDirectory, dllUsbCopy } from '@/api/cardsoon'
|
||||||
|
import { preflightPrinterStatus } from '@/utils/validateJobPreflight'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -102,6 +103,12 @@ async function onSubmit(): Promise<void> {
|
|||||||
notify.warning(t('notify.selectDirFirst'))
|
notify.warning(t('notify.selectDirFirst'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 打印机硬件预检:未连接或故障码(如夹卡)时拦截提交
|
||||||
|
const fault = await preflightPrinterStatus()
|
||||||
|
if (fault) {
|
||||||
|
notify.warning(fault)
|
||||||
|
return
|
||||||
|
}
|
||||||
const r = await dllUsbCopy(dest, collectStore.cardOutput)
|
const r = await dllUsbCopy(dest, collectStore.cardOutput)
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
notify.error(r.message || t('notify.startUsbFailed'))
|
notify.error(r.message || t('notify.startUsbFailed'))
|
||||||
|
|||||||
@@ -70,13 +70,23 @@
|
|||||||
<span class="dog-hint">{{ dongleHint }}</span>
|
<span class="dog-hint">{{ dongleHint }}</span>
|
||||||
<template v-if="formStore.dongleEnabled">
|
<template v-if="formStore.dongleEnabled">
|
||||||
<span class="dog-auth-label">{{ t('distributeConfig.dongleAuthLabel') }}</span>
|
<span class="dog-auth-label">{{ t('distributeConfig.dongleAuthLabel') }}</span>
|
||||||
<input
|
<div class="dog-auth-wrap">
|
||||||
v-model="formStore.dongleAuthCode"
|
<input
|
||||||
type="password"
|
v-model="formStore.dongleAuthCode"
|
||||||
class="c-input dog-auth"
|
:type="showAuthCode ? 'text' : 'password'"
|
||||||
:placeholder="t('distributeConfig.donglePassword')"
|
class="c-input dog-auth"
|
||||||
@input="onDongleAuthInput"
|
:placeholder="t('distributeConfig.donglePassword')"
|
||||||
/>
|
@input="onDongleAuthInput"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dog-auth-toggle"
|
||||||
|
:title="showAuthCode ? t('distributeConfig.hideAuth') : t('distributeConfig.showAuth')"
|
||||||
|
@click="showAuthCode = !showAuthCode"
|
||||||
|
>
|
||||||
|
<AppIcon :name="showAuthCode ? 'eye-slash' : 'eye'" size="sm" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,6 +116,14 @@
|
|||||||
<button type="button" class="c-button-cs" @click="pickTemplate">
|
<button type="button" class="c-button-cs" @click="pickTemplate">
|
||||||
{{ t('distributeConfig.addLabel') }}
|
{{ t('distributeConfig.addLabel') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="formStore.templateFile"
|
||||||
|
type="button"
|
||||||
|
class="c-button-cs"
|
||||||
|
@click="removeTemplate"
|
||||||
|
>
|
||||||
|
{{ t('distributeConfig.removeLabel') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="templatePreviewLoaded && hasDoubleSide" class="m-print-side-picker">
|
<div v-if="templatePreviewLoaded && hasDoubleSide" class="m-print-side-picker">
|
||||||
@@ -202,7 +220,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { cleanPathPattern } from '@shared/path-pattern'
|
import { cleanPathPattern } from '@shared/path-pattern'
|
||||||
@@ -249,6 +267,18 @@ const dongleStore = useDongleAuthStore()
|
|||||||
const networkDialogVisible = ref(false)
|
const networkDialogVisible = ref(false)
|
||||||
const networkDialogHost = ref('')
|
const networkDialogHost = ref('')
|
||||||
const networkDialogShare = ref('')
|
const networkDialogShare = ref('')
|
||||||
|
const showAuthCode = ref(false)
|
||||||
|
|
||||||
|
// 单面打印机检测到后,重置 printFlag(避免默认双面)
|
||||||
|
watch(
|
||||||
|
() => configStore.printer.isSingleSide,
|
||||||
|
(isSingle) => {
|
||||||
|
if (isSingle && formStore.printFlag === 1) {
|
||||||
|
const flag = formStore.templatePreview?.templateFlag ?? 1
|
||||||
|
formStore.printFlag = defaultPrintFlagForTemplate(flag, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
let dongleAuthPersistTimer: ReturnType<typeof setTimeout> | null = null
|
let dongleAuthPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
@@ -347,6 +377,13 @@ function onClear(): void {
|
|||||||
notify.info(t('distributeConfig.cleared'))
|
notify.info(t('distributeConfig.cleared'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeTemplate(): void {
|
||||||
|
formStore.templateFile = ''
|
||||||
|
formStore.templatePreview = null
|
||||||
|
formStore.printFlag = 1
|
||||||
|
notify.info(t('distributeConfig.templateRemoved'))
|
||||||
|
}
|
||||||
|
|
||||||
const dongleInputValue = computed(() =>
|
const dongleInputValue = computed(() =>
|
||||||
formStore.dongleEnabled ? formStore.dongleInstallCount : -1
|
formStore.dongleEnabled ? formStore.dongleInstallCount : -1
|
||||||
)
|
)
|
||||||
@@ -492,6 +529,8 @@ async function onSubmit(): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
jobStore.submitting = true
|
jobStore.submitting = true
|
||||||
|
// 新任务开始前重置次数计数器(不重置 jobId/lastJobJson,避免影响并发逻辑)
|
||||||
|
jobStore.resetCounts()
|
||||||
try {
|
try {
|
||||||
const created = await createDistributeJob(formStore)
|
const created = await createDistributeJob(formStore)
|
||||||
if (!created.ok) {
|
if (!created.ok) {
|
||||||
|
|||||||
@@ -115,6 +115,11 @@ import type { CardPositionPollPayload, JobPollPayload, UsbPollPayload } from '@/
|
|||||||
|
|
||||||
const CIRCLE_LEN = 283
|
const CIRCLE_LEN = 283
|
||||||
|
|
||||||
|
/** 等待进卡/读 U 盘超过该时长仍无进展,给出可操作提示(每个等待回合提示一次) */
|
||||||
|
const WAIT_WARN_MS = 45000
|
||||||
|
/** 看门狗检查间隔 */
|
||||||
|
const WAIT_WATCHDOG_MS = 5000
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -147,6 +152,9 @@ let lastCardPosition = -1
|
|||||||
let usbAwaitNewCycle = false
|
let usbAwaitNewCycle = false
|
||||||
let queryFailStreak = 0
|
let queryFailStreak = 0
|
||||||
let usbQueryFailStreak = 0
|
let usbQueryFailStreak = 0
|
||||||
|
let waitSince = 0
|
||||||
|
let waitWarned = false
|
||||||
|
let waitWatchdog: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
const displayProgress = computed(() => Math.round(progress.value))
|
const displayProgress = computed(() => Math.round(progress.value))
|
||||||
|
|
||||||
@@ -193,6 +201,41 @@ function clearPollListeners(): void {
|
|||||||
unsubCard = null
|
unsubCard = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 进入"等待进卡/读 U 盘"回合(已在等待中则不重复计时) */
|
||||||
|
function markWaiting(): void {
|
||||||
|
if (waitSince === 0) {
|
||||||
|
waitSince = Date.now()
|
||||||
|
waitWarned = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 离开等待状态(进卡成功 / 任务结束 / 重新提交) */
|
||||||
|
function clearWaiting(): void {
|
||||||
|
waitSince = 0
|
||||||
|
waitWarned = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopWaitWatchdog(): void {
|
||||||
|
if (waitWatchdog) {
|
||||||
|
clearInterval(waitWatchdog)
|
||||||
|
waitWatchdog = null
|
||||||
|
}
|
||||||
|
clearWaiting()
|
||||||
|
}
|
||||||
|
|
||||||
|
function startWaitWatchdog(): void {
|
||||||
|
stopWaitWatchdog()
|
||||||
|
waitWatchdog = setInterval(() => {
|
||||||
|
if (phase.value !== 'running' || waitSince === 0 || waitWarned) return
|
||||||
|
if (Date.now() - waitSince < WAIT_WARN_MS) return
|
||||||
|
waitWarned = true
|
||||||
|
// 等待过久无进展:收集多为 U 盘数据线/卡片方向问题,分发多为卡片未到位/夹卡
|
||||||
|
notify.warning(
|
||||||
|
isCollect.value ? t('notify.waitTimeoutCollect') : t('notify.waitTimeoutDistribute')
|
||||||
|
)
|
||||||
|
}, WAIT_WATCHDOG_MS)
|
||||||
|
}
|
||||||
|
|
||||||
async function releasePolls(resetMode: boolean): Promise<void> {
|
async function releasePolls(resetMode: boolean): Promise<void> {
|
||||||
clearPollListeners()
|
clearPollListeners()
|
||||||
await pollCardPositionStop()
|
await pollCardPositionStop()
|
||||||
@@ -230,6 +273,7 @@ async function startCardPositionWatch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function enterWaitPhase(next: 'completed' | 'failed', errorText = ''): Promise<void> {
|
async function enterWaitPhase(next: 'completed' | 'failed', errorText = ''): Promise<void> {
|
||||||
|
clearWaiting()
|
||||||
if (phase.value !== next) {
|
if (phase.value !== next) {
|
||||||
phase.value = next
|
phase.value = next
|
||||||
if (next === 'completed') {
|
if (next === 'completed') {
|
||||||
@@ -311,6 +355,7 @@ async function resubmitTask(): Promise<void> {
|
|||||||
collectHint.value = ''
|
collectHint.value = ''
|
||||||
queryFailStreak = 0
|
queryFailStreak = 0
|
||||||
usbQueryFailStreak = 0
|
usbQueryFailStreak = 0
|
||||||
|
clearWaiting()
|
||||||
const r = await dllUsbCopy(dest, collectStore.cardOutput, { resubmit: true })
|
const r = await dllUsbCopy(dest, collectStore.cardOutput, { resubmit: true })
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
await backToWait(r.message || t('notify.resubmitCollectFailed'))
|
await backToWait(r.message || t('notify.resubmitCollectFailed'))
|
||||||
@@ -318,6 +363,7 @@ async function resubmitTask(): Promise<void> {
|
|||||||
}
|
}
|
||||||
usbAwaitNewCycle = true
|
usbAwaitNewCycle = true
|
||||||
collectHint.value = usbTaskStatusHint(USB_TASK_PREPARING)
|
collectHint.value = usbTaskStatusHint(USB_TASK_PREPARING)
|
||||||
|
markWaiting()
|
||||||
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
|
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
|
||||||
void refreshLiveStatus()
|
void refreshLiveStatus()
|
||||||
return
|
return
|
||||||
@@ -336,6 +382,7 @@ async function resubmitTask(): Promise<void> {
|
|||||||
workflowStep.value = 1
|
workflowStep.value = 1
|
||||||
waitCard.value = false
|
waitCard.value = false
|
||||||
queryFailStreak = 0
|
queryFailStreak = 0
|
||||||
|
clearWaiting()
|
||||||
const created = await createDistributeJob(formStore, { resubmit: true })
|
const created = await createDistributeJob(formStore, { resubmit: true })
|
||||||
if (!created.ok) {
|
if (!created.ok) {
|
||||||
await backToWait(created.message)
|
await backToWait(created.message)
|
||||||
@@ -371,9 +418,12 @@ function applyJobProgress(p: JobPollPayload): void {
|
|||||||
const ui = mapJobStateToUi(p.jobState)
|
const ui = mapJobStateToUi(p.jobState)
|
||||||
workflowStep.value = ui.workflowStep
|
workflowStep.value = ui.workflowStep
|
||||||
waitCard.value = ui.hint === 'waitCard'
|
waitCard.value = ui.hint === 'waitCard'
|
||||||
|
if (ui.hint === 'waitCard') markWaiting()
|
||||||
|
else clearWaiting()
|
||||||
if (p.failed) {
|
if (p.failed) {
|
||||||
jobStore.failCount += 1
|
jobStore.failCount += 1
|
||||||
void enterFailedPhase()
|
// 优先使用主进程在失败当刻取到的错误串,避免事后取到通用文案
|
||||||
|
void enterFailedPhase(p.errorMessage || '')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (p.cancelled) {
|
if (p.cancelled) {
|
||||||
@@ -419,6 +469,9 @@ function applyUsbProgress(p: UsbPollPayload): void {
|
|||||||
const copyProgress = clampUsbCopyProgress(p.progress)
|
const copyProgress = clampUsbCopyProgress(p.progress)
|
||||||
workflowStep.value = p.taskStatus === USB_TASK_COPYING ? 2 : 1
|
workflowStep.value = p.taskStatus === USB_TASK_COPYING ? 2 : 1
|
||||||
collectHint.value = usbTaskStatusHint(p.taskStatus)
|
collectHint.value = usbTaskStatusHint(p.taskStatus)
|
||||||
|
// PREPARING(等待插卡/读 U 盘)开始计时;进入 COPYING 等后续阶段后清除
|
||||||
|
if (p.taskStatus === USB_TASK_PREPARING) markWaiting()
|
||||||
|
else clearWaiting()
|
||||||
setProgress(copyProgress)
|
setProgress(copyProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,6 +512,8 @@ onMounted(async () => {
|
|||||||
workflowStep.value = 1
|
workflowStep.value = 1
|
||||||
setProgress(0)
|
setProgress(0)
|
||||||
collectHint.value = usbTaskStatusHint(USB_TASK_PREPARING)
|
collectHint.value = usbTaskStatusHint(USB_TASK_PREPARING)
|
||||||
|
startWaitWatchdog()
|
||||||
|
markWaiting()
|
||||||
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
|
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
|
||||||
void refreshLiveStatus()
|
void refreshLiveStatus()
|
||||||
return
|
return
|
||||||
@@ -470,6 +525,8 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
appStore.setMode('distributing')
|
appStore.setMode('distributing')
|
||||||
setProgress(0)
|
setProgress(0)
|
||||||
|
clearWaiting()
|
||||||
|
startWaitWatchdog()
|
||||||
const started = await pollJobStart(jobStore.jobId)
|
const started = await pollJobStart(jobStore.jobId)
|
||||||
if (!started.ok) {
|
if (!started.ok) {
|
||||||
notify.error(started.message || t('notify.pollStartFailed'))
|
notify.error(started.message || t('notify.pollStartFailed'))
|
||||||
@@ -481,6 +538,7 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
stopWaitWatchdog()
|
||||||
if (finishing || phase.value !== 'running') return
|
if (finishing || phase.value !== 'running') return
|
||||||
void releasePolls(true)
|
void releasePolls(true)
|
||||||
if (isCollect.value && appStore.mode === 'usbCopying') {
|
if (isCollect.value && appStore.mode === 'usbCopying') {
|
||||||
@@ -537,11 +595,12 @@ async function onStop(): Promise<void> {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.m-error-detail {
|
.m-error-detail {
|
||||||
margin-top: 6px;
|
margin-top: 8px;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
color: #dc3545;
|
color: #dc3545;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
max-width: 320px;
|
max-width: 340px;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,34 @@
|
|||||||
<AppIcon name="redo" />
|
<AppIcon name="redo" />
|
||||||
<span>{{ t('common.resetPrinter') }}</span>
|
<span>{{ t('common.resetPrinter') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="m-tool-btn" @click="onReject">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="m-tool-btn"
|
||||||
|
:disabled="!configStore.rejectApiAvailable"
|
||||||
|
:title="configStore.rejectApiAvailable ? t('common.discardCard') : t('notify.rejectUnavailable')"
|
||||||
|
@click="onReject"
|
||||||
|
>
|
||||||
<AppIcon name="trash" />
|
<AppIcon name="trash" />
|
||||||
<span>{{ t('common.discardCard') }}</span>
|
<span>{{ t('common.discardCard') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="m-tool-row">
|
<div class="m-tool-versus">
|
||||||
<button type="button" class="m-tool-btn m-tool-btn--half" @click="onReadCard">
|
<button
|
||||||
<AppIcon name="id-card" />
|
type="button"
|
||||||
|
class="m-versus-half m-versus-half--read"
|
||||||
|
:title="t('home.readCard')"
|
||||||
|
@click="onReadCard"
|
||||||
|
>
|
||||||
|
<AppIcon name="id-card" size="sm" />
|
||||||
<span>{{ t('home.readCard') }}</span>
|
<span>{{ t('home.readCard') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="m-tool-btn m-tool-btn--half" @click="onEjectCard">
|
<button
|
||||||
<AppIcon name="eject" />
|
type="button"
|
||||||
|
class="m-versus-half m-versus-half--eject"
|
||||||
|
:title="t('home.ejectCard')"
|
||||||
|
@click="onEjectCard"
|
||||||
|
>
|
||||||
<span>{{ t('home.ejectCard') }}</span>
|
<span>{{ t('home.ejectCard') }}</span>
|
||||||
|
<AppIcon name="eject" size="sm" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="m-tool-btn" @click="onTemplate">
|
<button type="button" class="m-tool-btn" @click="onTemplate">
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user