From f1b73ee3d3579523859bff1dfd4dfdf941250e81 Mon Sep 17 00:00:00 2001 From: 24kycj <1637269896@qq.com> Date: Wed, 10 Jun 2026 23:50:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scripts/network-path-selftest.mjs | 78 +++++ .../src/components/NetworkPathDialog.vue | 266 ++++++++++++++++++ app/src/renderer/src/main.ts | 4 + app/src/renderer/src/stores/distributeForm.ts | 4 + app/src/renderer/src/stores/networkAuth.ts | 66 +++++ app/src/renderer/src/types/network.ts | 13 + app/src/renderer/src/utils/buildJobConfig.ts | 9 + .../renderer/src/utils/createDistributeJob.ts | 6 +- app/src/renderer/src/utils/networkPath.ts | 50 ++++ .../src/utils/validateJobPreflight.ts | 18 +- .../src/views/DistributeConfigView.vue | 48 +++- app/tsconfig.web.tsbuildinfo | 2 +- 12 files changed, 553 insertions(+), 11 deletions(-) create mode 100644 app/scripts/network-path-selftest.mjs create mode 100644 app/src/renderer/src/components/NetworkPathDialog.vue create mode 100644 app/src/renderer/src/stores/networkAuth.ts create mode 100644 app/src/renderer/src/types/network.ts create mode 100644 app/src/renderer/src/utils/networkPath.ts diff --git a/app/scripts/network-path-selftest.mjs b/app/scripts/network-path-selftest.mjs new file mode 100644 index 0000000..e28026c --- /dev/null +++ b/app/scripts/network-path-selftest.mjs @@ -0,0 +1,78 @@ +// 临时自测:验证 networkPath.ts 的纯函数行为 +// 跑法:node scripts/network-path-selftest.mjs +import { + isNetworkPath, + extractHostName, + buildNetworkUrl, + buildNetInfo +} from '../src/renderer/src/utils/networkPath.ts' + +const cases = [] +function eq(name, actual, expected) { + // 两边按字面字符串比较(不走 JSON.stringify 避免转义歧义) + const ok = actual === expected + cases.push({ name, ok, actual, expected }) + if (!ok) { + console.error(`FAIL ${name}`) + console.error(` expected: ${JSON.stringify(expected)}`) + console.error(` actual: ${JSON.stringify(actual)}`) + } +} + +// 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 empty', isNetworkPath(''), false) + +// extractHostName +const unc = '\\\\192.168.1.100\\share\\a.pdf' // 实际 \\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('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('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' }, + { hostName: '192.168.1.100', userName: 'b', password: 'q' } + ], + () => null +) +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) +} catch (e) { + 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 +const fail = cases.length - pass +console.log(`PASS ${pass} / ${cases.length}`) +if (fail > 0) process.exit(1) diff --git a/app/src/renderer/src/components/NetworkPathDialog.vue b/app/src/renderer/src/components/NetworkPathDialog.vue new file mode 100644 index 0000000..7ff226f --- /dev/null +++ b/app/src/renderer/src/components/NetworkPathDialog.vue @@ -0,0 +1,266 @@ + + + + + + + 添加网络位置 + × + + + 系统将按此 UNC 路径访问网络共享,请确认主机可访问且账号有效。 + + + 主机(IP 或主机名)* + + + + + 共享名(可选) + + + + + 用户名* + + + + + 密码* + + + + + 将添加为 + {{ previewUrl }} + + + {{ errorText }} + + + + + + + + + + + diff --git a/app/src/renderer/src/main.ts b/app/src/renderer/src/main.ts index 2ed6d75..54d51e0 100644 --- a/app/src/renderer/src/main.ts +++ b/app/src/renderer/src/main.ts @@ -1,6 +1,7 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import { configGet } from '@/api/cardsoon' +import { useNetworkAuthStore } from '@/stores/networkAuth' import App from './App.vue' import router from './router' import './styles/design-base.css' @@ -31,4 +32,7 @@ void (async () => { const app = createApp(App) app.use(createPinia()) app.use(router) + +useNetworkAuthStore().loadFromStorage() + app.mount('#app') diff --git a/app/src/renderer/src/stores/distributeForm.ts b/app/src/renderer/src/stores/distributeForm.ts index 4243c87..82cb849 100644 --- a/app/src/renderer/src/stores/distributeForm.ts +++ b/app/src/renderer/src/stores/distributeForm.ts @@ -18,6 +18,10 @@ export interface PathListItem { path: string meta: string sizeBytes: number + isNetwork?: boolean + hostName?: string + userName?: string + password?: string } export interface DistributeFormState { diff --git a/app/src/renderer/src/stores/networkAuth.ts b/app/src/renderer/src/stores/networkAuth.ts new file mode 100644 index 0000000..d56da5b --- /dev/null +++ b/app/src/renderer/src/stores/networkAuth.ts @@ -0,0 +1,66 @@ +// TODO: 后续用 electron safeStorage 加密 +import { defineStore } from 'pinia' +import type { StoredCredential } from '@/types/network' + +const STORAGE_KEY = 'networkCredentials' + +export const useNetworkAuthStore = defineStore('networkAuth', { + state: () => ({ + credentials: {} as Record + }), + getters: { + hasCredentials: (state) => Object.keys(state.credentials).length > 0 + }, + actions: { + loadFromStorage(): void { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return + const parsed = JSON.parse(raw) as Record + if (parsed && typeof parsed === 'object') { + this.credentials = { ...this.credentials, ...parsed } + } + } catch (e) { + console.warn('[networkAuth] loadFromStorage 失败,已忽略', e) + try { + localStorage.removeItem(STORAGE_KEY) + } catch { + // localStorage 不可用时静默忽略 + } + } + }, + setCredential(hostName: string, userName: string, password: string): void { + const host = (hostName || '').trim() + if (!host) return + this.credentials[host] = { + userName: userName || '', + password: password || '', + lastUsed: new Date().toISOString() + } + this.persist() + }, + getCredential(hostName: string): StoredCredential | null { + const host = (hostName || '').trim() + if (!host) return null + return this.credentials[host] || null + }, + removeCredential(hostName: string): void { + const host = (hostName || '').trim() + if (!host) return + if (delete this.credentials[host]) { + this.persist() + } + }, + clearAll(): void { + this.credentials = {} + this.persist() + }, + persist(): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(this.credentials)) + } catch (e) { + console.warn('[networkAuth] 持久化失败', e) + } + } + } +}) diff --git a/app/src/renderer/src/types/network.ts b/app/src/renderer/src/types/network.ts new file mode 100644 index 0000000..d1a7ef0 --- /dev/null +++ b/app/src/renderer/src/types/network.ts @@ -0,0 +1,13 @@ +/** 提交到后端的网络凭据结构(JSON 字符串里的一项) */ +export interface NetworkCredential { + host_name: string + user_name: string + password: string +} + +/** 客户端缓存的凭据(以 host 为 key) */ +export interface StoredCredential { + userName: string + password: string + lastUsed: string +} diff --git a/app/src/renderer/src/utils/buildJobConfig.ts b/app/src/renderer/src/utils/buildJobConfig.ts index c9dfa5d..29c76ff 100644 --- a/app/src/renderer/src/utils/buildJobConfig.ts +++ b/app/src/renderer/src/utils/buildJobConfig.ts @@ -1,6 +1,8 @@ import type { DistributeFormState } from '@/stores/distributeForm' import { cleanPathPattern } from '@shared/path-pattern' import { resolveJobTasks } from '@/utils/validateJobConfig' +import { useNetworkAuthStore } from '@/stores/networkAuth' +import { buildNetInfo } from '@/utils/networkPath' export interface BuildJobOptions { taskId: string @@ -57,5 +59,12 @@ export function buildJobConfig( if (form.generateZip) body.is_generate_zip = true if (form.failPrintLabel) body.is_printer_record_logo = true + const networkItems = form.pathList.filter((x) => x.isNetwork) + if (networkItems.length > 0) { + const netStore = useNetworkAuthStore() + const netInfo = buildNetInfo(networkItems, (host) => netStore.getCredential(host)) + if (netInfo) body.net_info = netInfo + } + return body } diff --git a/app/src/renderer/src/utils/createDistributeJob.ts b/app/src/renderer/src/utils/createDistributeJob.ts index 002b083..279678b 100644 --- a/app/src/renderer/src/utils/createDistributeJob.ts +++ b/app/src/renderer/src/utils/createDistributeJob.ts @@ -31,7 +31,11 @@ async function buildJobJson(form: DistributeFormState): Promise<{ ok: true; json } } - return { ok: true, json: JSON.stringify(buildJobConfig(form, { taskId, udfFile })) } + try { + return { ok: true, json: JSON.stringify(buildJobConfig(form, { taskId, udfFile })) } + } catch (e) { + return { ok: false, message: e instanceof Error ? e.message : String(e) } + } } export async function createDistributeJob( diff --git a/app/src/renderer/src/utils/networkPath.ts b/app/src/renderer/src/utils/networkPath.ts new file mode 100644 index 0000000..6ebca4c --- /dev/null +++ b/app/src/renderer/src/utils/networkPath.ts @@ -0,0 +1,50 @@ +import type { NetworkCredential, StoredCredential } from '@/types/network' + +export function isNetworkPath(p: string): boolean { + if (!p) return false + return p.startsWith('\\\\') || p.startsWith('//') +} + +export function extractHostName(p: string): string { + if (!p) return '' + if (p.startsWith('\\\\')) { + const part = p.substring(2).split('\\')[0] + return part || '' + } + if (p.startsWith('//')) { + const part = p.substring(2).split('/')[0] + return part || '' + } + return '' +} + +export function buildNetworkUrl(host: string, share: string): string { + const h = host.trim() + const s = share.trim().replace(/^[\\/]+/, '').replace(/[\\/]+$/, '') + return s ? `\\\\${h}\\${s}` : `\\\\${h}` +} + +export function buildNetInfo( + networkItems: { hostName?: string; userName?: string; password?: string }[], + getStored: (host: string) => StoredCredential | null +): string { + const creds: NetworkCredential[] = [] + const seen = new Set() + for (const it of networkItems) { + const host = (it.hostName || '').trim() + if (!host || seen.has(host)) continue + seen.add(host) + const stored = getStored(host) + const user_name = (it.userName || stored?.userName || '').trim() + const password = it.password || stored?.password || '' + if (!user_name || !password) { + throw new Error(`缺少网络凭据: ${host}`) + } + creds.push({ + host_name: `\\\\${host}`, + user_name, + password + }) + } + return creds.length > 0 ? JSON.stringify(creds) : '' +} diff --git a/app/src/renderer/src/utils/validateJobPreflight.ts b/app/src/renderer/src/utils/validateJobPreflight.ts index aa15af4..7702a4e 100644 --- a/app/src/renderer/src/utils/validateJobPreflight.ts +++ b/app/src/renderer/src/utils/validateJobPreflight.ts @@ -12,13 +12,17 @@ export async function validateJobPreflight(form: DistributeFormState): Promise x.path) - const ex = await fsPathExists(paths) - if (ex.ok && ex.data?.missing.length) { - return `路径不存在: ${ex.data.missing.join(', ')}` - } - if (totalCopyBytes(form) <= 0) { - return '拷贝路径下没有可拷贝的文件' + // 网络项在主进程 fs.existsSync 必返 false,跳过其存在性 / 总大小校验 + const localItems = form.pathList.filter((x) => !x.isNetwork) + const localPaths = localItems.map((x) => x.path) + if (localPaths.length > 0) { + const ex = await fsPathExists(localPaths) + if (ex.ok && ex.data?.missing.length) { + return `路径不存在: ${ex.data.missing.join(', ')}` + } + if (totalCopyBytes(form) <= 0) { + return '拷贝路径下没有可拷贝的文件' + } } } if (hasPrint) { diff --git a/app/src/renderer/src/views/DistributeConfigView.vue b/app/src/renderer/src/views/DistributeConfigView.vue index efb525f..70fb891 100644 --- a/app/src/renderer/src/views/DistributeConfigView.vue +++ b/app/src/renderer/src/views/DistributeConfigView.vue @@ -22,6 +22,9 @@ 添加路径 + + 添加网络位置 + @@ -68,7 +71,10 @@ - {{ item.path }} + + {{ item.path }} + 网络 + {{ item.meta }} @@ -141,11 +147,12 @@ +
系统将按此 UNC 路径访问网络共享,请确认主机可访问且账号有效。
{{ previewUrl }}
{{ errorText }}