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:
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] : []
|
||||
}
|
||||
Reference in new issue
Block a user