更新
This commit is contained in:
+20
-1
@@ -8,7 +8,8 @@
|
||||
"node": "16.15.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "electron-vite dev -- --skip-dll-init",
|
||||
"dev": "electron-vite dev",
|
||||
"dev:dll": "electron-vite dev -- --with-dll",
|
||||
"build": "electron-vite build",
|
||||
"preview": "electron-vite preview",
|
||||
"typecheck": "vue-tsc --noEmit -p tsconfig.web.json",
|
||||
@@ -39,12 +40,20 @@
|
||||
{
|
||||
"from": "resources/native",
|
||||
"to": "native"
|
||||
},
|
||||
{
|
||||
"from": "resources/cardsoon.config.json",
|
||||
"to": "cardsoon.config.json"
|
||||
}
|
||||
],
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "resources/native/CapSettings.json",
|
||||
"to": "CapSettings.json"
|
||||
},
|
||||
{
|
||||
"from": "resources/cardsoon.config.json",
|
||||
"to": "cardsoon.config.json"
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
@@ -52,6 +61,16 @@
|
||||
"nsis"
|
||||
],
|
||||
"signAndEditExecutable": false
|
||||
},
|
||||
"mac": {
|
||||
"target": [
|
||||
"dmg"
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage"
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"designAppPath": "C:\\myData\\projects\\sideline\\shanghaikashu\\SoonMachine\\app\\release\\卡树数据卡打印系统-0.0.1-win\\卡树数据卡打印系统.exe"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"designAppPath": "C:\\myData\\projects\\sideline\\shanghaikashu\\SoonMachine\\app\\release\\卡树数据卡打印系统-0.0.1-win\\卡树数据卡打印系统.exe"
|
||||
}
|
||||
+14
-3
@@ -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()
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
/** true:IPC/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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user