From cba3b0cccdda866b07956a6cd09b5a70ffde3efd Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Wed, 22 Jul 2026 23:02:19 +0800 Subject: [PATCH] feat(web-admin): add related-approval print block assembler Pure map-based recursion with maxDepth=3 and visited dedupe; async wrapper batch-fetches summaries for PDF export. Co-authored-by: Cursor --- .../src/utils/oa-approval-print-related.ts | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 web-admin/src/utils/oa-approval-print-related.ts diff --git a/web-admin/src/utils/oa-approval-print-related.ts b/web-admin/src/utils/oa-approval-print-related.ts new file mode 100644 index 00000000..480cdfb4 --- /dev/null +++ b/web-admin/src/utils/oa-approval-print-related.ts @@ -0,0 +1,212 @@ +/** + * OA 关联审批单 · 打印块组装(最多 3 层递归 + visited 去重) + * Spec: docs/superpowers/specs/2026-07-22-oa-related-approval-nested-print-design.md + */ +import { + fetchOaRelatedSummaries, + type OaRelatedSummary +} from '@/api/oaApproval' + +export const OA_RELATED_PRINT_MAX_DEPTH = 3 + +export type RelatedPrintBlock = { + indexLabel: string + title: string + lines?: { label: string; value: string }[] + children?: RelatedPrintBlock[] +} + +function datePart(applyTime: string) { + const t = String(applyTime || '').trim() + return t ? t.slice(0, 10) : '' +} + +function titleOf(s: OaRelatedSummary) { + if (s.unavailable) return '无法查看' + const name = String(s.appName || '审批单').trim() || '审批单' + const d = datePart(s.applyTime) + return d ? `${name} · ${d}` : name +} + +/** Pure: build blocks from an already-loaded summary map. */ +export function buildRelatedPrintBlocksFromMap( + ids: string[], + summaryById: Map, + opts: { + depth: number + maxDepth?: number + visited: Set + indexPrefix?: string + } +): RelatedPrintBlock[] { + const maxDepth = opts.maxDepth ?? OA_RELATED_PRINT_MAX_DEPTH + const depth = opts.depth + const visited = opts.visited + const prefix = opts.indexPrefix || '' + const blocks: RelatedPrintBlock[] = [] + + ids.forEach((rawId, i) => { + const id = String(rawId || '').trim() + if (!id) return + const indexLabel = prefix ? `${prefix}.${i + 1}` : String(i + 1) + + if (visited.has(id)) { + blocks.push({ indexLabel, title: '(已在上文出现)' }) + return + } + visited.add(id) + + const s = summaryById.get(id) + if (!s) { + blocks.push({ indexLabel, title: '关联信息加载失败' }) + return + } + if (s.unavailable) { + blocks.push({ indexLabel, title: '无法查看' }) + return + } + + const lines: { label: string; value: string }[] = [] + const children: RelatedPrintBlock[] = [] + const fields = Array.isArray(s.fields) ? s.fields : [] + + for (const f of fields) { + const label = String(f.label || '').trim() || '-' + const value = String(f.value || '').trim() || '-' + const nestedIds = (f.relatedIds || []).map((x) => String(x || '').trim()).filter(Boolean) + + lines.push({ label, value }) + if (nestedIds.length && depth < maxDepth) { + children.push( + ...buildRelatedPrintBlocksFromMap(nestedIds, summaryById, { + depth: depth + 1, + maxDepth, + visited, + indexPrefix: indexLabel + }) + ) + } + } + + blocks.push({ + indexLabel, + title: titleOf(s), + lines, + children: children.length ? children : undefined + }) + }) + + return blocks +} + +function placeholderSummary(id: string, unavailable = true): OaRelatedSummary { + return { + id, + unavailable, + appName: '', + applyTime: '', + status: '', + fields: [] + } +} + +async function loadSummariesIntoMap(ids: string[], map: Map) { + const missing = [...new Set(ids.map((x) => String(x || '').trim()).filter(Boolean))].filter( + (id) => !map.has(id) + ) + if (!missing.length) return + try { + const res: any = await fetchOaRelatedSummaries(missing, 'view') + const list: OaRelatedSummary[] = Array.isArray(res?.list) + ? res.list + : Array.isArray(res) + ? res + : [] + for (const s of list) { + if (s?.id) map.set(String(s.id), s) + } + for (const id of missing) { + if (!map.has(id)) map.set(id, placeholderSummary(id)) + } + } catch { + for (const id of missing) { + if (!map.has(id)) map.set(id, placeholderSummary(id)) + } + } +} + +/** Collect all reachable ids from roots up to maxDepth (inclusive of roots). */ +function collectReachableIds( + rootIds: string[], + map: Map, + maxDepth: number +): string[] { + const out = new Set() + const queue: { id: string; depth: number }[] = rootIds + .map((id) => String(id || '').trim()) + .filter(Boolean) + .map((id) => ({ id, depth: 1 })) + + while (queue.length) { + const { id, depth } = queue.shift()! + if (out.has(id)) continue + out.add(id) + if (depth >= maxDepth) continue + const s = map.get(id) + if (!s || s.unavailable) continue + for (const f of s.fields || []) { + for (const n of f.relatedIds || []) { + const tid = String(n || '').trim() + if (tid && !out.has(tid)) queue.push({ id: tid, depth: depth + 1 }) + } + } + } + return [...out] +} + +/** + * Prefetch summaries (waves up to maxDepth) then build blocks. + * depth starts at 1 for ids on the printed form's relatedApproval field. + */ +export async function assembleRelatedPrintBlocks( + ids: string[], + opts?: { maxDepth?: number } +): Promise { + const maxDepth = opts?.maxDepth ?? OA_RELATED_PRINT_MAX_DEPTH + const rootIds = normalizeRelatedIdsFromFormValue(ids) + if (!rootIds.length) return [] + + const map = new Map() + await loadSummariesIntoMap(rootIds, map) + + // Wave: after each load, discover nested ids still missing + for (let wave = 0; wave < maxDepth; wave++) { + const reachable = collectReachableIds(rootIds, map, maxDepth) + const missing = reachable.filter((id) => !map.has(id)) + if (!missing.length) break + await loadSummariesIntoMap(missing, map) + } + + return buildRelatedPrintBlocksFromMap(rootIds, map, { + depth: 1, + maxDepth, + visited: new Set() + }) +} + +export function normalizeRelatedIdsFromFormValue(raw: unknown): string[] { + if (Array.isArray(raw)) { + const out: string[] = [] + const seen = new Set() + for (const x of raw) { + const t = String(x || '').trim() + if (!t || seen.has(t)) continue + seen.add(t) + out.push(t) + } + return out + } + if (raw == null || raw === '') return [] + const t = String(raw).trim() + return t ? [t] : [] +}