This commit is contained in:
24kycj
2026-05-28 08:41:10 +08:00
parent 22b17aed35
commit 505e72ce28
19 changed files with 589 additions and 134 deletions
+14 -3
View File
@@ -5,6 +5,8 @@ app.commandLine.appendSwitch('disable-gpu-shader-disk-cache')
import log from 'electron-log'
import { suppressKnownDllStderr } from './utils/suppress-dll-stderr'
import { loadAppFileConfig } from './services/app-config'
import { migrateTraceConfig, setTraceWebContents } from './utils/trace-bridge'
import { setupNativeWorkingDir } from './services/native-path'
import { configStore } from './services/config-store'
@@ -49,6 +51,7 @@ function createWindow(): void {
})
setPollMainWindow(mainWindow)
setTraceWebContents(mainWindow.webContents)
mainWindow.on('ready-to-show', () => {
if (mainWindow) {
@@ -79,6 +82,7 @@ function createWindow(): void {
})
mainWindow.on('closed', () => {
setTraceWebContents(null)
mainWindow = null
})
@@ -93,11 +97,18 @@ app.whenReady().then(() => {
try {
if (app.isPackaged) {
configStore.set('skipDllInit', false)
} else if (process.argv.includes('--skip-dll-init')) {
configStore.set('skipDllInit', true)
log.warn('skipDllInit enabled by argv: --skip-dll-init (dev only)')
} else {
const withDll =
process.argv.includes('--with-dll') ||
process.argv.includes('--no-skip-dll-init')
configStore.set('skipDllInit', !withDll)
if (!withDll) {
log.info('skipDllInit enabled (dev default); use npm run dev:dll to load workDll')
}
}
migrateTraceConfig()
loadAppFileConfig()
setupNativeWorkingDir()
registerIpcHandlers()
createWindow()
+72 -38
View File
@@ -1,12 +1,10 @@
import { app, dialog, ipcMain, shell } from 'electron'
import { app, dialog, shell } from 'electron'
import fs from 'fs'
import path from 'path'
import log from 'electron-log'
import { CS_FAIL, CS_OK } from '../constants'
import { assertNotBusy, assertReady, mainAppState } from '../services/app-state'
import { configStore } from '../services/config-store'
import {
getActiveJobId,
startJobPoll,
startUsbPoll,
stopAllPolls,
@@ -14,7 +12,10 @@ import {
stopUsbPoll,
getPollMainWindow
} from '../services/poll-manager'
import { parsePrinterInfoFromDll, type PrinterStatusSnapshot } from '@shared/printer-info'
import { cleanPathPattern, getDirectorySizeBytes } from '../utils/dir-size'
import { getDesignAppPath } from '../services/app-config'
import { openDesignApp } from '../services/open-design-app'
import { parseSoonTemplate } from '../utils/parse-soon'
import {
dllAdminJobCancel,
@@ -28,6 +29,7 @@ import {
isCancelApiAvailable,
isRejectApiAvailable
} from '../services/work-dll.service'
import { tracedHandle } from './traced-handler'
function ok<T>(data?: T) {
return { ok: true as const, code: CS_OK, data }
@@ -39,16 +41,23 @@ function fail(code: number, message: string) {
let dllInitAttempted = false
function parseBool(v: unknown): boolean {
return v === true || v === 'true' || v === 1 || v === '1' || String(v).toLowerCase() === 'true'
}
export function registerIpcHandlers(): void {
ipcMain.handle('dll:init', (_e, params) => {
tracedHandle('dll:init', (_e, params) => {
if (dllInitAttempted) {
return fail(CS_FAIL, '请勿重复初始化,请完全退出应用后重新启动再试')
return ok({
skipped: true,
printerReady: false,
warning: '已初始化,跳过重复 Init'
})
}
stopAllPolls()
try {
const sharedDir = params?.sharedDir || (configStore.get('sharedDir') as string)
fs.mkdirSync(sharedDir, { recursive: true })
log.info(`SAPI_Init starting, sharedDir=${sharedDir}`)
const code = dllInit({
sharedDir,
keepCombinedImage: params?.keepCombinedImage,
@@ -59,36 +68,51 @@ export function registerIpcHandlers(): void {
logLevel: params?.logLevel,
outBack: params?.outBack
})
log.info(`SAPI_Init finished, code=${code}`)
dllInitAttempted = true
mainAppState.initialized = true
configStore.set('sharedDir', sharedDir)
if (code === CS_OK) {
return ok()
return ok({ code, printerReady: true })
}
log.warn(`SAPI_Init returned ${code}; UI ready, printer ops may fail until device connected`)
return ok({
code,
printerReady: false,
warning: '打印机未连接或驱动未就绪,界面可浏览,接好设备后可在设置中重试 Init'
})
} catch (err) {
mainAppState.initialized = false
log.error('dll:init', err)
return fail(CS_FAIL, String(err))
}
})
ipcMain.handle('dll:printer-info', () => {
tracedHandle('dll:printer-info', () => {
try {
assertReady()
const r = dllGetPrinterInfo()
if (!r.json) return fail(0, '未连接打印机')
return ok(r.json)
if (!r.json) {
const cached = configStore.get('lastPrinterStatus')
if (cached) {
return ok({
...cached,
fromCache: true,
liveError: r.code <= 0 ? '未连接打印机' : `GetPrinterInfo code=${r.code}`
})
}
return fail(0, '未连接打印机')
}
const snapshot = parsePrinterInfoFromDll(r.json)
configStore.set('lastPrinterStatus', snapshot)
return ok({ ...r.json, snapshot })
} catch (err) {
const cached = configStore.get('lastPrinterStatus')
if (cached) {
return ok({ ...cached, fromCache: true, liveError: String(err) })
}
return fail(CS_FAIL, String(err))
}
})
ipcMain.handle('dll:printer-reset', () => {
tracedHandle('dll:printer-reset', () => {
try {
assertReady()
const code = dllPrinterReset()
@@ -98,7 +122,7 @@ export function registerIpcHandlers(): void {
}
})
ipcMain.handle('dll:printer-reject', () => {
tracedHandle('dll:printer-reject', () => {
try {
assertReady()
if (!isRejectApiAvailable()) return fail(CS_FAIL, 'REJECT_API_UNAVAILABLE')
@@ -109,7 +133,7 @@ export function registerIpcHandlers(): void {
}
})
ipcMain.handle('dll:printer-error-str', (_e, errorNo?: number) => {
tracedHandle('dll:printer-error-str', (_e, errorNo?: number) => {
try {
assertReady()
return ok({ text: dllGetPrinterErrorStr(errorNo ?? -1) })
@@ -118,7 +142,7 @@ export function registerIpcHandlers(): void {
}
})
ipcMain.handle('dll:job-create', (_e, json: string) => {
tracedHandle('dll:job-create', (_e, json: string) => {
try {
assertReady()
assertNotBusy()
@@ -133,7 +157,7 @@ export function registerIpcHandlers(): void {
}
})
ipcMain.handle('dll:job-cancel', (_e, jobId: string) => {
tracedHandle('dll:job-cancel', (_e, jobId: string) => {
try {
assertReady()
const id = jobId || mainAppState.activeJobId
@@ -141,8 +165,6 @@ export function registerIpcHandlers(): void {
let code = CS_OK
if (isCancelApiAvailable()) {
code = dllAdminJobCancel(id)
} else {
log.info('dll:job-cancel: SAPI_AdminJobCancel not in DLL, poll stopped only')
}
mainAppState.mode = 'ready'
mainAppState.activeJobId = ''
@@ -152,7 +174,7 @@ export function registerIpcHandlers(): void {
}
})
ipcMain.handle('dll:usb-copy', (_e, req: { destFolder: string; cardOutput: number }) => {
tracedHandle('dll:usb-copy', (_e, req: { destFolder: string; cardOutput: number }) => {
try {
assertReady()
assertNotBusy()
@@ -168,28 +190,28 @@ export function registerIpcHandlers(): void {
}
})
ipcMain.handle('poll:job-start', (_e, jobId: string) => {
tracedHandle('poll:job-start', (_e, jobId: string) => {
startJobPoll(jobId)
return ok()
})
ipcMain.handle('poll:job-stop', () => {
tracedHandle('poll:job-stop', () => {
stopJobPoll(true)
return ok()
})
ipcMain.handle('poll:usb-start', () => {
tracedHandle('poll:usb-start', () => {
startUsbPoll()
return ok()
})
ipcMain.handle('poll:usb-stop', () => {
tracedHandle('poll:usb-stop', () => {
stopUsbPoll()
mainAppState.mode = 'ready'
return ok()
})
ipcMain.handle('dialog:open-directory', async () => {
tracedHandle('dialog:open-directory', async () => {
const win = getPollMainWindow()
const r = await dialog.showOpenDialog(win ?? undefined, {
properties: ['openDirectory', 'multiSelections']
@@ -198,7 +220,7 @@ export function registerIpcHandlers(): void {
return ok({ paths: r.filePaths })
})
ipcMain.handle('dialog:open-file', async (_e, filters?: { name: string; extensions: string[] }[]) => {
tracedHandle('dialog:open-file', async (_e, filters?: { name: string; extensions: string[] }[]) => {
const win = getPollMainWindow()
const r = await dialog.showOpenDialog(win ?? undefined, {
properties: ['openFile'],
@@ -208,7 +230,7 @@ export function registerIpcHandlers(): void {
return ok({ path: r.filePaths[0] })
})
ipcMain.handle('fs:path-exists', (_e, paths: string[]) => {
tracedHandle('fs:path-exists', (_e, paths: string[]) => {
const missing = paths
.map((raw) => ({ raw, dir: cleanPathPattern(raw) }))
.filter(({ dir }) => !dir || !fs.existsSync(dir))
@@ -216,7 +238,7 @@ export function registerIpcHandlers(): void {
return ok({ missing })
})
ipcMain.handle('fs:dir-size', (_e, paths: string[]) => {
tracedHandle('fs:dir-size', (_e, paths: string[]) => {
const items = paths.map((raw) => {
const dir = cleanPathPattern(raw)
if (!fs.existsSync(dir)) return { path: raw, bytes: 0, missing: true as const }
@@ -231,7 +253,7 @@ export function registerIpcHandlers(): void {
return ok({ items })
})
ipcMain.handle('fs:parse-soon', (_e, filePath: string) => {
tracedHandle('fs:parse-soon', (_e, filePath: string) => {
try {
const soonPath = String(filePath || '').trim()
if (!soonPath) return fail(CS_FAIL, '模板路径为空')
@@ -244,14 +266,18 @@ export function registerIpcHandlers(): void {
}
})
ipcMain.handle('config:get', () => {
tracedHandle('config:get', () => {
const payload: {
sharedDir: string
templateDir: string
traceEnabled: boolean
lastPrinterStatus?: PrinterStatusSnapshot
skipDllInit?: boolean
} = {
sharedDir: configStore.get('sharedDir'),
templateDir: configStore.get('templateDir')
templateDir: configStore.get('templateDir'),
traceEnabled: configStore.get('traceEnabled', true),
lastPrinterStatus: configStore.get('lastPrinterStatus')
}
if (!app.isPackaged) {
payload.skipDllInit = configStore.get('skipDllInit', false)
@@ -259,13 +285,15 @@ export function registerIpcHandlers(): void {
return ok(payload)
})
ipcMain.handle('config:set', (_e, patch: Record<string, unknown>) => {
tracedHandle('config:set', (_e, patch: Record<string, unknown>) => {
Object.entries(patch).forEach(([k, v]) => {
if (k === 'skipDllInit') {
if (app.isPackaged) return
const b =
v === true || v === 'true' || v === 1 || v === '1' || String(v).toLowerCase() === 'true'
configStore.set(k, b)
configStore.set(k, parseBool(v))
return
}
if (k === 'traceEnabled' || k === 'dllTraceEnabled') {
configStore.set('traceEnabled', parseBool(v))
return
}
configStore.set(k, v as string)
@@ -273,14 +301,20 @@ export function registerIpcHandlers(): void {
return ok(configStore.store)
})
ipcMain.handle('shell:open-path', (_e, target: string) => {
tracedHandle('shell:open-path', (_e, target: string) => {
const dir = target || (configStore.get('templateDir') as string)
fs.mkdirSync(dir, { recursive: true })
shell.openPath(dir)
return ok()
})
ipcMain.handle('dll:reject-available', () => ok({ available: isRejectApiAvailable() }))
tracedHandle('design:open', async () => {
const r = await openDesignApp(getDesignAppPath())
if (!r.ok) return fail(CS_FAIL, r.message)
return ok()
})
tracedHandle('dll:reject-available', () => ok({ available: isRejectApiAvailable() }))
}
export async function handleBeforeQuit(): Promise<void> {
+46
View File
@@ -0,0 +1,46 @@
import { ipcMain, type IpcMainInvokeEvent } from 'electron'
import { emitTrace } from '../utils/trace-bridge'
type IpcHandler = (event: IpcMainInvokeEvent, ...args: unknown[]) => unknown | Promise<unknown>
function summarizeArgs(args: unknown[]): Record<string, unknown> {
if (args.length === 0) return {}
const a = args[0]
if (typeof a === 'string') {
if (a.length > 240) return { text: `${a.slice(0, 240)}`, bytes: a.length }
return { arg0: a }
}
if (typeof a === 'object' && a !== null) return { ...(a as Record<string, unknown>) }
return { arg0: a }
}
function summarizeResult(r: unknown): Record<string, unknown> {
if (!r || typeof r !== 'object') return { value: r }
const o = r as { ok?: boolean; code?: number; message?: string; data?: unknown }
const out: Record<string, unknown> = {}
if (o.ok !== undefined) out.ok = o.ok
if (o.code !== undefined) out.code = o.code
if (o.message) out.message = o.message
if (o.data !== undefined && o.data !== null && typeof o.data === 'object') {
const d = o.data as Record<string, unknown>
for (const k of ['warning', 'skipped', 'jobId', 'path', 'paths', 'missing', 'available']) {
if (k in d) out[k] = d[k]
}
}
return out
}
export function tracedHandle(channel: string, handler: IpcHandler): void {
ipcMain.handle(channel, async (event, ...args) => {
const start = Date.now()
emitTrace(`[ipc] ${channel}`, summarizeArgs(args))
try {
const result = await handler(event, ...args)
emitTrace(`[ipc] ${channel}`, { ms: Date.now() - start, ...summarizeResult(result) })
return result
} catch (e) {
emitTrace(`[ipc] ${channel}`, { ms: Date.now() - start, error: String(e) }, 'error')
throw e
}
})
}
+72
View File
@@ -0,0 +1,72 @@
import { app } from 'electron'
import fs from 'fs'
import path from 'path'
import log from 'electron-log'
import { getProcessExecDir } from './native-path'
export const APP_CONFIG_FILENAME = 'cardsoon.config.json'
/** 与 cardsoon.config.json 键名一致,后续配置在此扩展 */
export interface AppFileConfig {
designAppPath: string
}
const defaults: AppFileConfig = {
designAppPath: ''
}
let cached: AppFileConfig | null = null
let loadedFrom = ''
function bundledConfigPath(): string {
if (app.isPackaged) {
return path.join(process.resourcesPath, APP_CONFIG_FILENAME)
}
return path.join(app.getAppPath(), 'resources', APP_CONFIG_FILENAME)
}
function configSearchPaths(): string[] {
const besideExe = path.join(getProcessExecDir(), APP_CONFIG_FILENAME)
const bundled = bundledConfigPath()
if (besideExe === bundled) return [besideExe]
return [besideExe, bundled]
}
function parseConfigFile(filePath: string): AppFileConfig {
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>
return {
designAppPath: String(raw.designAppPath ?? '').trim()
}
}
export function loadAppFileConfig(): AppFileConfig {
if (cached) return cached
for (const filePath of configSearchPaths()) {
if (!fs.existsSync(filePath)) continue
try {
cached = parseConfigFile(filePath)
loadedFrom = filePath
log.info(`Loaded ${APP_CONFIG_FILENAME} from ${filePath}`)
return cached
} catch (e) {
log.warn(`Skip invalid ${APP_CONFIG_FILENAME}: ${filePath}`, e)
}
}
cached = { ...defaults }
loadedFrom = ''
log.warn(
`${APP_CONFIG_FILENAME} not found (checked: ${configSearchPaths().join(', ')}), using defaults`
)
return cached
}
export function getAppConfigLoadedPath(): string {
loadAppFileConfig()
return loadedFrom
}
export function getDesignAppPath(): string {
return loadAppFileConfig().designAppPath
}
+7 -1
View File
@@ -1,12 +1,17 @@
import Store from 'electron-store'
import { app } from 'electron'
import path from 'path'
import type { PrinterStatusSnapshot } from '@shared/printer-info'
interface AppConfig {
sharedDir: string
templateDir: string
/** G2 门禁 false:启动即 SAPI_Init;仅调试可改 true */
skipDllInit: boolean
/** trueIPC/DLL 等调用输出到 DevTools 控制台 */
traceEnabled: boolean
/** 上次成功的 GetPrinterInfo 解析结果,供离线/失败时展示 */
lastPrinterStatus?: PrinterStatusSnapshot
}
const defaultShared = path.join('C:', 'PrintTasks')
@@ -17,6 +22,7 @@ export const configStore = new Store<AppConfig>({
sharedDir: defaultShared,
templateDir: path.join(app.getPath('userData'), 'Cardsoon', 'templates'),
// 正式版始终 Init;仅开发时可通过 --skip-dll-init 临时跳过
skipDllInit: false
skipDllInit: false,
traceEnabled: true
}
})
+30
View File
@@ -0,0 +1,30 @@
import { shell } from 'electron'
import fs from 'fs'
import path from 'path'
export function validateDesignAppPath(exePath: string): { ok: true } | { ok: false; message: string } {
const p = exePath.trim()
if (!p) {
return { ok: false, message: '请在 cardsoon.config.json 中配置 designAppPath' }
}
const resolved = path.resolve(p)
if (!fs.existsSync(resolved)) {
return { ok: false, message: `设计软件不存在: ${resolved}` }
}
return { ok: true }
}
/** 由系统启动外部程序;空字符串表示成功,非空为失败原因 */
export async function openDesignApp(
exePath: string
): Promise<{ ok: true } | { ok: false; message: string }> {
const check = validateDesignAppPath(exePath)
if (!check.ok) return check
const target = path.resolve(exePath.trim())
const err = await shell.openPath(target)
if (err) {
return { ok: false, message: err }
}
return { ok: true }
}
+9 -4
View File
@@ -2,6 +2,7 @@ import { BrowserWindow } from 'electron'
import log from 'electron-log'
import { POLL_INTERVAL_MS } from '../constants'
import { mainAppState } from './app-state'
import { emitTrace } from '../utils/trace-bridge'
import { dllGetJobStateById, dllGetUsbCopyState } from './work-dll.service'
let jobTimer: ReturnType<typeof setInterval> | null = null
@@ -56,7 +57,7 @@ export function startJobPoll(id: string): void {
const cancelled = r.jobState === 6
const finished = r.jobState === 100
const terminal = failed || cancelled
send('job:poll-tick', {
const tick = {
jobId,
queryErrorCode: r.queryErrorCode,
jobState: r.jobState,
@@ -65,7 +66,9 @@ export function startJobPoll(id: string): void {
failed,
cancelled,
finished
})
}
emitTrace('[poll] job:poll-tick', tick)
send('job:poll-tick', tick)
if (r.queryErrorCode !== 0) {
log.warn('GetJobStateById query failed', r.queryErrorCode)
stopJobPoll(true)
@@ -89,13 +92,15 @@ export function startUsbPoll(): void {
const failed = r.taskStatus === 3
const success = r.taskStatus === 2
const terminal = failed || success
send('usb:poll-tick', {
const tick = {
taskStatus: r.taskStatus,
progress: r.progress,
terminal,
failed,
success
})
}
emitTrace('[poll] usb:poll-tick', tick)
send('usb:poll-tick', tick)
if (terminal) {
stopUsbPoll()
mainAppState.mode = 'ready'
+118 -58
View File
@@ -1,7 +1,7 @@
import path from 'path'
import koffi from 'koffi'
import log from 'electron-log'
import { CS_OK, JOB_ID_BUF_SIZE, LOG_FATAL_FLAG } from '../constants'
import { emitTrace, isTraceEnabled } from '../utils/trace-bridge'
import { getNativeDir } from './native-path'
export interface InitParams {
@@ -42,6 +42,27 @@ let hasCancelApi = false
let loggedCancelMissing = false
let loggedRejectMissing = false
function tracePayload(r: unknown): Record<string, unknown> {
if (r === null || r === undefined) return {}
if (typeof r !== 'object') return { value: r }
return { ...(r as Record<string, unknown>) }
}
function traceCall<T>(name: string, args: Record<string, unknown> | undefined, fn: () => T): T {
if (!isTraceEnabled()) return fn()
const tag = `[dll] ${name}`
const start = Date.now()
emitTrace(`${tag}`, args)
try {
const r = fn()
emitTrace(`${tag}`, { ms: Date.now() - start, ...tracePayload(r) })
return r
} catch (e) {
emitTrace(`${tag}`, { ms: Date.now() - start, error: String(e) }, 'error')
throw e
}
}
function loadLibrary(): void {
if (lib) return
const dllPath = path.join(getNativeDir(), 'workDll.dll')
@@ -66,7 +87,7 @@ function loadLibrary(): void {
hasCancelApi = false
if (!loggedCancelMissing) {
loggedCancelMissing = true
log.info('SAPI_AdminJobCancel not in workDll (optional); stop uses poll-stop only')
emitTrace('[dll] SAPI_AdminJobCancel not in workDll (optional)')
}
}
@@ -77,7 +98,7 @@ function loadLibrary(): void {
hasRejectApi = false
if (!loggedRejectMissing) {
loggedRejectMissing = true
log.info('SAPI_PrinterMovetoreject not in workDll (optional); reject card disabled')
emitTrace('[dll] SAPI_PrinterMovetoreject not in workDll (optional)')
}
}
}
@@ -93,46 +114,73 @@ export function isCancelApiAvailable(): boolean {
}
export function dllInit(params: InitParams): number {
loadLibrary()
return SAPI_Init!(
params.sharedDir,
params.keepCombinedImage ?? true,
params.stopOnFailure ?? false,
params.cleanTaskFile ?? true,
params.autoRetryTimes ?? 0,
params.rejectConfig ?? false,
params.logLevel ?? LOG_FATAL_FLAG,
params.outBack ?? false
) as number
return traceCall(
'SAPI_Init',
{
sharedDir: params.sharedDir,
keepCombinedImage: params.keepCombinedImage ?? true,
stopOnFailure: params.stopOnFailure ?? false,
cleanTaskFile: params.cleanTaskFile ?? true,
autoRetryTimes: params.autoRetryTimes ?? 0,
rejectConfig: params.rejectConfig ?? false,
logLevel: params.logLevel ?? LOG_FATAL_FLAG,
outBack: params.outBack ?? false
},
() => {
loadLibrary()
return SAPI_Init!(
params.sharedDir,
params.keepCombinedImage ?? true,
params.stopOnFailure ?? false,
params.cleanTaskFile ?? true,
params.autoRetryTimes ?? 0,
params.rejectConfig ?? false,
params.logLevel ?? LOG_FATAL_FLAG,
params.outBack ?? false
) as number
}
)
}
export function dllGetPrinterInfo(): { code: number; json?: Record<string, unknown> } {
loadLibrary()
const outPtr = koffi.alloc('void *', 8)
try {
const len = SAPI_GetPrinterInfo!(outPtr) as number
if (len <= 0) return { code: len }
const ptr = koffi.decode(outPtr, 0, 'void *') as number
const jsonStr = koffi.decode(ptr, 'char', len) as string
koffi.free(ptr)
return { code: len, json: JSON.parse(jsonStr) as Record<string, unknown> }
} finally {
koffi.free(outPtr)
}
return traceCall('SAPI_GetPrinterInfo', undefined, () => {
loadLibrary()
const outPtr = koffi.alloc('void *', 8)
try {
const len = SAPI_GetPrinterInfo!(outPtr) as number
if (len <= 0) return { code: len }
const ptr = koffi.decode(outPtr, 0, 'void *') as number
if (!ptr) return { code: len }
const jsonStr = koffi.decode(ptr, 'char', len) as string
koffi.free(ptr)
if (!jsonStr?.trim()) return { code: len }
try {
return { code: len, json: JSON.parse(jsonStr) as Record<string, unknown> }
} catch {
return { code: len }
}
} finally {
koffi.free(outPtr)
}
})
}
export function dllGetPrinterErrorStr(errorNo = -1): string {
loadLibrary()
const s = SAPI_GetPrinterErrorStr!(errorNo) as string
return s || ''
return traceCall('SAPI_GetPrinterErrorStr', { errorNo }, () => {
loadLibrary()
const s = SAPI_GetPrinterErrorStr!(errorNo) as string
return s || ''
})
}
export function dllRestJobEx(json: string): { code: number; jobId: string } {
loadLibrary()
const buf = Buffer.alloc(JOB_ID_BUF_SIZE)
const code = SAPI_RestJobEx!(json, buf, JOB_ID_BUF_SIZE) as number
const jobId = buf.toString('utf8').replace(/\0.*$/, '').trim()
return { code, jobId }
return traceCall('SAPI_RestJobEx', { jsonBytes: Buffer.byteLength(json ?? '', 'utf8') }, () => {
loadLibrary()
const buf = Buffer.alloc(JOB_ID_BUF_SIZE)
const code = SAPI_RestJobEx!(json, buf, JOB_ID_BUF_SIZE) as number
const jobId = buf.toString('utf8').replace(/\0.*$/, '').trim()
return { code, jobId }
})
}
export function dllGetJobStateById(jobId: string): {
@@ -140,43 +188,55 @@ export function dllGetJobStateById(jobId: string): {
jobState: number
progress: number
} {
loadLibrary()
const jobState = [0]
const copyScheduler = [0]
const queryErrorCode = SAPI_GetJobStateById!(jobId, jobState, copyScheduler) as number
return {
queryErrorCode,
jobState: jobState[0],
progress: copyScheduler[0]
}
return traceCall('SAPI_GetJobStateById', { jobId }, () => {
loadLibrary()
const jobState = [0]
const copyScheduler = [0]
const queryErrorCode = SAPI_GetJobStateById!(jobId, jobState, copyScheduler) as number
return {
queryErrorCode,
jobState: jobState[0],
progress: copyScheduler[0]
}
})
}
export function dllAdminJobCancel(jobId: string): number {
loadLibrary()
if (!SAPI_AdminJobCancel) throw new Error('CANCEL_API_UNAVAILABLE')
return SAPI_AdminJobCancel(jobId) as number
return traceCall('SAPI_AdminJobCancel', { jobId }, () => {
loadLibrary()
if (!SAPI_AdminJobCancel) throw new Error('CANCEL_API_UNAVAILABLE')
return SAPI_AdminJobCancel(jobId) as number
})
}
export function dllCopyFromUsb(destFolder: string, cardOutput: number): number {
loadLibrary()
return SAPI_CopyFromUsb!(destFolder, cardOutput) as number
return traceCall('SAPI_CopyFromUsb', { destFolder, cardOutput }, () => {
loadLibrary()
return SAPI_CopyFromUsb!(destFolder, cardOutput) as number
})
}
export function dllGetUsbCopyState(): { taskStatus: number; progress: number } {
loadLibrary()
const taskStatus = [0]
const progress = [0]
SAPI_GetUsbCopyState!(taskStatus, progress)
return { taskStatus: taskStatus[0], progress: progress[0] }
return traceCall('SAPI_GetUsbCopyState', undefined, () => {
loadLibrary()
const taskStatus = [0]
const progress = [0]
SAPI_GetUsbCopyState!(taskStatus, progress)
return { taskStatus: taskStatus[0], progress: progress[0] }
})
}
export function dllPrinterReset(): number {
loadLibrary()
return SAPI_PrinterResetprinter!() as number
return traceCall('SAPI_PrinterResetprinter', undefined, () => {
loadLibrary()
return SAPI_PrinterResetprinter!() as number
})
}
export function dllPrinterReject(): number {
loadLibrary()
if (!SAPI_PrinterMovetoreject) throw new Error('REJECT_API_UNAVAILABLE')
return SAPI_PrinterMovetoreject() as number
return traceCall('SAPI_PrinterMovetoreject', undefined, () => {
loadLibrary()
if (!SAPI_PrinterMovetoreject) throw new Error('REJECT_API_UNAVAILABLE')
return SAPI_PrinterMovetoreject() as number
})
}
+38
View File
@@ -0,0 +1,38 @@
import type { WebContents } from 'electron'
import { configStore } from '../services/config-store'
export interface TracePayload {
level: 'info' | 'error'
message: string
data?: Record<string, unknown>
}
let target: WebContents | null = null
export function setTraceWebContents(wc: WebContents | null): void {
target = wc && !wc.isDestroyed() ? wc : null
}
export function isTraceEnabled(): boolean {
return configStore.get('traceEnabled', true)
}
export function emitTrace(
message: string,
data?: Record<string, unknown>,
level: 'info' | 'error' = 'info'
): void {
if (!isTraceEnabled()) return
if (!target || target.isDestroyed()) return
target.send('app:trace', { level, message, data } satisfies TracePayload)
}
/** 升级旧配置:曾写入 dllTraceEnabled:false 会导致一直无日志 */
export function migrateTraceConfig(): void {
if (configStore.get('_traceV2') === true) return
configStore.set('traceEnabled', true)
configStore.set('_traceV2', true)
if (configStore.has('dllTraceEnabled')) {
configStore.delete('dllTraceEnabled')
}
}
+2 -1
View File
@@ -22,9 +22,10 @@ const channels = {
'config:get',
'config:set',
'shell:open-path',
'design:open',
'dll:reject-available'
] as const,
on: ['job:poll-tick', 'usb:poll-tick'] as const
on: ['job:poll-tick', 'usb:poll-tick', 'app:trace'] as const
}
const cardsoonApi = {
+20 -12
View File
@@ -1,3 +1,4 @@
import { parsePrinterInfoFromDll } from '@shared/printer-info'
import type { InitParamsDTO, IpcResult, JobPollPayload, UsbPollPayload } from '@/types/ipc'
import type { PrinterStatusDisplay } from '@/types/printer'
@@ -14,16 +15,7 @@ export async function dllPrinterInfo(): Promise<IpcResult<Record<string, unknown
}
export function parsePrinterInfo(json: Record<string, unknown>): PrinterStatusDisplay {
const list = (json.printerList as Record<string, unknown>[]) || []
const p = list[0] || {}
const serial =
p.szPrinterSerial ?? p.PrinterSerial ?? p.SerialNo ?? p.PrinterName ?? '—'
return {
ribbonType: String(p.RibbonType ?? '—'),
statusText: String(p.PrinterType ?? '—'),
serialNo: String(serial),
printedCount: Number(p.PrintedCount ?? 0)
}
return parsePrinterInfoFromDll(json)
}
export async function dllPrinterReset(): Promise<IpcResult> {
@@ -109,13 +101,29 @@ export async function fsParseSoon(filePath: string): Promise<
}
export async function configGet(): Promise<
IpcResult<{ sharedDir: string; templateDir: string; skipDllInit?: boolean }>
IpcResult<{
sharedDir: string
templateDir: string
traceEnabled: boolean
lastPrinterStatus?: PrinterStatusDisplay
skipDllInit?: boolean
}>
> {
return api().invoke('config:get') as Promise<
IpcResult<{ sharedDir: string; templateDir: string; skipDllInit?: boolean }>
IpcResult<{
sharedDir: string
templateDir: string
traceEnabled: boolean
lastPrinterStatus?: PrinterStatusDisplay
skipDllInit?: boolean
}>
>
}
export async function openDesignApp(): Promise<IpcResult> {
return api().invoke('design:open') as Promise<IpcResult>
}
export async function configSet(patch: Record<string, unknown>): Promise<IpcResult> {
return api().invoke('config:set', patch) as Promise<IpcResult>
}
@@ -7,11 +7,34 @@ import {
dllRejectAvailable,
parsePrinterInfo
} from '@/api/cardsoon'
import type { PrinterStatusDisplay } from '@/types/printer'
import { useAppStore } from '@/stores/app'
import { useConfigStore } from '@/stores/config'
let bootstrapped = false
function applyPrinterPayload(
configStore: ReturnType<typeof useConfigStore>,
data: Record<string, unknown>
): void {
const snapshot = data.snapshot as PrinterStatusDisplay | undefined
if (snapshot) {
configStore.setPrinter(snapshot)
return
}
if (data.fromCache && !data.printerList) {
const { fromCache: _f, liveError: _e, ribbonType, statusText, serialNo, printedCount } = data
configStore.setPrinter({
ribbonType: String(ribbonType ?? '—'),
statusText: String(statusText ?? '—'),
serialNo: String(serialNo ?? '—'),
printedCount: Number(printedCount ?? 0)
})
return
}
configStore.setPrinter(parsePrinterInfo(data))
}
export function useAppBootstrap(): {
retryInit: () => Promise<void>
refreshHeader: () => Promise<void>
@@ -19,20 +42,46 @@ export function useAppBootstrap(): {
const appStore = useAppStore()
const configStore = useConfigStore()
async function hydratePrinterFromLocal(): Promise<void> {
const cfg = await configGet()
if (cfg.ok && cfg.data?.lastPrinterStatus) {
configStore.setPrinter(cfg.data.lastPrinterStatus)
}
}
async function refreshHeader(): Promise<void> {
const info = await dllPrinterInfo()
if (info.ok && info.data) {
configStore.setPrinter(parsePrinterInfo(info.data))
applyPrinterPayload(configStore, info.data)
if (info.data.fromCache) {
const msg = String(info.data.liveError || '未连接打印机')
configStore.setPrinter({
...configStore.printer,
statusText: msg
})
}
return
}
configStore.setPrinter({ ...configStore.printer, statusText: '未连接打印机' })
const cfg = await configGet()
if (cfg.ok && cfg.data?.lastPrinterStatus) {
configStore.setPrinter({
...cfg.data.lastPrinterStatus,
statusText: info.message || '未连接打印机'
})
return
}
configStore.setPrinter({
...configStore.printer,
statusText: info.message || '未连接打印机'
})
}
async function doInit(): Promise<void> {
await hydratePrinterFromLocal()
const cfg = await configGet()
const sharedDir = cfg.data?.sharedDir || ''
configStore.setSharedDir(sharedDir)
if (import.meta.env.DEV && cfg.data?.skipDllInit === true) {
appStore.setInitialized(false, '开发模式已跳过 DLL 初始化')
configStore.setPrinter({ ...configStore.printer, statusText: '未初始化(开发)' })
@@ -47,15 +96,27 @@ export function useAppBootstrap(): {
return
}
appStore.setInitialized(true)
configStore.setPrinter({ ...configStore.printer, statusText: '系统已初始化' })
const warn = (init.data as { warning?: string } | undefined)?.warning
if (warn) notify.warning(warn)
try {
const rej = await dllRejectAvailable()
if (rej.ok && rej.data) configStore.rejectApiAvailable = rej.data.available
} catch {
/* optional API */
const initMeta = init.data as
| { warning?: string; skipped?: boolean; printerReady?: boolean }
| undefined
if (initMeta?.warning) notify.warning(initMeta.warning)
// Init 未就绪时 GetPrinterInfo 可能触发原生 DLL 崩溃,仅用本地缓存
if (initMeta?.skipped) {
await hydratePrinterFromLocal()
return
}
if (initMeta?.printerReady === true) {
await refreshHeader()
try {
const rej = await dllRejectAvailable()
if (rej.ok && rej.data) configStore.rejectApiAvailable = rej.data.available
} catch {
/* optional API */
}
return
}
await hydratePrinterFromLocal()
}
onMounted(() => {
+22
View File
@@ -1,11 +1,33 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { configGet } from '@/api/cardsoon'
import App from './App.vue'
import router from './router'
import './styles/design-base.css'
import './styles/icons-font.css'
import './styles/shell.css'
window.cardsoonApi.on('app:trace', (payload) => {
const p = payload as { level: string; message: string; data?: Record<string, unknown> }
if (p.level === 'error') console.error(p.message, p.data ?? '')
else console.log(p.message, p.data ?? '')
})
async function setTrace(on: boolean): Promise<void> {
await window.cardsoonApi.invoke('config:set', { traceEnabled: on })
console.info(`[trace] 控制台日志已${on ? '开启' : '关闭'}`)
}
const w = window as Window & { trace?: (on?: boolean) => Promise<void>; dllTrace?: (on?: boolean) => Promise<void> }
w.trace = async (on = true) => setTrace(on)
w.dllTrace = w.trace
void (async () => {
const cfg = await configGet()
const on = cfg.ok && cfg.data?.traceEnabled === true
console.info(`[trace] 控制台日志: ${on ? '已开启' : '已关闭'},执行 trace(false) 关闭`)
})()
const app = createApp(App)
app.use(createPinia())
app.use(router)
+7 -4
View File
@@ -58,7 +58,7 @@ import AppFooter from '@/components/AppFooter.vue'
import AppIcon from '@/components/AppIcon.vue'
import { useAppStore } from '@/stores/app'
import { useConfigStore } from '@/stores/config'
import { dllPrinterReject, dllPrinterReset, shellOpenTemplateDir } from '@/api/cardsoon'
import { dllPrinterReject, dllPrinterReset, openDesignApp } from '@/api/cardsoon'
const router = useRouter()
const appStore = useAppStore()
@@ -90,9 +90,12 @@ async function onReject(): Promise<void> {
}
async function onTemplate(): Promise<void> {
if (!guardInit('打开模板目录')) return
const r = await shellOpenTemplateDir()
if (!r.ok) notify.error(r.message || '打开模板目录失败')
const r = await openDesignApp()
if (!r.ok) {
notify.error(r.message || '打开设计软件失败,请检查 cardsoon.config.json')
return
}
notify.success('已启动设计软件')
}
function guardBusy(): boolean {
+33
View File
@@ -0,0 +1,33 @@
/** DLL GetPrinterInfo JSON → Header 展示字段(与 mocks/printer 一致) */
export interface PrinterStatusSnapshot {
ribbonType: string
statusText: string
serialNo: string
printedCount: number
}
export function parsePrinterInfoFromDll(json: Record<string, unknown>): PrinterStatusSnapshot {
const list = (json.printerList as Record<string, unknown>[]) || []
const p = list[0] || {}
const serial =
p.szPrinterSerial ?? p.PrinterSerial ?? p.SerialNo ?? p.PrinterName ?? '—'
let statusText = '—'
const direct = p.PrinterType ?? p.PrinterStatus ?? p.Status
if (direct != null && String(direct).trim() !== '') {
statusText = String(direct)
} else {
const remain = p.RibbonRemain ?? p.RemainCount
const capacity = p.RibbonCapacity ?? p.Capacity ?? p.MaxCount
if (remain != null && capacity != null) {
statusText = `${remain}/${capacity}`
}
}
return {
ribbonType: String(p.RibbonType ?? '—'),
statusText,
serialNo: String(serial),
printedCount: Number(p.PrintedCount ?? p.PrintCount ?? 0)
}
}