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 <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-22 23:02:19 +08:00
1 parent 6bb8621578
commit cba3b0cccd
1 file changed
+212
@@ -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<string, OaRelatedSummary>,
opts: {
depth: number
maxDepth?: number
visited: Set<string>
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<string, OaRelatedSummary>) {
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<string, OaRelatedSummary>,
maxDepth: number
): string[] {
const out = new Set<string>()
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<RelatedPrintBlock[]> {
const maxDepth = opts?.maxDepth ?? OA_RELATED_PRINT_MAX_DEPTH
const rootIds = normalizeRelatedIdsFromFormValue(ids)
if (!rootIds.length) return []
const map = new Map<string, OaRelatedSummary>()
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<string>()
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] : []
}