Compare commits
2 Commits
f1b73ee3d3
...
82d7431e6f
| Author | SHA1 | Date | |
|---|---|---|---|
| 82d7431e6f | |||
| 931fcf90a4 |
@@ -1,16 +1,16 @@
|
|||||||
// 临时自测:验证 networkPath.ts 的纯函数行为
|
|
||||||
// 跑法:node scripts/network-path-selftest.mjs
|
|
||||||
import {
|
import {
|
||||||
isNetworkPath,
|
isNetworkPath,
|
||||||
extractHostName,
|
extractHostName,
|
||||||
|
extractDriveLetter,
|
||||||
buildNetworkUrl,
|
buildNetworkUrl,
|
||||||
|
collectHostsForCopyPaths,
|
||||||
buildNetInfo
|
buildNetInfo
|
||||||
} from '../src/renderer/src/utils/networkPath.ts'
|
} from '../src/shared/network-host.ts'
|
||||||
|
|
||||||
const cases = []
|
const cases = []
|
||||||
function eq(name, actual, expected) {
|
function eq(name, actual, expected) {
|
||||||
// 两边按字面字符串比较(不走 JSON.stringify 避免转义歧义)
|
const ok =
|
||||||
const ok = actual === expected
|
Array.isArray(expected) ? JSON.stringify(actual) === JSON.stringify(expected) : actual === expected
|
||||||
cases.push({ name, ok, actual, expected })
|
cases.push({ name, ok, actual, expected })
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
console.error(`FAIL ${name}`)
|
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('\\\\192.168.1.100\\share'), true)
|
||||||
eq('isNetworkPath //host', isNetworkPath('//nas/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)
|
eq('isNetworkPath empty', isNetworkPath(''), false)
|
||||||
|
|
||||||
// extractHostName
|
const unc = '\\\\192.168.1.100\\share\\a.pdf'
|
||||||
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(unc), '192.168.1.100')
|
||||||
eq('extractHostName //host/share', extractHostName('//nas/share'), 'nas')
|
eq('extractHostName //host/share', extractHostName('//nas/share'), 'nas')
|
||||||
eq('extractHostName local', extractHostName('D:\\data'), '')
|
eq('extractHostName local', extractHostName('D:\\data'), '')
|
||||||
eq('extractHostName empty', extractHostName(''), '')
|
eq('extractHostName empty', extractHostName(''), '')
|
||||||
|
|
||||||
// buildNetworkUrl
|
eq('extractDriveLetter Z', extractDriveLetter('Z:\\folder'), 'Z')
|
||||||
// 期望: \\192.168.1.100 -> 字面 '\\\\192.168.1.100'
|
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')
|
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 share', buildNetworkUrl('192.168.1.100', 'share'), '\\\\192.168.1.100\\share')
|
||||||
eq('buildNetworkUrl slashes stripped', buildNetworkUrl('host', '/share/'), '\\\\host\\share')
|
eq('buildNetworkUrl slashes stripped', buildNetworkUrl('host', '/share/'), '\\\\host\\share')
|
||||||
|
|
||||||
// buildNetInfo
|
eq(
|
||||||
// 期望产物是 JSON 文本(JSON 字符串里 \\ 表示 1 个 \ 字符)
|
'collectHosts unc + drive',
|
||||||
// 反序列化后 host_name 值是 2 个 \ 字符 -> JSON 文本中需要 4 个 \ 字符
|
collectHostsForCopyPaths(['\\\\192.168.1.100\\share\\a', 'Z:\\data'], { Z: '192.168.1.200' }),
|
||||||
// 4 个 \ 字符 = JS 字面 '\\\\\\\\' (8 个 \)
|
['192.168.1.100', '192.168.1.200']
|
||||||
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"}]')
|
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(
|
const r2 = buildNetInfo(
|
||||||
[
|
[
|
||||||
{ hostName: '192.168.1.100', userName: 'a', password: 'p' },
|
{ hostName: '192.168.1.100', userName: 'a', password: 'p' },
|
||||||
@@ -58,9 +56,8 @@ const r2 = buildNetInfo(
|
|||||||
],
|
],
|
||||||
() => null
|
() => 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
|
let threw = false
|
||||||
try {
|
try {
|
||||||
buildNetInfo([{ hostName: 'h1' }], () => null)
|
buildNetInfo([{ hostName: 'h1' }], () => null)
|
||||||
@@ -68,8 +65,6 @@ try {
|
|||||||
threw = e.message.includes('缺少网络凭据')
|
threw = e.message.includes('缺少网络凭据')
|
||||||
}
|
}
|
||||||
eq('buildNetInfo missing throws', threw, true)
|
eq('buildNetInfo missing throws', threw, true)
|
||||||
|
|
||||||
// empty -> empty string
|
|
||||||
eq('buildNetInfo empty list', buildNetInfo([], () => null), '')
|
eq('buildNetInfo empty list', buildNetInfo([], () => null), '')
|
||||||
|
|
||||||
const pass = cases.filter((c) => c.ok).length
|
const pass = cases.filter((c) => c.ok).length
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { openDesignApp } from '../services/open-design-app'
|
|||||||
import { writeJobCsv, type JobCsvRow } from '../utils/job-csv'
|
import { writeJobCsv, type JobCsvRow } from '../utils/job-csv'
|
||||||
import { parseSoonTemplate } from '../utils/parse-soon'
|
import { parseSoonTemplate } from '../utils/parse-soon'
|
||||||
import { stageJobPayloadJson } from '../utils/stage-job-payload'
|
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 { ensureDllInitialized, isDllInitAttempted } from '../services/dll-bootstrap'
|
||||||
import { loadDllModule } from '../services/dll-loader'
|
import { loadDllModule } from '../services/dll-loader'
|
||||||
import { tracedHandle } from './traced-handler'
|
import { tracedHandle } from './traced-handler'
|
||||||
@@ -103,6 +105,7 @@ export function registerIpcHandlers(): void {
|
|||||||
if (parsed.ok) {
|
if (parsed.ok) {
|
||||||
const snapshot: PrinterStatusSnapshot = {
|
const snapshot: PrinterStatusSnapshot = {
|
||||||
ribbonType: cached?.ribbonType ?? '—',
|
ribbonType: cached?.ribbonType ?? '—',
|
||||||
|
ribbonAmount: cached?.ribbonAmount ?? '—',
|
||||||
statusText: parsed.statusText,
|
statusText: parsed.statusText,
|
||||||
serialNo: cached?.serialNo ?? '—'
|
serialNo: cached?.serialNo ?? '—'
|
||||||
}
|
}
|
||||||
@@ -350,6 +353,29 @@ export function registerIpcHandlers(): void {
|
|||||||
return ok({ items })
|
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<SecretsPayload>) => {
|
||||||
|
if (!patch || typeof patch !== 'object') return ok(getSecrets())
|
||||||
|
const toMerge: Partial<SecretsPayload> = {}
|
||||||
|
if (patch.networkCredentials !== undefined) {
|
||||||
|
toMerge.networkCredentials = patch.networkCredentials
|
||||||
|
}
|
||||||
|
if (patch.dongleAuthCode !== undefined) {
|
||||||
|
toMerge.dongleAuthCode = patch.dongleAuthCode
|
||||||
|
}
|
||||||
|
return ok(setSecrets(toMerge))
|
||||||
|
})
|
||||||
|
|
||||||
tracedHandle(
|
tracedHandle(
|
||||||
'fs:write-job-csv',
|
'fs:write-job-csv',
|
||||||
(_e, payload: { taskId: string; rows: JobCsvRow[] }) => {
|
(_e, payload: { taskId: string; rows: JobCsvRow[] }) => {
|
||||||
|
|||||||
@@ -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<string, StoredNetworkCredential>
|
||||||
|
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<SecretsPayload>
|
||||||
|
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>): 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()
|
||||||
|
}
|
||||||
@@ -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<string>()
|
||||||
|
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<string, string> {
|
||||||
|
const map: Record<string, string> = {}
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -13,18 +13,20 @@ export interface ParsedSoonTemplate {
|
|||||||
frontImageUrl: string
|
frontImageUrl: string
|
||||||
backImageUrl: string
|
backImageUrl: string
|
||||||
fields: TemplateFieldRow[]
|
fields: TemplateFieldRow[]
|
||||||
printFlag: number
|
/** soon 模板 flag:1 双面 / 2 正面 / 3 背面 */
|
||||||
|
templateFlag: 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 {
|
/** soon 文件元数据 flag,用于与任务 print_flag 校验 */
|
||||||
|
export function readSoonTemplateFlag(raw: Record<string, unknown>): number {
|
||||||
const flag = Number(raw.flag)
|
const flag = Number(raw.flag)
|
||||||
if (flag === 1 || flag === 2) return flag
|
if (flag === 1 || flag === 2 || flag === 3) return flag
|
||||||
const hasBack =
|
const hasBack =
|
||||||
!!String(raw.backDisplayPic ?? '').trim() ||
|
!!String(raw.backDisplayPic ?? '').trim() ||
|
||||||
(Array.isArray(raw.backData) && raw.backData.length > 0)
|
(Array.isArray(raw.backData) && raw.backData.length > 0)
|
||||||
return hasBack ? 2 : 1
|
return hasBack ? 1 : 2
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickArray(obj: Record<string, unknown>, key: string): Record<string, unknown>[] {
|
function pickArray(obj: Record<string, unknown>, key: string): Record<string, unknown>[] {
|
||||||
@@ -96,7 +98,7 @@ function parseSoonWorkerDisk(soonPath: string, raw: Record<string, unknown>): Pa
|
|||||||
frontImageUrl: toImageUrl(soonPath, frontPic),
|
frontImageUrl: toImageUrl(soonPath, frontPic),
|
||||||
backImageUrl: toImageUrl(soonPath, backPic),
|
backImageUrl: toImageUrl(soonPath, backPic),
|
||||||
fields,
|
fields,
|
||||||
printFlag: readSoonPrintFlag(raw)
|
templateFlag: readSoonTemplateFlag(raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +134,12 @@ function parseSoonLegacy(soonPath: string, raw: Record<string, unknown>): Parsed
|
|||||||
fields.push({ label: toFieldLabel(name, side), value, originName: name, fieldType: 5 })
|
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<string, unknown>): ParsedSoonTemplate {
|
export function parseSoonTemplate(soonPath: string, raw: Record<string, unknown>): ParsedSoonTemplate {
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ const channels = {
|
|||||||
'fs:path-exists',
|
'fs:path-exists',
|
||||||
'fs:dir-size',
|
'fs:dir-size',
|
||||||
'fs:parse-soon',
|
'fs:parse-soon',
|
||||||
|
'fs:resolve-network-hosts',
|
||||||
'fs:write-job-csv',
|
'fs:write-job-csv',
|
||||||
|
'secrets:get',
|
||||||
|
'secrets:set',
|
||||||
'config:get',
|
'config:get',
|
||||||
'config:set',
|
'config:set',
|
||||||
'shell:open-path',
|
'shell:open-path',
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAppBootstrap } from '@/composables/useAppBootstrap'
|
import { useAppBootstrap } from '@/composables/useAppBootstrap'
|
||||||
|
import { usePrinterStatusPoll } from '@/composables/usePrinterStatusPoll'
|
||||||
|
|
||||||
useAppBootstrap()
|
useAppBootstrap()
|
||||||
|
usePrinterStatusPoll()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export async function fsParseSoon(filePath: string): Promise<
|
|||||||
frontImageUrl: string
|
frontImageUrl: string
|
||||||
backImageUrl: string
|
backImageUrl: string
|
||||||
fields: { label: string; value: string; originName: string; fieldType: number }[]
|
fields: { label: string; value: string; originName: string; fieldType: number }[]
|
||||||
printFlag: number
|
templateFlag: number
|
||||||
}>
|
}>
|
||||||
> {
|
> {
|
||||||
return api().invoke('fs:parse-soon', filePath) as Promise<
|
return api().invoke('fs:parse-soon', filePath) as Promise<
|
||||||
@@ -143,11 +143,32 @@ export async function fsParseSoon(filePath: string): Promise<
|
|||||||
frontImageUrl: string
|
frontImageUrl: string
|
||||||
backImageUrl: string
|
backImageUrl: string
|
||||||
fields: { label: string; value: string; originName: string; fieldType: number }[]
|
fields: { label: string; value: string; originName: string; fieldType: number }[]
|
||||||
printFlag: number
|
templateFlag: number
|
||||||
}>
|
}>
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fsResolveNetworkHosts(
|
||||||
|
paths: string[]
|
||||||
|
): Promise<IpcResult<{ hosts: string[]; driveHostMap: Record<string, string> }>> {
|
||||||
|
return api().invoke('fs:resolve-network-hosts', paths) as Promise<
|
||||||
|
IpcResult<{ hosts: string[]; driveHostMap: Record<string, string> }>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecretsPayloadDTO {
|
||||||
|
networkCredentials?: Record<string, { userName: string; password: string; lastUsed: string }>
|
||||||
|
dongleAuthCode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function secretsGet(): Promise<IpcResult<SecretsPayloadDTO>> {
|
||||||
|
return api().invoke('secrets:get') as Promise<IpcResult<SecretsPayloadDTO>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function secretsSet(patch: SecretsPayloadDTO): Promise<IpcResult<SecretsPayloadDTO>> {
|
||||||
|
return api().invoke('secrets:set', patch) as Promise<IpcResult<SecretsPayloadDTO>>
|
||||||
|
}
|
||||||
|
|
||||||
export async function configGet(): Promise<
|
export async function configGet(): Promise<
|
||||||
IpcResult<{
|
IpcResult<{
|
||||||
sharedDir: string
|
sharedDir: string
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<div v-if="mode" class="c-mode-badge c-mode-badge--home">{{ mode }}</div>
|
<div v-if="mode" class="c-mode-badge c-mode-badge--home">{{ mode }}</div>
|
||||||
<div class="c-status-capsule">
|
<div class="c-status-capsule">
|
||||||
<span>色带: <b>{{ status.ribbonType }}</b></span>
|
<span>色带: <b>{{ status.ribbonType }}</b></span>
|
||||||
|
<span>余量: <b>{{ status.ribbonAmount }}</b></span>
|
||||||
<span
|
<span
|
||||||
>状态: <b :class="statusTone">{{ status.statusText }}</b></span
|
>状态: <b :class="statusTone">{{ status.statusText }}</b></span
|
||||||
>
|
>
|
||||||
@@ -22,21 +23,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
import { useAppStore } from '@/stores/app'
|
|
||||||
import { refreshLiveStatus } 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 refreshLiveStatus()
|
|
||||||
})
|
|
||||||
|
|
||||||
const statusTone = computed(() => {
|
const statusTone = computed(() => {
|
||||||
const t = status.value.statusText
|
const t = status.value.statusText
|
||||||
if (t.includes('未初始化') || t.includes('未连接') || t.includes('失败')) return 'c-status-warn'
|
if (t.includes('未初始化') || t.includes('未连接') || t.includes('失败')) return 'c-status-warn'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<button type="button" class="npd-close" aria-label="关闭" @click="onCancel">×</button>
|
<button type="button" class="npd-close" aria-label="关闭" @click="onCancel">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="npd-body">
|
<div class="npd-body">
|
||||||
<p class="npd-hint">系统将按此 UNC 路径访问网络共享,请确认主机可访问且账号有效。</p>
|
<p class="npd-hint">凭据用于访问已添加的映射盘或网络共享路径,请确认主机可访问且账号有效。</p>
|
||||||
|
|
||||||
<label class="npd-field">
|
<label class="npd-field">
|
||||||
<span class="npd-label">主机(IP 或主机名)<span class="npd-req">*</span></span>
|
<span class="npd-label">主机(IP 或主机名)<span class="npd-req">*</span></span>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div v-if="previewUrl" class="npd-preview">
|
<div v-if="previewUrl" class="npd-preview">
|
||||||
<span class="npd-preview-label">将添加为</span>
|
<span class="npd-preview-label">目标 UNC</span>
|
||||||
<code class="npd-preview-path">{{ previewUrl }}</code>
|
<code class="npd-preview-path">{{ previewUrl }}</code>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -77,15 +77,22 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { buildNetworkUrl } from '@/utils/networkPath'
|
import { buildNetworkUrl } from '@shared/network-host'
|
||||||
|
import { useNetworkAuthStore } from '@/stores/networkAuth'
|
||||||
|
|
||||||
const props = defineProps<{ visible: boolean }>()
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
initialHost?: string
|
||||||
|
initialShare?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:visible': [boolean]
|
'update:visible': [boolean]
|
||||||
confirm: [{ path: string; hostName: string; userName: string; password: string }]
|
confirm: [{ path: string; hostName: string; userName: string; password: string }]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const netStore = useNetworkAuthStore()
|
||||||
|
|
||||||
const host = ref('')
|
const host = ref('')
|
||||||
const share = ref('')
|
const share = ref('')
|
||||||
const userName = ref('')
|
const userName = ref('')
|
||||||
@@ -139,6 +146,16 @@ watch(
|
|||||||
(v) => {
|
(v) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
reset()
|
reset()
|
||||||
|
if (props.initialHost?.trim()) host.value = props.initialHost.trim()
|
||||||
|
if (props.initialShare?.trim()) share.value = props.initialShare.trim()
|
||||||
|
const storedHost = host.value
|
||||||
|
if (storedHost) {
|
||||||
|
const cred = netStore.getCredential(storedHost)
|
||||||
|
if (cred) {
|
||||||
|
userName.value = cred.userName
|
||||||
|
password.value = cred.password
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { dllPrinterInfo, dllPrinterStatus, parsePrinterInfo } from '@/api/cardsoon'
|
import { dllPrinterInfo, dllPrinterStatus, parsePrinterInfo } from '@/api/cardsoon'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
|
|
||||||
/** SAPI_PrinterCheckstatus:仅刷新 Header 状态文案 */
|
|
||||||
export async function refreshLiveStatus(): Promise<void> {
|
export async function refreshLiveStatus(): Promise<void> {
|
||||||
const store = useConfigStore()
|
const store = useConfigStore()
|
||||||
try {
|
try {
|
||||||
@@ -10,11 +9,10 @@ export async function refreshLiveStatus(): Promise<void> {
|
|||||||
store.setPrinter({ ...store.printer, statusText: r.data.statusText })
|
store.setPrinter({ ...store.printer, statusText: r.data.statusText })
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* 无打印机时不阻塞 */
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GetPrinterInfoEx:仅刷新色带、序列号(启动时一次) */
|
|
||||||
export async function refreshPrinterInfo(): Promise<void> {
|
export async function refreshPrinterInfo(): Promise<void> {
|
||||||
const store = useConfigStore()
|
const store = useConfigStore()
|
||||||
try {
|
try {
|
||||||
@@ -27,14 +25,14 @@ export async function refreshPrinterInfo(): Promise<void> {
|
|||||||
store.setPrinter({
|
store.setPrinter({
|
||||||
...store.printer,
|
...store.printer,
|
||||||
ribbonType: parsed.ribbonType,
|
ribbonType: parsed.ribbonType,
|
||||||
|
ribbonAmount: parsed.ribbonAmount,
|
||||||
serialNo: parsed.serialNo
|
serialNo: parsed.serialNo
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
/* 无打印机时不阻塞 */
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 启动后:先拉静态信息,再查实时状态 */
|
|
||||||
export async function refreshPrinterAfterInit(): Promise<void> {
|
export async function refreshPrinterAfterInit(): Promise<void> {
|
||||||
await refreshPrinterInfo()
|
await refreshPrinterInfo()
|
||||||
await refreshLiveStatus()
|
await refreshLiveStatus()
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { refreshLiveStatus, refreshPrinterInfo } from '@/composables/usePrinterStatus'
|
||||||
|
|
||||||
|
const STATUS_ACTIVE_MS = 1500
|
||||||
|
const STATUS_HIDDEN_MS = 8000
|
||||||
|
const INFO_MS = 30000
|
||||||
|
|
||||||
|
let statusTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
let infoTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
let statusInFlight = false
|
||||||
|
let infoInFlight = false
|
||||||
|
let mountedCount = 0
|
||||||
|
|
||||||
|
function statusIntervalMs(): number {
|
||||||
|
if (typeof document !== 'undefined' && document.hidden) return STATUS_HIDDEN_MS
|
||||||
|
return STATUS_ACTIVE_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tickStatus(): Promise<void> {
|
||||||
|
if (statusInFlight) return
|
||||||
|
statusInFlight = true
|
||||||
|
try {
|
||||||
|
await refreshLiveStatus()
|
||||||
|
} finally {
|
||||||
|
statusInFlight = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tickInfo(): Promise<void> {
|
||||||
|
if (infoInFlight) return
|
||||||
|
infoInFlight = true
|
||||||
|
try {
|
||||||
|
await refreshPrinterInfo()
|
||||||
|
} finally {
|
||||||
|
infoInFlight = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTimers(): void {
|
||||||
|
if (statusTimer) {
|
||||||
|
clearInterval(statusTimer)
|
||||||
|
statusTimer = null
|
||||||
|
}
|
||||||
|
if (infoTimer) {
|
||||||
|
clearInterval(infoTimer)
|
||||||
|
infoTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTimers(): void {
|
||||||
|
clearTimers()
|
||||||
|
statusTimer = setInterval(() => void tickStatus(), statusIntervalMs())
|
||||||
|
infoTimer = setInterval(() => void tickInfo(), INFO_MS)
|
||||||
|
void tickStatus()
|
||||||
|
void tickInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVisibilityChange(): void {
|
||||||
|
if (mountedCount <= 0) return
|
||||||
|
startTimers()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePrinterStatusPoll(): void {
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
mountedCount += 1
|
||||||
|
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||||
|
if (appStore.initialized) startTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
mountedCount = Math.max(0, mountedCount - 1)
|
||||||
|
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||||
|
if (mountedCount === 0) clearTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => appStore.initialized,
|
||||||
|
(ready) => {
|
||||||
|
if (ready && mountedCount > 0) startTimers()
|
||||||
|
else if (!ready) clearTimers()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,38 +1,92 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
import { configGet } from '@/api/cardsoon'
|
import { configGet } from '@/api/cardsoon'
|
||||||
|
|
||||||
import { useNetworkAuthStore } from '@/stores/networkAuth'
|
import { useNetworkAuthStore } from '@/stores/networkAuth'
|
||||||
|
|
||||||
|
import { useDongleAuthStore } from '@/stores/dongleAuth'
|
||||||
|
|
||||||
|
import { useDistributeFormStore } from '@/stores/distributeForm'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
import './styles/design-base.css'
|
import './styles/design-base.css'
|
||||||
|
|
||||||
import './styles/icons-font.css'
|
import './styles/icons-font.css'
|
||||||
|
|
||||||
import './styles/shell.css'
|
import './styles/shell.css'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
window.cardsoonApi.on('app:trace', (payload) => {
|
window.cardsoonApi.on('app:trace', (payload) => {
|
||||||
|
|
||||||
const p = payload as { level: string; message: string; data?: Record<string, unknown> }
|
const p = payload as { level: string; message: string; data?: Record<string, unknown> }
|
||||||
|
|
||||||
if (p.level === 'error') console.error(p.message, p.data ?? '')
|
if (p.level === 'error') console.error(p.message, p.data ?? '')
|
||||||
|
|
||||||
else console.log(p.message, p.data ?? '')
|
else console.log(p.message, p.data ?? '')
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function setTrace(on: boolean): Promise<void> {
|
async function setTrace(on: boolean): Promise<void> {
|
||||||
|
|
||||||
await window.cardsoonApi.invoke('config:set', { traceEnabled: on })
|
await window.cardsoonApi.invoke('config:set', { traceEnabled: on })
|
||||||
|
|
||||||
console.info(`[trace] 控制台日志已${on ? '开启' : '关闭'}`)
|
console.info(`[trace] 控制台日志已${on ? '开启' : '关闭'}`)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const w = window as Window & { trace?: (on?: boolean) => Promise<void>; dllTrace?: (on?: boolean) => Promise<void> }
|
const w = window as Window & { trace?: (on?: boolean) => Promise<void>; dllTrace?: (on?: boolean) => Promise<void> }
|
||||||
|
|
||||||
w.trace = async (on = true) => setTrace(on)
|
w.trace = async (on = true) => setTrace(on)
|
||||||
|
|
||||||
w.dllTrace = w.trace
|
w.dllTrace = w.trace
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
|
|
||||||
const cfg = await configGet()
|
const cfg = await configGet()
|
||||||
|
|
||||||
const on = cfg.ok && cfg.data?.traceEnabled === true
|
const on = cfg.ok && cfg.data?.traceEnabled === true
|
||||||
|
|
||||||
console.info(`[trace] 控制台日志: ${on ? '已开启' : '已关闭'},执行 trace(false) 关闭`)
|
console.info(`[trace] 控制台日志: ${on ? '已开启' : '已关闭'},执行 trace(false) 关闭`)
|
||||||
|
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(createPinia())
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
app.use(pinia)
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
useNetworkAuthStore().loadFromStorage()
|
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
|
||||||
|
await useNetworkAuthStore().loadFromStorage()
|
||||||
|
|
||||||
|
const dongleStore = useDongleAuthStore()
|
||||||
|
|
||||||
|
await dongleStore.loadFromSecrets()
|
||||||
|
|
||||||
|
useDistributeFormStore().dongleAuthCode = dongleStore.authCode
|
||||||
|
|
||||||
|
})()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
||||||
|
|||||||
@@ -1,77 +1,162 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface TemplateFieldRow {
|
export interface TemplateFieldRow {
|
||||||
|
|
||||||
label: string
|
label: string
|
||||||
|
|
||||||
value: string
|
value: string
|
||||||
|
|
||||||
originName: string
|
originName: string
|
||||||
|
|
||||||
fieldType: number
|
fieldType: number
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface TemplatePreview {
|
export interface TemplatePreview {
|
||||||
|
|
||||||
frontImageUrl: string
|
frontImageUrl: string
|
||||||
|
|
||||||
backImageUrl: string
|
backImageUrl: string
|
||||||
|
|
||||||
fields: TemplateFieldRow[]
|
fields: TemplateFieldRow[]
|
||||||
printFlag: number
|
|
||||||
|
/** soon 模板 flag:1 双面 / 2 正面 / 3 背面 */
|
||||||
|
|
||||||
|
templateFlag: number
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface PathListItem {
|
export interface PathListItem {
|
||||||
|
|
||||||
path: string
|
path: string
|
||||||
|
|
||||||
meta: string
|
meta: string
|
||||||
|
|
||||||
sizeBytes: number
|
sizeBytes: number
|
||||||
isNetwork?: boolean
|
|
||||||
hostName?: string
|
|
||||||
userName?: string
|
|
||||||
password?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface DistributeFormState {
|
export interface DistributeFormState {
|
||||||
|
|
||||||
pathList: PathListItem[]
|
pathList: PathListItem[]
|
||||||
|
|
||||||
volumeLabel: string
|
volumeLabel: string
|
||||||
|
|
||||||
templateFile: string
|
templateFile: string
|
||||||
|
|
||||||
templatePreview: TemplatePreview | null
|
templatePreview: TemplatePreview | null
|
||||||
|
|
||||||
|
/** 任务 print_flag:1 双面 / 2 仅正面 / 3 仅背面 */
|
||||||
|
|
||||||
|
printFlag: number
|
||||||
|
|
||||||
copyType: 0 | 1
|
copyType: 0 | 1
|
||||||
|
|
||||||
formatType: 'none' | 'fat32' | 'exfat' | 'ntfs'
|
formatType: 'none' | 'fat32' | 'exfat' | 'ntfs'
|
||||||
|
|
||||||
dongleEnabled: boolean
|
dongleEnabled: boolean
|
||||||
|
|
||||||
/** 勾选加密狗时有效:0 默认,1-101 为次数(101=不限次数) */
|
/** 勾选加密狗时有效:0 默认,1-101 为次数(101=不限次数) */
|
||||||
|
|
||||||
dongleInstallCount: number
|
dongleInstallCount: number
|
||||||
|
|
||||||
|
dongleAuthCode: string
|
||||||
|
|
||||||
priority: 'low' | 'mid' | 'high'
|
priority: 'low' | 'mid' | 'high'
|
||||||
|
|
||||||
ribbonType: 'any' | 'YMCKO' | 'YMCK'
|
ribbonType: 'any' | 'YMCKO' | 'YMCK'
|
||||||
|
|
||||||
generateIso: boolean
|
generateIso: boolean
|
||||||
|
|
||||||
generateZip: boolean
|
generateZip: boolean
|
||||||
|
|
||||||
printCmdToHasi: boolean
|
printCmdToHasi: boolean
|
||||||
|
|
||||||
presetCopy: boolean
|
presetCopy: boolean
|
||||||
|
|
||||||
generateHasi: boolean
|
generateHasi: boolean
|
||||||
|
|
||||||
dongleCountCheck: boolean
|
dongleCountCheck: boolean
|
||||||
|
|
||||||
failPrintLabel: boolean
|
failPrintLabel: boolean
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function createDefaultForm(): DistributeFormState {
|
function createDefaultForm(): DistributeFormState {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
pathList: [],
|
pathList: [],
|
||||||
|
|
||||||
volumeLabel: 'DATA_CARD',
|
volumeLabel: 'DATA_CARD',
|
||||||
|
|
||||||
templateFile: '',
|
templateFile: '',
|
||||||
|
|
||||||
templatePreview: null,
|
templatePreview: null,
|
||||||
|
|
||||||
|
printFlag: 1,
|
||||||
|
|
||||||
copyType: 0,
|
copyType: 0,
|
||||||
|
|
||||||
formatType: 'fat32',
|
formatType: 'fat32',
|
||||||
|
|
||||||
dongleEnabled: false,
|
dongleEnabled: false,
|
||||||
|
|
||||||
dongleInstallCount: 0,
|
dongleInstallCount: 0,
|
||||||
|
|
||||||
|
dongleAuthCode: '',
|
||||||
|
|
||||||
priority: 'low',
|
priority: 'low',
|
||||||
|
|
||||||
ribbonType: 'any',
|
ribbonType: 'any',
|
||||||
|
|
||||||
generateIso: false,
|
generateIso: false,
|
||||||
|
|
||||||
generateZip: false,
|
generateZip: false,
|
||||||
|
|
||||||
printCmdToHasi: false,
|
printCmdToHasi: false,
|
||||||
|
|
||||||
presetCopy: false,
|
presetCopy: false,
|
||||||
|
|
||||||
generateHasi: false,
|
generateHasi: false,
|
||||||
|
|
||||||
dongleCountCheck: false,
|
dongleCountCheck: false,
|
||||||
|
|
||||||
failPrintLabel: false
|
failPrintLabel: false
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const useDistributeFormStore = defineStore('distributeForm', {
|
export const useDistributeFormStore = defineStore('distributeForm', {
|
||||||
|
|
||||||
state: (): DistributeFormState => createDefaultForm(),
|
state: (): DistributeFormState => createDefaultForm(),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
|
||||||
|
const preservedAuth = this.dongleAuthCode
|
||||||
|
|
||||||
Object.assign(this, createDefaultForm())
|
Object.assign(this, createDefaultForm())
|
||||||
|
|
||||||
|
this.dongleAuthCode = preservedAuth
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { secretsGet, secretsSet } from '@/api/cardsoon'
|
||||||
|
|
||||||
|
export const useDongleAuthStore = defineStore('dongleAuth', {
|
||||||
|
state: () => ({
|
||||||
|
authCode: '' as string,
|
||||||
|
loaded: false
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async loadFromSecrets(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const r = await secretsGet()
|
||||||
|
if (r.ok && r.data) {
|
||||||
|
this.authCode = r.data.dongleAuthCode || ''
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[dongleAuth] load failed', e)
|
||||||
|
}
|
||||||
|
this.loaded = true
|
||||||
|
},
|
||||||
|
async persist(authCode: string): Promise<void> {
|
||||||
|
this.authCode = authCode
|
||||||
|
try {
|
||||||
|
await secretsSet({ dongleAuthCode: authCode })
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[dongleAuth] persist failed', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,31 +1,44 @@
|
|||||||
// TODO: 后续用 electron safeStorage 加密
|
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { StoredCredential } from '@/types/network'
|
import type { StoredCredential } from '@/types/network'
|
||||||
|
import { secretsGet, secretsSet } from '@/api/cardsoon'
|
||||||
|
|
||||||
const STORAGE_KEY = 'networkCredentials'
|
const LEGACY_KEY = 'networkCredentials'
|
||||||
|
|
||||||
export const useNetworkAuthStore = defineStore('networkAuth', {
|
export const useNetworkAuthStore = defineStore('networkAuth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
credentials: {} as Record<string, StoredCredential>
|
credentials: {} as Record<string, StoredCredential>,
|
||||||
|
driveHosts: {} as Record<string, string>
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
hasCredentials: (state) => Object.keys(state.credentials).length > 0
|
hasCredentials: (state) => Object.keys(state.credentials).length > 0,
|
||||||
|
configuredHosts: (state) => Object.keys(state.credentials)
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
loadFromStorage(): void {
|
async loadFromStorage(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY)
|
const r = await secretsGet()
|
||||||
|
if (r.ok && r.data?.networkCredentials) {
|
||||||
|
this.credentials = { ...r.data.networkCredentials }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[networkAuth] secrets load failed', e)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LEGACY_KEY)
|
||||||
if (!raw) return
|
if (!raw) return
|
||||||
const parsed = JSON.parse(raw) as Record<string, StoredCredential>
|
const parsed = JSON.parse(raw) as Record<string, StoredCredential>
|
||||||
if (parsed && typeof parsed === 'object') {
|
if (parsed && typeof parsed === 'object') {
|
||||||
this.credentials = { ...this.credentials, ...parsed }
|
this.credentials = { ...parsed }
|
||||||
|
await this.persist()
|
||||||
|
localStorage.removeItem(LEGACY_KEY)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[networkAuth] loadFromStorage 失败,已忽略', e)
|
console.warn('[networkAuth] legacy load failed', e)
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem(STORAGE_KEY)
|
localStorage.removeItem(LEGACY_KEY)
|
||||||
} catch {
|
} catch {
|
||||||
// localStorage 不可用时静默忽略
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -37,7 +50,16 @@ export const useNetworkAuthStore = defineStore('networkAuth', {
|
|||||||
password: password || '',
|
password: password || '',
|
||||||
lastUsed: new Date().toISOString()
|
lastUsed: new Date().toISOString()
|
||||||
}
|
}
|
||||||
this.persist()
|
void this.persist()
|
||||||
|
},
|
||||||
|
setDriveHost(letter: string, hostName: string): void {
|
||||||
|
const L = (letter || '').trim().toUpperCase()
|
||||||
|
const host = (hostName || '').trim()
|
||||||
|
if (!L || L.length !== 1 || !host) return
|
||||||
|
this.driveHosts[L] = host
|
||||||
|
},
|
||||||
|
getDriveHostMap(): Record<string, string> {
|
||||||
|
return { ...this.driveHosts }
|
||||||
},
|
},
|
||||||
getCredential(hostName: string): StoredCredential | null {
|
getCredential(hostName: string): StoredCredential | null {
|
||||||
const host = (hostName || '').trim()
|
const host = (hostName || '').trim()
|
||||||
@@ -48,18 +70,19 @@ export const useNetworkAuthStore = defineStore('networkAuth', {
|
|||||||
const host = (hostName || '').trim()
|
const host = (hostName || '').trim()
|
||||||
if (!host) return
|
if (!host) return
|
||||||
if (delete this.credentials[host]) {
|
if (delete this.credentials[host]) {
|
||||||
this.persist()
|
void this.persist()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearAll(): void {
|
clearAll(): void {
|
||||||
this.credentials = {}
|
this.credentials = {}
|
||||||
this.persist()
|
this.driveHosts = {}
|
||||||
|
void this.persist()
|
||||||
},
|
},
|
||||||
persist(): void {
|
async persist(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.credentials))
|
await secretsSet({ networkCredentials: this.credentials })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[networkAuth] 持久化失败', e)
|
console.warn('[networkAuth] persist failed', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
export interface PrinterStatusDisplay {
|
export interface PrinterStatusDisplay {
|
||||||
ribbonType: string
|
ribbonType: string
|
||||||
|
ribbonAmount: string
|
||||||
statusText: string
|
statusText: string
|
||||||
serialNo: string
|
serialNo: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultPrinterStatus: PrinterStatusDisplay = {
|
export const defaultPrinterStatus: PrinterStatusDisplay = {
|
||||||
ribbonType: '—',
|
ribbonType: '—',
|
||||||
|
ribbonAmount: '—',
|
||||||
statusText: '—',
|
statusText: '—',
|
||||||
serialNo: '—'
|
serialNo: '—'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import type { DistributeFormState } from '@/stores/distributeForm'
|
import type { DistributeFormState } from '@/stores/distributeForm'
|
||||||
import { cleanPathPattern } from '@shared/path-pattern'
|
import { cleanPathPattern } from '@shared/path-pattern'
|
||||||
import { resolveJobTasks } from '@/utils/validateJobConfig'
|
import { resolveJobTasks } from '@/utils/validateJobConfig'
|
||||||
import { useNetworkAuthStore } from '@/stores/networkAuth'
|
|
||||||
import { buildNetInfo } from '@/utils/networkPath'
|
|
||||||
|
|
||||||
export interface BuildJobOptions {
|
export interface BuildJobOptions {
|
||||||
taskId: string
|
taskId: string
|
||||||
udfFile?: string
|
udfFile?: string
|
||||||
|
netInfo?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFileForApi(formatType: Exclude<DistributeFormState['formatType'], 'none'>): string {
|
function formatFileForApi(formatType: Exclude<DistributeFormState['formatType'], 'none'>): string {
|
||||||
@@ -40,6 +39,10 @@ export function buildJobConfig(
|
|||||||
dongle_install_count: form.dongleEnabled ? form.dongleInstallCount : -1
|
dongle_install_count: form.dongleEnabled ? form.dongleInstallCount : -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (form.dongleEnabled) {
|
||||||
|
body.auth_code = form.dongleAuthCode.trim()
|
||||||
|
}
|
||||||
|
|
||||||
if (needFormat) {
|
if (needFormat) {
|
||||||
body.format_file = formatFileForApi(form.formatType as Exclude<DistributeFormState['formatType'], 'none'>)
|
body.format_file = formatFileForApi(form.formatType as Exclude<DistributeFormState['formatType'], 'none'>)
|
||||||
}
|
}
|
||||||
@@ -50,7 +53,7 @@ export function buildJobConfig(
|
|||||||
|
|
||||||
if (hasPrint) {
|
if (hasPrint) {
|
||||||
body.json_file = form.templateFile.trim()
|
body.json_file = form.templateFile.trim()
|
||||||
body.print_flag = form.templatePreview?.printFlag ?? 1
|
body.print_flag = form.printFlag
|
||||||
const udf = opts.udfFile?.trim()
|
const udf = opts.udfFile?.trim()
|
||||||
if (udf) body.udf_file = udf
|
if (udf) body.udf_file = udf
|
||||||
}
|
}
|
||||||
@@ -59,12 +62,8 @@ export function buildJobConfig(
|
|||||||
if (form.generateZip) body.is_generate_zip = true
|
if (form.generateZip) body.is_generate_zip = true
|
||||||
if (form.failPrintLabel) body.is_printer_record_logo = true
|
if (form.failPrintLabel) body.is_printer_record_logo = true
|
||||||
|
|
||||||
const networkItems = form.pathList.filter((x) => x.isNetwork)
|
const netInfo = opts.netInfo?.trim()
|
||||||
if (networkItems.length > 0) {
|
|
||||||
const netStore = useNetworkAuthStore()
|
|
||||||
const netInfo = buildNetInfo(networkItems, (host) => netStore.getCredential(host))
|
|
||||||
if (netInfo) body.net_info = netInfo
|
if (netInfo) body.net_info = netInfo
|
||||||
}
|
|
||||||
|
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { cleanPathPattern } from '@shared/path-pattern'
|
||||||
|
import { buildNetInfo, collectHostsForCopyPaths } from '@shared/network-host'
|
||||||
|
import type { DistributeFormState } from '@/stores/distributeForm'
|
||||||
|
import { fsResolveNetworkHosts } from '@/api/cardsoon'
|
||||||
|
import { useNetworkAuthStore } from '@/stores/networkAuth'
|
||||||
|
|
||||||
|
async function mergedDriveHostMap(form: DistributeFormState): Promise<Record<string, string>> {
|
||||||
|
const paths = form.pathList.map((x) => cleanPathPattern(x.path))
|
||||||
|
const netStore = useNetworkAuthStore()
|
||||||
|
if (!paths.length) return { ...netStore.getDriveHostMap() }
|
||||||
|
const r = await fsResolveNetworkHosts(paths)
|
||||||
|
const fromNetUse = r.ok && r.data?.driveHostMap ? r.data.driveHostMap : {}
|
||||||
|
return { ...fromNetUse, ...netStore.getDriveHostMap() }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveCopyNetworkHosts(form: DistributeFormState): Promise<string[]> {
|
||||||
|
const paths = form.pathList.map((x) => cleanPathPattern(x.path))
|
||||||
|
if (!paths.length) return []
|
||||||
|
const driveHostMap = await mergedDriveHostMap(form)
|
||||||
|
return collectHostsForCopyPaths(paths, driveHostMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildNetInfoForForm(form: DistributeFormState): Promise<string> {
|
||||||
|
const hosts = await resolveCopyNetworkHosts(form)
|
||||||
|
if (!hosts.length) return ''
|
||||||
|
const netStore = useNetworkAuthStore()
|
||||||
|
return buildNetInfo(
|
||||||
|
hosts.map((h) => ({ hostName: h })),
|
||||||
|
(host) => netStore.getCredential(host)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { genTaskId } from '@shared/gen-task-id'
|
import { genTaskId } from '@shared/gen-task-id'
|
||||||
import { buildJobConfig } from '@/utils/buildJobConfig'
|
import { buildJobConfig } from '@/utils/buildJobConfig'
|
||||||
import { resolveJobTasks } from '@/utils/validateJobConfig'
|
import { resolveJobTasks } from '@/utils/validateJobConfig'
|
||||||
|
import { buildNetInfoForForm } from '@/utils/copyNetworkHosts'
|
||||||
import { dllJobCreate, fsWriteJobCsv } from '@/api/cardsoon'
|
import { dllJobCreate, fsWriteJobCsv } from '@/api/cardsoon'
|
||||||
import { useJobStore } from '@/stores/job'
|
import { useJobStore } from '@/stores/job'
|
||||||
import type { DistributeFormState } from '@/stores/distributeForm'
|
import type { DistributeFormState } from '@/stores/distributeForm'
|
||||||
@@ -12,7 +13,9 @@ function printFieldRows(form: DistributeFormState) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildJobJson(form: DistributeFormState): Promise<{ ok: true; json: string } | { ok: false; message: string }> {
|
async function buildJobJson(
|
||||||
|
form: DistributeFormState
|
||||||
|
): Promise<{ ok: true; json: string } | { ok: false; message: string }> {
|
||||||
const { hasCopy, hasPrint } = resolveJobTasks(form)
|
const { hasCopy, hasPrint } = resolveJobTasks(form)
|
||||||
if (!hasCopy && !hasPrint) {
|
if (!hasCopy && !hasPrint) {
|
||||||
return { ok: false, message: '请配置拷贝路径或打印模板' }
|
return { ok: false, message: '请配置拷贝路径或打印模板' }
|
||||||
@@ -31,8 +34,20 @@ async function buildJobJson(form: DistributeFormState): Promise<{ ok: true; json
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let netInfo = ''
|
||||||
|
if (hasCopy) {
|
||||||
try {
|
try {
|
||||||
return { ok: true, json: JSON.stringify(buildJobConfig(form, { taskId, udfFile })) }
|
netInfo = await buildNetInfoForForm(form)
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, message: e instanceof Error ? e.message : String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: JSON.stringify(buildJobConfig(form, { taskId, udfFile, netInfo }))
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { ok: false, message: e instanceof Error ? e.message : String(e) }
|
return { ok: false, message: e instanceof Error ? e.message : String(e) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
import type { DistributeFormState } from '@/stores/distributeForm'
|
import type { DistributeFormState } from '@/stores/distributeForm'
|
||||||
import { fsPathExists } from '@/api/cardsoon'
|
import { fsPathExists } from '@/api/cardsoon'
|
||||||
import { resolveJobTasks, validateJobConfig } from '@/utils/validateJobConfig'
|
import { resolveJobTasks, validateJobConfig } from '@/utils/validateJobConfig'
|
||||||
|
import { resolveCopyNetworkHosts } from '@/utils/copyNetworkHosts'
|
||||||
|
import { useNetworkAuthStore } from '@/stores/networkAuth'
|
||||||
|
|
||||||
function totalCopyBytes(form: DistributeFormState): number {
|
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 function printFlagMismatch(printFlag: number, templateFlag: number): boolean {
|
||||||
|
return (
|
||||||
|
(printFlag === 1 && templateFlag !== 1) ||
|
||||||
|
(printFlag === 2 && templateFlag === 3) ||
|
||||||
|
(printFlag === 3 && templateFlag === 2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
const { hasCopy, hasPrint } = resolveJobTasks(form)
|
const { hasCopy, hasPrint } = resolveJobTasks(form)
|
||||||
|
|
||||||
if (hasCopy) {
|
if (hasCopy) {
|
||||||
// 网络项在主进程 fs.existsSync 必返 false,跳过其存在性 / 总大小校验
|
const paths = form.pathList.map((x) => x.path)
|
||||||
const localItems = form.pathList.filter((x) => !x.isNetwork)
|
if (paths.length > 0) {
|
||||||
const localPaths = localItems.map((x) => x.path)
|
const ex = await fsPathExists(paths)
|
||||||
if (localPaths.length > 0) {
|
|
||||||
const ex = await fsPathExists(localPaths)
|
|
||||||
if (ex.ok && ex.data?.missing.length) {
|
if (ex.ok && ex.data?.missing.length) {
|
||||||
return `路径不存在: ${ex.data.missing.join(', ')}`
|
return `路径不存在: ${ex.data.missing.join(', ')}`
|
||||||
}
|
}
|
||||||
@@ -24,19 +33,41 @@ export async function validateJobPreflight(form: DistributeFormState): Promise<s
|
|||||||
return '拷贝路径下没有可拷贝的文件'
|
return '拷贝路径下没有可拷贝的文件'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hosts = await resolveCopyNetworkHosts(form)
|
||||||
|
if (hosts.length > 0) {
|
||||||
|
const netStore = useNetworkAuthStore()
|
||||||
|
for (const host of hosts) {
|
||||||
|
const cred = netStore.getCredential(host)
|
||||||
|
if (!cred?.userName?.trim() || !cred.password) {
|
||||||
|
return `请先配置网络位置凭据: ${host}`
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasPrint) {
|
if (hasPrint) {
|
||||||
const soon = form.templateFile.trim()
|
const soon = form.templateFile.trim()
|
||||||
const ex = await fsPathExists([soon])
|
const ex = await fsPathExists([soon])
|
||||||
if (ex.ok && ex.data?.missing.length) {
|
if (ex.ok && ex.data?.missing.length) {
|
||||||
return '模板文件不存在'
|
return '模板文件不存在'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const preview = form.templatePreview
|
||||||
|
if (preview && printFlagMismatch(form.printFlag, preview.templateFlag)) {
|
||||||
|
return '打印面数不匹配,请重新选择'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (form.dongleEnabled) {
|
if (form.dongleEnabled) {
|
||||||
|
if (!form.dongleAuthCode.trim()) {
|
||||||
|
return '请输入授权码'
|
||||||
|
}
|
||||||
const n = form.dongleInstallCount
|
const n = form.dongleInstallCount
|
||||||
if (!Number.isInteger(n) || n < 0 || n > 101) {
|
if (!Number.isInteger(n) || n < 0 || n > 101) {
|
||||||
return '加密狗次数须为 0 或 1-101'
|
return '加密狗次数须为 0 或 1-101'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<button type="button" class="c-button-cs" @click="addPath">
|
<button type="button" class="c-button-cs" @click="addPath">
|
||||||
添加路径
|
添加路径
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="c-button-cs" @click="networkDialogVisible = true">
|
<button type="button" class="c-button-cs" @click="openNetworkDialog">
|
||||||
添加网络位置
|
添加网络位置
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,6 +31,9 @@
|
|||||||
<AppIcon name="info-circle" size="sm" />
|
<AppIcon name="info-circle" size="sm" />
|
||||||
<span>系统将拷贝该目录下的所有子项,但不包含文件夹本身</span>
|
<span>系统将拷贝该目录下的所有子项,但不包含文件夹本身</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="configuredHosts.length" class="m-net-cred-hint">
|
||||||
|
已配置网络凭据:{{ configuredHosts.join('、') }}
|
||||||
|
</div>
|
||||||
<div class="m-panel-toolbar">
|
<div class="m-panel-toolbar">
|
||||||
<div class="toolbar-row">
|
<div class="toolbar-row">
|
||||||
<div class="toolbar-item">
|
<div class="toolbar-item">
|
||||||
@@ -67,14 +70,22 @@
|
|||||||
<span class="dog-hint">{{ dongleHint }}</span>
|
<span class="dog-hint">{{ dongleHint }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="formStore.dongleEnabled" class="toolbar-row">
|
||||||
|
<div class="toolbar-item toolbar-item--full">
|
||||||
|
<input
|
||||||
|
v-model="formStore.dongleAuthCode"
|
||||||
|
type="text"
|
||||||
|
class="c-input dog-auth"
|
||||||
|
placeholder="请输入授权码"
|
||||||
|
@input="onDongleAuthInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="c-panel__body">
|
<div class="c-panel__body">
|
||||||
<div v-for="(item, idx) in formStore.pathList" :key="idx" class="c-path-item">
|
<div v-for="(item, idx) in formStore.pathList" :key="idx" class="c-path-item">
|
||||||
<div class="c-path-item__info">
|
<div class="c-path-item__info">
|
||||||
<div class="c-path-item__name">
|
<div class="c-path-item__name">{{ item.path }}</div>
|
||||||
{{ item.path }}
|
|
||||||
<span v-if="item.isNetwork" class="c-path-item__tag">网络</span>
|
|
||||||
</div>
|
|
||||||
<div class="c-path-item__meta">{{ item.meta }}</div>
|
<div class="c-path-item__meta">{{ item.meta }}</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="c-path-item__delete" @click="removePath(idx)">
|
<button type="button" class="c-path-item__delete" @click="removePath(idx)">
|
||||||
@@ -98,8 +109,21 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="hasTemplatePreview && hasDoubleSide" class="m-print-side-picker">
|
||||||
|
<label class="m-print-side-option">
|
||||||
|
<input v-model="formStore.printFlag" type="radio" :value="1" />
|
||||||
|
<span>双面</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="c-preview-area">
|
<div class="c-preview-area">
|
||||||
<div class="c-card-small c-card-small--slot">
|
<div
|
||||||
|
class="c-card-small c-card-small--slot c-card-side-pick"
|
||||||
|
:class="{
|
||||||
|
'c-card-side--dim': formStore.printFlag === 3,
|
||||||
|
'c-card-side-pick--off': !canPickFront
|
||||||
|
}"
|
||||||
|
@click="selectPrintSide(2)"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-if="formStore.templatePreview?.frontImageUrl"
|
v-if="formStore.templatePreview?.frontImageUrl"
|
||||||
class="c-card-small__img"
|
class="c-card-small__img"
|
||||||
@@ -108,7 +132,14 @@
|
|||||||
/>
|
/>
|
||||||
<span v-else class="c-card-side-label">FRONT</span>
|
<span v-else class="c-card-side-label">FRONT</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="c-card-small c-card-small--slot c-card-small--back">
|
<div
|
||||||
|
class="c-card-small c-card-small--slot c-card-small--back c-card-side-pick"
|
||||||
|
:class="{
|
||||||
|
'c-card-side--dim': formStore.printFlag === 2,
|
||||||
|
'c-card-side-pick--off': !canPickBack
|
||||||
|
}"
|
||||||
|
@click="selectPrintSide(3)"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-if="formStore.templatePreview?.backImageUrl"
|
v-if="formStore.templatePreview?.backImageUrl"
|
||||||
class="c-card-small__img"
|
class="c-card-small__img"
|
||||||
@@ -147,13 +178,20 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
<NetworkPathDialog v-model:visible="networkDialogVisible" @confirm="onNetworkConfirm" />
|
<NetworkPathDialog
|
||||||
|
v-model:visible="networkDialogVisible"
|
||||||
|
:initial-host="networkDialogHost"
|
||||||
|
:initial-share="networkDialogShare"
|
||||||
|
@confirm="onNetworkConfirm"
|
||||||
|
/>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { cleanPathPattern } from '@shared/path-pattern'
|
||||||
|
import { extractDriveLetter } from '@shared/network-host'
|
||||||
import { notify, notifyRequireInit } from '@/composables/useNotify'
|
import { notify, notifyRequireInit } from '@/composables/useNotify'
|
||||||
import AppShell from '@/layouts/AppShell.vue'
|
import AppShell from '@/layouts/AppShell.vue'
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
import AppHeader from '@/components/AppHeader.vue'
|
||||||
@@ -168,6 +206,7 @@ import { useDistributeFormStore } from '@/stores/distributeForm'
|
|||||||
import { useJobStore } from '@/stores/job'
|
import { useJobStore } from '@/stores/job'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useNetworkAuthStore } from '@/stores/networkAuth'
|
import { useNetworkAuthStore } from '@/stores/networkAuth'
|
||||||
|
import { useDongleAuthStore } from '@/stores/dongleAuth'
|
||||||
import { validateJobPreflight } from '@/utils/validateJobPreflight'
|
import { validateJobPreflight } from '@/utils/validateJobPreflight'
|
||||||
import { createDistributeJob } from '@/utils/createDistributeJob'
|
import { createDistributeJob } from '@/utils/createDistributeJob'
|
||||||
import { formatBytesAsGb, formatBytesCompact } from '@/utils/formatBytes'
|
import { formatBytesAsGb, formatBytesCompact } from '@/utils/formatBytes'
|
||||||
@@ -178,7 +217,8 @@ import {
|
|||||||
dialogOpenSoon,
|
dialogOpenSoon,
|
||||||
dllJobCancel,
|
dllJobCancel,
|
||||||
fsDirSize,
|
fsDirSize,
|
||||||
fsParseSoon
|
fsParseSoon,
|
||||||
|
fsResolveNetworkHosts
|
||||||
} from '@/api/cardsoon'
|
} from '@/api/cardsoon'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -186,14 +226,21 @@ const formStore = useDistributeFormStore()
|
|||||||
const jobStore = useJobStore()
|
const jobStore = useJobStore()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const netStore = useNetworkAuthStore()
|
const netStore = useNetworkAuthStore()
|
||||||
|
const dongleStore = useDongleAuthStore()
|
||||||
|
|
||||||
const networkDialogVisible = ref(false)
|
const networkDialogVisible = ref(false)
|
||||||
|
const networkDialogHost = ref('')
|
||||||
|
const networkDialogShare = ref('')
|
||||||
|
|
||||||
|
let dongleAuthPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
const canUse = computed(() => appStore.initialized)
|
const canUse = computed(() => appStore.initialized)
|
||||||
const canSubmit = computed(
|
const canSubmit = computed(
|
||||||
() => canUse.value && !jobStore.submitting && appStore.mode !== 'usbCopying'
|
() => canUse.value && !jobStore.submitting && appStore.mode !== 'usbCopying'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const configuredHosts = computed(() => netStore.configuredHosts)
|
||||||
|
|
||||||
const totalLoadedBytes = computed(() =>
|
const totalLoadedBytes = computed(() =>
|
||||||
formStore.pathList.reduce((sum, item) => sum + (item.sizeBytes || 0), 0)
|
formStore.pathList.reduce((sum, item) => sum + (item.sizeBytes || 0), 0)
|
||||||
)
|
)
|
||||||
@@ -208,6 +255,14 @@ const hasTemplatePreview = computed(
|
|||||||
() => !!formStore.templatePreview && formStore.templatePreview.fields.length > 0
|
() => !!formStore.templatePreview && formStore.templatePreview.fields.length > 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const templateFlag = computed(() => formStore.templatePreview?.templateFlag ?? 0)
|
||||||
|
|
||||||
|
const hasDoubleSide = computed(() => templateFlag.value === 1)
|
||||||
|
|
||||||
|
const canPickFront = computed(() => templateFlag.value !== 3)
|
||||||
|
|
||||||
|
const canPickBack = computed(() => templateFlag.value !== 2)
|
||||||
|
|
||||||
const loadProgressText = computed(() => {
|
const loadProgressText = computed(() => {
|
||||||
const loadedGb = formatBytesAsGb(totalLoadedBytes.value)
|
const loadedGb = formatBytesAsGb(totalLoadedBytes.value)
|
||||||
return `已加载: ${loadedGb} GB / ${CARD_CAPACITY_GB} GB (${loadPercent.value}%)`
|
return `已加载: ${loadedGb} GB / ${CARD_CAPACITY_GB} GB (${loadPercent.value}%)`
|
||||||
@@ -228,6 +283,17 @@ function imageFieldLabel(value: string): string {
|
|||||||
return parts[parts.length - 1] || v
|
return parts[parts.length - 1] || v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultPrintFlagForTemplate(flag: number): number {
|
||||||
|
if (flag === 1 || flag === 2 || flag === 3) return flag
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPrintSide(flag: 2 | 3): void {
|
||||||
|
if (flag === 2 && !canPickFront.value) return
|
||||||
|
if (flag === 3 && !canPickBack.value) return
|
||||||
|
formStore.printFlag = flag
|
||||||
|
}
|
||||||
|
|
||||||
async function pickFieldImage(idx: number): Promise<void> {
|
async function pickFieldImage(idx: number): Promise<void> {
|
||||||
const preview = formStore.templatePreview
|
const preview = formStore.templatePreview
|
||||||
if (!preview) return
|
if (!preview) return
|
||||||
@@ -273,6 +339,13 @@ function onDongleCountInput(e: Event): void {
|
|||||||
formStore.dongleInstallCount = clampDongleCount(raw)
|
formStore.dongleInstallCount = clampDongleCount(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onDongleAuthInput(): void {
|
||||||
|
if (dongleAuthPersistTimer) clearTimeout(dongleAuthPersistTimer)
|
||||||
|
dongleAuthPersistTimer = setTimeout(() => {
|
||||||
|
void dongleStore.persist(formStore.dongleAuthCode)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
async function addPath(): Promise<void> {
|
async function addPath(): Promise<void> {
|
||||||
const r = await dialogOpenDirectory()
|
const r = await dialogOpenDirectory()
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
@@ -308,6 +381,26 @@ function removePath(idx: number): void {
|
|||||||
formStore.pathList.splice(idx, 1)
|
formStore.pathList.splice(idx, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openNetworkDialog(): Promise<void> {
|
||||||
|
networkDialogHost.value = ''
|
||||||
|
networkDialogShare.value = ''
|
||||||
|
for (const item of formStore.pathList) {
|
||||||
|
const dir = cleanPathPattern(item.path)
|
||||||
|
const letter = extractDriveLetter(dir)
|
||||||
|
if (!letter) continue
|
||||||
|
const r = await fsResolveNetworkHosts([dir])
|
||||||
|
if (r.ok && r.data?.driveHostMap?.[letter]) {
|
||||||
|
networkDialogHost.value = r.data.driveHostMap[letter]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (r.ok && r.data?.hosts?.length) {
|
||||||
|
networkDialogHost.value = r.data.hosts[0]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
networkDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
function onNetworkConfirm(payload: {
|
function onNetworkConfirm(payload: {
|
||||||
path: string
|
path: string
|
||||||
hostName: string
|
hostName: string
|
||||||
@@ -315,17 +408,12 @@ function onNetworkConfirm(payload: {
|
|||||||
password: string
|
password: string
|
||||||
}): void {
|
}): void {
|
||||||
netStore.setCredential(payload.hostName, payload.userName, payload.password)
|
netStore.setCredential(payload.hostName, payload.userName, payload.password)
|
||||||
formStore.pathList.push({
|
for (const item of formStore.pathList) {
|
||||||
path: payload.path,
|
const letter = extractDriveLetter(cleanPathPattern(item.path))
|
||||||
meta: '网络位置 · 待提交',
|
if (letter) netStore.setDriveHost(letter, payload.hostName)
|
||||||
sizeBytes: 0,
|
}
|
||||||
isNetwork: true,
|
|
||||||
hostName: payload.hostName,
|
|
||||||
userName: payload.userName,
|
|
||||||
password: payload.password
|
|
||||||
})
|
|
||||||
networkDialogVisible.value = false
|
networkDialogVisible.value = false
|
||||||
notify.info(`已添加网络位置: ${payload.path}`)
|
notify.success('网络凭据已保存')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pickTemplate(): Promise<void> {
|
async function pickTemplate(): Promise<void> {
|
||||||
@@ -343,6 +431,7 @@ async function pickTemplate(): Promise<void> {
|
|||||||
}
|
}
|
||||||
formStore.templateFile = soonPath
|
formStore.templateFile = soonPath
|
||||||
formStore.templatePreview = parsed.data
|
formStore.templatePreview = parsed.data
|
||||||
|
formStore.printFlag = defaultPrintFlagForTemplate(parsed.data.templateFlag)
|
||||||
const { fields, frontImageUrl, backImageUrl } = parsed.data
|
const { fields, frontImageUrl, backImageUrl } = parsed.data
|
||||||
if (!fields.length && !frontImageUrl && !backImageUrl) {
|
if (!fields.length && !frontImageUrl && !backImageUrl) {
|
||||||
notify.warning('模板已打开,但未解析到可预览内容')
|
notify.warning('模板已打开,但未解析到可预览内容')
|
||||||
@@ -393,16 +482,34 @@ async function onSubmit(): Promise<void> {
|
|||||||
<style src="@/styles/pages/page4.css"></style>
|
<style src="@/styles/pages/page4.css"></style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.c-path-item__tag {
|
.m-net-cred-hint {
|
||||||
display: inline-block;
|
margin: 0 8px 6px;
|
||||||
margin-left: 8px;
|
font-size: 12px;
|
||||||
padding: 0 6px;
|
color: #606266;
|
||||||
font-size: 10px;
|
}
|
||||||
line-height: 16px;
|
|
||||||
color: #fff;
|
.toolbar-item--full {
|
||||||
background: #409eff;
|
flex: 1;
|
||||||
border-radius: 8px;
|
}
|
||||||
vertical-align: middle;
|
|
||||||
|
.dog-auth {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-print-side-picker {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 6px 12px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-print-side-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-card-small--slot {
|
.c-card-small--slot {
|
||||||
@@ -431,4 +538,16 @@ async function onSubmit(): Promise<void> {
|
|||||||
.c-card-small--back {
|
.c-card-small--back {
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c-card-side-pick {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-card-side-pick--off {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-card-side--dim {
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { NetworkCredential, StoredCredential } from '@/types/network'
|
|
||||||
|
|
||||||
export function isNetworkPath(p: string): boolean {
|
export function isNetworkPath(p: string): boolean {
|
||||||
if (!p) return false
|
if (!p) return false
|
||||||
return p.startsWith('\\\\') || p.startsWith('//')
|
return p.startsWith('\\\\') || p.startsWith('//')
|
||||||
@@ -18,17 +16,44 @@ export function extractHostName(p: string): string {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractDriveLetter(p: string): string {
|
||||||
|
const trimmed = (p || '').trim()
|
||||||
|
const m = /^([A-Za-z]):[\\/]/.exec(trimmed)
|
||||||
|
return m ? m[1].toUpperCase() : ''
|
||||||
|
}
|
||||||
|
|
||||||
export function buildNetworkUrl(host: string, share: string): string {
|
export function buildNetworkUrl(host: string, share: string): string {
|
||||||
const h = host.trim()
|
const h = host.trim()
|
||||||
const s = share.trim().replace(/^[\\/]+/, '').replace(/[\\/]+$/, '')
|
const s = share.trim().replace(/^[\\/]+/, '').replace(/[\\/]+$/, '')
|
||||||
return s ? `\\\\${h}\\${s}` : `\\\\${h}`
|
return s ? `\\\\${h}\\${s}` : `\\\\${h}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function collectHostsForCopyPaths(
|
||||||
|
paths: string[],
|
||||||
|
driveHostMap: Record<string, string>
|
||||||
|
): string[] {
|
||||||
|
const hosts = new Set<string>()
|
||||||
|
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 && driveHostMap[letter]) {
|
||||||
|
hosts.add(driveHostMap[letter])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(hosts)
|
||||||
|
}
|
||||||
|
|
||||||
export function buildNetInfo(
|
export function buildNetInfo(
|
||||||
networkItems: { hostName?: string; userName?: string; password?: string }[],
|
networkItems: { hostName?: string; userName?: string; password?: string }[],
|
||||||
getStored: (host: string) => StoredCredential | null
|
getStored: (host: string) => { userName?: string; password?: string } | null
|
||||||
): string {
|
): string {
|
||||||
const creds: NetworkCredential[] = []
|
const creds: { host_name: string; user_name: string; password: string }[] = []
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
for (const it of networkItems) {
|
for (const it of networkItems) {
|
||||||
const host = (it.hostName || '').trim()
|
const host = (it.hostName || '').trim()
|
||||||
@@ -41,7 +66,7 @@ export function buildNetInfo(
|
|||||||
throw new Error(`缺少网络凭据: ${host}`)
|
throw new Error(`缺少网络凭据: ${host}`)
|
||||||
}
|
}
|
||||||
creds.push({
|
creds.push({
|
||||||
host_name: `\\\\${host}`,
|
host_name: host,
|
||||||
user_name,
|
user_name,
|
||||||
password
|
password
|
||||||
})
|
})
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export interface PrinterStatusSnapshot {
|
export interface PrinterStatusSnapshot {
|
||||||
ribbonType: string
|
ribbonType: string
|
||||||
|
ribbonAmount: string
|
||||||
statusText: string
|
statusText: string
|
||||||
serialNo: string
|
serialNo: string
|
||||||
}
|
}
|
||||||
@@ -59,10 +60,12 @@ function snapshotFromRecord(row: Record<string, unknown>): PrinterStatusSnapshot
|
|||||||
'PrinterSerial',
|
'PrinterSerial',
|
||||||
'PrinterName'
|
'PrinterName'
|
||||||
])
|
])
|
||||||
const ribbon = pickFirst(row, ['ribbon_type', 'RibbonType', 'RibbonAmount'])
|
const ribbonType = pickFirst(row, ['ribbon_type', 'RibbonType'])
|
||||||
|
const ribbonAmount = pickFirst(row, ['RibbonAmount', 'ribbon_amount'])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ribbonType: String(ribbon ?? '—'),
|
ribbonType: String(ribbonType ?? '—'),
|
||||||
|
ribbonAmount: String(ribbonAmount ?? '—'),
|
||||||
statusText: '—',
|
statusText: '—',
|
||||||
serialNo: String(serial ?? '—')
|
serialNo: String(serial ?? '—')
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user