From 931fcf90a4f01208f8693be5ffb719efef3dd024 Mon Sep 17 00:00:00 2001 From: 24kycj <1637269896@qq.com> Date: Thu, 11 Jun 2026 10:34:05 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=90=BD=E5=AE=9E=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E5=8F=8D=E9=A6=88=E4=BA=94=E9=A1=B9=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=88=86=E5=8F=91=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 色带余量 RibbonAmount 独立展示与轮询刷新 - 加密狗授权码 UI、校验、auth_code 提交及 secrets 持久化 - 打印机状态全局轮询(前台 1.5s / 后台 8s) - 网络路径:列表仅映射目录,凭据单独配置,net_info 对齐 SoonWoker - 标签打印面:点击正/反面选择,print_flag 1/2/3 与模板校验 Co-authored-by: Cursor --- app/scripts/network-path-selftest.mjs | 47 +++-- app/src/main/ipc/register-handlers.ts | 26 +++ app/src/main/services/secrets-store.ts | 92 +++++++++ app/src/main/utils/network-drive.ts | 56 ++++++ app/src/main/utils/parse-soon.ts | 19 +- app/src/preload/index.ts | 3 + app/src/renderer/src/App.vue | 2 + app/src/renderer/src/api/cardsoon.ts | 25 ++- app/src/renderer/src/components/AppHeader.vue | 10 +- .../src/components/NetworkPathDialog.vue | 25 ++- .../src/composables/usePrinterStatus.ts | 8 +- .../src/composables/usePrinterStatusPoll.ts | 86 +++++++++ app/src/renderer/src/main.ts | 58 +++++- app/src/renderer/src/stores/distributeForm.ts | 95 +++++++++- app/src/renderer/src/stores/dongleAuth.ts | 30 +++ app/src/renderer/src/stores/networkAuth.ts | 55 ++++-- app/src/renderer/src/types/printer.ts | 2 + app/src/renderer/src/utils/buildJobConfig.ts | 17 +- .../renderer/src/utils/copyNetworkHosts.ts | 31 +++ .../renderer/src/utils/createDistributeJob.ts | 19 +- .../src/utils/validateJobPreflight.ts | 41 +++- .../src/views/DistributeConfigView.vue | 177 +++++++++++++++--- .../networkPath.ts => shared/network-host.ts} | 35 +++- app/src/shared/printer-info.ts | 7 +- 24 files changed, 840 insertions(+), 126 deletions(-) create mode 100644 app/src/main/services/secrets-store.ts create mode 100644 app/src/main/utils/network-drive.ts create mode 100644 app/src/renderer/src/composables/usePrinterStatusPoll.ts create mode 100644 app/src/renderer/src/stores/dongleAuth.ts create mode 100644 app/src/renderer/src/utils/copyNetworkHosts.ts rename app/src/{renderer/src/utils/networkPath.ts => shared/network-host.ts} (60%) diff --git a/app/scripts/network-path-selftest.mjs b/app/scripts/network-path-selftest.mjs index e28026c..eeb2c0e 100644 --- a/app/scripts/network-path-selftest.mjs +++ b/app/scripts/network-path-selftest.mjs @@ -1,16 +1,16 @@ -// 临时自测:验证 networkPath.ts 的纯函数行为 -// 跑法:node scripts/network-path-selftest.mjs import { isNetworkPath, extractHostName, + extractDriveLetter, buildNetworkUrl, + collectHostsForCopyPaths, buildNetInfo -} from '../src/renderer/src/utils/networkPath.ts' +} from '../src/shared/network-host.ts' const cases = [] function eq(name, actual, expected) { - // 两边按字面字符串比较(不走 JSON.stringify 避免转义歧义) - const ok = actual === expected + const ok = + Array.isArray(expected) ? JSON.stringify(actual) === JSON.stringify(expected) : actual === expected cases.push({ name, ok, actual, expected }) if (!ok) { console.error(`FAIL ${name}`) @@ -19,38 +19,36 @@ function eq(name, actual, expected) { } } -// isNetworkPath eq('isNetworkPath \\host', isNetworkPath('\\\\192.168.1.100\\share'), true) eq('isNetworkPath //host', isNetworkPath('//nas/share'), true) -eq('isNetworkPath D:\\a (应 false)', isNetworkPath('D:\\data'), false) +eq('isNetworkPath D:\\a', isNetworkPath('D:\\data'), false) eq('isNetworkPath empty', isNetworkPath(''), false) -// extractHostName -const unc = '\\\\192.168.1.100\\share\\a.pdf' // 实际 \\192.168.1.100\share\a.pdf +const unc = '\\\\192.168.1.100\\share\\a.pdf' eq('extractHostName \\host\\share', extractHostName(unc), '192.168.1.100') eq('extractHostName //host/share', extractHostName('//nas/share'), 'nas') eq('extractHostName local', extractHostName('D:\\data'), '') eq('extractHostName empty', extractHostName(''), '') -// buildNetworkUrl -// 期望: \\192.168.1.100 -> 字面 '\\\\192.168.1.100' +eq('extractDriveLetter Z', extractDriveLetter('Z:\\folder'), 'Z') +eq('extractDriveLetter C', extractDriveLetter('C:\\data\\sub'), 'C') +eq('extractDriveLetter unc', extractDriveLetter(unc), '') + eq('buildNetworkUrl bare', buildNetworkUrl('192.168.1.100', ''), '\\\\192.168.1.100') -// 期望: \\192.168.1.100\share -> 字面 '\\\\192.168.1.100\\share' eq('buildNetworkUrl share', buildNetworkUrl('192.168.1.100', 'share'), '\\\\192.168.1.100\\share') eq('buildNetworkUrl slashes stripped', buildNetworkUrl('host', '/share/'), '\\\\host\\share') -// buildNetInfo -// 期望产物是 JSON 文本(JSON 字符串里 \\ 表示 1 个 \ 字符) -// 反序列化后 host_name 值是 2 个 \ 字符 -> JSON 文本中需要 4 个 \ 字符 -// 4 个 \ 字符 = JS 字面 '\\\\\\\\' (8 个 \) -const stored = new Map([['192.168.1.100', { userName: 'admin', password: 'pass', lastUsed: '' }]]) -const r1 = buildNetInfo( - [{ hostName: '192.168.1.100' }], - (h) => stored.get(h) || null +eq( + 'collectHosts unc + drive', + collectHostsForCopyPaths(['\\\\192.168.1.100\\share\\a', 'Z:\\data'], { Z: '192.168.1.200' }), + ['192.168.1.100', '192.168.1.200'] ) -eq('buildNetInfo single', r1, '[{"host_name":"\\\\\\\\192.168.1.100","user_name":"admin","password":"pass"}]') +eq('collectHosts local only', collectHostsForCopyPaths(['E:\\data'], {}), []) + +const stored = new Map([['192.168.1.100', { userName: 'admin', password: 'pass', lastUsed: '' }]]) +const r1 = buildNetInfo([{ hostName: '192.168.1.100' }], (h) => stored.get(h) || null) +eq('buildNetInfo single', r1, '[{"host_name":"192.168.1.100","user_name":"admin","password":"pass"}]') -// dedup const r2 = buildNetInfo( [ { hostName: '192.168.1.100', userName: 'a', password: 'p' }, @@ -58,9 +56,8 @@ const r2 = buildNetInfo( ], () => null ) -eq('buildNetInfo dedup', r2, '[{"host_name":"\\\\\\\\192.168.1.100","user_name":"a","password":"p"}]') +eq('buildNetInfo dedup', r2, '[{"host_name":"192.168.1.100","user_name":"a","password":"p"}]') -// missing throws let threw = false try { buildNetInfo([{ hostName: 'h1' }], () => null) @@ -68,8 +65,6 @@ try { threw = e.message.includes('缺少网络凭据') } eq('buildNetInfo missing throws', threw, true) - -// empty -> empty string eq('buildNetInfo empty list', buildNetInfo([], () => null), '') const pass = cases.filter((c) => c.ok).length diff --git a/app/src/main/ipc/register-handlers.ts b/app/src/main/ipc/register-handlers.ts index 9145dc3..217ef65 100644 --- a/app/src/main/ipc/register-handlers.ts +++ b/app/src/main/ipc/register-handlers.ts @@ -12,6 +12,8 @@ import { openDesignApp } from '../services/open-design-app' import { writeJobCsv, type JobCsvRow } from '../utils/job-csv' import { parseSoonTemplate } from '../utils/parse-soon' import { stageJobPayloadJson } from '../utils/stage-job-payload' +import { resolveDriveHostMap, resolveHostsFromPaths } from '../utils/network-drive' +import { getSecrets, setSecrets, type SecretsPayload } from '../services/secrets-store' import { ensureDllInitialized, isDllInitAttempted } from '../services/dll-bootstrap' import { loadDllModule } from '../services/dll-loader' import { tracedHandle } from './traced-handler' @@ -103,6 +105,7 @@ export function registerIpcHandlers(): void { if (parsed.ok) { const snapshot: PrinterStatusSnapshot = { ribbonType: cached?.ribbonType ?? '—', + ribbonAmount: cached?.ribbonAmount ?? '—', statusText: parsed.statusText, serialNo: cached?.serialNo ?? '—' } @@ -350,6 +353,29 @@ export function registerIpcHandlers(): void { return ok({ items }) }) + tracedHandle('fs:resolve-network-hosts', (_e, paths: string[]) => { + const list = Array.isArray(paths) ? paths.map((p) => String(p || '')) : [] + const driveHostMap = resolveDriveHostMap(list) + const hosts = resolveHostsFromPaths(list) + return ok({ hosts, driveHostMap }) + }) + + tracedHandle('secrets:get', () => { + return ok(getSecrets()) + }) + + tracedHandle('secrets:set', (_e, patch: Partial) => { + if (!patch || typeof patch !== 'object') return ok(getSecrets()) + const toMerge: Partial = {} + if (patch.networkCredentials !== undefined) { + toMerge.networkCredentials = patch.networkCredentials + } + if (patch.dongleAuthCode !== undefined) { + toMerge.dongleAuthCode = patch.dongleAuthCode + } + return ok(setSecrets(toMerge)) + }) + tracedHandle( 'fs:write-job-csv', (_e, payload: { taskId: string; rows: JobCsvRow[] }) => { diff --git a/app/src/main/services/secrets-store.ts b/app/src/main/services/secrets-store.ts new file mode 100644 index 0000000..c4f2314 --- /dev/null +++ b/app/src/main/services/secrets-store.ts @@ -0,0 +1,92 @@ +import fs from 'fs' +import path from 'path' +import { app, safeStorage } from 'electron' +import log from 'electron-log' + +export interface StoredNetworkCredential { + userName: string + password: string + lastUsed: string +} + +export interface SecretsPayload { + networkCredentials: Record + dongleAuthCode: string +} + +const FILE_NAME = 'secrets.bin' + +function secretsPath(): string { + return path.join(app.getPath('userData'), FILE_NAME) +} + +function emptyPayload(): SecretsPayload { + return { networkCredentials: {}, dongleAuthCode: '' } +} + +function canEncrypt(): boolean { + try { + return safeStorage.isEncryptionAvailable() + } catch { + return false + } +} + +function readRaw(): SecretsPayload { + const file = secretsPath() + if (!fs.existsSync(file)) return emptyPayload() + try { + const buf = fs.readFileSync(file) + if (!buf.length) return emptyPayload() + let text: string + if (canEncrypt()) { + text = safeStorage.decryptString(buf) + } else { + text = buf.toString('utf8') + } + const parsed = JSON.parse(text) as Partial + return { + networkCredentials: + parsed.networkCredentials && typeof parsed.networkCredentials === 'object' + ? parsed.networkCredentials + : {}, + dongleAuthCode: typeof parsed.dongleAuthCode === 'string' ? parsed.dongleAuthCode : '' + } + } catch (e) { + log.warn('[secrets-store] read failed', e) + return emptyPayload() + } +} + +function writeRaw(payload: SecretsPayload): void { + const text = JSON.stringify(payload) + try { + const buf = canEncrypt() ? safeStorage.encryptString(text) : Buffer.from(text, 'utf8') + fs.writeFileSync(secretsPath(), buf) + } catch (e) { + log.warn('[secrets-store] write failed', e) + } +} + +let cache: SecretsPayload | null = null + +export function getSecrets(): SecretsPayload { + if (!cache) cache = readRaw() + return { + networkCredentials: { ...cache.networkCredentials }, + dongleAuthCode: cache.dongleAuthCode + } +} + +export function setSecrets(patch: Partial): SecretsPayload { + const current = getSecrets() + if (patch.networkCredentials !== undefined) { + current.networkCredentials = { ...patch.networkCredentials } + } + if (patch.dongleAuthCode !== undefined) { + current.dongleAuthCode = patch.dongleAuthCode + } + cache = current + writeRaw(current) + return getSecrets() +} diff --git a/app/src/main/utils/network-drive.ts b/app/src/main/utils/network-drive.ts new file mode 100644 index 0000000..325f4b3 --- /dev/null +++ b/app/src/main/utils/network-drive.ts @@ -0,0 +1,56 @@ +import { execSync } from 'child_process' +import { + extractDriveLetter, + extractHostName, + isNetworkPath +} from '@shared/network-host' + +function resolveUncForDrive(letter: string): string | null { + const L = (letter || '').trim().toUpperCase() + if (!L || L.length !== 1) return null + if (process.platform !== 'win32') return null + try { + const out = execSync(`net use ${L}:`, { encoding: 'utf8', windowsHide: true }) + const m = /Remote\s+(\S+)/i.exec(out) || /远程\s+(\S+)/i.exec(out) + const unc = m?.[1]?.trim() + if (!unc || !unc.startsWith('\\\\')) return null + return unc + } catch { + return null + } +} + +export function resolveHostsFromPaths(paths: string[]): string[] { + const hosts = new Set() + for (const raw of paths) { + const p = (raw || '').trim() + if (!p) continue + if (isNetworkPath(p)) { + const h = extractHostName(p) + if (h) hosts.add(h) + continue + } + const letter = extractDriveLetter(p) + if (!letter) continue + const unc = resolveUncForDrive(letter) + if (!unc) continue + const h = extractHostName(unc) + if (h) hosts.add(h) + } + return Array.from(hosts) +} + +export function resolveDriveHostMap(paths: string[]): Record { + const map: Record = {} + for (const raw of paths) { + const p = (raw || '').trim() + if (!p || isNetworkPath(p)) continue + const letter = extractDriveLetter(p) + if (!letter || map[letter]) continue + const unc = resolveUncForDrive(letter) + if (!unc) continue + const h = extractHostName(unc) + if (h) map[letter] = h + } + return map +} diff --git a/app/src/main/utils/parse-soon.ts b/app/src/main/utils/parse-soon.ts index 721876a..1af82c7 100644 --- a/app/src/main/utils/parse-soon.ts +++ b/app/src/main/utils/parse-soon.ts @@ -13,18 +13,20 @@ export interface ParsedSoonTemplate { frontImageUrl: string backImageUrl: string fields: TemplateFieldRow[] - printFlag: number + /** soon 模板 flag:1 双面 / 2 正面 / 3 背面 */ + templateFlag: number } const SOON_FIELD_TYPES = new Set([1, 3, 4, 5]) -export function readSoonPrintFlag(raw: Record): number { +/** soon 文件元数据 flag,用于与任务 print_flag 校验 */ +export function readSoonTemplateFlag(raw: Record): number { const flag = Number(raw.flag) - if (flag === 1 || flag === 2) return flag + if (flag === 1 || flag === 2 || flag === 3) return flag const hasBack = !!String(raw.backDisplayPic ?? '').trim() || (Array.isArray(raw.backData) && raw.backData.length > 0) - return hasBack ? 2 : 1 + return hasBack ? 1 : 2 } function pickArray(obj: Record, key: string): Record[] { @@ -96,7 +98,7 @@ function parseSoonWorkerDisk(soonPath: string, raw: Record): Pa frontImageUrl: toImageUrl(soonPath, frontPic), backImageUrl: toImageUrl(soonPath, backPic), fields, - printFlag: readSoonPrintFlag(raw) + templateFlag: readSoonTemplateFlag(raw) } } @@ -132,7 +134,12 @@ function parseSoonLegacy(soonPath: string, raw: Record): Parsed fields.push({ label: toFieldLabel(name, side), value, originName: name, fieldType: 5 }) }) - return { frontImageUrl, backImageUrl, fields, printFlag: readSoonPrintFlag(raw) } + return { + frontImageUrl, + backImageUrl, + fields, + templateFlag: readSoonTemplateFlag(raw) + } } export function parseSoonTemplate(soonPath: string, raw: Record): ParsedSoonTemplate { diff --git a/app/src/preload/index.ts b/app/src/preload/index.ts index 4378659..c40aaae 100644 --- a/app/src/preload/index.ts +++ b/app/src/preload/index.ts @@ -22,7 +22,10 @@ const channels = { 'fs:path-exists', 'fs:dir-size', 'fs:parse-soon', + 'fs:resolve-network-hosts', 'fs:write-job-csv', + 'secrets:get', + 'secrets:set', 'config:get', 'config:set', 'shell:open-path', diff --git a/app/src/renderer/src/App.vue b/app/src/renderer/src/App.vue index 32e9f7a..c378dd7 100644 --- a/app/src/renderer/src/App.vue +++ b/app/src/renderer/src/App.vue @@ -4,6 +4,8 @@ diff --git a/app/src/renderer/src/api/cardsoon.ts b/app/src/renderer/src/api/cardsoon.ts index be1baad..c816512 100644 --- a/app/src/renderer/src/api/cardsoon.ts +++ b/app/src/renderer/src/api/cardsoon.ts @@ -135,7 +135,7 @@ export async function fsParseSoon(filePath: string): Promise< frontImageUrl: string backImageUrl: string fields: { label: string; value: string; originName: string; fieldType: number }[] - printFlag: number + templateFlag: number }> > { return api().invoke('fs:parse-soon', filePath) as Promise< @@ -143,11 +143,32 @@ export async function fsParseSoon(filePath: string): Promise< frontImageUrl: string backImageUrl: string fields: { label: string; value: string; originName: string; fieldType: number }[] - printFlag: number + templateFlag: number }> > } +export async function fsResolveNetworkHosts( + paths: string[] +): Promise }>> { + return api().invoke('fs:resolve-network-hosts', paths) as Promise< + IpcResult<{ hosts: string[]; driveHostMap: Record }> + > +} + +export interface SecretsPayloadDTO { + networkCredentials?: Record + dongleAuthCode?: string +} + +export async function secretsGet(): Promise> { + return api().invoke('secrets:get') as Promise> +} + +export async function secretsSet(patch: SecretsPayloadDTO): Promise> { + return api().invoke('secrets:set', patch) as Promise> +} + export async function configGet(): Promise< IpcResult<{ sharedDir: string diff --git a/app/src/renderer/src/components/AppHeader.vue b/app/src/renderer/src/components/AppHeader.vue index 23bd153..46e2276 100644 --- a/app/src/renderer/src/components/AppHeader.vue +++ b/app/src/renderer/src/components/AppHeader.vue @@ -6,6 +6,7 @@
{{ mode }}
色带: {{ status.ribbonType }} + 余量: {{ status.ribbonAmount }} 状态: {{ status.statusText }} @@ -22,21 +23,14 @@