更新
This commit is contained in:
+20
-1
@@ -8,7 +8,8 @@
|
|||||||
"node": "16.15.0"
|
"node": "16.15.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "electron-vite dev -- --skip-dll-init",
|
"dev": "electron-vite dev",
|
||||||
|
"dev:dll": "electron-vite dev -- --with-dll",
|
||||||
"build": "electron-vite build",
|
"build": "electron-vite build",
|
||||||
"preview": "electron-vite preview",
|
"preview": "electron-vite preview",
|
||||||
"typecheck": "vue-tsc --noEmit -p tsconfig.web.json",
|
"typecheck": "vue-tsc --noEmit -p tsconfig.web.json",
|
||||||
@@ -39,12 +40,20 @@
|
|||||||
{
|
{
|
||||||
"from": "resources/native",
|
"from": "resources/native",
|
||||||
"to": "native"
|
"to": "native"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/cardsoon.config.json",
|
||||||
|
"to": "cardsoon.config.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"extraFiles": [
|
"extraFiles": [
|
||||||
{
|
{
|
||||||
"from": "resources/native/CapSettings.json",
|
"from": "resources/native/CapSettings.json",
|
||||||
"to": "CapSettings.json"
|
"to": "CapSettings.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/cardsoon.config.json",
|
||||||
|
"to": "cardsoon.config.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
@@ -52,6 +61,16 @@
|
|||||||
"nsis"
|
"nsis"
|
||||||
],
|
],
|
||||||
"signAndEditExecutable": false
|
"signAndEditExecutable": false
|
||||||
|
},
|
||||||
|
"mac": {
|
||||||
|
"target": [
|
||||||
|
"dmg"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": [
|
||||||
|
"AppImage"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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 log from 'electron-log'
|
||||||
import { suppressKnownDllStderr } from './utils/suppress-dll-stderr'
|
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 { setupNativeWorkingDir } from './services/native-path'
|
||||||
import { configStore } from './services/config-store'
|
import { configStore } from './services/config-store'
|
||||||
|
|
||||||
@@ -49,6 +51,7 @@ function createWindow(): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
setPollMainWindow(mainWindow)
|
setPollMainWindow(mainWindow)
|
||||||
|
setTraceWebContents(mainWindow.webContents)
|
||||||
|
|
||||||
mainWindow.on('ready-to-show', () => {
|
mainWindow.on('ready-to-show', () => {
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
@@ -79,6 +82,7 @@ function createWindow(): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
|
setTraceWebContents(null)
|
||||||
mainWindow = null
|
mainWindow = null
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -93,11 +97,18 @@ app.whenReady().then(() => {
|
|||||||
try {
|
try {
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
configStore.set('skipDllInit', false)
|
configStore.set('skipDllInit', false)
|
||||||
} else if (process.argv.includes('--skip-dll-init')) {
|
} else {
|
||||||
configStore.set('skipDllInit', true)
|
const withDll =
|
||||||
log.warn('skipDllInit enabled by argv: --skip-dll-init (dev only)')
|
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()
|
setupNativeWorkingDir()
|
||||||
registerIpcHandlers()
|
registerIpcHandlers()
|
||||||
createWindow()
|
createWindow()
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { app, dialog, ipcMain, shell } from 'electron'
|
import { app, dialog, shell } from 'electron'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
import { CS_FAIL, CS_OK } from '../constants'
|
import { CS_FAIL, CS_OK } from '../constants'
|
||||||
import { assertNotBusy, assertReady, mainAppState } from '../services/app-state'
|
import { assertNotBusy, assertReady, mainAppState } from '../services/app-state'
|
||||||
import { configStore } from '../services/config-store'
|
import { configStore } from '../services/config-store'
|
||||||
import {
|
import {
|
||||||
getActiveJobId,
|
|
||||||
startJobPoll,
|
startJobPoll,
|
||||||
startUsbPoll,
|
startUsbPoll,
|
||||||
stopAllPolls,
|
stopAllPolls,
|
||||||
@@ -14,7 +12,10 @@ import {
|
|||||||
stopUsbPoll,
|
stopUsbPoll,
|
||||||
getPollMainWindow
|
getPollMainWindow
|
||||||
} from '../services/poll-manager'
|
} from '../services/poll-manager'
|
||||||
|
import { parsePrinterInfoFromDll, type PrinterStatusSnapshot } from '@shared/printer-info'
|
||||||
import { cleanPathPattern, getDirectorySizeBytes } from '../utils/dir-size'
|
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 { parseSoonTemplate } from '../utils/parse-soon'
|
||||||
import {
|
import {
|
||||||
dllAdminJobCancel,
|
dllAdminJobCancel,
|
||||||
@@ -28,6 +29,7 @@ import {
|
|||||||
isCancelApiAvailable,
|
isCancelApiAvailable,
|
||||||
isRejectApiAvailable
|
isRejectApiAvailable
|
||||||
} from '../services/work-dll.service'
|
} from '../services/work-dll.service'
|
||||||
|
import { tracedHandle } from './traced-handler'
|
||||||
|
|
||||||
function ok<T>(data?: T) {
|
function ok<T>(data?: T) {
|
||||||
return { ok: true as const, code: CS_OK, data }
|
return { ok: true as const, code: CS_OK, data }
|
||||||
@@ -39,16 +41,23 @@ function fail(code: number, message: string) {
|
|||||||
|
|
||||||
let dllInitAttempted = false
|
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 {
|
export function registerIpcHandlers(): void {
|
||||||
ipcMain.handle('dll:init', (_e, params) => {
|
tracedHandle('dll:init', (_e, params) => {
|
||||||
if (dllInitAttempted) {
|
if (dllInitAttempted) {
|
||||||
return fail(CS_FAIL, '请勿重复初始化,请完全退出应用后重新启动再试')
|
return ok({
|
||||||
|
skipped: true,
|
||||||
|
printerReady: false,
|
||||||
|
warning: '已初始化,跳过重复 Init'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
stopAllPolls()
|
stopAllPolls()
|
||||||
try {
|
try {
|
||||||
const sharedDir = params?.sharedDir || (configStore.get('sharedDir') as string)
|
const sharedDir = params?.sharedDir || (configStore.get('sharedDir') as string)
|
||||||
fs.mkdirSync(sharedDir, { recursive: true })
|
fs.mkdirSync(sharedDir, { recursive: true })
|
||||||
log.info(`SAPI_Init starting, sharedDir=${sharedDir}`)
|
|
||||||
const code = dllInit({
|
const code = dllInit({
|
||||||
sharedDir,
|
sharedDir,
|
||||||
keepCombinedImage: params?.keepCombinedImage,
|
keepCombinedImage: params?.keepCombinedImage,
|
||||||
@@ -59,36 +68,51 @@ export function registerIpcHandlers(): void {
|
|||||||
logLevel: params?.logLevel,
|
logLevel: params?.logLevel,
|
||||||
outBack: params?.outBack
|
outBack: params?.outBack
|
||||||
})
|
})
|
||||||
log.info(`SAPI_Init finished, code=${code}`)
|
|
||||||
dllInitAttempted = true
|
dllInitAttempted = true
|
||||||
mainAppState.initialized = true
|
mainAppState.initialized = true
|
||||||
configStore.set('sharedDir', sharedDir)
|
configStore.set('sharedDir', sharedDir)
|
||||||
if (code === CS_OK) {
|
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({
|
return ok({
|
||||||
|
code,
|
||||||
|
printerReady: false,
|
||||||
warning: '打印机未连接或驱动未就绪,界面可浏览,接好设备后可在设置中重试 Init'
|
warning: '打印机未连接或驱动未就绪,界面可浏览,接好设备后可在设置中重试 Init'
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
mainAppState.initialized = false
|
mainAppState.initialized = false
|
||||||
log.error('dll:init', err)
|
|
||||||
return fail(CS_FAIL, String(err))
|
return fail(CS_FAIL, String(err))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('dll:printer-info', () => {
|
tracedHandle('dll:printer-info', () => {
|
||||||
try {
|
try {
|
||||||
assertReady()
|
assertReady()
|
||||||
const r = dllGetPrinterInfo()
|
const r = dllGetPrinterInfo()
|
||||||
if (!r.json) return fail(0, '未连接打印机')
|
if (!r.json) {
|
||||||
return ok(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) {
|
} catch (err) {
|
||||||
|
const cached = configStore.get('lastPrinterStatus')
|
||||||
|
if (cached) {
|
||||||
|
return ok({ ...cached, fromCache: true, liveError: String(err) })
|
||||||
|
}
|
||||||
return fail(CS_FAIL, String(err))
|
return fail(CS_FAIL, String(err))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('dll:printer-reset', () => {
|
tracedHandle('dll:printer-reset', () => {
|
||||||
try {
|
try {
|
||||||
assertReady()
|
assertReady()
|
||||||
const code = dllPrinterReset()
|
const code = dllPrinterReset()
|
||||||
@@ -98,7 +122,7 @@ export function registerIpcHandlers(): void {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('dll:printer-reject', () => {
|
tracedHandle('dll:printer-reject', () => {
|
||||||
try {
|
try {
|
||||||
assertReady()
|
assertReady()
|
||||||
if (!isRejectApiAvailable()) return fail(CS_FAIL, 'REJECT_API_UNAVAILABLE')
|
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 {
|
try {
|
||||||
assertReady()
|
assertReady()
|
||||||
return ok({ text: dllGetPrinterErrorStr(errorNo ?? -1) })
|
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 {
|
try {
|
||||||
assertReady()
|
assertReady()
|
||||||
assertNotBusy()
|
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 {
|
try {
|
||||||
assertReady()
|
assertReady()
|
||||||
const id = jobId || mainAppState.activeJobId
|
const id = jobId || mainAppState.activeJobId
|
||||||
@@ -141,8 +165,6 @@ export function registerIpcHandlers(): void {
|
|||||||
let code = CS_OK
|
let code = CS_OK
|
||||||
if (isCancelApiAvailable()) {
|
if (isCancelApiAvailable()) {
|
||||||
code = dllAdminJobCancel(id)
|
code = dllAdminJobCancel(id)
|
||||||
} else {
|
|
||||||
log.info('dll:job-cancel: SAPI_AdminJobCancel not in DLL, poll stopped only')
|
|
||||||
}
|
}
|
||||||
mainAppState.mode = 'ready'
|
mainAppState.mode = 'ready'
|
||||||
mainAppState.activeJobId = ''
|
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 {
|
try {
|
||||||
assertReady()
|
assertReady()
|
||||||
assertNotBusy()
|
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)
|
startJobPoll(jobId)
|
||||||
return ok()
|
return ok()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('poll:job-stop', () => {
|
tracedHandle('poll:job-stop', () => {
|
||||||
stopJobPoll(true)
|
stopJobPoll(true)
|
||||||
return ok()
|
return ok()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('poll:usb-start', () => {
|
tracedHandle('poll:usb-start', () => {
|
||||||
startUsbPoll()
|
startUsbPoll()
|
||||||
return ok()
|
return ok()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('poll:usb-stop', () => {
|
tracedHandle('poll:usb-stop', () => {
|
||||||
stopUsbPoll()
|
stopUsbPoll()
|
||||||
mainAppState.mode = 'ready'
|
mainAppState.mode = 'ready'
|
||||||
return ok()
|
return ok()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('dialog:open-directory', async () => {
|
tracedHandle('dialog:open-directory', async () => {
|
||||||
const win = getPollMainWindow()
|
const win = getPollMainWindow()
|
||||||
const r = await dialog.showOpenDialog(win ?? undefined, {
|
const r = await dialog.showOpenDialog(win ?? undefined, {
|
||||||
properties: ['openDirectory', 'multiSelections']
|
properties: ['openDirectory', 'multiSelections']
|
||||||
@@ -198,7 +220,7 @@ export function registerIpcHandlers(): void {
|
|||||||
return ok({ paths: r.filePaths })
|
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 win = getPollMainWindow()
|
||||||
const r = await dialog.showOpenDialog(win ?? undefined, {
|
const r = await dialog.showOpenDialog(win ?? undefined, {
|
||||||
properties: ['openFile'],
|
properties: ['openFile'],
|
||||||
@@ -208,7 +230,7 @@ export function registerIpcHandlers(): void {
|
|||||||
return ok({ path: r.filePaths[0] })
|
return ok({ path: r.filePaths[0] })
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('fs:path-exists', (_e, paths: string[]) => {
|
tracedHandle('fs:path-exists', (_e, paths: string[]) => {
|
||||||
const missing = paths
|
const missing = paths
|
||||||
.map((raw) => ({ raw, dir: cleanPathPattern(raw) }))
|
.map((raw) => ({ raw, dir: cleanPathPattern(raw) }))
|
||||||
.filter(({ dir }) => !dir || !fs.existsSync(dir))
|
.filter(({ dir }) => !dir || !fs.existsSync(dir))
|
||||||
@@ -216,7 +238,7 @@ export function registerIpcHandlers(): void {
|
|||||||
return ok({ missing })
|
return ok({ missing })
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('fs:dir-size', (_e, paths: string[]) => {
|
tracedHandle('fs:dir-size', (_e, paths: string[]) => {
|
||||||
const items = paths.map((raw) => {
|
const items = paths.map((raw) => {
|
||||||
const dir = cleanPathPattern(raw)
|
const dir = cleanPathPattern(raw)
|
||||||
if (!fs.existsSync(dir)) return { path: raw, bytes: 0, missing: true as const }
|
if (!fs.existsSync(dir)) return { path: raw, bytes: 0, missing: true as const }
|
||||||
@@ -231,7 +253,7 @@ export function registerIpcHandlers(): void {
|
|||||||
return ok({ items })
|
return ok({ items })
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('fs:parse-soon', (_e, filePath: string) => {
|
tracedHandle('fs:parse-soon', (_e, filePath: string) => {
|
||||||
try {
|
try {
|
||||||
const soonPath = String(filePath || '').trim()
|
const soonPath = String(filePath || '').trim()
|
||||||
if (!soonPath) return fail(CS_FAIL, '模板路径为空')
|
if (!soonPath) return fail(CS_FAIL, '模板路径为空')
|
||||||
@@ -244,14 +266,18 @@ export function registerIpcHandlers(): void {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('config:get', () => {
|
tracedHandle('config:get', () => {
|
||||||
const payload: {
|
const payload: {
|
||||||
sharedDir: string
|
sharedDir: string
|
||||||
templateDir: string
|
templateDir: string
|
||||||
|
traceEnabled: boolean
|
||||||
|
lastPrinterStatus?: PrinterStatusSnapshot
|
||||||
skipDllInit?: boolean
|
skipDllInit?: boolean
|
||||||
} = {
|
} = {
|
||||||
sharedDir: configStore.get('sharedDir'),
|
sharedDir: configStore.get('sharedDir'),
|
||||||
templateDir: configStore.get('templateDir')
|
templateDir: configStore.get('templateDir'),
|
||||||
|
traceEnabled: configStore.get('traceEnabled', true),
|
||||||
|
lastPrinterStatus: configStore.get('lastPrinterStatus')
|
||||||
}
|
}
|
||||||
if (!app.isPackaged) {
|
if (!app.isPackaged) {
|
||||||
payload.skipDllInit = configStore.get('skipDllInit', false)
|
payload.skipDllInit = configStore.get('skipDllInit', false)
|
||||||
@@ -259,13 +285,15 @@ export function registerIpcHandlers(): void {
|
|||||||
return ok(payload)
|
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]) => {
|
Object.entries(patch).forEach(([k, v]) => {
|
||||||
if (k === 'skipDllInit') {
|
if (k === 'skipDllInit') {
|
||||||
if (app.isPackaged) return
|
if (app.isPackaged) return
|
||||||
const b =
|
configStore.set(k, parseBool(v))
|
||||||
v === true || v === 'true' || v === 1 || v === '1' || String(v).toLowerCase() === 'true'
|
return
|
||||||
configStore.set(k, b)
|
}
|
||||||
|
if (k === 'traceEnabled' || k === 'dllTraceEnabled') {
|
||||||
|
configStore.set('traceEnabled', parseBool(v))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
configStore.set(k, v as string)
|
configStore.set(k, v as string)
|
||||||
@@ -273,14 +301,20 @@ export function registerIpcHandlers(): void {
|
|||||||
return ok(configStore.store)
|
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)
|
const dir = target || (configStore.get('templateDir') as string)
|
||||||
fs.mkdirSync(dir, { recursive: true })
|
fs.mkdirSync(dir, { recursive: true })
|
||||||
shell.openPath(dir)
|
shell.openPath(dir)
|
||||||
return ok()
|
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> {
|
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 Store from 'electron-store'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import type { PrinterStatusSnapshot } from '@shared/printer-info'
|
||||||
|
|
||||||
interface AppConfig {
|
interface AppConfig {
|
||||||
sharedDir: string
|
sharedDir: string
|
||||||
templateDir: string
|
templateDir: string
|
||||||
/** G2 门禁 false:启动即 SAPI_Init;仅调试可改 true */
|
/** G2 门禁 false:启动即 SAPI_Init;仅调试可改 true */
|
||||||
skipDllInit: boolean
|
skipDllInit: boolean
|
||||||
|
/** true:IPC/DLL 等调用输出到 DevTools 控制台 */
|
||||||
|
traceEnabled: boolean
|
||||||
|
/** 上次成功的 GetPrinterInfo 解析结果,供离线/失败时展示 */
|
||||||
|
lastPrinterStatus?: PrinterStatusSnapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultShared = path.join('C:', 'PrintTasks')
|
const defaultShared = path.join('C:', 'PrintTasks')
|
||||||
@@ -17,6 +22,7 @@ export const configStore = new Store<AppConfig>({
|
|||||||
sharedDir: defaultShared,
|
sharedDir: defaultShared,
|
||||||
templateDir: path.join(app.getPath('userData'), 'Cardsoon', 'templates'),
|
templateDir: path.join(app.getPath('userData'), 'Cardsoon', 'templates'),
|
||||||
// 正式版始终 Init;仅开发时可通过 --skip-dll-init 临时跳过
|
// 正式版始终 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 log from 'electron-log'
|
||||||
import { POLL_INTERVAL_MS } from '../constants'
|
import { POLL_INTERVAL_MS } from '../constants'
|
||||||
import { mainAppState } from './app-state'
|
import { mainAppState } from './app-state'
|
||||||
|
import { emitTrace } from '../utils/trace-bridge'
|
||||||
import { dllGetJobStateById, dllGetUsbCopyState } from './work-dll.service'
|
import { dllGetJobStateById, dllGetUsbCopyState } from './work-dll.service'
|
||||||
|
|
||||||
let jobTimer: ReturnType<typeof setInterval> | null = null
|
let jobTimer: ReturnType<typeof setInterval> | null = null
|
||||||
@@ -56,7 +57,7 @@ export function startJobPoll(id: string): void {
|
|||||||
const cancelled = r.jobState === 6
|
const cancelled = r.jobState === 6
|
||||||
const finished = r.jobState === 100
|
const finished = r.jobState === 100
|
||||||
const terminal = failed || cancelled
|
const terminal = failed || cancelled
|
||||||
send('job:poll-tick', {
|
const tick = {
|
||||||
jobId,
|
jobId,
|
||||||
queryErrorCode: r.queryErrorCode,
|
queryErrorCode: r.queryErrorCode,
|
||||||
jobState: r.jobState,
|
jobState: r.jobState,
|
||||||
@@ -65,7 +66,9 @@ export function startJobPoll(id: string): void {
|
|||||||
failed,
|
failed,
|
||||||
cancelled,
|
cancelled,
|
||||||
finished
|
finished
|
||||||
})
|
}
|
||||||
|
emitTrace('[poll] job:poll-tick', tick)
|
||||||
|
send('job:poll-tick', tick)
|
||||||
if (r.queryErrorCode !== 0) {
|
if (r.queryErrorCode !== 0) {
|
||||||
log.warn('GetJobStateById query failed', r.queryErrorCode)
|
log.warn('GetJobStateById query failed', r.queryErrorCode)
|
||||||
stopJobPoll(true)
|
stopJobPoll(true)
|
||||||
@@ -89,13 +92,15 @@ export function startUsbPoll(): void {
|
|||||||
const failed = r.taskStatus === 3
|
const failed = r.taskStatus === 3
|
||||||
const success = r.taskStatus === 2
|
const success = r.taskStatus === 2
|
||||||
const terminal = failed || success
|
const terminal = failed || success
|
||||||
send('usb:poll-tick', {
|
const tick = {
|
||||||
taskStatus: r.taskStatus,
|
taskStatus: r.taskStatus,
|
||||||
progress: r.progress,
|
progress: r.progress,
|
||||||
terminal,
|
terminal,
|
||||||
failed,
|
failed,
|
||||||
success
|
success
|
||||||
})
|
}
|
||||||
|
emitTrace('[poll] usb:poll-tick', tick)
|
||||||
|
send('usb:poll-tick', tick)
|
||||||
if (terminal) {
|
if (terminal) {
|
||||||
stopUsbPoll()
|
stopUsbPoll()
|
||||||
mainAppState.mode = 'ready'
|
mainAppState.mode = 'ready'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import koffi from 'koffi'
|
import koffi from 'koffi'
|
||||||
import log from 'electron-log'
|
|
||||||
import { CS_OK, JOB_ID_BUF_SIZE, LOG_FATAL_FLAG } from '../constants'
|
import { CS_OK, JOB_ID_BUF_SIZE, LOG_FATAL_FLAG } from '../constants'
|
||||||
|
import { emitTrace, isTraceEnabled } from '../utils/trace-bridge'
|
||||||
import { getNativeDir } from './native-path'
|
import { getNativeDir } from './native-path'
|
||||||
|
|
||||||
export interface InitParams {
|
export interface InitParams {
|
||||||
@@ -42,6 +42,27 @@ let hasCancelApi = false
|
|||||||
let loggedCancelMissing = false
|
let loggedCancelMissing = false
|
||||||
let loggedRejectMissing = 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 {
|
function loadLibrary(): void {
|
||||||
if (lib) return
|
if (lib) return
|
||||||
const dllPath = path.join(getNativeDir(), 'workDll.dll')
|
const dllPath = path.join(getNativeDir(), 'workDll.dll')
|
||||||
@@ -66,7 +87,7 @@ function loadLibrary(): void {
|
|||||||
hasCancelApi = false
|
hasCancelApi = false
|
||||||
if (!loggedCancelMissing) {
|
if (!loggedCancelMissing) {
|
||||||
loggedCancelMissing = true
|
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
|
hasRejectApi = false
|
||||||
if (!loggedRejectMissing) {
|
if (!loggedRejectMissing) {
|
||||||
loggedRejectMissing = true
|
loggedRejectMissing = true
|
||||||
log.info('SAPI_PrinterMovetoreject not in workDll (optional); reject card disabled')
|
emitTrace('[dll] SAPI_PrinterMovetoreject not in workDll (optional)')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,6 +114,19 @@ export function isCancelApiAvailable(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function dllInit(params: InitParams): number {
|
export function dllInit(params: InitParams): 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()
|
loadLibrary()
|
||||||
return SAPI_Init!(
|
return SAPI_Init!(
|
||||||
params.sharedDir,
|
params.sharedDir,
|
||||||
@@ -104,35 +138,49 @@ export function dllInit(params: InitParams): number {
|
|||||||
params.logLevel ?? LOG_FATAL_FLAG,
|
params.logLevel ?? LOG_FATAL_FLAG,
|
||||||
params.outBack ?? false
|
params.outBack ?? false
|
||||||
) as number
|
) as number
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dllGetPrinterInfo(): { code: number; json?: Record<string, unknown> } {
|
export function dllGetPrinterInfo(): { code: number; json?: Record<string, unknown> } {
|
||||||
|
return traceCall('SAPI_GetPrinterInfo', undefined, () => {
|
||||||
loadLibrary()
|
loadLibrary()
|
||||||
const outPtr = koffi.alloc('void *', 8)
|
const outPtr = koffi.alloc('void *', 8)
|
||||||
try {
|
try {
|
||||||
const len = SAPI_GetPrinterInfo!(outPtr) as number
|
const len = SAPI_GetPrinterInfo!(outPtr) as number
|
||||||
if (len <= 0) return { code: len }
|
if (len <= 0) return { code: len }
|
||||||
const ptr = koffi.decode(outPtr, 0, 'void *') as number
|
const ptr = koffi.decode(outPtr, 0, 'void *') as number
|
||||||
|
if (!ptr) return { code: len }
|
||||||
const jsonStr = koffi.decode(ptr, 'char', len) as string
|
const jsonStr = koffi.decode(ptr, 'char', len) as string
|
||||||
koffi.free(ptr)
|
koffi.free(ptr)
|
||||||
|
if (!jsonStr?.trim()) return { code: len }
|
||||||
|
try {
|
||||||
return { code: len, json: JSON.parse(jsonStr) as Record<string, unknown> }
|
return { code: len, json: JSON.parse(jsonStr) as Record<string, unknown> }
|
||||||
|
} catch {
|
||||||
|
return { code: len }
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
koffi.free(outPtr)
|
koffi.free(outPtr)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dllGetPrinterErrorStr(errorNo = -1): string {
|
export function dllGetPrinterErrorStr(errorNo = -1): string {
|
||||||
|
return traceCall('SAPI_GetPrinterErrorStr', { errorNo }, () => {
|
||||||
loadLibrary()
|
loadLibrary()
|
||||||
const s = SAPI_GetPrinterErrorStr!(errorNo) as string
|
const s = SAPI_GetPrinterErrorStr!(errorNo) as string
|
||||||
return s || ''
|
return s || ''
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dllRestJobEx(json: string): { code: number; jobId: string } {
|
export function dllRestJobEx(json: string): { code: number; jobId: string } {
|
||||||
|
return traceCall('SAPI_RestJobEx', { jsonBytes: Buffer.byteLength(json ?? '', 'utf8') }, () => {
|
||||||
loadLibrary()
|
loadLibrary()
|
||||||
const buf = Buffer.alloc(JOB_ID_BUF_SIZE)
|
const buf = Buffer.alloc(JOB_ID_BUF_SIZE)
|
||||||
const code = SAPI_RestJobEx!(json, buf, JOB_ID_BUF_SIZE) as number
|
const code = SAPI_RestJobEx!(json, buf, JOB_ID_BUF_SIZE) as number
|
||||||
const jobId = buf.toString('utf8').replace(/\0.*$/, '').trim()
|
const jobId = buf.toString('utf8').replace(/\0.*$/, '').trim()
|
||||||
return { code, jobId }
|
return { code, jobId }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dllGetJobStateById(jobId: string): {
|
export function dllGetJobStateById(jobId: string): {
|
||||||
@@ -140,6 +188,7 @@ export function dllGetJobStateById(jobId: string): {
|
|||||||
jobState: number
|
jobState: number
|
||||||
progress: number
|
progress: number
|
||||||
} {
|
} {
|
||||||
|
return traceCall('SAPI_GetJobStateById', { jobId }, () => {
|
||||||
loadLibrary()
|
loadLibrary()
|
||||||
const jobState = [0]
|
const jobState = [0]
|
||||||
const copyScheduler = [0]
|
const copyScheduler = [0]
|
||||||
@@ -149,34 +198,45 @@ export function dllGetJobStateById(jobId: string): {
|
|||||||
jobState: jobState[0],
|
jobState: jobState[0],
|
||||||
progress: copyScheduler[0]
|
progress: copyScheduler[0]
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dllAdminJobCancel(jobId: string): number {
|
export function dllAdminJobCancel(jobId: string): number {
|
||||||
|
return traceCall('SAPI_AdminJobCancel', { jobId }, () => {
|
||||||
loadLibrary()
|
loadLibrary()
|
||||||
if (!SAPI_AdminJobCancel) throw new Error('CANCEL_API_UNAVAILABLE')
|
if (!SAPI_AdminJobCancel) throw new Error('CANCEL_API_UNAVAILABLE')
|
||||||
return SAPI_AdminJobCancel(jobId) as number
|
return SAPI_AdminJobCancel(jobId) as number
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dllCopyFromUsb(destFolder: string, cardOutput: number): number {
|
export function dllCopyFromUsb(destFolder: string, cardOutput: number): number {
|
||||||
|
return traceCall('SAPI_CopyFromUsb', { destFolder, cardOutput }, () => {
|
||||||
loadLibrary()
|
loadLibrary()
|
||||||
return SAPI_CopyFromUsb!(destFolder, cardOutput) as number
|
return SAPI_CopyFromUsb!(destFolder, cardOutput) as number
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dllGetUsbCopyState(): { taskStatus: number; progress: number } {
|
export function dllGetUsbCopyState(): { taskStatus: number; progress: number } {
|
||||||
|
return traceCall('SAPI_GetUsbCopyState', undefined, () => {
|
||||||
loadLibrary()
|
loadLibrary()
|
||||||
const taskStatus = [0]
|
const taskStatus = [0]
|
||||||
const progress = [0]
|
const progress = [0]
|
||||||
SAPI_GetUsbCopyState!(taskStatus, progress)
|
SAPI_GetUsbCopyState!(taskStatus, progress)
|
||||||
return { taskStatus: taskStatus[0], progress: progress[0] }
|
return { taskStatus: taskStatus[0], progress: progress[0] }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dllPrinterReset(): number {
|
export function dllPrinterReset(): number {
|
||||||
|
return traceCall('SAPI_PrinterResetprinter', undefined, () => {
|
||||||
loadLibrary()
|
loadLibrary()
|
||||||
return SAPI_PrinterResetprinter!() as number
|
return SAPI_PrinterResetprinter!() as number
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dllPrinterReject(): number {
|
export function dllPrinterReject(): number {
|
||||||
|
return traceCall('SAPI_PrinterMovetoreject', undefined, () => {
|
||||||
loadLibrary()
|
loadLibrary()
|
||||||
if (!SAPI_PrinterMovetoreject) throw new Error('REJECT_API_UNAVAILABLE')
|
if (!SAPI_PrinterMovetoreject) throw new Error('REJECT_API_UNAVAILABLE')
|
||||||
return SAPI_PrinterMovetoreject() as number
|
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:get',
|
||||||
'config:set',
|
'config:set',
|
||||||
'shell:open-path',
|
'shell:open-path',
|
||||||
|
'design:open',
|
||||||
'dll:reject-available'
|
'dll:reject-available'
|
||||||
] as const,
|
] as const,
|
||||||
on: ['job:poll-tick', 'usb:poll-tick'] as const
|
on: ['job:poll-tick', 'usb:poll-tick', 'app:trace'] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardsoonApi = {
|
const cardsoonApi = {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { parsePrinterInfoFromDll } from '@shared/printer-info'
|
||||||
import type { InitParamsDTO, IpcResult, JobPollPayload, UsbPollPayload } from '@/types/ipc'
|
import type { InitParamsDTO, IpcResult, JobPollPayload, UsbPollPayload } from '@/types/ipc'
|
||||||
import type { PrinterStatusDisplay } from '@/types/printer'
|
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 {
|
export function parsePrinterInfo(json: Record<string, unknown>): PrinterStatusDisplay {
|
||||||
const list = (json.printerList as Record<string, unknown>[]) || []
|
return parsePrinterInfoFromDll(json)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function dllPrinterReset(): Promise<IpcResult> {
|
export async function dllPrinterReset(): Promise<IpcResult> {
|
||||||
@@ -109,13 +101,29 @@ export async function fsParseSoon(filePath: string): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function configGet(): 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<
|
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> {
|
export async function configSet(patch: Record<string, unknown>): Promise<IpcResult> {
|
||||||
return api().invoke('config:set', patch) as Promise<IpcResult>
|
return api().invoke('config:set', patch) as Promise<IpcResult>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,34 @@ import {
|
|||||||
dllRejectAvailable,
|
dllRejectAvailable,
|
||||||
parsePrinterInfo
|
parsePrinterInfo
|
||||||
} from '@/api/cardsoon'
|
} from '@/api/cardsoon'
|
||||||
|
import type { PrinterStatusDisplay } from '@/types/printer'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
|
|
||||||
let bootstrapped = false
|
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(): {
|
export function useAppBootstrap(): {
|
||||||
retryInit: () => Promise<void>
|
retryInit: () => Promise<void>
|
||||||
refreshHeader: () => Promise<void>
|
refreshHeader: () => Promise<void>
|
||||||
@@ -19,20 +42,46 @@ export function useAppBootstrap(): {
|
|||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const configStore = useConfigStore()
|
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> {
|
async function refreshHeader(): Promise<void> {
|
||||||
const info = await dllPrinterInfo()
|
const info = await dllPrinterInfo()
|
||||||
if (info.ok && info.data) {
|
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
|
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> {
|
async function doInit(): Promise<void> {
|
||||||
|
await hydratePrinterFromLocal()
|
||||||
|
|
||||||
const cfg = await configGet()
|
const cfg = await configGet()
|
||||||
const sharedDir = cfg.data?.sharedDir || ''
|
const sharedDir = cfg.data?.sharedDir || ''
|
||||||
configStore.setSharedDir(sharedDir)
|
configStore.setSharedDir(sharedDir)
|
||||||
|
|
||||||
if (import.meta.env.DEV && cfg.data?.skipDllInit === true) {
|
if (import.meta.env.DEV && cfg.data?.skipDllInit === true) {
|
||||||
appStore.setInitialized(false, '开发模式已跳过 DLL 初始化')
|
appStore.setInitialized(false, '开发模式已跳过 DLL 初始化')
|
||||||
configStore.setPrinter({ ...configStore.printer, statusText: '未初始化(开发)' })
|
configStore.setPrinter({ ...configStore.printer, statusText: '未初始化(开发)' })
|
||||||
@@ -47,15 +96,27 @@ export function useAppBootstrap(): {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
appStore.setInitialized(true)
|
appStore.setInitialized(true)
|
||||||
configStore.setPrinter({ ...configStore.printer, statusText: '系统已初始化' })
|
const initMeta = init.data as
|
||||||
const warn = (init.data as { warning?: string } | undefined)?.warning
|
| { warning?: string; skipped?: boolean; printerReady?: boolean }
|
||||||
if (warn) notify.warning(warn)
|
| undefined
|
||||||
|
if (initMeta?.warning) notify.warning(initMeta.warning)
|
||||||
|
|
||||||
|
// Init 未就绪时 GetPrinterInfo 可能触发原生 DLL 崩溃,仅用本地缓存
|
||||||
|
if (initMeta?.skipped) {
|
||||||
|
await hydratePrinterFromLocal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (initMeta?.printerReady === true) {
|
||||||
|
await refreshHeader()
|
||||||
try {
|
try {
|
||||||
const rej = await dllRejectAvailable()
|
const rej = await dllRejectAvailable()
|
||||||
if (rej.ok && rej.data) configStore.rejectApiAvailable = rej.data.available
|
if (rej.ok && rej.data) configStore.rejectApiAvailable = rej.data.available
|
||||||
} catch {
|
} catch {
|
||||||
/* optional API */
|
/* optional API */
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await hydratePrinterFromLocal()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -1,11 +1,33 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
import { configGet } from '@/api/cardsoon'
|
||||||
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) => {
|
||||||
|
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)
|
const app = createApp(App)
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ import AppFooter from '@/components/AppFooter.vue'
|
|||||||
import AppIcon from '@/components/AppIcon.vue'
|
import AppIcon from '@/components/AppIcon.vue'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
import { dllPrinterReject, dllPrinterReset, shellOpenTemplateDir } from '@/api/cardsoon'
|
import { dllPrinterReject, dllPrinterReset, openDesignApp } from '@/api/cardsoon'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
@@ -90,9 +90,12 @@ async function onReject(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onTemplate(): Promise<void> {
|
async function onTemplate(): Promise<void> {
|
||||||
if (!guardInit('打开模板目录')) return
|
const r = await openDesignApp()
|
||||||
const r = await shellOpenTemplateDir()
|
if (!r.ok) {
|
||||||
if (!r.ok) notify.error(r.message || '打开模板目录失败')
|
notify.error(r.message || '打开设计软件失败,请检查 cardsoon.config.json')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notify.success('已启动设计软件')
|
||||||
}
|
}
|
||||||
|
|
||||||
function guardBusy(): boolean {
|
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