|
|
@@ -11,6 +11,7 @@ import {
|
|
|
normalizeRelatedIdsFromFormValue,
|
|
|
type RelatedPrintBlock
|
|
|
} from '@/utils/oa-approval-print-related'
|
|
|
+import { formatProgressPersonLines } from '@/utils/oa-progress-person-lines'
|
|
|
|
|
|
export const OA_PRINT_COMPANY_NAME = '安徽中科兴皖医学研究有限公司'
|
|
|
|
|
|
@@ -31,7 +32,10 @@ export type OaPrintFieldRow = {
|
|
|
export type OaPrintFlowItem = {
|
|
|
left: string
|
|
|
right: string
|
|
|
+ /** @deprecated 多人节点改用 personLines;单人/发起人可保留 */
|
|
|
comment?: string
|
|
|
+ /** 或签/会签按人纯文本行 */
|
|
|
+ personLines?: string[]
|
|
|
}
|
|
|
|
|
|
export type OaPrintModel = {
|
|
|
@@ -254,57 +258,70 @@ async function buildFormRows(detail: any): Promise<OaPrintFieldRow[]> {
|
|
|
return out
|
|
|
}
|
|
|
|
|
|
-function buildFlowItems(detail: any): OaPrintFlowItem[] {
|
|
|
+/** 钉钉风格:人名不加状态括号,状态放在整行末尾 */
|
|
|
+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): string[] {
|
|
|
+ const people = Array.isArray(p?.people) ? p.people : []
|
|
|
+ if (!people.length) return []
|
|
|
+ return formatProgressPersonLines(people).map((x) => x.text)
|
|
|
+}
|
|
|
+
|
|
|
+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()
|
|
|
+ // 主判据:taskType / signMode;标题兜底仅匹配默认「抄送人」或「抄送…」节点名
|
|
|
+ return (
|
|
|
+ taskType === 'cc' ||
|
|
|
+ signMode === 'cc' ||
|
|
|
+ title === '抄送人' ||
|
|
|
+ /^抄送/.test(title)
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 从 progress 组装审批流程行;抄送节点聚合为「抄送A, B, C」对齐钉钉导出。
|
|
|
+ * 若 progress 漏抄送,则用 tasks 兜底补一行。
|
|
|
+ */
|
|
|
+export function buildFlowItems(detail: any): OaPrintFlowItem[] {
|
|
|
const list = Array.isArray(detail?.progress) ? detail.progress : []
|
|
|
const items: OaPrintFlowItem[] = []
|
|
|
|
|
|
- // 聚合连续抄送任务,接近钉钉「抄送A,B,C」
|
|
|
+ // 聚合连续抄送任务,对齐钉钉「抄送A, B, C」
|
|
|
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: `抄送${ccBuf.names.join(',')}`,
|
|
|
+ left: uniq.length ? `抄送: ${uniq.join(', ')}` : '抄送:',
|
|
|
right: ccBuf.ts || ''
|
|
|
})
|
|
|
ccBuf = null
|
|
|
}
|
|
|
|
|
|
- const peopleNames = (p: any): string[] => {
|
|
|
- const people = Array.isArray(p?.people) ? p.people : []
|
|
|
- const fromPeople = people
|
|
|
- .map((x: any) => {
|
|
|
- const n = String(x?.nickName || x?.id || '').trim()
|
|
|
- if (!n) return ''
|
|
|
- const st = String(x?.status || '')
|
|
|
- if (st === 'approved') return `${n}(已同意)`
|
|
|
- if (st === 'rejected') return `${n}(已拒绝)`
|
|
|
- if (st === 'returned') return `${n}(已打回)`
|
|
|
- if (st === 'pending' || st === 'running') return `${n}(待处理)`
|
|
|
- if (st === 'done') return `${n}(无需处理)`
|
|
|
- return n
|
|
|
- })
|
|
|
- .filter(Boolean)
|
|
|
- if (fromPeople.length) return fromPeople
|
|
|
- const name = String(p.assigneeUserName || p.assigneeUserId || '').trim()
|
|
|
- return name ? [name] : []
|
|
|
- }
|
|
|
-
|
|
|
for (const p of list) {
|
|
|
const kind = String(p.kind || '')
|
|
|
- const taskType = String(p.taskType || '')
|
|
|
const title = String(p.title || '-').trim()
|
|
|
- const names = peopleNames(p)
|
|
|
- const name = names.join(',')
|
|
|
+ const names = peopleDisplayNames(p)
|
|
|
+ const name = names.join(', ')
|
|
|
const ts = String(p.timestamp || '').trim()
|
|
|
const comment = String(p.commentText || '').trim()
|
|
|
const st = statusText(String(p.status || ''))
|
|
|
- const signMode = String(p.signMode || '')
|
|
|
+ const signMode = String(p.signMode || p.sign_mode || '')
|
|
|
const signLabel = signMode === 'and' ? '会签' : signMode === 'or' ? '或签' : ''
|
|
|
|
|
|
- if (taskType === 'cc') {
|
|
|
+ if (isCcProgressItem(p)) {
|
|
|
if (!ccBuf) ccBuf = { names: [], ts }
|
|
|
- if (names.length) ccBuf.names.push(...names.map((n) => n.replace(/\(.*?\)\s*$/, '')))
|
|
|
- else if (name) ccBuf.names.push(name)
|
|
|
+ if (names.length) ccBuf.names.push(...names)
|
|
|
if (ts) ccBuf.ts = ts
|
|
|
continue
|
|
|
}
|
|
|
@@ -319,6 +336,10 @@ function buildFlowItems(detail: any): OaPrintFlowItem[] {
|
|
|
continue
|
|
|
}
|
|
|
if (kind === 'terminal') {
|
|
|
+ // 打回结果已落在审批人节点,不单独输出终态「已打回」行
|
|
|
+ if (String(p.status || '') === 'returned' || title === '已打回') {
|
|
|
+ continue
|
|
|
+ }
|
|
|
items.push({
|
|
|
left: title || st,
|
|
|
right: ts
|
|
|
@@ -326,14 +347,30 @@ function buildFlowItems(detail: any): OaPrintFlowItem[] {
|
|
|
continue
|
|
|
}
|
|
|
if (kind === 'pending_node') {
|
|
|
+ const personLines = progressPersonLinesFromItem(p)
|
|
|
+ const head = signLabel ? `【${title}·${signLabel}】` : `【${title}】`
|
|
|
items.push({
|
|
|
- left: `【${title}】 ${st}`,
|
|
|
- right: ts
|
|
|
+ left: personLines.length
|
|
|
+ ? `${head} ${st}`
|
|
|
+ : name
|
|
|
+ ? `${head} ${name} ${st}`
|
|
|
+ : `${head} ${st}`,
|
|
|
+ right: personLines.length ? '' : ts,
|
|
|
+ personLines: personLines.length ? personLines : undefined
|
|
|
})
|
|
|
continue
|
|
|
}
|
|
|
// task(或签/会签同节点多人已合并)
|
|
|
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,
|
|
|
@@ -341,6 +378,34 @@ function buildFlowItems(detail: any): OaPrintFlowItem[] {
|
|
|
})
|
|
|
}
|
|
|
flushCc()
|
|
|
+
|
|
|
+ // progress 未带抄送时,用 tasks 兜底(对齐钉钉:流程末展示抄送人)
|
|
|
+ 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
|
|
|
}
|
|
|
|
|
|
@@ -397,6 +462,35 @@ export function buildOaPrintFilename(detail: any): string {
|
|
|
return `${safe(initiator)}提交的${safe(appName)}.pdf`
|
|
|
}
|
|
|
|
|
|
+/** 用 canvas 绘制中文页码(jsPDF 内置字体不支持汉字,会乱码) */
|
|
|
+function renderPageLabelCanvas(text: string): HTMLCanvasElement {
|
|
|
+ const font = '20px "PingFang SC", "Microsoft YaHei", "Heiti SC", sans-serif'
|
|
|
+ const measure = document.createElement('canvas')
|
|
|
+ const mctx = measure.getContext('2d')
|
|
|
+ if (!mctx) {
|
|
|
+ const empty = document.createElement('canvas')
|
|
|
+ empty.width = 1
|
|
|
+ empty.height = 1
|
|
|
+ return empty
|
|
|
+ }
|
|
|
+ mctx.font = font
|
|
|
+ const tw = Math.ceil(mctx.measureText(text).width) + 8
|
|
|
+ const th = 28
|
|
|
+ const scale = 2
|
|
|
+ const c = document.createElement('canvas')
|
|
|
+ c.width = tw * scale
|
|
|
+ c.height = th * scale
|
|
|
+ const ctx = c.getContext('2d')
|
|
|
+ if (!ctx) return c
|
|
|
+ ctx.scale(scale, scale)
|
|
|
+ ctx.clearRect(0, 0, tw, th)
|
|
|
+ ctx.font = font
|
|
|
+ ctx.fillStyle = '#646464'
|
|
|
+ ctx.textBaseline = 'middle'
|
|
|
+ ctx.fillText(text, 4, th / 2)
|
|
|
+ return c
|
|
|
+}
|
|
|
+
|
|
|
async function canvasToPdf(canvas: HTMLCanvasElement, filename: string) {
|
|
|
const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' })
|
|
|
const pageWidth = pdf.internal.pageSize.getWidth()
|
|
|
@@ -404,25 +498,31 @@ async function canvasToPdf(canvas: HTMLCanvasElement, filename: string) {
|
|
|
const imgWidth = pageWidth
|
|
|
const imgHeight = (canvas.height * imgWidth) / canvas.width
|
|
|
|
|
|
+ // 略超一页(原 min-height / 浮点)时不产生空白尾页;余量 < 1.5mm 仍算一页
|
|
|
+ const EPS_MM = 1.5
|
|
|
+ const totalPages = Math.min(40, Math.max(1, Math.ceil((imgHeight - EPS_MM) / pageHeight)))
|
|
|
+
|
|
|
const pageData = canvas.toDataURL('image/jpeg', 0.95)
|
|
|
- let heightLeft = imgHeight
|
|
|
- let position = 0
|
|
|
- let pageIndex = 0
|
|
|
+ const labelMmH = 4.5
|
|
|
|
|
|
- while (heightLeft > 0) {
|
|
|
+ for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
|
|
if (pageIndex > 0) pdf.addPage()
|
|
|
+ const position = -pageIndex * pageHeight
|
|
|
pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
|
|
|
- // 页码
|
|
|
- const totalHint = Math.max(1, Math.ceil(imgHeight / pageHeight))
|
|
|
- pdf.setFontSize(10)
|
|
|
- pdf.setTextColor(100)
|
|
|
- pdf.text(`第 ${pageIndex + 1} 页`, pageWidth / 2, pageHeight - 8, { align: 'center' })
|
|
|
- void totalHint
|
|
|
- heightLeft -= pageHeight
|
|
|
- position -= pageHeight
|
|
|
- pageIndex += 1
|
|
|
- // 安全上限
|
|
|
- if (pageIndex > 40) break
|
|
|
+
|
|
|
+ const label = `第${pageIndex + 1}页`
|
|
|
+ const labelCanvas = renderPageLabelCanvas(label)
|
|
|
+ if (labelCanvas.width > 1) {
|
|
|
+ const labelMmW = (labelCanvas.width / labelCanvas.height) * labelMmH
|
|
|
+ pdf.addImage(
|
|
|
+ labelCanvas.toDataURL('image/png'),
|
|
|
+ 'PNG',
|
|
|
+ (pageWidth - labelMmW) / 2,
|
|
|
+ pageHeight - 10,
|
|
|
+ labelMmW,
|
|
|
+ labelMmH
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
pdf.save(filename)
|