import type { PrintLogGetListInputVo, PrintLogItemDto, UsAppLabelCategoryTreeNodeDto, UsAppLabelingProductNodeDto, UsAppLabelPreviewInputVo, UsAppLabelPrintInputVo, UsAppLabelPrintOutputDto, UsAppLabelReprintInputVo, UsAppLabelTypeNodeDto, UsAppProductCategoryNodeDto, } from '../types/usAppLabeling' import { extractPagedItems } from '../utils/pagedList' import { usAppApiRequest } from '../utils/usAppApiRequest' import { enqueueOfflineMutation, fetchWithOfflineCache, isNetworkOnline } from '../utils/sqliteSync' /** 接口 9:与文档路径一致,供日志与请求共用 */ export const US_APP_LABEL_PRINT_PATH = '/api/app/us-app-labeling/print' as const function asArr(v: unknown): unknown[] { return Array.isArray(v) ? v : [] } /** 兼容 camelCase / PascalCase,对齐《标签模块接口对接说明(6).md》8.1 四级树 */ function normalizeLabelingTreePayload(raw: unknown): UsAppLabelCategoryTreeNodeDto[] { const list = asArr(raw) return list.map((node: any) => { const pcs = asArr(node?.productCategories ?? node?.ProductCategories) const productCategories: UsAppProductCategoryNodeDto[] = pcs.map((p: any) => { const prods = asArr(p?.products ?? p?.Products) const products: UsAppLabelingProductNodeDto[] = prods.map((x: any) => { const lts = asArr(x?.labelTypes ?? x?.LabelTypes) const labelTypes: UsAppLabelTypeNodeDto[] = lts.map((t: any) => ({ labelTypeId: String(t?.labelTypeId ?? t?.LabelTypeId ?? ''), typeName: String(t?.typeName ?? t?.TypeName ?? ''), orderNum: Number(t?.orderNum ?? t?.OrderNum ?? 0), labelCode: String(t?.labelCode ?? t?.LabelCode ?? ''), templateCode: (t?.templateCode ?? t?.TemplateCode ?? null) as string | null, labelSizeText: (t?.labelSizeText ?? t?.LabelSizeText ?? null) as string | null, })) return { productId: String(x?.productId ?? x?.ProductId ?? ''), productName: String(x?.productName ?? x?.ProductName ?? ''), productCode: String(x?.productCode ?? x?.ProductCode ?? ''), productImageUrl: (x?.productImageUrl ?? x?.ProductImageUrl ?? null) as string | null, subtitle: String(x?.subtitle ?? x?.Subtitle ?? ''), labelTypeCount: Number(x?.labelTypeCount ?? x?.LabelTypeCount ?? labelTypes.length), labelTypes, } }) const catId = p?.categoryId ?? p?.CategoryId return { categoryId: catId != null && String(catId).trim() !== '' ? String(catId) : null, categoryPhotoUrl: (p?.categoryPhotoUrl ?? p?.CategoryPhotoUrl ?? null) as string | null, // 透传平台端扩展字段(用于 APP 按 buttonAppearance 渲染 icon) displayText: (p?.displayText ?? p?.DisplayText ?? null) as string | null, buttonAppearance: (p?.buttonAppearance ?? p?.ButtonAppearance ?? null) as string | null, buttonTextColor: (p?.buttonTextColor ?? p?.ButtonTextColor ?? null) as string | null, buttonBgColor: (p?.buttonBgColor ?? p?.ButtonBgColor ?? null) as string | null, buttonImageUrl: (p?.buttonImageUrl ?? p?.ButtonImageUrl ?? null) as string | null, buttonStyleJson: (p?.buttonStyleJson ?? p?.ButtonStyleJson ?? null) as string | null, availabilityType: (p?.availabilityType ?? p?.AvailabilityType ?? null) as string | null, name: String(p?.name ?? p?.Name ?? ''), itemCount: Number(p?.itemCount ?? p?.ItemCount ?? products.length), products, } }) return { id: String(node?.id ?? node?.Id ?? ''), categoryName: String(node?.categoryName ?? node?.CategoryName ?? ''), categoryPhotoUrl: (node?.categoryPhotoUrl ?? node?.CategoryPhotoUrl ?? null) as string | null, displayText: (node?.displayText ?? node?.DisplayText ?? null) as string | null, buttonBgColor: (node?.buttonBgColor ?? node?.ButtonBgColor ?? null) as string | null, buttonImageUrl: (node?.buttonImageUrl ?? node?.ButtonImageUrl ?? null) as string | null, buttonTextColor: (node?.buttonTextColor ?? node?.ButtonTextColor ?? null) as string | null, // 透传平台端扩展字段(用于 APP 按 buttonAppearance 渲染 icon) buttonAppearance: (node?.buttonAppearance ?? node?.ButtonAppearance ?? null) as string | string[] | null, buttonStyleJson: (node?.buttonStyleJson ?? node?.ButtonStyleJson ?? null) as string | null, orderNum: Number(node?.orderNum ?? node?.OrderNum ?? 0), productCategories, } }) } function buildLabelingTreePath(params: { locationId: string keyword?: string labelCategoryId?: string }): string { const q: string[] = [`locationId=${encodeURIComponent(params.locationId)}`] if (params.keyword != null && String(params.keyword).trim() !== '') { q.push(`keyword=${encodeURIComponent(String(params.keyword).trim())}`) } if (params.labelCategoryId != null && String(params.labelCategoryId).trim() !== '') { q.push(`labelCategoryId=${encodeURIComponent(String(params.labelCategoryId).trim())}`) } return `/api/app/us-app-labeling/labeling-tree?${q.join('&')}` } /** 接口 8.1 */ export async function fetchUsAppLabelingTree(input: { locationId: string keyword?: string labelCategoryId?: string }): Promise { const key = `labeling-tree:${input.locationId}:${input.keyword || ''}:${input.labelCategoryId || ''}` return fetchWithOfflineCache('labeling', key, async () => { const raw = await usAppApiRequest({ path: buildLabelingTreePath(input), method: 'GET', auth: true, }) return normalizeLabelingTreePayload(raw) }) } export function buildLabelPreviewCacheKey(body: UsAppLabelPreviewInputVo): string { const loc = String(body.locationId || '').trim() const code = String(body.labelCode || '').trim() const pid = String(body.productId || '').trim() return `preview:${loc}:${code}:${pid}` } /** 接口 8.2(在线写入 SQLite,离线读缓存) */ export async function postUsAppLabelPreview(body: UsAppLabelPreviewInputVo): Promise { const key = buildLabelPreviewCacheKey(body) return fetchWithOfflineCache('labeling', key, async () => { return usAppApiRequest({ path: '/api/app/us-app-labeling/preview', method: 'POST', auth: true, data: body, }) }) } const PREVIEW_PREFETCH_MAX_PER_LOCATION = 80 /** 同步时预拉标签预览,供断网进入预览页 */ export async function prefetchUsAppLabelPreviewsForLocation( locationId: string, tree: UsAppLabelCategoryTreeNodeDto[] ): Promise { const loc = String(locationId || '').trim() if (!loc) return const jobs: UsAppLabelPreviewInputVo[] = [] outer: for (const cat of tree) { for (const pc of cat.productCategories ?? []) { for (const prod of pc.products ?? []) { for (const lt of prod.labelTypes ?? []) { const labelCode = String(lt.labelCode || '').trim() if (!labelCode) continue jobs.push({ locationId: loc, labelCode, productId: String(prod.productId || '').trim() || undefined, }) if (jobs.length >= PREVIEW_PREFETCH_MAX_PER_LOCATION) break outer } } } } for (const body of jobs) { try { await postUsAppLabelPreview(body) } catch { // 单条失败不阻断整店同步 } } } /** * 接口 9.1 原始请求。 * 注意:仅供业务落库场景调用;打印机设置页「测试打印」、蓝牙页 Test Print 等 **不得** 使用(避免脏数据)。 */ export async function postUsAppLabelPrint(body: UsAppLabelPrintInputVo): Promise { const online = await isNetworkOnline() if (!online) { await enqueueOfflineMutation('/api/app/us-app-labeling/print', 'POST', body) return { taskId: body.clientRequestId || `offline-${Date.now()}`, printQuantity: Math.max(1, Number(body.printQuantity || 1)), } } console.log('[UsAppLabelPrint] 接口 9 请求前 — path:', US_APP_LABEL_PRINT_PATH) console.log('[UsAppLabelPrint] 接口 9 请求体 body:', body) try { console.log('[UsAppLabelPrint] 接口 9 请求体 JSON:', JSON.stringify(body)) } catch { console.log('[UsAppLabelPrint] 接口 9 请求体 JSON 序列化失败(含循环引用等)') } try { return await usAppApiRequest({ path: US_APP_LABEL_PRINT_PATH, method: 'POST', auth: true, data: body, }) } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e) // 在线请求失败时若是网络类错误,自动降级写入本地队列,避免用户操作丢失 if (/network|timeout|offline|断网|网络/i.test(msg)) { await enqueueOfflineMutation('/api/app/us-app-labeling/print', 'POST', body) return { taskId: body.clientRequestId || `offline-${Date.now()}`, printQuantity: Math.max(1, Number(body.printQuantity || 1)), } } throw e } } export function buildUsAppLabelPrintRequestBody(input: { locationId?: string | null labelCode?: string | null productId?: string | null printQuantity: number /** 写入接口 9 `printInputJson`:合并模板快照(可与 `buildPrintInputJson` 结果浅合并),应对齐列表 `renderTemplateJson` */ mergedTemplate: Record clientRequestId?: string | null printerMac?: string | null printerAddress?: string | null }): UsAppLabelPrintInputVo | null { const locationId = String(input.locationId || '').trim() const labelCode = String(input.labelCode || '').trim() if (!locationId || !labelCode) return null let printInputJson: Record try { printInputJson = JSON.parse(JSON.stringify(input.mergedTemplate)) as Record } catch { return null } const body: UsAppLabelPrintInputVo = { locationId, labelCode, printQuantity: Math.max(1, Math.round(Number(input.printQuantity) || 1)), printInputJson, baseTime: new Date().toISOString(), } const cid = String(input.clientRequestId || '').trim() if (cid) body.clientRequestId = cid const pid = String(input.productId || '').trim() if (pid) body.productId = pid const mac = String(input.printerMac || '').trim() if (mac) body.printerMac = mac const addr = String(input.printerAddress || '').trim() if (addr) body.printerAddress = addr return body } /** * 接口 9:仅在 **标签预览页**(`pages/labels/preview`)用户打印真实标签、出纸成功后落库。 * `mergedTemplate` 写入请求体 `printInputJson`(与 label-template 同构);缺少 `locationId` 或 `labelCode` 时不发请求。 * 测试打印模板(printers / bluetooth Test Print)不走此函数。 */ export async function reportUsAppLabelPrintIfReady(input: { locationId?: string | null labelCode?: string | null productId?: string | null printQuantity: number mergedTemplate: Record clientRequestId?: string | null printerMac?: string | null printerAddress?: string | null }): Promise { const body = buildUsAppLabelPrintRequestBody(input) if (!body) return null return postUsAppLabelPrint(body) } /** 接口 10:分页打印日志 */ export async function fetchUsAppPrintLogList (input: PrintLogGetListInputVo) { const key = `print-log:${input.locationId}:${input.skipCount ?? 1}:${input.maxResultCount ?? 20}` return fetchWithOfflineCache('labeling', key, async () => { const raw = await usAppApiRequest({ path: '/api/app/us-app-labeling/get-print-log-list', method: 'POST', auth: true, data: { locationId: input.locationId, skipCount: input.skipCount ?? 1, maxResultCount: input.maxResultCount ?? 20, }, }) return extractPagedItems(raw) }) } /** 接口 11:重打并返回含 mergedTemplateJson 的出参 */ export async function postUsAppLabelReprint (body: UsAppLabelReprintInputVo): Promise { const online = await isNetworkOnline() if (!online) { await enqueueOfflineMutation('/api/app/us-app-labeling/reprint', 'POST', body) return { taskId: body.clientRequestId || body.taskId || `offline-reprint-${Date.now()}`, printQuantity: Math.max(1, Number(body.printQuantity || 1)), } } try { return await usAppApiRequest({ path: '/api/app/us-app-labeling/reprint', method: 'POST', auth: true, data: body, }) } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e) if (/network|timeout|offline|断网|网络/i.test(msg)) { await enqueueOfflineMutation('/api/app/us-app-labeling/reprint', 'POST', body) return { taskId: body.clientRequestId || body.taskId || `offline-reprint-${Date.now()}`, printQuantity: Math.max(1, Number(body.printQuantity || 1)), } } throw e } }