标签字段可编辑,Init 参数改从 cardsoon.config.json 读取

- 模板 type 3/4/5 文本可编辑,type 1 支持选图
- cardsoon.config.json 增加 sharedDir、logLevel、cleanTaskFile(默认 0)
- cleanTaskFile 默认改为 false,启动时同步 sharedDir 到运行时配置

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24kycj
2026-06-03 09:43:10 +08:00
parent a6fc966d8b
commit 3ec168350c
10 changed files with 165 additions and 23 deletions
+4 -1
View File
@@ -1,3 +1,6 @@
{ {
"designAppPath": "D:\\SoonProject\\SoonDesign\\build\\win-unpacked\\SoonDesign.exe" "designAppPath": "D:\\SoonProject\\SoonDesign\\build\\win-unpacked\\SoonDesign.exe",
"sharedDir": "C:\\PrintTasks",
"logLevel": 3,
"cleanTaskFile": 0
} }
+2 -1
View File
@@ -15,7 +15,7 @@ if (!gotSingleInstanceLock) {
} }
import { suppressKnownDllStderr } from './utils/suppress-dll-stderr' import { suppressKnownDllStderr } from './utils/suppress-dll-stderr'
import { loadAppFileConfig } from './services/app-config' import { loadAppFileConfig, applyFileConfigToStore } from './services/app-config'
import { migrateTraceConfig, setTraceWebContents } from './utils/trace-bridge' import { migrateTraceConfig, setTraceWebContents } from './utils/trace-bridge'
suppressKnownDllStderr() suppressKnownDllStderr()
@@ -133,6 +133,7 @@ app.whenReady().then(async () => {
try { try {
migrateTraceConfig() migrateTraceConfig()
loadAppFileConfig() loadAppFileConfig()
applyFileConfigToStore()
log.info('app startup', { packaged: app.isPackaged, execPath: process.execPath }) log.info('app startup', { packaged: app.isPackaged, execPath: process.execPath })
registerIpcHandlers() registerIpcHandlers()
try { try {
+54 -3
View File
@@ -2,20 +2,59 @@ import { app } from 'electron'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import log from 'electron-log' import log from 'electron-log'
import { LOG_FATAL_FLAG } from '../constants'
import { configStore } from './config-store'
import { getProcessExecDir } from './native-path' import { getProcessExecDir } from './native-path'
export const APP_CONFIG_FILENAME = 'cardsoon.config.json' export const APP_CONFIG_FILENAME = 'cardsoon.config.json'
const DEFAULT_SHARED_DIR = path.join('C:', 'PrintTasks')
export interface AppFileConfig { export interface AppFileConfig {
designAppPath: string designAppPath: string
sharedDir: string
logLevel: number
cleanTaskFile: boolean
} }
const defaults: AppFileConfig = { const defaults: AppFileConfig = {
designAppPath: '' designAppPath: '',
sharedDir: DEFAULT_SHARED_DIR,
logLevel: LOG_FATAL_FLAG,
cleanTaskFile: false
} }
let cached: AppFileConfig | null = null let cached: AppFileConfig | null = null
function pickRaw(raw: Record<string, unknown>, ...keys: string[]): unknown {
for (const key of keys) {
const hit = Object.entries(raw).find(([name]) => name.toLowerCase() === key.toLowerCase())
if (hit && hit[1] != null && String(hit[1]).trim() !== '') return hit[1]
}
return undefined
}
function parseCleanTaskFile(raw: unknown): boolean {
if (raw === undefined || raw === null) return defaults.cleanTaskFile
if (typeof raw === 'boolean') return raw
if (typeof raw === 'number') return raw !== 0
const s = String(raw).trim().toLowerCase()
if (s === '0' || s === 'false') return false
if (s === '1' || s === 'true') return true
return defaults.cleanTaskFile
}
function parseLogLevel(raw: unknown): number {
if (raw === undefined || raw === null) return defaults.logLevel
const n = Number(raw)
return Number.isFinite(n) ? Math.round(n) : defaults.logLevel
}
function parseSharedDir(raw: unknown): string {
const s = String(raw ?? '').trim()
return s || DEFAULT_SHARED_DIR
}
function bundledConfigPath(): string { function bundledConfigPath(): string {
if (app.isPackaged) { if (app.isPackaged) {
return path.join(process.resourcesPath, APP_CONFIG_FILENAME) return path.join(process.resourcesPath, APP_CONFIG_FILENAME)
@@ -33,7 +72,10 @@ function configSearchPaths(): string[] {
function parseConfigFile(filePath: string): AppFileConfig { function parseConfigFile(filePath: string): AppFileConfig {
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown> const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>
return { return {
designAppPath: String(raw.designAppPath ?? '').trim() designAppPath: String(pickRaw(raw, 'designAppPath') ?? '').trim(),
sharedDir: parseSharedDir(pickRaw(raw, 'sharedDir', 'shareddir')),
logLevel: parseLogLevel(pickRaw(raw, 'logLevel', 'loglevel')),
cleanTaskFile: parseCleanTaskFile(pickRaw(raw, 'cleanTaskFile', 'cleantaskfile'))
} }
} }
@@ -44,7 +86,11 @@ export function loadAppFileConfig(): AppFileConfig {
if (!fs.existsSync(filePath)) continue if (!fs.existsSync(filePath)) continue
try { try {
cached = parseConfigFile(filePath) cached = parseConfigFile(filePath)
log.info(`Loaded ${APP_CONFIG_FILENAME} from ${filePath}`) log.info(`Loaded ${APP_CONFIG_FILENAME} from ${filePath}`, {
sharedDir: cached.sharedDir,
logLevel: cached.logLevel,
cleanTaskFile: cached.cleanTaskFile
})
return cached return cached
} catch (e) { } catch (e) {
log.warn(`Skip invalid ${APP_CONFIG_FILENAME}: ${filePath}`, e) log.warn(`Skip invalid ${APP_CONFIG_FILENAME}: ${filePath}`, e)
@@ -58,6 +104,11 @@ export function loadAppFileConfig(): AppFileConfig {
return cached return cached
} }
export function applyFileConfigToStore(): void {
const cfg = loadAppFileConfig()
configStore.set('sharedDir', cfg.sharedDir)
}
export function getDesignAppPath(): string { export function getDesignAppPath(): string {
return loadAppFileConfig().designAppPath return loadAppFileConfig().designAppPath
} }
+5 -3
View File
@@ -3,6 +3,7 @@ import log from 'electron-log'
import { CS_OK } from '../constants' import { CS_OK } from '../constants'
import { mainAppState } from './app-state' import { mainAppState } from './app-state'
import { configStore } from './config-store' import { configStore } from './config-store'
import { loadAppFileConfig } from './app-config'
import { loadDllModule } from './dll-loader' import { loadDllModule } from './dll-loader'
import type { InitParams } from './work-dll.service' import type { InitParams } from './work-dll.service'
@@ -18,8 +19,9 @@ export async function ensureDllInitialized(
if (dllInitAttempted) { if (dllInitAttempted) {
return { code: CS_OK } return { code: CS_OK }
} }
const fileCfg = loadAppFileConfig()
const sharedDir = const sharedDir =
params?.sharedDir || (configStore.get('sharedDir') as string) || 'C:\\PrintTasks' params?.sharedDir?.trim() || fileCfg.sharedDir || (configStore.get('sharedDir') as string)
try { try {
const dll = await loadDllModule() const dll = await loadDllModule()
fs.mkdirSync(sharedDir, { recursive: true }) fs.mkdirSync(sharedDir, { recursive: true })
@@ -27,10 +29,10 @@ export async function ensureDllInitialized(
sharedDir, sharedDir,
keepCombinedImage: params?.keepCombinedImage, keepCombinedImage: params?.keepCombinedImage,
stopOnFailure: params?.stopOnFailure, stopOnFailure: params?.stopOnFailure,
cleanTaskFile: params?.cleanTaskFile, cleanTaskFile: params?.cleanTaskFile ?? fileCfg.cleanTaskFile,
autoRetryTimes: params?.autoRetryTimes, autoRetryTimes: params?.autoRetryTimes,
rejectConfig: params?.rejectConfig, rejectConfig: params?.rejectConfig,
logLevel: params?.logLevel, logLevel: params?.logLevel ?? fileCfg.logLevel,
outBack: params?.outBack outBack: params?.outBack
}) })
dllInitAttempted = true dllInitAttempted = true
+2 -2
View File
@@ -239,7 +239,7 @@ export function dllInit(params: InitParams): number {
sharedDir: params.sharedDir, sharedDir: params.sharedDir,
keepCombinedImage: params.keepCombinedImage ?? true, keepCombinedImage: params.keepCombinedImage ?? true,
stopOnFailure: params.stopOnFailure ?? false, stopOnFailure: params.stopOnFailure ?? false,
cleanTaskFile: params.cleanTaskFile ?? true, cleanTaskFile: params.cleanTaskFile ?? false,
autoRetryTimes: params.autoRetryTimes ?? 0, autoRetryTimes: params.autoRetryTimes ?? 0,
rejectConfig: params.rejectConfig ?? false, rejectConfig: params.rejectConfig ?? false,
logLevel: params.logLevel ?? LOG_FATAL_FLAG, logLevel: params.logLevel ?? LOG_FATAL_FLAG,
@@ -251,7 +251,7 @@ export function dllInit(params: InitParams): number {
params.sharedDir, params.sharedDir,
params.keepCombinedImage ?? true, params.keepCombinedImage ?? true,
params.stopOnFailure ?? false, params.stopOnFailure ?? false,
params.cleanTaskFile ?? true, params.cleanTaskFile ?? false,
params.autoRetryTimes ?? 0, params.autoRetryTimes ?? 0,
params.rejectConfig ?? false, params.rejectConfig ?? false,
params.logLevel ?? LOG_FATAL_FLAG, params.logLevel ?? LOG_FATAL_FLAG,
+5 -3
View File
@@ -5,6 +5,8 @@ export interface TemplateFieldRow {
label: string label: string
value: string value: string
originName: string originName: string
/** soon frontData/backData 的 type1=图片,3/4/5=文本可编辑 */
fieldType: number
} }
export interface ParsedSoonTemplate { export interface ParsedSoonTemplate {
@@ -80,7 +82,7 @@ function parseSoonWorkerDisk(soonPath: string, raw: Record<string, unknown>): Pa
const name = String(o.name ?? '').trim() const name = String(o.name ?? '').trim()
if (!name) continue if (!name) continue
const value = o.DefaultText == null ? '' : String(o.DefaultText) const value = o.DefaultText == null ? '' : String(o.DefaultText)
fields.push({ label: `${name}[${side}]`, value, originName: name }) fields.push({ label: `${name}[${side}]`, value, originName: name, fieldType: type })
} }
} }
@@ -115,7 +117,7 @@ function parseSoonLegacy(soonPath: string, raw: Record<string, unknown>): Parsed
if (side === 'front') { if (side === 'front') {
if (!frontImageUrl) frontImageUrl = url if (!frontImageUrl) frontImageUrl = url
const name = pickStr(item, ['name', 'field', 'key']) || 'IMAGE' const name = pickStr(item, ['name', 'field', 'key']) || 'IMAGE'
fields.push({ label: toFieldLabel(name, 'front'), value: fileRef, originName: name }) fields.push({ label: toFieldLabel(name, 'front'), value: fileRef, originName: name, fieldType: 1 })
} else if (!backImageUrl) { } else if (!backImageUrl) {
backImageUrl = url backImageUrl = url
} }
@@ -127,7 +129,7 @@ function parseSoonLegacy(soonPath: string, raw: Record<string, unknown>): Parsed
const value = pickStr(item, ['value', 'text', 'default', 'content', 'data']) const value = pickStr(item, ['value', 'text', 'default', 'content', 'data'])
let side = sideOf(item) let side = sideOf(item)
if (!side) side = /image|img|front/i.test(name) ? 'front' : 'back' if (!side) side = /image|img|front/i.test(name) ? 'front' : 'back'
fields.push({ label: toFieldLabel(name, side), value, originName: name }) fields.push({ label: toFieldLabel(name, side), value, originName: name, fieldType: 5 })
}) })
return { frontImageUrl, backImageUrl, fields, printFlag: readSoonPrintFlag(raw) } return { frontImageUrl, backImageUrl, fields, printFlag: readSoonPrintFlag(raw) }
+8 -2
View File
@@ -103,6 +103,12 @@ export async function dialogOpenSoon(): Promise<IpcResult<{ path: string }>> {
> >
} }
export async function dialogOpenImage(): Promise<IpcResult<{ path: string }>> {
return api().invoke('dialog:open-file', [
{ name: 'Image', extensions: ['jpg', 'jpeg', 'png', 'bmp', 'gif', 'webp'] }
]) as Promise<IpcResult<{ path: string }>>
}
export async function fsPathExists(paths: string[]): Promise<IpcResult<{ missing: string[] }>> { export async function fsPathExists(paths: string[]): Promise<IpcResult<{ missing: string[] }>> {
return api().invoke('fs:path-exists', paths) as Promise<IpcResult<{ missing: string[] }>> return api().invoke('fs:path-exists', paths) as Promise<IpcResult<{ missing: string[] }>>
} }
@@ -126,7 +132,7 @@ export async function fsParseSoon(filePath: string): Promise<
IpcResult<{ IpcResult<{
frontImageUrl: string frontImageUrl: string
backImageUrl: string backImageUrl: string
fields: { label: string; value: string; originName: string }[] fields: { label: string; value: string; originName: string; fieldType: number }[]
printFlag: number printFlag: number
}> }>
> { > {
@@ -134,7 +140,7 @@ export async function fsParseSoon(filePath: string): Promise<
IpcResult<{ IpcResult<{
frontImageUrl: string frontImageUrl: string
backImageUrl: string backImageUrl: string
fields: { label: string; value: string; originName: string }[] fields: { label: string; value: string; originName: string; fieldType: number }[]
printFlag: number printFlag: number
}> }>
> >
@@ -4,6 +4,7 @@ export interface TemplateFieldRow {
label: string label: string
value: string value: string
originName: string originName: string
fieldType: number
} }
export interface TemplatePreview { export interface TemplatePreview {
@@ -566,6 +566,43 @@
min-width: 0; min-width: 0;
} }
.c-data-table-mini .c-field-input {
width: 100%;
height: 16px;
border: none;
background: transparent;
font-size: inherit;
font-weight: inherit;
color: inherit;
outline: none;
min-width: 0;
padding: 0;
}
.c-field-pick {
flex-shrink: 0;
border: 1px solid #dcdfe6;
border-radius: 3px;
background: #fff;
font-size: 8px;
font-weight: 700;
color: #495057;
padding: 0 6px;
height: 18px;
cursor: pointer;
}
.c-field-pick:hover {
background: #f1f3f5;
}
.c-field-value {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.c-path-cell__text { .c-path-cell__text {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
@@ -117,7 +117,21 @@
<table class="c-data-table-mini"> <table class="c-data-table-mini">
<tr v-for="(row, idx) in formStore.templatePreview!.fields" :key="idx"> <tr v-for="(row, idx) in formStore.templatePreview!.fields" :key="idx">
<td>{{ row.label }}</td> <td>{{ row.label }}</td>
<td> <td v-if="isImageField(row)" class="c-path-cell">
<span class="c-path-cell__text" :title="row.value">{{
imageFieldLabel(row.value)
}}</span>
<button type="button" class="c-field-pick" @click="pickFieldImage(idx)">选择</button>
</td>
<td v-else-if="isTextField(row)">
<input
v-model="row.value"
type="text"
class="c-field-input"
:placeholder="row.label"
/>
</td>
<td v-else>
<span class="c-field-value" :title="row.value">{{ row.value || '—' }}</span> <span class="c-field-value" :title="row.value">{{ row.value || '—' }}</span>
</td> </td>
</tr> </tr>
@@ -148,8 +162,10 @@ import { useAppStore } from '@/stores/app'
import { validateJobPreflight } from '@/utils/validateJobPreflight' import { validateJobPreflight } from '@/utils/validateJobPreflight'
import { createDistributeJob } from '@/utils/createDistributeJob' import { createDistributeJob } from '@/utils/createDistributeJob'
import { formatBytesAsGb, formatBytesCompact } from '@/utils/formatBytes' import { formatBytesAsGb, formatBytesCompact } from '@/utils/formatBytes'
import type { TemplateFieldRow } from '@/stores/distributeForm'
import { import {
dialogOpenDirectory, dialogOpenDirectory,
dialogOpenImage,
dialogOpenSoon, dialogOpenSoon,
dllJobCancel, dllJobCancel,
fsDirSize, fsDirSize,
@@ -185,6 +201,36 @@ const loadProgressText = computed(() => {
return `已加载: ${loadedGb} GB / ${CARD_CAPACITY_GB} GB (${loadPercent.value}%)` return `已加载: ${loadedGb} GB / ${CARD_CAPACITY_GB} GB (${loadPercent.value}%)`
}) })
function isTextField(row: TemplateFieldRow): boolean {
return row.fieldType === 3 || row.fieldType === 4 || row.fieldType === 5
}
function isImageField(row: TemplateFieldRow): boolean {
return row.fieldType === 1
}
function imageFieldLabel(value: string): string {
const v = value.trim()
if (!v) return '未选择图片'
const parts = v.replace(/\\/g, '/').split('/')
return parts[parts.length - 1] || v
}
async function pickFieldImage(idx: number): Promise<void> {
const preview = formStore.templatePreview
if (!preview) return
const row = preview.fields[idx]
if (!row || !isImageField(row)) return
const r = await dialogOpenImage()
if (!r.ok) {
notify.error(r.message || '打开图片选择失败')
return
}
const picked = r.data?.path?.trim()
if (!picked) return
row.value = picked
}
function onClear(): void { function onClear(): void {
formStore.reset() formStore.reset()
notify.info('已清空,已恢复初始状态') notify.info('已清空,已恢复初始状态')
@@ -341,11 +387,4 @@ async function onSubmit(): Promise<void> {
.c-card-small--back { .c-card-small--back {
background: #f8f9fa; background: #f8f9fa;
} }
.c-field-value {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style> </style>