Merge discussion comments into the existing printed approval flow on Web and mobile OA, preserving detail timeline order and hiding revoked content. Include mentions and absolute attachment URLs, and add PDF link annotations so rasterized exports retain usable links. Add regression tests for ordering, attachments, mentions and revoked comments. Related to Gitea issue #30.
241 lines
7.9 KiB
TypeScript
241 lines
7.9 KiB
TypeScript
import { mergeProgressWithComments } from './oa-progress-merge-comments'
|
|
/**
|
|
* OA 打印「审批流程」行组装(无 Vue/canvas 副作用,便于单测)
|
|
*/
|
|
import { formatProgressPersonLines } from '@/utils/oa-progress-person-lines'
|
|
|
|
export type OaPrintPersonLine = {
|
|
left: string
|
|
right: string
|
|
}
|
|
|
|
export type OaPrintFlowItem = {
|
|
left: string
|
|
right: string
|
|
attachmentLinks?: string[]
|
|
/** @deprecated 多人节点改用 personLines;单人/发起人可保留 */
|
|
comment?: string
|
|
/** 或签/会签按人行:左侧文案 + 右侧时间(右对齐) */
|
|
personLines?: OaPrintPersonLine[]
|
|
}
|
|
|
|
const STATUS_TEXT: Record<string, string> = {
|
|
pending: '待处理',
|
|
running: '进行中',
|
|
approved: '已同意',
|
|
rejected: '已拒绝',
|
|
completed: '已完成',
|
|
cancelled: '已撤销',
|
|
done: '已结束',
|
|
submitted: '已提交',
|
|
not_reached: '未到达',
|
|
returned: '已打回',
|
|
transferred: '已转交'
|
|
}
|
|
|
|
function statusText(s: string) {
|
|
return STATUS_TEXT[s] || s || '-'
|
|
}
|
|
|
|
/** 任务节点 cancelled=系统清待办 →「无需处理」;终态 cancelled 仍为「已撤销」 */
|
|
function progressNodeStatusText(p: any): string {
|
|
const kind = String(p?.kind || '')
|
|
const s = String(p?.status || '').trim()
|
|
if (kind !== 'terminal' && s === 'cancelled') return '无需处理'
|
|
return statusText(s)
|
|
}
|
|
|
|
function peopleDisplayNames(p: any): string[] {
|
|
const people = Array.isArray(p?.people) ? p.people : []
|
|
const fromPeople = people
|
|
.map((x: any) => String(x?.nickName || x?.id || '').trim())
|
|
.filter(Boolean)
|
|
if (fromPeople.length) return fromPeople
|
|
const name = String(p?.assigneeUserName || p?.assigneeUserId || '').trim()
|
|
return name ? [name] : []
|
|
}
|
|
|
|
function progressPersonLinesFromItem(p: any): OaPrintPersonLine[] {
|
|
const people = Array.isArray(p?.people) ? p.people : []
|
|
if (!people.length) return []
|
|
return formatProgressPersonLines(people).map((x) => ({
|
|
left: x.body || x.text,
|
|
right: x.timestamp || ''
|
|
}))
|
|
}
|
|
|
|
function isCcProgressItem(p: any): boolean {
|
|
const taskType = String(p?.taskType || p?.task_type || '').trim()
|
|
const signMode = String(p?.signMode || p?.sign_mode || '').trim()
|
|
const title = String(p?.title || p?.nodeName || '').trim()
|
|
return taskType === 'cc' || signMode === 'cc' || title === '抄送人' || /^抄送/.test(title)
|
|
}
|
|
|
|
/**
|
|
* 从 progress 组装审批流程行;抄送节点聚合为「抄送A, B, C」对齐钉钉导出。
|
|
* 若 progress 漏抄送,则用 tasks 兜底补一行。
|
|
*/
|
|
export function buildFlowItems(
|
|
detail: any,
|
|
resolveUrl: (url: string) => string = (url) => url
|
|
): OaPrintFlowItem[] {
|
|
const progress = Array.isArray(detail?.progress) ? detail.progress : []
|
|
// 沿用详情页的时间顺序;无讨论时保持原有审批节点顺序。
|
|
const list =
|
|
Array.isArray(detail?.comments) && detail.comments.length
|
|
? mergeProgressWithComments(
|
|
progress.map((p: any, index: number) => ({
|
|
...p,
|
|
key: `${p.kind}-${p.nodeKey || p.taskId || index}-${p.roundNo || 1}`
|
|
})),
|
|
detail.comments
|
|
)
|
|
: progress
|
|
const items: OaPrintFlowItem[] = []
|
|
|
|
let ccBuf: { names: string[]; ts: string } | null = null
|
|
const flushCc = () => {
|
|
if (!ccBuf) return
|
|
const uniq = Array.from(new Set(ccBuf.names.map((n) => n.trim()).filter(Boolean)))
|
|
items.push({
|
|
left: uniq.length ? `抄送: ${uniq.join(', ')}` : '抄送:',
|
|
right: ccBuf.ts || ''
|
|
})
|
|
ccBuf = null
|
|
}
|
|
|
|
for (const p of list) {
|
|
const kind = String(p.kind || '')
|
|
const title = String(p.title || '-').trim()
|
|
const names = peopleDisplayNames(p)
|
|
const name = names.join(', ')
|
|
const ts = String(p.timestamp || '').trim()
|
|
const comment = String(p.commentText || '').trim()
|
|
const st = progressNodeStatusText(p)
|
|
const signMode = String(p.signMode || p.sign_mode || '')
|
|
const signLabel = signMode === 'and' ? '会签' : signMode === 'or' ? '或签' : ''
|
|
|
|
if (kind === 'comment') {
|
|
flushCc()
|
|
const revoked = p.status === 'revoked'
|
|
const author = String(p.assignee || p.title || '评论').trim()
|
|
const item: OaPrintFlowItem = {
|
|
left: `【评论】 ${author}${revoked ? ' 已撤回' : ''}`,
|
|
right: ts
|
|
}
|
|
if (!revoked) {
|
|
const lines = []
|
|
if (comment) lines.push({ left: comment, right: '' })
|
|
const mentions = (p.mentions || [])
|
|
.map((m: any) => String(m.userName || m.userId || '').trim())
|
|
.filter(Boolean)
|
|
if (mentions.length)
|
|
lines.push({
|
|
left: `提到:${mentions.map((name: string) => `@${name}`).join('、')}`,
|
|
right: ''
|
|
})
|
|
if (lines.length) item.personLines = lines
|
|
const links = (p.attachmentRefs || [])
|
|
.map((file: any) => String(file.url || '').trim())
|
|
.filter((url: string) => url && !/^(?!https?:)[a-z][a-z0-9+.-]*:/i.test(url))
|
|
.map(resolveUrl)
|
|
.filter((url: string) => /^https?:\/\//i.test(url))
|
|
if (links.length) item.attachmentLinks = links
|
|
}
|
|
items.push(item)
|
|
continue
|
|
}
|
|
|
|
if (isCcProgressItem(p)) {
|
|
if (!ccBuf) ccBuf = { names: [], ts }
|
|
if (names.length) ccBuf.names.push(...names)
|
|
if (ts) ccBuf.ts = ts
|
|
continue
|
|
}
|
|
flushCc()
|
|
|
|
if (kind === 'submit') {
|
|
items.push({
|
|
left: name ? `【${title}】 ${name} ${st}` : `【${title}】 ${st}`,
|
|
right: ts,
|
|
comment: comment || undefined
|
|
})
|
|
continue
|
|
}
|
|
if (kind === 'terminal') {
|
|
if (String(p.status || '') === 'returned' || title === '已打回') {
|
|
continue
|
|
}
|
|
const withdrawName = String(p.assigneeUserName || p.assignee || '').trim()
|
|
const withdrawTip = comment || (String(p.status || '') === 'cancelled' ? '发起人撤销' : '')
|
|
const leftExtra =
|
|
String(p.status || '') === 'cancelled' && (withdrawName || withdrawTip)
|
|
? ` ${[withdrawName, withdrawTip].filter(Boolean).join(' ')}`
|
|
: ''
|
|
items.push({
|
|
left: `${title || st}${leftExtra}`,
|
|
right: ts
|
|
})
|
|
continue
|
|
}
|
|
if (kind === 'pending_node') {
|
|
const personLines = progressPersonLinesFromItem(p)
|
|
const head = signLabel ? `【${title}·${signLabel}】` : `【${title}】`
|
|
items.push({
|
|
left: personLines.length
|
|
? `${head} ${st}`
|
|
: name
|
|
? `${head} ${name} ${st}`
|
|
: `${head} ${st}`,
|
|
right: personLines.length ? '' : ts,
|
|
personLines: personLines.length ? personLines : undefined
|
|
})
|
|
continue
|
|
}
|
|
const head = signLabel ? `【${title}·${signLabel}】` : `【${title}】`
|
|
const personLines = progressPersonLinesFromItem(p)
|
|
if (personLines.length) {
|
|
items.push({
|
|
left: `${head} ${st}`,
|
|
right: '',
|
|
personLines
|
|
})
|
|
continue
|
|
}
|
|
items.push({
|
|
left: name ? `${head} ${name} ${st}` : `${head} ${st}`,
|
|
right: ts,
|
|
comment: comment || undefined
|
|
})
|
|
}
|
|
flushCc()
|
|
|
|
const hasCcLine = items.some((it) => it.left.startsWith('抄送'))
|
|
if (!hasCcLine) {
|
|
const tasks = Array.isArray(detail?.tasks) ? detail.tasks : []
|
|
const ccTasks = tasks.filter((t: any) => {
|
|
const tt = String(t?.taskType || t?.task_type || '').trim()
|
|
const sm = String(t?.signMode || t?.sign_mode || '').trim()
|
|
return tt === 'cc' || sm === 'cc'
|
|
})
|
|
if (ccTasks.length) {
|
|
const names = ccTasks
|
|
.map((t: any) =>
|
|
String(t?.assigneeUserName || t?.assignee_user_name || t?.assigneeUserId || '').trim()
|
|
)
|
|
.filter(Boolean)
|
|
const uniq = Array.from(new Set(names))
|
|
let ts = ''
|
|
for (const t of ccTasks) {
|
|
const tTs = String(t?.updateTime || t?.createTime || '').trim()
|
|
if (tTs && (!ts || tTs > ts)) ts = tTs
|
|
}
|
|
if (uniq.length) {
|
|
items.push({ left: `抄送: ${uniq.join(', ')}`, right: ts })
|
|
}
|
|
}
|
|
}
|
|
|
|
return items
|
|
}
|