完善分发收集续做、打印机状态查询与任务字段对齐
- 完成/失败态统一卡位13自动续做,修复主进程失败时 mode 被重置导致续做被拒 - 接入 SAPI_PrinterCheckstatus,Header 轻量刷状态,Init 后一次 GetPrinterInfo - print_flag 取自 soon.flag,path_file 目录级 staging,加密狗与格式化选项对齐 - 失败页并入运行页,移除独立 DistributeFailedView Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { CS_FAIL, CS_OK } from '../constants'
|
|||||||
import { assertNotBusy, assertReady, mainAppState } from '../services/app-state'
|
import { assertNotBusy, assertReady, mainAppState } from '../services/app-state'
|
||||||
import { configStore } from '../services/config-store'
|
import { configStore } from '../services/config-store'
|
||||||
import { startUsbPoll, startJobPoll, stopAllPolls, stopJobPoll, stopUsbPoll, stopCardPositionPoll, startCardPositionPoll, getPollMainWindow, isJobPollActive } from '../services/poll-manager'
|
import { startUsbPoll, startJobPoll, stopAllPolls, stopJobPoll, stopUsbPoll, stopCardPositionPoll, startCardPositionPoll, getPollMainWindow, isJobPollActive } from '../services/poll-manager'
|
||||||
import { parsePrinterInfoFromDll, type PrinterStatusSnapshot } from '@shared/printer-info'
|
import { parsePrinterInfoFromDll, statusTextFromCheckstatus, type PrinterStatusSnapshot } from '@shared/printer-info'
|
||||||
import { cleanPathPattern, getDirectorySizeBytes } from '../utils/dir-size'
|
import { cleanPathPattern, getDirectorySizeBytes } from '../utils/dir-size'
|
||||||
import { getDesignAppPath } from '../services/app-config'
|
import { getDesignAppPath } from '../services/app-config'
|
||||||
import { openDesignApp } from '../services/open-design-app'
|
import { openDesignApp } from '../services/open-design-app'
|
||||||
@@ -83,6 +83,50 @@ export function registerIpcHandlers(): void {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tracedHandle('dll:printer-status', async () => {
|
||||||
|
if (!mainAppState.initialized) {
|
||||||
|
const cached = configStore.get('lastPrinterStatus')
|
||||||
|
return ok({
|
||||||
|
statusText: cached?.statusText ?? '未初始化',
|
||||||
|
fromCache: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
assertReady()
|
||||||
|
const dll = await loadDllModule()
|
||||||
|
if (!dll.isPrinterCheckstatusApiAvailable()) {
|
||||||
|
return fail(CS_FAIL, '当前 DLL 不支持打印机状态查询')
|
||||||
|
}
|
||||||
|
const code = dll.dllPrinterCheckstatus()
|
||||||
|
const parsed = statusTextFromCheckstatus(code)
|
||||||
|
const cached = configStore.get('lastPrinterStatus')
|
||||||
|
if (parsed.ok) {
|
||||||
|
const snapshot: PrinterStatusSnapshot = {
|
||||||
|
ribbonType: cached?.ribbonType ?? '—',
|
||||||
|
statusText: parsed.statusText,
|
||||||
|
serialNo: cached?.serialNo ?? '—',
|
||||||
|
printedCount: cached?.printedCount ?? 0
|
||||||
|
}
|
||||||
|
configStore.set('lastPrinterStatus', snapshot)
|
||||||
|
return ok({ statusText: parsed.statusText, statusCode: code })
|
||||||
|
}
|
||||||
|
if (cached) {
|
||||||
|
return ok({
|
||||||
|
statusText: parsed.statusText,
|
||||||
|
fromCache: true,
|
||||||
|
liveError: parsed.liveError
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return fail(code, parsed.liveError || parsed.statusText)
|
||||||
|
} catch (err) {
|
||||||
|
const cached = configStore.get('lastPrinterStatus')
|
||||||
|
if (cached) {
|
||||||
|
return ok({ statusText: cached.statusText, fromCache: true, liveError: String(err) })
|
||||||
|
}
|
||||||
|
return fail(CS_FAIL, String(err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
tracedHandle('dll:printer-reset', async () => {
|
tracedHandle('dll:printer-reset', async () => {
|
||||||
try {
|
try {
|
||||||
assertReady()
|
assertReady()
|
||||||
|
|||||||
@@ -95,14 +95,12 @@ export function startJobPoll(id: string): void {
|
|||||||
log.warn('GetJobStateById query failed', r.queryErrorCode)
|
log.warn('GetJobStateById query failed', r.queryErrorCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (failed || cancelled) {
|
if (failed || cancelled || finished) {
|
||||||
stopJobPoll(true)
|
|
||||||
} else if (finished) {
|
|
||||||
stopJobPoll(false)
|
stopJobPoll(false)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error('job poll error', e)
|
log.error('job poll error', e)
|
||||||
stopJobPoll(true)
|
stopJobPoll(false)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}, POLL_INTERVAL_MS)
|
}, POLL_INTERVAL_MS)
|
||||||
@@ -115,7 +113,7 @@ export function startUsbPoll(): void {
|
|||||||
usbTimer = setInterval(() => {
|
usbTimer = setInterval(() => {
|
||||||
void pollUsbOnce(gen).catch((e) => {
|
void pollUsbOnce(gen).catch((e) => {
|
||||||
log.error('usb poll error', e)
|
log.error('usb poll error', e)
|
||||||
stopUsbPoll(true)
|
stopUsbPoll(false)
|
||||||
})
|
})
|
||||||
}, POLL_INTERVAL_MS)
|
}, POLL_INTERVAL_MS)
|
||||||
}
|
}
|
||||||
@@ -156,21 +154,24 @@ async function pollUsbOnce(gen: number): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function pollCardPositionOnce(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const dll = await loadDllModule()
|
||||||
|
if (!dll.isCardPositionApiAvailable()) return
|
||||||
|
const r = dll.dllGetPrinterCardPosition()
|
||||||
|
const tick = { queryCode: r.queryCode, position: r.position }
|
||||||
|
emitTrace('[poll] card:position-tick', tick)
|
||||||
|
send('card:position-tick', tick)
|
||||||
|
} catch (e) {
|
||||||
|
log.error('card position poll error', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function startCardPositionPoll(): void {
|
export function startCardPositionPoll(): void {
|
||||||
stopCardPositionPoll()
|
stopCardPositionPoll()
|
||||||
|
void pollCardPositionOnce()
|
||||||
cardTimer = setInterval(() => {
|
cardTimer = setInterval(() => {
|
||||||
void (async () => {
|
void pollCardPositionOnce()
|
||||||
try {
|
|
||||||
const dll = await loadDllModule()
|
|
||||||
if (!dll.isCardPositionApiAvailable()) return
|
|
||||||
const r = dll.dllGetPrinterCardPosition()
|
|
||||||
const tick = { queryCode: r.queryCode, position: r.position }
|
|
||||||
emitTrace('[poll] card:position-tick', tick)
|
|
||||||
send('card:position-tick', tick)
|
|
||||||
} catch (e) {
|
|
||||||
log.error('card position poll error', e)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}, POLL_INTERVAL_MS)
|
}, POLL_INTERVAL_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,9 +47,12 @@ 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_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
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let SAPI_UploadFile: any = null
|
let SAPI_UploadFile: any = null
|
||||||
let hasRejectApi = false
|
let hasRejectApi = false
|
||||||
let hasCardPositionApi = false
|
let hasCardPositionApi = false
|
||||||
|
let hasCheckstatusApi = false
|
||||||
let hasCancelApi = false
|
let hasCancelApi = false
|
||||||
let hasUploadApi = false
|
let hasUploadApi = false
|
||||||
let hasPrinterInfoEx = false
|
let hasPrinterInfoEx = false
|
||||||
@@ -179,13 +182,23 @@ function loadLibrary(): void {
|
|||||||
emitTrace('[dll] SAPI_GetPrinterCardPosition not in workDll (optional)')
|
emitTrace('[dll] SAPI_GetPrinterCardPosition not in workDll (optional)')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
SAPI_PrinterCheckstatus = lib.func('int __stdcall SAPI_PrinterCheckstatus()')
|
||||||
|
hasCheckstatusApi = true
|
||||||
|
} catch {
|
||||||
|
SAPI_PrinterCheckstatus = null
|
||||||
|
hasCheckstatusApi = false
|
||||||
|
emitTrace('[dll] SAPI_PrinterCheckstatus not in workDll (optional)')
|
||||||
|
}
|
||||||
|
|
||||||
log.info('workDll loaded', {
|
log.info('workDll loaded', {
|
||||||
upload: hasUploadApi,
|
upload: hasUploadApi,
|
||||||
printerInfoEx: hasPrinterInfoEx,
|
printerInfoEx: hasPrinterInfoEx,
|
||||||
cancel: hasCancelApi,
|
cancel: hasCancelApi,
|
||||||
reject: hasRejectApi,
|
reject: hasRejectApi,
|
||||||
usbReader: hasUsbReaderApi,
|
usbReader: hasUsbReaderApi,
|
||||||
cardPosition: hasCardPositionApi
|
cardPosition: hasCardPositionApi,
|
||||||
|
checkstatus: hasCheckstatusApi
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +227,11 @@ export function isCardPositionApiAvailable(): boolean {
|
|||||||
return hasCardPositionApi
|
return hasCardPositionApi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPrinterCheckstatusApiAvailable(): boolean {
|
||||||
|
loadLibrary()
|
||||||
|
return hasCheckstatusApi
|
||||||
|
}
|
||||||
|
|
||||||
export function dllInit(params: InitParams): number {
|
export function dllInit(params: InitParams): number {
|
||||||
return traceCall(
|
return traceCall(
|
||||||
'SAPI_Init',
|
'SAPI_Init',
|
||||||
@@ -336,7 +354,6 @@ export function dllCopyFromUsb(destFolder: string, cardOutput: number): number {
|
|||||||
export function dllGetUsbCopyState(): {
|
export function dllGetUsbCopyState(): {
|
||||||
queryCode: number
|
queryCode: number
|
||||||
taskStatus: number
|
taskStatus: number
|
||||||
/** copy_progress 0-100 */
|
|
||||||
progress: number
|
progress: number
|
||||||
} {
|
} {
|
||||||
return traceCall('SAPI_GetUsbCopyState', undefined, () => {
|
return traceCall('SAPI_GetUsbCopyState', undefined, () => {
|
||||||
@@ -384,3 +401,11 @@ export function dllGetPrinterCardPosition(): { queryCode: number; position: numb
|
|||||||
return { queryCode, position: position[0] }
|
return { queryCode, position: position[0] }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dllPrinterCheckstatus(): number {
|
||||||
|
return traceCall('SAPI_PrinterCheckstatus', undefined, () => {
|
||||||
|
loadLibrary()
|
||||||
|
if (!SAPI_PrinterCheckstatus) throw new Error('CHECKSTATUS_API_UNAVAILABLE')
|
||||||
|
return SAPI_PrinterCheckstatus!() as number
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,10 +11,20 @@ export interface ParsedSoonTemplate {
|
|||||||
frontImageUrl: string
|
frontImageUrl: string
|
||||||
backImageUrl: string
|
backImageUrl: string
|
||||||
fields: TemplateFieldRow[]
|
fields: TemplateFieldRow[]
|
||||||
|
printFlag: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const SOON_FIELD_TYPES = new Set([1, 3, 4, 5])
|
const SOON_FIELD_TYPES = new Set([1, 3, 4, 5])
|
||||||
|
|
||||||
|
export function readSoonPrintFlag(raw: Record<string, unknown>): number {
|
||||||
|
const flag = Number(raw.flag)
|
||||||
|
if (flag === 1 || flag === 2) return flag
|
||||||
|
const hasBack =
|
||||||
|
!!String(raw.backDisplayPic ?? '').trim() ||
|
||||||
|
(Array.isArray(raw.backData) && raw.backData.length > 0)
|
||||||
|
return hasBack ? 2 : 1
|
||||||
|
}
|
||||||
|
|
||||||
function pickArray(obj: Record<string, unknown>, key: string): Record<string, unknown>[] {
|
function pickArray(obj: Record<string, unknown>, key: string): Record<string, unknown>[] {
|
||||||
const entry = Object.entries(obj).find(([k]) => k.toLowerCase() === key.toLowerCase())
|
const entry = Object.entries(obj).find(([k]) => k.toLowerCase() === key.toLowerCase())
|
||||||
if (!Array.isArray(entry?.[1])) return []
|
if (!Array.isArray(entry?.[1])) return []
|
||||||
@@ -83,7 +93,8 @@ function parseSoonWorkerDisk(soonPath: string, raw: Record<string, unknown>): Pa
|
|||||||
return {
|
return {
|
||||||
frontImageUrl: toImageUrl(soonPath, frontPic),
|
frontImageUrl: toImageUrl(soonPath, frontPic),
|
||||||
backImageUrl: toImageUrl(soonPath, backPic),
|
backImageUrl: toImageUrl(soonPath, backPic),
|
||||||
fields
|
fields,
|
||||||
|
printFlag: readSoonPrintFlag(raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +130,7 @@ function parseSoonLegacy(soonPath: string, raw: Record<string, unknown>): Parsed
|
|||||||
fields.push({ label: toFieldLabel(name, side), value, originName: name })
|
fields.push({ label: toFieldLabel(name, side), value, originName: name })
|
||||||
})
|
})
|
||||||
|
|
||||||
return { frontImageUrl, backImageUrl, fields }
|
return { frontImageUrl, backImageUrl, fields, printFlag: readSoonPrintFlag(raw) }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseSoonTemplate(soonPath: string, raw: Record<string, unknown>): ParsedSoonTemplate {
|
export function parseSoonTemplate(soonPath: string, raw: Record<string, unknown>): ParsedSoonTemplate {
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ function stageSoonAssets(taskDir: string, soonSrc: string, soonDest: string): vo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保留目录级 path_file,不展开为单文件列表 */
|
|
||||||
function normalizeCopyPaths(payload: Record<string, unknown>): void {
|
function normalizeCopyPaths(payload: Record<string, unknown>): void {
|
||||||
if (!Array.isArray(payload.path_file)) {
|
if (!Array.isArray(payload.path_file)) {
|
||||||
throw new Error('拷贝任务缺少 path_file')
|
throw new Error('拷贝任务缺少 path_file')
|
||||||
@@ -87,7 +86,6 @@ function normalizeCopyPaths(payload: Record<string, unknown>): void {
|
|||||||
payload.path_file = normalized
|
payload.path_file = normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
/** staging 后按 has_* 裁剪字段,并校验 RestJobEx 必填项 */
|
|
||||||
function finalizeRestJobPayload(payload: Record<string, unknown>): void {
|
function finalizeRestJobPayload(payload: Record<string, unknown>): void {
|
||||||
const hasCopy = payload.has_copy_task === true
|
const hasCopy = payload.has_copy_task === true
|
||||||
const hasPrint = payload.has_print_task === true
|
const hasPrint = payload.has_print_task === true
|
||||||
@@ -119,7 +117,6 @@ function finalizeRestJobPayload(payload: Record<string, unknown>): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通过 SAPI_UploadFile + 本地落盘,准备 RestJobEx 所需路径 */
|
|
||||||
export function stageJobPayloadJson(
|
export function stageJobPayloadJson(
|
||||||
json: string,
|
json: string,
|
||||||
sharedDir: string,
|
sharedDir: string,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const channels = {
|
|||||||
invoke: [
|
invoke: [
|
||||||
'dll:init',
|
'dll:init',
|
||||||
'dll:printer-info',
|
'dll:printer-info',
|
||||||
|
'dll:printer-status',
|
||||||
'dll:printer-reset',
|
'dll:printer-reset',
|
||||||
'dll:printer-reject',
|
'dll:printer-reject',
|
||||||
'dll:printer-error-str',
|
'dll:printer-error-str',
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ export async function dllPrinterInfo(): Promise<IpcResult<Record<string, unknown
|
|||||||
return api().invoke('dll:printer-info') as Promise<IpcResult<Record<string, unknown>>>
|
return api().invoke('dll:printer-info') as Promise<IpcResult<Record<string, unknown>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function dllPrinterStatus(): Promise<
|
||||||
|
IpcResult<{ statusText: string; statusCode?: number; fromCache?: boolean; liveError?: string }>
|
||||||
|
> {
|
||||||
|
return api().invoke('dll:printer-status') as Promise<
|
||||||
|
IpcResult<{ statusText: string; statusCode?: number; fromCache?: boolean; liveError?: string }>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
export function parsePrinterInfo(json: Record<string, unknown>): PrinterStatusDisplay {
|
export function parsePrinterInfo(json: Record<string, unknown>): PrinterStatusDisplay {
|
||||||
return parsePrinterInfoFromDll(json)
|
return parsePrinterInfoFromDll(json)
|
||||||
}
|
}
|
||||||
@@ -119,6 +127,7 @@ export async function fsParseSoon(filePath: string): Promise<
|
|||||||
frontImageUrl: string
|
frontImageUrl: string
|
||||||
backImageUrl: string
|
backImageUrl: string
|
||||||
fields: { label: string; value: string; originName: string }[]
|
fields: { label: string; value: string; originName: string }[]
|
||||||
|
printFlag: number
|
||||||
}>
|
}>
|
||||||
> {
|
> {
|
||||||
return api().invoke('fs:parse-soon', filePath) as Promise<
|
return api().invoke('fs:parse-soon', filePath) as Promise<
|
||||||
@@ -126,6 +135,7 @@ export async function fsParseSoon(filePath: string): Promise<
|
|||||||
frontImageUrl: string
|
frontImageUrl: string
|
||||||
backImageUrl: string
|
backImageUrl: string
|
||||||
fields: { label: string; value: string; originName: string }[]
|
fields: { label: string; value: string; originName: string }[]
|
||||||
|
printFlag: number
|
||||||
}>
|
}>
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { refreshPrinterHeader } from '@/composables/usePrinterStatus'
|
||||||
|
|
||||||
defineProps<{ mode?: string }>()
|
defineProps<{ mode?: string }>()
|
||||||
|
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
const status = computed(() => configStore.printer)
|
const status = computed(() => configStore.printer)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (appStore.initialized) void refreshPrinterHeader(configStore)
|
||||||
|
})
|
||||||
|
|
||||||
const statusTone = computed(() => {
|
const statusTone = computed(() => {
|
||||||
const t = status.value.statusText
|
const t = status.value.statusText
|
||||||
if (t.includes('未初始化') || t.includes('未连接')) return 'c-status-warn'
|
if (t.includes('未初始化') || t.includes('未连接')) return 'c-status-warn'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { notify } from '@/composables/useNotify'
|
import { notify } from '@/composables/useNotify'
|
||||||
import { applyPrinterPayload, refreshPrinterHeader } from '@/composables/usePrinterStatus'
|
import { applyPrinterPayload, refreshPrinterFullInfo } from '@/composables/usePrinterStatus'
|
||||||
import { configGet, dllInit, dllRejectAvailable } from '@/api/cardsoon'
|
import { configGet, dllInit, dllRejectAvailable } from '@/api/cardsoon'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
@@ -43,7 +43,7 @@ export function useAppBootstrap(): void {
|
|||||||
appStore.setInitialized(true)
|
appStore.setInitialized(true)
|
||||||
placeholderStatus(configStore, '就绪')
|
placeholderStatus(configStore, '就绪')
|
||||||
await syncRejectApi()
|
await syncRejectApi()
|
||||||
window.setTimeout(() => void refreshPrinterHeader(configStore), 1500)
|
window.setTimeout(() => void refreshPrinterFullInfo(configStore), 1500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export function useAppBootstrap(): void {
|
|||||||
if (initMeta?.warning) notify.warning(initMeta.warning)
|
if (initMeta?.warning) notify.warning(initMeta.warning)
|
||||||
placeholderStatus(configStore, initMeta?.warning ? '未连接打印机' : '就绪')
|
placeholderStatus(configStore, initMeta?.warning ? '未连接打印机' : '就绪')
|
||||||
await syncRejectApi()
|
await syncRejectApi()
|
||||||
window.setTimeout(() => void refreshPrinterHeader(configStore), 1500)
|
window.setTimeout(() => void refreshPrinterFullInfo(configStore), 1500)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import { dllPrinterInfo, parsePrinterInfo } from '@/api/cardsoon'
|
import { dllPrinterInfo, dllPrinterStatus, parsePrinterInfo } from '@/api/cardsoon'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
import type { PrinterStatusDisplay } from '@/types/printer'
|
import type { PrinterStatusDisplay } from '@/types/printer'
|
||||||
|
|
||||||
|
function isFullPrinterPayload(data: Record<string, unknown>): boolean {
|
||||||
|
return (
|
||||||
|
data.snapshot != null ||
|
||||||
|
data.printerList != null ||
|
||||||
|
data.serial_no != null ||
|
||||||
|
data.SerialNo != null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function applyPrinterPayload(
|
export function applyPrinterPayload(
|
||||||
configStore: ReturnType<typeof useConfigStore>,
|
configStore: ReturnType<typeof useConfigStore>,
|
||||||
data: Record<string, unknown>
|
data: Record<string, unknown>
|
||||||
@@ -11,10 +20,17 @@ export function applyPrinterPayload(
|
|||||||
configStore.setPrinter(snapshot)
|
configStore.setPrinter(snapshot)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (data.printerList != null || data.serial_no != null || data.SerialNo != null) {
|
if (isFullPrinterPayload(data)) {
|
||||||
configStore.setPrinter(parsePrinterInfo(data))
|
configStore.setPrinter(parsePrinterInfo(data))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (typeof data.statusText === 'string') {
|
||||||
|
configStore.setPrinter({
|
||||||
|
...configStore.printer,
|
||||||
|
statusText: data.statusText
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
if (data.fromCache) {
|
if (data.fromCache) {
|
||||||
configStore.setPrinter({
|
configStore.setPrinter({
|
||||||
ribbonType: String(data.ribbonType ?? configStore.printer.ribbonType),
|
ribbonType: String(data.ribbonType ?? configStore.printer.ribbonType),
|
||||||
@@ -27,6 +43,19 @@ export function applyPrinterPayload(
|
|||||||
|
|
||||||
export async function refreshPrinterHeader(
|
export async function refreshPrinterHeader(
|
||||||
configStore: ReturnType<typeof useConfigStore>
|
configStore: ReturnType<typeof useConfigStore>
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const r = await dllPrinterStatus()
|
||||||
|
if (r.ok && r.data) {
|
||||||
|
applyPrinterPayload(configStore, r.data as Record<string, unknown>)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 无打印机时不阻塞 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshPrinterFullInfo(
|
||||||
|
configStore: ReturnType<typeof useConfigStore>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const info = await dllPrinterInfo()
|
const info = await dllPrinterInfo()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const COPY_TYPE_OPTIONS: SelectOption[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const FORMAT_TYPE_OPTIONS: SelectOption[] = [
|
export const FORMAT_TYPE_OPTIONS: SelectOption[] = [
|
||||||
|
{ label: '不格式化', value: 'none' },
|
||||||
{ label: 'Fat32', value: 'fat32' },
|
{ label: 'Fat32', value: 'fat32' },
|
||||||
{ label: 'exFat', value: 'exfat' },
|
{ label: 'exFat', value: 'exfat' },
|
||||||
{ label: 'NTFS', value: 'ntfs' }
|
{ label: 'NTFS', value: 'ntfs' }
|
||||||
|
|||||||
@@ -1,43 +1,39 @@
|
|||||||
import type { Router } from 'vue-router'
|
import type { Router } from 'vue-router'
|
||||||
|
import { useJobStore } from '@/stores/job'
|
||||||
import { useJobStore } from '@/stores/job'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
import { useAppStore } from '@/stores/app'
|
export function setupRouterGuards(router: Router): void {
|
||||||
|
router.beforeEach((to, from) => {
|
||||||
|
const job = useJobStore()
|
||||||
|
const app = useAppStore()
|
||||||
export function setupRouterGuards(router: Router): void {
|
|
||||||
|
if (to.path === '/distribute/running' && !job.jobId && app.mode !== 'distributing') {
|
||||||
router.beforeEach((to, from) => {
|
return { path: '/distribute/config' }
|
||||||
|
}
|
||||||
const job = useJobStore()
|
|
||||||
|
if (to.path === '/collect/running' && app.mode !== 'usbCopying' && app.mode !== 'ready') {
|
||||||
const app = useAppStore()
|
return { path: '/collect' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (to.path === '/collect' && app.mode === 'distributing') {
|
||||||
if (to.path === '/distribute/running' && !job.jobId && app.mode !== 'distributing') {
|
return { path: '/home' }
|
||||||
|
}
|
||||||
return { path: '/distribute/config' }
|
|
||||||
|
if (app.mode === 'usbCopying' && to.path.startsWith('/distribute')) {
|
||||||
}
|
return { path: '/collect/running' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from.path === '/distribute/running' && app.mode === 'distributing') {
|
||||||
if (to.path === '/distribute/failed') {
|
if (to.path === '/distribute/config') return true
|
||||||
|
return { path: '/distribute/config' }
|
||||||
return { path: '/distribute/config' }
|
}
|
||||||
|
|
||||||
}
|
if (from.path === '/collect/running' && app.mode === 'usbCopying') {
|
||||||
|
const allowed = ['/collect/running', '/collect', '/home']
|
||||||
|
if (!allowed.includes(to.path)) return { path: '/collect/running' }
|
||||||
|
}
|
||||||
if (to.path === '/collect/running' && app.mode !== 'usbCopying' && app.mode !== 'ready') {
|
|
||||||
|
return true
|
||||||
return { path: '/collect' }
|
})
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -16,11 +16,6 @@ const router = createRouter({
|
|||||||
name: 'distribute-running',
|
name: 'distribute-running',
|
||||||
component: () => import('@/views/DistributeRunningView.vue')
|
component: () => import('@/views/DistributeRunningView.vue')
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/distribute/failed',
|
|
||||||
name: 'distribute-failed',
|
|
||||||
component: () => import('@/views/DistributeFailedView.vue')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/collect/running',
|
path: '/collect/running',
|
||||||
name: 'collect-running',
|
name: 'collect-running',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface TemplatePreview {
|
|||||||
frontImageUrl: string
|
frontImageUrl: string
|
||||||
backImageUrl: string
|
backImageUrl: string
|
||||||
fields: TemplateFieldRow[]
|
fields: TemplateFieldRow[]
|
||||||
|
printFlag: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PathListItem {
|
export interface PathListItem {
|
||||||
@@ -24,8 +25,10 @@ export interface DistributeFormState {
|
|||||||
templateFile: string
|
templateFile: string
|
||||||
templatePreview: TemplatePreview | null
|
templatePreview: TemplatePreview | null
|
||||||
copyType: 0 | 1
|
copyType: 0 | 1
|
||||||
formatType: 'fat32' | 'exfat' | 'ntfs'
|
formatType: 'none' | 'fat32' | 'exfat' | 'ntfs'
|
||||||
dongleUnlimited: boolean
|
dongleEnabled: boolean
|
||||||
|
/** 勾选加密狗时有效:0 默认,1-101 为次数(101=不限次数) */
|
||||||
|
dongleInstallCount: number
|
||||||
priority: 'low' | 'mid' | 'high'
|
priority: 'low' | 'mid' | 'high'
|
||||||
ribbonType: 'any' | 'YMCKO' | 'YMCK'
|
ribbonType: 'any' | 'YMCKO' | 'YMCK'
|
||||||
generateIso: boolean
|
generateIso: boolean
|
||||||
@@ -45,7 +48,8 @@ function createDefaultForm(): DistributeFormState {
|
|||||||
templatePreview: null,
|
templatePreview: null,
|
||||||
copyType: 0,
|
copyType: 0,
|
||||||
formatType: 'fat32',
|
formatType: 'fat32',
|
||||||
dongleUnlimited: false,
|
dongleEnabled: false,
|
||||||
|
dongleInstallCount: 0,
|
||||||
priority: 'low',
|
priority: 'low',
|
||||||
ribbonType: 'any',
|
ribbonType: 'any',
|
||||||
generateIso: false,
|
generateIso: false,
|
||||||
|
|||||||
@@ -48,6 +48,11 @@
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.m-panel-toolbar .toolbar-row + .toolbar-row {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar-item {
|
.toolbar-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -76,6 +81,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
@@ -105,22 +112,14 @@
|
|||||||
|
|
||||||
/* 提示文字 */
|
/* 提示文字 */
|
||||||
.m-panel-toolbar .c-checkbox-item .dog-hint {
|
.m-panel-toolbar .c-checkbox-item .dog-hint {
|
||||||
font-size: 8px;
|
font-size: 9px;
|
||||||
color: #adb5bd;
|
color: #adb5bd;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
flex-shrink: 1;
|
flex-shrink: 0;
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.m-panel-toolbar .c-checkbox-item {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 列表业务项 (File Items) */
|
/* 列表业务项 (File Items) */
|
||||||
.m-file-item {
|
.m-file-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface BuildJobOptions {
|
|||||||
udfFile?: string
|
udfFile?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFileForApi(formatType: DistributeFormState['formatType']): string {
|
function formatFileForApi(formatType: Exclude<DistributeFormState['formatType'], 'none'>): string {
|
||||||
switch (formatType) {
|
switch (formatType) {
|
||||||
case 'exfat':
|
case 'exfat':
|
||||||
return 'exFAT'
|
return 'exFAT'
|
||||||
@@ -23,6 +23,7 @@ export function buildJobConfig(
|
|||||||
opts: BuildJobOptions
|
opts: BuildJobOptions
|
||||||
): Record<string, unknown> {
|
): Record<string, unknown> {
|
||||||
const { hasCopy, hasPrint } = resolveJobTasks(form)
|
const { hasCopy, hasPrint } = resolveJobTasks(form)
|
||||||
|
const needFormat = form.formatType !== 'none'
|
||||||
|
|
||||||
const body: Record<string, unknown> = {
|
const body: Record<string, unknown> = {
|
||||||
task_id: opts.taskId,
|
task_id: opts.taskId,
|
||||||
@@ -32,10 +33,13 @@ export function buildJobConfig(
|
|||||||
label: form.volumeLabel || 'DATA_CARD',
|
label: form.volumeLabel || 'DATA_CARD',
|
||||||
file_type: String(form.copyType),
|
file_type: String(form.copyType),
|
||||||
zone_type: form.copyType === 1 ? '1' : '0',
|
zone_type: form.copyType === 1 ? '1' : '0',
|
||||||
need_format: true,
|
need_format: needFormat,
|
||||||
format_file: formatFileForApi(form.formatType),
|
|
||||||
disk_size: '16GB',
|
disk_size: '16GB',
|
||||||
dongle_install_count: form.dongleUnlimited ? 255 : 0
|
dongle_install_count: form.dongleEnabled ? form.dongleInstallCount : -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needFormat) {
|
||||||
|
body.format_file = formatFileForApi(form.formatType as Exclude<DistributeFormState['formatType'], 'none'>)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasCopy) {
|
if (hasCopy) {
|
||||||
@@ -44,7 +48,7 @@ export function buildJobConfig(
|
|||||||
|
|
||||||
if (hasPrint) {
|
if (hasPrint) {
|
||||||
body.json_file = form.templateFile.trim()
|
body.json_file = form.templateFile.trim()
|
||||||
body.print_flag = 1
|
body.print_flag = form.templatePreview?.printFlag ?? 1
|
||||||
const udf = opts.udfFile?.trim()
|
const udf = opts.udfFile?.trim()
|
||||||
if (udf) body.udf_file = udf
|
if (udf) body.udf_file = udf
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ function totalCopyBytes(form: DistributeFormState): number {
|
|||||||
return form.pathList.reduce((sum, item) => sum + (item.sizeBytes || 0), 0)
|
return form.pathList.reduce((sum, item) => sum + (item.sizeBytes || 0), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 提交/续做前:配置 + 路径存在性 + 拷贝体积 */
|
|
||||||
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
|
||||||
@@ -29,5 +28,11 @@ export async function validateJobPreflight(form: DistributeFormState): Promise<s
|
|||||||
return '模板文件不存在'
|
return '模板文件不存在'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (form.dongleEnabled) {
|
||||||
|
const n = form.dongleInstallCount
|
||||||
|
if (!Number.isInteger(n) || n < 0 || n > 101) {
|
||||||
|
return '加密狗次数须为 0 或 1-101'
|
||||||
|
}
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,23 @@
|
|||||||
<AppSelect v-model="formStore.formatType" :items="FORMAT_TYPE_OPTIONS" />
|
<AppSelect v-model="formStore.formatType" :items="FORMAT_TYPE_OPTIONS" />
|
||||||
</div>
|
</div>
|
||||||
<label class="c-checkbox-item">
|
<label class="c-checkbox-item">
|
||||||
<input v-model="formStore.dongleUnlimited" type="checkbox" />
|
<input
|
||||||
<span>不限次数</span>
|
v-model="formStore.dongleEnabled"
|
||||||
|
type="checkbox"
|
||||||
|
@change="onDongleToggle"
|
||||||
|
/>
|
||||||
|
<span>加密狗</span>
|
||||||
|
<input
|
||||||
|
:value="dongleInputValue"
|
||||||
|
type="number"
|
||||||
|
class="c-input dog-count"
|
||||||
|
min="0"
|
||||||
|
max="101"
|
||||||
|
step="1"
|
||||||
|
:disabled="!formStore.dongleEnabled"
|
||||||
|
@input="onDongleCountInput"
|
||||||
|
/>
|
||||||
|
<span class="dog-hint">{{ dongleHint }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,6 +190,31 @@ function onClear(): void {
|
|||||||
notify.info('已清空,已恢复初始状态')
|
notify.info('已清空,已恢复初始状态')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dongleInputValue = computed(() =>
|
||||||
|
formStore.dongleEnabled ? formStore.dongleInstallCount : -1
|
||||||
|
)
|
||||||
|
|
||||||
|
const dongleHint = computed(() =>
|
||||||
|
formStore.dongleEnabled ? '(101 为不限次数)' : ''
|
||||||
|
)
|
||||||
|
|
||||||
|
function clampDongleCount(n: number): number {
|
||||||
|
return Math.min(101, Math.max(0, Math.round(n)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDongleToggle(): void {
|
||||||
|
if (formStore.dongleEnabled) {
|
||||||
|
formStore.dongleInstallCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDongleCountInput(e: Event): void {
|
||||||
|
if (!formStore.dongleEnabled) return
|
||||||
|
const raw = Number((e.target as HTMLInputElement).value)
|
||||||
|
if (!Number.isFinite(raw)) return
|
||||||
|
formStore.dongleInstallCount = clampDongleCount(raw)
|
||||||
|
}
|
||||||
|
|
||||||
async function addPath(): Promise<void> {
|
async function addPath(): Promise<void> {
|
||||||
const r = await dialogOpenDirectory()
|
const r = await dialogOpenDirectory()
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
<template>
|
|
||||||
<AppShell>
|
|
||||||
<AppHeader mode="数据分发模式">
|
|
||||||
<div class="c-nav-group">
|
|
||||||
<NavButton icon="arrow-left" label="返回" @click="onBack" />
|
|
||||||
<NavButton icon="redo" label="重置" variant="primary" @click="onReset" />
|
|
||||||
</div>
|
|
||||||
</AppHeader>
|
|
||||||
<main class="app-shell__main l-main-full">
|
|
||||||
<section class="l-hero-container">
|
|
||||||
<div class="m-left-panel">
|
|
||||||
<div class="c-status-panel">
|
|
||||||
<h2 class="c-status-title is-error">任务失败</h2>
|
|
||||||
<p class="c-status-sub">请检查设备故障后重新插入数据卡</p>
|
|
||||||
<p class="c-status-counter">
|
|
||||||
任务已经完成<span class="ok">{{ jobStore.successCount }}</span
|
|
||||||
>次,其中失败次数是<span class="err">{{ jobStore.failCount }}</span>。
|
|
||||||
</p>
|
|
||||||
<p v-if="errorText" class="m-error-detail">{{ errorText }}</p>
|
|
||||||
</div>
|
|
||||||
<WorkflowSteps mode="failed" :failed-step="3" />
|
|
||||||
</div>
|
|
||||||
<div class="m-right-panel">
|
|
||||||
<div class="m-error-icon">
|
|
||||||
<AppIcon name="warning" size="xl" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<AppFooter />
|
|
||||||
</AppShell>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import AppShell from '@/layouts/AppShell.vue'
|
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
|
||||||
import AppFooter from '@/components/AppFooter.vue'
|
|
||||||
import NavButton from '@/components/NavButton.vue'
|
|
||||||
import AppIcon from '@/components/AppIcon.vue'
|
|
||||||
import WorkflowSteps from '@/components/WorkflowSteps.vue'
|
|
||||||
import { useDistributeFormStore } from '@/stores/distributeForm'
|
|
||||||
import { useJobStore } from '@/stores/job'
|
|
||||||
import { useAppStore } from '@/stores/app'
|
|
||||||
import { dllPrinterErrorStr, pollJobStop } from '@/api/cardsoon'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const formStore = useDistributeFormStore()
|
|
||||||
const jobStore = useJobStore()
|
|
||||||
const appStore = useAppStore()
|
|
||||||
const errorText = ref('')
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
appStore.setMode('ready')
|
|
||||||
await pollJobStop()
|
|
||||||
jobStore.clearActiveJob()
|
|
||||||
try {
|
|
||||||
const r = await dllPrinterErrorStr(-1)
|
|
||||||
errorText.value = r.ok && r.data?.text ? r.data.text : ''
|
|
||||||
} catch {
|
|
||||||
errorText.value = ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function onBack(): void {
|
|
||||||
jobStore.clearActiveJob()
|
|
||||||
router.push('/distribute/config')
|
|
||||||
}
|
|
||||||
|
|
||||||
function onReset(): void {
|
|
||||||
formStore.reset()
|
|
||||||
jobStore.reset()
|
|
||||||
router.push('/home')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style src="@/styles/pages/page3.css"></style>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.m-error-detail {
|
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #dc3545;
|
|
||||||
font-weight: 600;
|
|
||||||
max-width: 320px;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -154,7 +154,9 @@ let usbQueryFailStreak = 0
|
|||||||
const displayProgress = computed(() => Math.round(progress.value))
|
const displayProgress = computed(() => Math.round(progress.value))
|
||||||
|
|
||||||
const failedSub = computed(() =>
|
const failedSub = computed(() =>
|
||||||
isCollect.value ? '请检查读卡器与卡片后重新尝试' : '请检查设备故障后重新插入数据卡'
|
isCollect.value
|
||||||
|
? '请检查读卡器与卡片,插入备卡位可自动重试'
|
||||||
|
: '请检查设备故障,插入备卡位可自动重试'
|
||||||
)
|
)
|
||||||
|
|
||||||
const statusTitle = computed(() => {
|
const statusTitle = computed(() => {
|
||||||
@@ -169,7 +171,9 @@ const statusTitle = computed(() => {
|
|||||||
|
|
||||||
const statusSub = computed(() => {
|
const statusSub = computed(() => {
|
||||||
if (phase.value === 'completed') {
|
if (phase.value === 'completed') {
|
||||||
return '请点击返回,或插入数据卡继续下一张'
|
return isCollect.value
|
||||||
|
? '请点击返回,或插入备卡位继续下一张'
|
||||||
|
: '请点击返回,或插入备卡位自动开始下一张'
|
||||||
}
|
}
|
||||||
if (isCollect.value) {
|
if (isCollect.value) {
|
||||||
return collectHint.value || '正在从卡片读取并写入导入目录'
|
return collectHint.value || '正在从卡片读取并写入导入目录'
|
||||||
@@ -192,7 +196,6 @@ function clearPollListeners(): void {
|
|||||||
unsubCard = null
|
unsubCard = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 停止主进程轮询;resetMode=false 时保留 usbCopying/distributing 会话(完成态续做) */
|
|
||||||
async function releasePolls(resetMode: boolean): Promise<void> {
|
async function releasePolls(resetMode: boolean): Promise<void> {
|
||||||
clearPollListeners()
|
clearPollListeners()
|
||||||
await pollCardPositionStop()
|
await pollCardPositionStop()
|
||||||
@@ -203,48 +206,72 @@ async function releasePolls(resetMode: boolean): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enterFailedPhase(errorText = ''): Promise<void> {
|
async function stopTaskPollsOnly(): Promise<void> {
|
||||||
if (phase.value === 'failed') return
|
unsubJob?.()
|
||||||
phase.value = 'failed'
|
unsubJob = null
|
||||||
failureStep.value = workflowStep.value
|
unsubUsb?.()
|
||||||
failureErrorText.value = errorText
|
unsubUsb = null
|
||||||
if (!failureErrorText.value && !isCollect.value) {
|
|
||||||
try {
|
|
||||||
const r = await dllPrinterErrorStr(-1)
|
|
||||||
failureErrorText.value = r.ok && r.data?.text ? r.data.text : ''
|
|
||||||
} catch {
|
|
||||||
failureErrorText.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await releasePolls(true)
|
|
||||||
jobStore.clearActiveJob()
|
|
||||||
appStore.setMode('ready')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function enterCompletedPhase(): Promise<void> {
|
|
||||||
if (phase.value === 'completed') return
|
|
||||||
phase.value = 'completed'
|
|
||||||
setProgress(100)
|
|
||||||
if (isCollect.value) {
|
if (isCollect.value) {
|
||||||
workflowStep.value = 3
|
|
||||||
await pollUsbStop({ resetMode: false })
|
await pollUsbStop({ resetMode: false })
|
||||||
} else {
|
} else {
|
||||||
workflowStep.value = 4
|
|
||||||
await pollJobStop({ resetMode: false })
|
await pollJobStop({ resetMode: false })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startCardPositionWatch(): Promise<void> {
|
||||||
|
await pollCardPositionStop()
|
||||||
const cardStarted = await pollCardPositionStart()
|
const cardStarted = await pollCardPositionStart()
|
||||||
if (!cardStarted.ok) {
|
if (!cardStarted.ok) {
|
||||||
notify.warning(cardStarted.message || '无法监控卡位,请手动点击返回或重新提交')
|
notify.warning(cardStarted.message || '无法监控卡位,请手动点击返回或重新提交')
|
||||||
}
|
}
|
||||||
if (!unsubCard) {
|
unsubCard?.()
|
||||||
unsubCard = onCardPositionTick((payload) =>
|
unsubCard = onCardPositionTick((payload) =>
|
||||||
applyCardPosition(payload as CardPositionPollPayload)
|
applyCardPosition(payload as CardPositionPollPayload)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enterWaitPhase(next: 'completed' | 'failed', errorText = ''): Promise<void> {
|
||||||
|
if (phase.value !== next) {
|
||||||
|
phase.value = next
|
||||||
|
if (next === 'completed') {
|
||||||
|
setProgress(100)
|
||||||
|
workflowStep.value = isCollect.value ? 3 : 4
|
||||||
|
} else {
|
||||||
|
failureStep.value = workflowStep.value
|
||||||
|
failureErrorText.value = errorText
|
||||||
|
if (!failureErrorText.value && !isCollect.value) {
|
||||||
|
try {
|
||||||
|
const r = await dllPrinterErrorStr(-1)
|
||||||
|
failureErrorText.value = r.ok && r.data?.text ? r.data.text : ''
|
||||||
|
} catch {
|
||||||
|
failureErrorText.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobStore.clearActiveJob()
|
||||||
|
}
|
||||||
|
} else if (next === 'failed' && errorText) {
|
||||||
|
failureErrorText.value = errorText
|
||||||
}
|
}
|
||||||
|
await stopTaskPollsOnly()
|
||||||
|
appStore.setMode(isCollect.value ? 'usbCopying' : 'distributing')
|
||||||
|
lastCardPosition = -1
|
||||||
|
await startCardPositionWatch()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enterFailedPhase(errorText = ''): Promise<void> {
|
||||||
|
await enterWaitPhase('failed', errorText)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enterCompletedPhase(): Promise<void> {
|
||||||
|
await enterWaitPhase('completed')
|
||||||
|
}
|
||||||
|
|
||||||
|
function canAutoResubmit(): boolean {
|
||||||
|
return phase.value === 'completed' || phase.value === 'failed'
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyCardPosition(p: CardPositionPollPayload): void {
|
function applyCardPosition(p: CardPositionPollPayload): void {
|
||||||
if (phase.value !== 'completed' || resubmitting) return
|
if (!canAutoResubmit() || resubmitting) return
|
||||||
if (p.queryCode !== 0) return
|
if (p.queryCode !== 0) return
|
||||||
const pos = p.position
|
const pos = p.position
|
||||||
const enteredPrepare = pos === POSITION_PREPARE && lastCardPosition !== POSITION_PREPARE
|
const enteredPrepare = pos === POSITION_PREPARE && lastCardPosition !== POSITION_PREPARE
|
||||||
@@ -254,59 +281,66 @@ function applyCardPosition(p: CardPositionPollPayload): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resubmitTask(): Promise<void> {
|
async function resubmitTask(): Promise<void> {
|
||||||
if (resubmitting || phase.value !== 'completed') return
|
if (resubmitting || !canAutoResubmit()) return
|
||||||
|
const fromWaitPhase = phase.value
|
||||||
resubmitting = true
|
resubmitting = true
|
||||||
await pollCardPositionStop()
|
await pollCardPositionStop()
|
||||||
|
unsubCard?.()
|
||||||
|
unsubCard = null
|
||||||
phase.value = 'running'
|
phase.value = 'running'
|
||||||
setProgress(0)
|
setProgress(0)
|
||||||
workflowStep.value = 1
|
workflowStep.value = 1
|
||||||
collectHint.value = ''
|
collectHint.value = ''
|
||||||
waitCard.value = false
|
waitCard.value = false
|
||||||
|
queryFailStreak = 0
|
||||||
|
usbQueryFailStreak = 0
|
||||||
|
|
||||||
|
const backToWait = async (message?: string): Promise<void> => {
|
||||||
|
resubmitting = false
|
||||||
|
if (message) notify.error(message)
|
||||||
|
if (fromWaitPhase === 'failed') {
|
||||||
|
await enterFailedPhase(message || failureErrorText.value)
|
||||||
|
} else {
|
||||||
|
await enterCompletedPhase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isCollect.value) {
|
if (isCollect.value) {
|
||||||
const dest = collectStore.destPath.trim()
|
const dest = collectStore.destPath.trim()
|
||||||
if (!dest) {
|
if (!dest) {
|
||||||
notify.error('导入目录无效,无法继续')
|
await backToWait('导入目录无效,无法继续')
|
||||||
await enterCompletedPhase()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const r = await dllUsbCopy(dest, collectStore.cardOutput, { resubmit: true })
|
const r = await dllUsbCopy(dest, collectStore.cardOutput, { resubmit: true })
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
notify.error(r.message || '重新提交收集任务失败')
|
await backToWait(r.message || '重新提交收集任务失败')
|
||||||
await enterCompletedPhase()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
usbAwaitNewCycle = true
|
usbAwaitNewCycle = true
|
||||||
collectHint.value = usbTaskStatusHint(USB_TASK_PREPARING)
|
collectHint.value = usbTaskStatusHint(USB_TASK_PREPARING)
|
||||||
if (!unsubUsb) {
|
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
|
||||||
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const err = await validateJobPreflight(formStore)
|
const err = await validateJobPreflight(formStore)
|
||||||
if (err) {
|
if (err) {
|
||||||
notify.warning(err)
|
notify.warning(err)
|
||||||
await enterCompletedPhase()
|
await backToWait()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const created = await createDistributeJob(formStore, { resubmit: true })
|
const created = await createDistributeJob(formStore, { resubmit: true })
|
||||||
if (!created.ok) {
|
if (!created.ok) {
|
||||||
notify.error(created.message)
|
await backToWait(created.message)
|
||||||
await enterCompletedPhase()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jobStore.setActiveJob(created.jobId)
|
jobStore.setActiveJob(created.jobId)
|
||||||
const started = await pollJobStart(created.jobId)
|
const started = await pollJobStart(created.jobId)
|
||||||
if (!started.ok) {
|
if (!started.ok) {
|
||||||
notify.error(started.message || '启动任务轮询失败')
|
await backToWait(started.message || '启动任务轮询失败')
|
||||||
await enterCompletedPhase()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!unsubJob) {
|
unsubJob = onJobPollTick((payload) => applyJobProgress(payload as JobPollPayload))
|
||||||
unsubJob = onJobPollTick((payload) => applyJobProgress(payload as JobPollPayload))
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
resubmitting = false
|
resubmitting = false
|
||||||
}
|
}
|
||||||
@@ -354,13 +388,16 @@ function applyUsbProgress(p: UsbPollPayload): void {
|
|||||||
}
|
}
|
||||||
usbQueryFailStreak = 0
|
usbQueryFailStreak = 0
|
||||||
|
|
||||||
|
if (usbAwaitNewCycle && p.queryCode === 0 && p.taskStatus === USB_TASK_PREPARING) {
|
||||||
|
usbAwaitNewCycle = false
|
||||||
|
}
|
||||||
|
|
||||||
if (p.failed) {
|
if (p.failed) {
|
||||||
collectStore.failCount += 1
|
collectStore.failCount += 1
|
||||||
void enterFailedPhase(p.errorMessage || usbTaskStatusHint(p.taskStatus))
|
void enterFailedPhase(p.errorMessage || usbTaskStatusHint(p.taskStatus))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (p.success) {
|
if (p.success) {
|
||||||
if (usbAwaitNewCycle) return
|
|
||||||
collectStore.successCount += 1
|
collectStore.successCount += 1
|
||||||
workflowStep.value = 3
|
workflowStep.value = 3
|
||||||
collectHint.value = usbTaskStatusHint(p.taskStatus)
|
collectHint.value = usbTaskStatusHint(p.taskStatus)
|
||||||
@@ -373,9 +410,6 @@ function applyUsbProgress(p: UsbPollPayload): void {
|
|||||||
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)
|
||||||
setProgress(copyProgress)
|
setProgress(copyProgress)
|
||||||
if (usbAwaitNewCycle && p.taskStatus === USB_TASK_PREPARING) {
|
|
||||||
usbAwaitNewCycle = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function finishDistribute(
|
async function finishDistribute(
|
||||||
@@ -384,7 +418,7 @@ async function finishDistribute(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (finishing) return
|
if (finishing) return
|
||||||
finishing = true
|
finishing = true
|
||||||
const skipCancel = phase.value === 'completed'
|
const skipCancel = phase.value === 'completed' || phase.value === 'failed'
|
||||||
await releasePolls(true)
|
await releasePolls(true)
|
||||||
const id = jobStore.jobId
|
const id = jobStore.jobId
|
||||||
if (id && !skipCancel) {
|
if (id && !skipCancel) {
|
||||||
@@ -437,29 +471,36 @@ onMounted(async () => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (finishing || phase.value !== 'running') return
|
if (finishing || phase.value !== 'running') return
|
||||||
void releasePolls(true)
|
void releasePolls(true)
|
||||||
if (!isCollect.value && appStore.mode === 'distributing') {
|
if (isCollect.value && appStore.mode === 'usbCopying') {
|
||||||
|
appStore.setMode('ready')
|
||||||
|
} else if (!isCollect.value && appStore.mode === 'distributing') {
|
||||||
appStore.setMode('ready')
|
appStore.setMode('ready')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function onFailedBack(): Promise<void> {
|
async function leaveFailedPage(redirect: '/collect' | '/distribute/config' | '/home'): Promise<void> {
|
||||||
finishing = true
|
finishing = true
|
||||||
|
await releasePolls(true)
|
||||||
|
appStore.setMode('ready')
|
||||||
|
await router.push(redirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onFailedBack(): Promise<void> {
|
||||||
if (isCollect.value) {
|
if (isCollect.value) {
|
||||||
await router.push('/collect')
|
await leaveFailedPage('/collect')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await router.push('/distribute/config')
|
await leaveFailedPage('/distribute/config')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onFailedReset(): Promise<void> {
|
async function onFailedReset(): Promise<void> {
|
||||||
finishing = true
|
|
||||||
if (isCollect.value) {
|
if (isCollect.value) {
|
||||||
collectStore.reset()
|
collectStore.reset()
|
||||||
} else {
|
} else {
|
||||||
formStore.reset()
|
formStore.reset()
|
||||||
jobStore.reset()
|
jobStore.reset()
|
||||||
}
|
}
|
||||||
await router.push('/home')
|
await leaveFailedPage('/home')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onReturn(): Promise<void> {
|
async function onReturn(): Promise<void> {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { notify, notifyRequireInit } from '@/composables/useNotify'
|
import { notify, notifyRequireInit } from '@/composables/useNotify'
|
||||||
import { refreshPrinterHeader } from '@/composables/usePrinterStatus'
|
import { refreshPrinterHeader } from '@/composables/usePrinterStatus'
|
||||||
@@ -122,10 +122,6 @@ function goCollect(): void {
|
|||||||
if (!guardBusy()) return
|
if (!guardBusy()) return
|
||||||
router.push('/collect')
|
router.push('/collect')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (canUse.value) void refreshPrinterHeader(configStore)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style src="@/styles/pages/page2.css"></style>
|
<style src="@/styles/pages/page2.css"></style>
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
/** workDll 卡位:备卡位,检测到后可自动重提任务 */
|
|
||||||
export const POSITION_PREPARE = 13
|
export const POSITION_PREPARE = 13
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/** RestJobEx JSON 根字段 task_id */
|
|
||||||
export function genTaskId(): string {
|
export function genTaskId(): string {
|
||||||
return `T${Date.now()}`
|
return `T${Date.now()}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,26 @@ function normalizeStatus(raw: unknown): string {
|
|||||||
return PRINTER_STATUS_MAP[s] ?? s
|
return PRINTER_STATUS_MAP[s] ?? s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function statusTextFromCheckstatus(code: number): {
|
||||||
|
ok: boolean
|
||||||
|
statusText: string
|
||||||
|
liveError?: string
|
||||||
|
} {
|
||||||
|
if (code === 0) {
|
||||||
|
return { ok: true, statusText: PRINTER_STATUS_MAP.I ?? '空闲' }
|
||||||
|
}
|
||||||
|
if (code > 0 && code < 128) {
|
||||||
|
const statusText = normalizeStatus(code)
|
||||||
|
if (statusText !== '—') {
|
||||||
|
return { ok: true, statusText }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (code < 0) {
|
||||||
|
return { ok: false, statusText: '未连接打印机', liveError: `code=${code}` }
|
||||||
|
}
|
||||||
|
return { ok: false, statusText: '—', liveError: `code=${code}` }
|
||||||
|
}
|
||||||
|
|
||||||
function snapshotFromRecord(row: Record<string, unknown>): PrinterStatusSnapshot {
|
function snapshotFromRecord(row: Record<string, unknown>): PrinterStatusSnapshot {
|
||||||
const serial = pickFirst(row, [
|
const serial = pickFirst(row, [
|
||||||
'serial_no',
|
'serial_no',
|
||||||
@@ -65,7 +85,6 @@ function snapshotFromRecord(row: Record<string, unknown>): PrinterStatusSnapshot
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** SAPI_GetPrinterInfoEx(扁平 JSON)与 SAPI_GetPrinterInfo(printerList) */
|
|
||||||
export function parsePrinterInfoFromDll(json: Record<string, unknown>): PrinterStatusSnapshot {
|
export function parsePrinterInfoFromDll(json: Record<string, unknown>): PrinterStatusSnapshot {
|
||||||
const flatSerial = pickFirst(json, ['serial_no', 'SerialNo', 'serialNo', 'szPrinterSerial'])
|
const flatSerial = pickFirst(json, ['serial_no', 'SerialNo', 'serialNo', 'szPrinterSerial'])
|
||||||
const flatStatus = pickFirst(json, ['printer_status', 'PrinterStatus'])
|
const flatStatus = pickFirst(json, ['printer_status', 'PrinterStatus'])
|
||||||
|
|||||||
Reference in New Issue
Block a user