完善分发收集续做、打印机状态查询与任务字段对齐

- 完成/失败态统一卡位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:
24kycj
2026-06-02 12:16:03 +08:00
parent ece0a06a80
commit 3168daedf4
24 changed files with 389 additions and 255 deletions
+45 -1
View File
@@ -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()
+18 -17
View File
@@ -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)
} }
+27 -2
View File
@@ -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
})
}
+13 -2
View File
@@ -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 {
-3
View File
@@ -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,
+1
View File
@@ -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',
+10
View File
@@ -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' }
-4
View File
@@ -11,10 +11,6 @@ export function setupRouterGuards(router: Router): void {
return { path: '/distribute/config' } return { path: '/distribute/config' }
} }
const app = useAppStore()
if (to.path === '/collect/running' && app.mode !== 'usbCopying' && app.mode !== 'ready') { if (to.path === '/collect/running' && app.mode !== 'usbCopying' && app.mode !== 'ready') {
return { path: '/collect' } return { path: '/collect' }
} }
-5
View File
@@ -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,
+9 -10
View File
@@ -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;
+9 -5
View File
@@ -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> {
+1 -5
View File
@@ -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
View File
@@ -1,2 +1 @@
/** workDll 卡位:备卡位,检测到后可自动重提任务 */
export const POSITION_PREPARE = 13 export const POSITION_PREPARE = 13
-1
View File
@@ -1,4 +1,3 @@
/** RestJobEx JSON 根字段 task_id */
export function genTaskId(): string { export function genTaskId(): string {
return `T${Date.now()}` return `T${Date.now()}`
} }
+20 -1
View File
@@ -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_GetPrinterInfoprinterList */
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'])