From cf1caef444579f4b7760ef42ece872e77e3592c5 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Wed, 23 Sep 2026 16:49:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=8D=A1=E7=89=87=E6=89=93=E5=8D=B0?= =?UTF-8?q?=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/wallet/card-loading-guide.svg | 80 ++++++++--- src/views/wallet/card-template/render.ts | 12 +- .../wallet-active/card-print-drawer.vue | 128 ++++++++---------- 3 files changed, 130 insertions(+), 90 deletions(-) diff --git a/src/assets/wallet/card-loading-guide.svg b/src/assets/wallet/card-loading-guide.svg index 74f496e3..dddeb7cd 100644 --- a/src/assets/wallet/card-loading-guide.svg +++ b/src/assets/wallet/card-loading-guide.svg @@ -1,17 +1,65 @@ - - - - - - - - - - - - - - - - + + + 长边左右横放 | 预印面朝上 | 文字正对自己 + + + + ✓ + 正确放法 + + + + + 青鸟同文实验学校 + 学生卡 · STUDENT CARD + + + 吴至元 + 青鸟班 · Q4 + 2023·09·01022 + + ✓ 打印与预印同面、同向 + 成品直接可用 + + + + ✗ + 面放反了 + + + + 青鸟同文实验学校 + 学生卡 · STUDENT CARD + + + + 吴至元 + 青鸟班 · Q4 + 2023·09·01022 + + (隔卡料透显) + + ✗ 字印到背面,正面看是反字 + 整叠卡翻面重放即可 + + + + ✗ + 卡倒着放 + + + + + + 青鸟同文实验学校 + 学生卡 · STUDENT CARD + + + + 吴至元 + 青鸟班 · Q4 + 2023·09·01022 + + ✗ 打印字与预印字上下相反 + 把卡转正重放即可 diff --git a/src/views/wallet/card-template/render.ts b/src/views/wallet/card-template/render.ts index 16a6dbc6..089b82c2 100644 --- a/src/views/wallet/card-template/render.ts +++ b/src/views/wallet/card-template/render.ts @@ -57,13 +57,14 @@ export const sampleFieldsFor = (identity: string): Record => { // --------------------------------------------------------------------------- const inlineCache = new Map() -/** 单张图片转 data URL:fetch → crossOrigin img → 失败返回原地址(导出时再报错) */ +/** 单张图片转 data URL:fetch → crossOrigin img → 失败返回原地址(导出时再报错)。 + * 两条通道均有 8s 超时:外链偶发挂起时不再无限等待(否则多卡批量会卡死在该卡的导出上)。 */ export const inlineImage = async (url: string): Promise => { if (!url || /^(data:)/i.test(url)) return url const cached = inlineCache.get(url) if (cached) return cached try { - const res = await fetch(url, { mode: "cors" }) + const res = await fetch(url, { mode: "cors", signal: AbortSignal.timeout(8000) }) if (res.ok) { const blob = await res.blob() const dataUrl = await new Promise((resolve, reject) => { @@ -80,9 +81,11 @@ export const inlineImage = async (url: string): Promise => { } try { const dataUrl = await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("img load timeout")), 8000) const img = new Image() img.crossOrigin = "anonymous" img.onload = () => { + clearTimeout(timer) const canvas = document.createElement("canvas") canvas.width = img.naturalWidth canvas.height = img.naturalHeight @@ -93,7 +96,10 @@ export const inlineImage = async (url: string): Promise => { reject(err) } } - img.onerror = reject + img.onerror = () => { + clearTimeout(timer) + reject(new Error("img load fail")) + } img.src = url }) inlineCache.set(url, dataUrl) diff --git a/src/views/wallet/wallet-active/card-print-drawer.vue b/src/views/wallet/wallet-active/card-print-drawer.vue index 1fc58f59..5bf383d2 100644 --- a/src/views/wallet/wallet-active/card-print-drawer.vue +++ b/src/views/wallet/wallet-active/card-print-drawer.vue @@ -159,16 +159,13 @@
- 放卡方向示意图(横放·打印面朝上) - 横放 · 打印面朝上 + 放卡图例:预印面朝上为正确;面放反字透背面;卡倒放上下颠倒 +

+ 当前为{{ orientationLabel }}模板,放卡方式相同,无需转卡 + 双面模板:背面用下一张卡打印 + 「无背景」区域不上墨(白卡底) +

-
@@ -285,6 +282,8 @@ interface QueueItem { const queue = ref([]) const printing = ref(false) const abortRequested = ref(false) +/** 抽屉关闭时中止在途任务轮询(否则关抽屉后轮询还会空转到超时) */ +const onCloseCancellers: Array<() => void> = [] const identitySummary = computed(() => { const summary = { student: 0, teacher: 0, other: 0, unbound: 0 } @@ -421,32 +420,46 @@ const printOne = async (item: QueueItem) => { item.state = "printing" const { taskId } = await startPrint({ copies: copies.value, front, back }) - // 轮询任务:1.2s 一次,3 分钟超时 + // 轮询任务:1.2s 一次,3 分钟超时。递归 setTimeout 防止慢响应时轮询重叠; + // 任务号失效(助手重启过)快速失败,连续通讯失败 5 次中止,不再傻等满 3 分钟 await new Promise((resolve, reject) => { const startedAt = Date.now() - const timer = setInterval(async () => { + let failStreak = 0 + let timer: ReturnType | null = null + const tick = async () => { if (Date.now() - startedAt > 180_000) { - clearInterval(timer) reject(new Error("打印超时,请检查打印机状态")) return } try { const task = await getPrintTask(taskId) + failStreak = 0 if (task.state === "done") { - clearInterval(timer) resolve() - } else if (task.state === "error") { - clearInterval(timer) + return + } + if (task.state === "error") { reject(new Error(deviceStatusText(task.status, task.statusText || "打印机返回异常"))) + return } } catch (err: any) { - // 单次轮询失败不终止;助手失联则终止 - if (err?.message?.includes("助手")) { - clearInterval(timer) - reject(err) + const msg = String(err?.message || err) + if (msg.includes("任务不存在") || msg.includes("已过期")) { + reject(new Error("打印任务丢失(打印助手可能重启过),请重试")) + return + } + if (++failStreak >= 5) { + reject(new Error("与打印助手通讯中断:" + msg)) + return } } - }, 1200) + timer = setTimeout(tick, 1200) + } + timer = setTimeout(tick, 1200) + onCloseCancellers.push(() => { + if (timer) clearTimeout(timer) + reject(new Error("已停止打印")) + }) }) item.state = "done" } @@ -584,6 +597,14 @@ document.addEventListener("visibilitychange", onVisibilityChange) const onClose = () => { stopStatusPolling() abortRequested.value = true + while (onCloseCancellers.length) { + const cancel = onCloseCancellers.shift() + try { + cancel?.() + } catch { + // 已 settle 的 promise 再 reject 是空操作 + } + } } watch(visible, (v) => { @@ -870,13 +891,14 @@ onUnmounted(() => { } } - /* 放卡指引:紧贴打印按钮上方,打印前看一眼装卡方向 */ + /* 放卡指引:紧贴打印按钮上方,打印前看一眼装卡方向(醒目样式) */ .card-guide { flex-shrink: 0; margin-top: 12px; - padding: 10px 14px; + padding: 12px 14px; background: var(--el-color-primary-light-9); - border: 1px solid var(--el-color-primary-light-7); + border: 1px solid var(--el-color-primary-light-5); + border-left: 4px solid var(--el-color-primary); border-radius: 10px; .guide-head { @@ -891,9 +913,9 @@ onUnmounted(() => { } .guide-title { - font-size: 13px; + font-size: 14px; font-weight: 600; - color: var(--el-text-color-primary); + color: var(--el-color-primary); } .guide-sub { @@ -904,62 +926,26 @@ onUnmounted(() => { } .guide-body { - display: flex; - gap: 18px; - align-items: center; margin-top: 8px; padding-top: 10px; - border-top: 1px dashed var(--el-color-primary-light-7); + border-top: 1px dashed var(--el-color-primary-light-5); } .guide-fig { - display: flex; - flex-direction: column; - flex-shrink: 0; - gap: 2px; - align-items: center; - .guide-svg { display: block; - width: 138px; - height: 88px; + width: 100%; + min-width: 480px; + max-width: 756px; + height: auto; } - .fig-cap { + .guide-note { + display: flex; + gap: 16px; + margin: 6px 0 0; font-size: 12px; - font-weight: 600; - color: var(--el-color-primary); - } - } - - .guide-points { - flex: 1; - min-width: 0; - margin: 0; - padding: 0; - list-style: none; - - li { - position: relative; - padding-left: 14px; - font-size: 12px; - line-height: 1.8; - color: var(--el-text-color-regular); - - &::before { - position: absolute; - top: 0.72em; - left: 2px; - width: 5px; - height: 5px; - content: ""; - border-radius: 50%; - background: var(--el-color-primary); - } - - b { - color: var(--el-text-color-primary); - } + color: var(--el-text-color-secondary); } } }