Compare commits

...

2 Commits

Author SHA1 Message Date
24kycj 356e6a3d19 更新 2026-06-02 12:18:09 +08:00
24kycj 3168daedf4 完善分发收集续做、打印机状态查询与任务字段对齐
- 完成/失败态统一卡位13自动续做,修复主进程失败时 mode 被重置导致续做被拒
- 接入 SAPI_PrinterCheckstatus,Header 轻量刷状态,Init 后一次 GetPrinterInfo
- print_flag 取自 soon.flag,path_file 目录级 staging,加密狗与格式化选项对齐
- 失败页并入运行页,移除独立 DistributeFailedView

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 12:16:03 +08:00
25 changed files with 390 additions and 256 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 { configStore } from '../services/config-store'
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 { getDesignAppPath } from '../services/app-config'
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 () => {
try {
assertReady()
+18 -17
View File
@@ -95,14 +95,12 @@ export function startJobPoll(id: string): void {
log.warn('GetJobStateById query failed', r.queryErrorCode)
return
}
if (failed || cancelled) {
stopJobPoll(true)
} else if (finished) {
if (failed || cancelled || finished) {
stopJobPoll(false)
}
} catch (e) {
log.error('job poll error', e)
stopJobPoll(true)
stopJobPoll(false)
}
})()
}, POLL_INTERVAL_MS)
@@ -115,7 +113,7 @@ export function startUsbPoll(): void {
usbTimer = setInterval(() => {
void pollUsbOnce(gen).catch((e) => {
log.error('usb poll error', e)
stopUsbPoll(true)
stopUsbPoll(false)
})
}, 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 {
stopCardPositionPoll()
void pollCardPositionOnce()
cardTimer = setInterval(() => {
void (async () => {
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)
}
})()
void pollCardPositionOnce()
}, 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
let SAPI_GetPrinterCardPosition: any = null
// 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 hasRejectApi = false
let hasCardPositionApi = false
let hasCheckstatusApi = false
let hasCancelApi = false
let hasUploadApi = false
let hasPrinterInfoEx = false
@@ -179,13 +182,23 @@ function loadLibrary(): void {
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', {
upload: hasUploadApi,
printerInfoEx: hasPrinterInfoEx,
cancel: hasCancelApi,
reject: hasRejectApi,
usbReader: hasUsbReaderApi,
cardPosition: hasCardPositionApi
cardPosition: hasCardPositionApi,
checkstatus: hasCheckstatusApi
})
}
@@ -214,6 +227,11 @@ export function isCardPositionApiAvailable(): boolean {
return hasCardPositionApi
}
export function isPrinterCheckstatusApiAvailable(): boolean {
loadLibrary()
return hasCheckstatusApi
}
export function dllInit(params: InitParams): number {
return traceCall(
'SAPI_Init',
@@ -336,7 +354,6 @@ export function dllCopyFromUsb(destFolder: string, cardOutput: number): number {
export function dllGetUsbCopyState(): {
queryCode: number
taskStatus: number
/** copy_progress 0-100 */
progress: number
} {
return traceCall('SAPI_GetUsbCopyState', undefined, () => {
@@ -384,3 +401,11 @@ export function dllGetPrinterCardPosition(): { queryCode: number; position: numb
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
backImageUrl: string
fields: TemplateFieldRow[]
printFlag: number
}
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>[] {
const entry = Object.entries(obj).find(([k]) => k.toLowerCase() === key.toLowerCase())
if (!Array.isArray(entry?.[1])) return []
@@ -83,7 +93,8 @@ function parseSoonWorkerDisk(soonPath: string, raw: Record<string, unknown>): Pa
return {
frontImageUrl: toImageUrl(soonPath, frontPic),
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 })
})
return { frontImageUrl, backImageUrl, fields }
return { frontImageUrl, backImageUrl, fields, printFlag: readSoonPrintFlag(raw) }
}
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 {
if (!Array.isArray(payload.path_file)) {
throw new Error('拷贝任务缺少 path_file')
@@ -87,7 +86,6 @@ function normalizeCopyPaths(payload: Record<string, unknown>): void {
payload.path_file = normalized
}
/** staging 后按 has_* 裁剪字段,并校验 RestJobEx 必填项 */
function finalizeRestJobPayload(payload: Record<string, unknown>): void {
const hasCopy = payload.has_copy_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(
json: string,
sharedDir: string,
+1
View File
@@ -4,6 +4,7 @@ const channels = {
invoke: [
'dll:init',
'dll:printer-info',
'dll:printer-status',
'dll:printer-reset',
'dll:printer-reject',
'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>>>
}
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 {
return parsePrinterInfoFromDll(json)
}
@@ -119,6 +127,7 @@ export async function fsParseSoon(filePath: string): Promise<
frontImageUrl: string
backImageUrl: string
fields: { label: string; value: string; originName: string }[]
printFlag: number
}>
> {
return api().invoke('fs:parse-soon', filePath) as Promise<
@@ -126,6 +135,7 @@ export async function fsParseSoon(filePath: string): Promise<
frontImageUrl: string
backImageUrl: string
fields: { label: string; value: string; originName: string }[]
printFlag: number
}>
>
}
@@ -23,14 +23,21 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onMounted } from 'vue'
import { useConfigStore } from '@/stores/config'
import { useAppStore } from '@/stores/app'
import { refreshPrinterHeader } from '@/composables/usePrinterStatus'
defineProps<{ mode?: string }>()
const configStore = useConfigStore()
const appStore = useAppStore()
const status = computed(() => configStore.printer)
onMounted(() => {
if (appStore.initialized) void refreshPrinterHeader(configStore)
})
const statusTone = computed(() => {
const t = status.value.statusText
if (t.includes('未初始化') || t.includes('未连接')) return 'c-status-warn'
@@ -1,6 +1,6 @@
import { onMounted } from 'vue'
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 { useAppStore } from '@/stores/app'
import { useConfigStore } from '@/stores/config'
@@ -43,7 +43,7 @@ export function useAppBootstrap(): void {
appStore.setInitialized(true)
placeholderStatus(configStore, '就绪')
await syncRejectApi()
window.setTimeout(() => void refreshPrinterHeader(configStore), 1500)
window.setTimeout(() => void refreshPrinterFullInfo(configStore), 1500)
return
}
@@ -62,7 +62,7 @@ export function useAppBootstrap(): void {
if (initMeta?.warning) notify.warning(initMeta.warning)
placeholderStatus(configStore, initMeta?.warning ? '未连接打印机' : '就绪')
await syncRejectApi()
window.setTimeout(() => void refreshPrinterHeader(configStore), 1500)
window.setTimeout(() => void refreshPrinterFullInfo(configStore), 1500)
}
onMounted(() => {
@@ -1,7 +1,16 @@
import { dllPrinterInfo, parsePrinterInfo } from '@/api/cardsoon'
import { dllPrinterInfo, dllPrinterStatus, parsePrinterInfo } from '@/api/cardsoon'
import { useConfigStore } from '@/stores/config'
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(
configStore: ReturnType<typeof useConfigStore>,
data: Record<string, unknown>
@@ -11,10 +20,17 @@ export function applyPrinterPayload(
configStore.setPrinter(snapshot)
return
}
if (data.printerList != null || data.serial_no != null || data.SerialNo != null) {
if (isFullPrinterPayload(data)) {
configStore.setPrinter(parsePrinterInfo(data))
return
}
if (typeof data.statusText === 'string') {
configStore.setPrinter({
...configStore.printer,
statusText: data.statusText
})
return
}
if (data.fromCache) {
configStore.setPrinter({
ribbonType: String(data.ribbonType ?? configStore.printer.ribbonType),
@@ -27,6 +43,19 @@ export function applyPrinterPayload(
export async function refreshPrinterHeader(
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> {
try {
const info = await dllPrinterInfo()
@@ -9,6 +9,7 @@ export const COPY_TYPE_OPTIONS: SelectOption[] = [
]
export const FORMAT_TYPE_OPTIONS: SelectOption[] = [
{ label: '不格式化', value: 'none' },
{ label: 'Fat32', value: 'fat32' },
{ label: 'exFat', value: 'exfat' },
{ label: 'NTFS', value: 'ntfs' }
+38 -42
View File
@@ -1,43 +1,39 @@
import type { Router } from 'vue-router'
import { useJobStore } from '@/stores/job'
import { useAppStore } from '@/stores/app'
export function setupRouterGuards(router: Router): void {
router.beforeEach((to, from) => {
const job = useJobStore()
const app = useAppStore()
if (to.path === '/distribute/running' && !job.jobId && app.mode !== 'distributing') {
return { path: '/distribute/config' }
}
if (to.path === '/distribute/failed') {
return { path: '/distribute/config' }
}
if (to.path === '/collect/running' && app.mode !== 'usbCopying' && app.mode !== 'ready') {
return { path: '/collect' }
}
import type { Router } from 'vue-router'
import { useJobStore } from '@/stores/job'
import { useAppStore } from '@/stores/app'
export function setupRouterGuards(router: Router): void {
router.beforeEach((to, from) => {
const job = useJobStore()
const app = useAppStore()
if (to.path === '/distribute/running' && !job.jobId && app.mode !== 'distributing') {
return { path: '/distribute/config' }
}
if (to.path === '/collect/running' && app.mode !== 'usbCopying' && app.mode !== 'ready') {
return { path: '/collect' }
}
if (to.path === '/collect' && app.mode === 'distributing') {
return { path: '/home' }
}
if (app.mode === 'usbCopying' && to.path.startsWith('/distribute')) {
return { path: '/collect/running' }
}
if (from.path === '/distribute/running' && app.mode === 'distributing') {
if (to.path === '/distribute/config') return true
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' }
}
return true
})
}
-5
View File
@@ -16,11 +16,6 @@ const router = createRouter({
name: 'distribute-running',
component: () => import('@/views/DistributeRunningView.vue')
},
{
path: '/distribute/failed',
name: 'distribute-failed',
component: () => import('@/views/DistributeFailedView.vue')
},
{
path: '/collect/running',
name: 'collect-running',
@@ -10,6 +10,7 @@ export interface TemplatePreview {
frontImageUrl: string
backImageUrl: string
fields: TemplateFieldRow[]
printFlag: number
}
export interface PathListItem {
@@ -24,8 +25,10 @@ export interface DistributeFormState {
templateFile: string
templatePreview: TemplatePreview | null
copyType: 0 | 1
formatType: 'fat32' | 'exfat' | 'ntfs'
dongleUnlimited: boolean
formatType: 'none' | 'fat32' | 'exfat' | 'ntfs'
dongleEnabled: boolean
/** 勾选加密狗时有效:0 默认,1-101 为次数(101=不限次数) */
dongleInstallCount: number
priority: 'low' | 'mid' | 'high'
ribbonType: 'any' | 'YMCKO' | 'YMCK'
generateIso: boolean
@@ -45,7 +48,8 @@ function createDefaultForm(): DistributeFormState {
templatePreview: null,
copyType: 0,
formatType: 'fat32',
dongleUnlimited: false,
dongleEnabled: false,
dongleInstallCount: 0,
priority: 'low',
ribbonType: 'any',
generateIso: false,
+9 -10
View File
@@ -48,6 +48,11 @@
flex-wrap: nowrap;
}
.m-panel-toolbar .toolbar-row + .toolbar-row {
flex-wrap: wrap;
row-gap: 4px;
}
.toolbar-item {
display: flex;
align-items: center;
@@ -76,6 +81,8 @@
display: flex;
align-items: center;
gap: 5px;
flex: 0 1 auto;
min-width: 0;
font-size: 10px;
font-weight: 600;
color: #6c757d;
@@ -105,22 +112,14 @@
/* 提示文字 */
.m-panel-toolbar .c-checkbox-item .dog-hint {
font-size: 8px;
font-size: 9px;
color: #adb5bd;
font-weight: 500;
margin-left: 2px;
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
white-space: nowrap;
}
.m-panel-toolbar .c-checkbox-item {
flex: 1;
min-width: 0;
}
/* 列表业务项 (File Items) */
.m-file-item {
display: flex;
+9 -5
View File
@@ -7,7 +7,7 @@ export interface BuildJobOptions {
udfFile?: string
}
function formatFileForApi(formatType: DistributeFormState['formatType']): string {
function formatFileForApi(formatType: Exclude<DistributeFormState['formatType'], 'none'>): string {
switch (formatType) {
case 'exfat':
return 'exFAT'
@@ -23,6 +23,7 @@ export function buildJobConfig(
opts: BuildJobOptions
): Record<string, unknown> {
const { hasCopy, hasPrint } = resolveJobTasks(form)
const needFormat = form.formatType !== 'none'
const body: Record<string, unknown> = {
task_id: opts.taskId,
@@ -32,10 +33,13 @@ export function buildJobConfig(
label: form.volumeLabel || 'DATA_CARD',
file_type: String(form.copyType),
zone_type: form.copyType === 1 ? '1' : '0',
need_format: true,
format_file: formatFileForApi(form.formatType),
need_format: needFormat,
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) {
@@ -44,7 +48,7 @@ export function buildJobConfig(
if (hasPrint) {
body.json_file = form.templateFile.trim()
body.print_flag = 1
body.print_flag = form.templatePreview?.printFlag ?? 1
const udf = opts.udfFile?.trim()
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)
}
/** 提交/续做前:配置 + 路径存在性 + 拷贝体积 */
export async function validateJobPreflight(form: DistributeFormState): Promise<string | null> {
const err = validateJobConfig(form)
if (err) return err
@@ -29,5 +28,11 @@ export async function validateJobPreflight(form: DistributeFormState): Promise<s
return '模板文件不存在'
}
}
if (form.dongleEnabled) {
const n = form.dongleInstallCount
if (!Number.isInteger(n) || n < 0 || n > 101) {
return '加密狗次数须为 0 或 1-101'
}
}
return null
}
@@ -45,8 +45,23 @@
<AppSelect v-model="formStore.formatType" :items="FORMAT_TYPE_OPTIONS" />
</div>
<label class="c-checkbox-item">
<input v-model="formStore.dongleUnlimited" type="checkbox" />
<span>不限次数</span>
<input
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>
</div>
</div>
@@ -175,6 +190,31 @@ function onClear(): void {
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> {
const r = await dialogOpenDirectory()
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 failedSub = computed(() =>
isCollect.value ? '请检查读卡器与卡片后重新尝试' : '请检查设备故障后重新插入数据卡'
isCollect.value
? '请检查读卡器与卡片,插入备卡位可自动重试'
: '请检查设备故障,插入备卡位可自动重试'
)
const statusTitle = computed(() => {
@@ -169,7 +171,9 @@ const statusTitle = computed(() => {
const statusSub = computed(() => {
if (phase.value === 'completed') {
return '请点击返回,或插入数据卡继续下一张'
return isCollect.value
? '请点击返回,或插入备卡位继续下一张'
: '请点击返回,或插入备卡位自动开始下一张'
}
if (isCollect.value) {
return collectHint.value || '正在从卡片读取并写入导入目录'
@@ -192,7 +196,6 @@ function clearPollListeners(): void {
unsubCard = null
}
/** 停止主进程轮询;resetMode=false 时保留 usbCopying/distributing 会话(完成态续做) */
async function releasePolls(resetMode: boolean): Promise<void> {
clearPollListeners()
await pollCardPositionStop()
@@ -203,48 +206,72 @@ async function releasePolls(resetMode: boolean): Promise<void> {
}
}
async function enterFailedPhase(errorText = ''): Promise<void> {
if (phase.value === 'failed') return
phase.value = 'failed'
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 = ''
}
}
await releasePolls(true)
jobStore.clearActiveJob()
appStore.setMode('ready')
}
async function enterCompletedPhase(): Promise<void> {
if (phase.value === 'completed') return
phase.value = 'completed'
setProgress(100)
async function stopTaskPollsOnly(): Promise<void> {
unsubJob?.()
unsubJob = null
unsubUsb?.()
unsubUsb = null
if (isCollect.value) {
workflowStep.value = 3
await pollUsbStop({ resetMode: false })
} else {
workflowStep.value = 4
await pollJobStop({ resetMode: false })
}
}
async function startCardPositionWatch(): Promise<void> {
await pollCardPositionStop()
const cardStarted = await pollCardPositionStart()
if (!cardStarted.ok) {
notify.warning(cardStarted.message || '无法监控卡位,请手动点击返回或重新提交')
}
if (!unsubCard) {
unsubCard = onCardPositionTick((payload) =>
applyCardPosition(payload as CardPositionPollPayload)
)
unsubCard?.()
unsubCard = onCardPositionTick((payload) =>
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 {
if (phase.value !== 'completed' || resubmitting) return
if (!canAutoResubmit() || resubmitting) return
if (p.queryCode !== 0) return
const pos = p.position
const enteredPrepare = pos === POSITION_PREPARE && lastCardPosition !== POSITION_PREPARE
@@ -254,59 +281,66 @@ function applyCardPosition(p: CardPositionPollPayload): void {
}
async function resubmitTask(): Promise<void> {
if (resubmitting || phase.value !== 'completed') return
if (resubmitting || !canAutoResubmit()) return
const fromWaitPhase = phase.value
resubmitting = true
await pollCardPositionStop()
unsubCard?.()
unsubCard = null
phase.value = 'running'
setProgress(0)
workflowStep.value = 1
collectHint.value = ''
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 {
if (isCollect.value) {
const dest = collectStore.destPath.trim()
if (!dest) {
notify.error('导入目录无效,无法继续')
await enterCompletedPhase()
await backToWait('导入目录无效,无法继续')
return
}
const r = await dllUsbCopy(dest, collectStore.cardOutput, { resubmit: true })
if (!r.ok) {
notify.error(r.message || '重新提交收集任务失败')
await enterCompletedPhase()
await backToWait(r.message || '重新提交收集任务失败')
return
}
usbAwaitNewCycle = true
collectHint.value = usbTaskStatusHint(USB_TASK_PREPARING)
if (!unsubUsb) {
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
}
unsubUsb = onUsbPollTick((payload) => applyUsbProgress(payload as UsbPollPayload))
return
}
const err = await validateJobPreflight(formStore)
if (err) {
notify.warning(err)
await enterCompletedPhase()
await backToWait()
return
}
const created = await createDistributeJob(formStore, { resubmit: true })
if (!created.ok) {
notify.error(created.message)
await enterCompletedPhase()
await backToWait(created.message)
return
}
jobStore.setActiveJob(created.jobId)
const started = await pollJobStart(created.jobId)
if (!started.ok) {
notify.error(started.message || '启动任务轮询失败')
await enterCompletedPhase()
await backToWait(started.message || '启动任务轮询失败')
return
}
if (!unsubJob) {
unsubJob = onJobPollTick((payload) => applyJobProgress(payload as JobPollPayload))
}
unsubJob = onJobPollTick((payload) => applyJobProgress(payload as JobPollPayload))
} finally {
resubmitting = false
}
@@ -354,13 +388,16 @@ function applyUsbProgress(p: UsbPollPayload): void {
}
usbQueryFailStreak = 0
if (usbAwaitNewCycle && p.queryCode === 0 && p.taskStatus === USB_TASK_PREPARING) {
usbAwaitNewCycle = false
}
if (p.failed) {
collectStore.failCount += 1
void enterFailedPhase(p.errorMessage || usbTaskStatusHint(p.taskStatus))
return
}
if (p.success) {
if (usbAwaitNewCycle) return
collectStore.successCount += 1
workflowStep.value = 3
collectHint.value = usbTaskStatusHint(p.taskStatus)
@@ -373,9 +410,6 @@ function applyUsbProgress(p: UsbPollPayload): void {
workflowStep.value = p.taskStatus === USB_TASK_COPYING ? 2 : 1
collectHint.value = usbTaskStatusHint(p.taskStatus)
setProgress(copyProgress)
if (usbAwaitNewCycle && p.taskStatus === USB_TASK_PREPARING) {
usbAwaitNewCycle = false
}
}
async function finishDistribute(
@@ -384,7 +418,7 @@ async function finishDistribute(
): Promise<void> {
if (finishing) return
finishing = true
const skipCancel = phase.value === 'completed'
const skipCancel = phase.value === 'completed' || phase.value === 'failed'
await releasePolls(true)
const id = jobStore.jobId
if (id && !skipCancel) {
@@ -437,29 +471,36 @@ onMounted(async () => {
onUnmounted(() => {
if (finishing || phase.value !== 'running') return
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')
}
})
async function onFailedBack(): Promise<void> {
async function leaveFailedPage(redirect: '/collect' | '/distribute/config' | '/home'): Promise<void> {
finishing = true
await releasePolls(true)
appStore.setMode('ready')
await router.push(redirect)
}
async function onFailedBack(): Promise<void> {
if (isCollect.value) {
await router.push('/collect')
await leaveFailedPage('/collect')
return
}
await router.push('/distribute/config')
await leaveFailedPage('/distribute/config')
}
async function onFailedReset(): Promise<void> {
finishing = true
if (isCollect.value) {
collectStore.reset()
} else {
formStore.reset()
jobStore.reset()
}
await router.push('/home')
await leaveFailedPage('/home')
}
async function onReturn(): Promise<void> {
+1 -5
View File
@@ -49,7 +49,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { notify, notifyRequireInit } from '@/composables/useNotify'
import { refreshPrinterHeader } from '@/composables/usePrinterStatus'
@@ -122,10 +122,6 @@ function goCollect(): void {
if (!guardBusy()) return
router.push('/collect')
}
onMounted(() => {
if (canUse.value) void refreshPrinterHeader(configStore)
})
</script>
<style src="@/styles/pages/page2.css"></style>
-1
View File
@@ -1,2 +1 @@
/** workDll 卡位:备卡位,检测到后可自动重提任务 */
export const POSITION_PREPARE = 13
-1
View File
@@ -1,4 +1,3 @@
/** RestJobEx JSON 根字段 task_id */
export function genTaskId(): string {
return `T${Date.now()}`
}
+20 -1
View File
@@ -30,6 +30,26 @@ function normalizeStatus(raw: unknown): string {
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 {
const serial = pickFirst(row, [
'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 {
const flatSerial = pickFirst(json, ['serial_no', 'SerialNo', 'serialNo', 'szPrinterSerial'])
const flatStatus = pickFirst(json, ['printer_status', 'PrinterStatus'])
File diff suppressed because one or more lines are too long