|
|
@@ -0,0 +1,629 @@
|
|
|
+# OA 或签 / 会签进度与 PDF 展示 Implementation Plan
|
|
|
+
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** 审批详情「审批进度」与打印 PDF「审批流程」对或签/会签多人节点统一按纯文本行展示:`姓名 意见 时间`(未批「待处理」、或签免处理「无需处理」且仍列出)。
|
|
|
+
|
|
|
+**Architecture:** 抽纯函数 `formatProgressPersonLines`(及行态 `tone`)供 `OaProgressChain` 与 `buildFlowItems` 共用;PDF 模型增加 `personLines`,模板分行渲染。不改引擎收口语义,不改发起页预览。
|
|
|
+
|
|
|
+**Tech Stack:** Vue 3 · TypeScript · 现有 `OaProgressChain` / `oa-approval-print.ts` · `tsx` + `node:assert`(仓库无 vitest,用脚本测纯函数)
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-07-23-oa-or-and-sign-display-design.md`
|
|
|
+**Demo:** `docs/mockups/oa-or-and-sign-progress-display-demo.html`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File map
|
|
|
+
|
|
|
+| Path | Responsibility |
|
|
|
+|------|----------------|
|
|
|
+| Create: `web-admin/src/utils/oa-progress-person-lines.ts` | 纯函数:`people[]` → 行文案 + tone |
|
|
|
+| Create: `web-admin/scripts/check-oa-progress-person-lines.ts` | 用 `tsx` 跑断言(无 vitest) |
|
|
|
+| Modify: `web-admin/src/components/oa-approval/OaProgressChain.vue` | 节点下渲染 person-lines;有行时隐藏节点级「意见:」避免双份 |
|
|
|
+| Modify: `web-admin/src/utils/oa-approval-print.ts` | `OaPrintFlowItem.personLines`;`buildFlowItems` 接入格式化 |
|
|
|
+| Modify: `web-admin/src/components/oa-approval/OaApprovalPrintSheet.vue` | 渲染 `personLines` 多行;弱化旧 `comment` 双份 |
|
|
|
+
|
|
|
+**Locked rules (from spec):**
|
|
|
+
|
|
|
+- 行格式:已处理 `{姓名} {意见或状态文案} {时间}`;未处理 `{姓名} 待处理`;或签免处理 `{姓名} 无需处理`(必须仍列出)。
|
|
|
+- `done` → 无需处理;`pending|running|not_reached` → 待处理。
|
|
|
+- 意见非空用原文;已处理意见空则用 `已同意`/`已拒绝`/`已打回` 等,禁止双空格。
|
|
|
+- 后端原则上不改;仅当前端缺字段时再补。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: 纯函数 + 断言脚本(TDD)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `web-admin/src/utils/oa-progress-person-lines.ts`
|
|
|
+- Create: `web-admin/scripts/check-oa-progress-person-lines.ts`
|
|
|
+
|
|
|
+- [ ] **Step 1: Write the failing check script first**
|
|
|
+
|
|
|
+Create `web-admin/scripts/check-oa-progress-person-lines.ts`:
|
|
|
+
|
|
|
+```ts
|
|
|
+import assert from 'node:assert/strict'
|
|
|
+import {
|
|
|
+ formatProgressPersonLines,
|
|
|
+ type ProgressPersonInput
|
|
|
+} from '../src/utils/oa-progress-person-lines'
|
|
|
+
|
|
|
+function run() {
|
|
|
+ // 会签部分完成
|
|
|
+ const andPartial: ProgressPersonInput[] = [
|
|
|
+ {
|
|
|
+ nickName: '甲',
|
|
|
+ status: 'approved',
|
|
|
+ commentText: '同意',
|
|
|
+ timestamp: '2026-07-23 10:01:00'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ nickName: '乙',
|
|
|
+ status: 'approved',
|
|
|
+ commentText: '无异议',
|
|
|
+ timestamp: '2026-07-23 10:20:00'
|
|
|
+ },
|
|
|
+ { nickName: '丙', status: 'pending', commentText: '', timestamp: '' }
|
|
|
+ ]
|
|
|
+ const andLines = formatProgressPersonLines(andPartial)
|
|
|
+ assert.deepEqual(
|
|
|
+ andLines.map((x) => x.text),
|
|
|
+ [
|
|
|
+ '甲 同意 2026-07-23 10:01:00',
|
|
|
+ '乙 无异议 2026-07-23 10:20:00',
|
|
|
+ '丙 待处理'
|
|
|
+ ]
|
|
|
+ )
|
|
|
+ assert.equal(andLines[0].tone, 'ok')
|
|
|
+ assert.equal(andLines[2].tone, 'pending')
|
|
|
+
|
|
|
+ // 或签一人已批:操作人 + 免处理仍列出
|
|
|
+ const orDone: ProgressPersonInput[] = [
|
|
|
+ {
|
|
|
+ nickName: '甲',
|
|
|
+ status: 'approved',
|
|
|
+ commentText: '同意,材料齐全',
|
|
|
+ timestamp: '2026-07-23 10:05:00'
|
|
|
+ },
|
|
|
+ { nickName: '乙', status: 'done', commentText: '', timestamp: '2026-07-23 10:00:00' },
|
|
|
+ { nickName: '丙', status: 'done', commentText: '', timestamp: '' }
|
|
|
+ ]
|
|
|
+ const orLines = formatProgressPersonLines(orDone)
|
|
|
+ assert.deepEqual(
|
|
|
+ orLines.map((x) => x.text),
|
|
|
+ ['甲 同意,材料齐全 2026-07-23 10:05:00', '乙 无需处理', '丙 无需处理']
|
|
|
+ )
|
|
|
+ assert.equal(orLines[0].tone, 'ok')
|
|
|
+ assert.equal(orLines[1].tone, 'mute')
|
|
|
+ assert.equal(orLines[2].tone, 'mute')
|
|
|
+
|
|
|
+ // 已同意但无意见 → 状态文案兜底
|
|
|
+ const emptyComment = formatProgressPersonLines([
|
|
|
+ {
|
|
|
+ nickName: '甲',
|
|
|
+ status: 'approved',
|
|
|
+ commentText: '',
|
|
|
+ timestamp: '2026-07-23 10:05:00'
|
|
|
+ }
|
|
|
+ ])
|
|
|
+ assert.equal(emptyComment[0].text, '甲 已同意 2026-07-23 10:05:00')
|
|
|
+
|
|
|
+ // 拒绝
|
|
|
+ const rejected = formatProgressPersonLines([
|
|
|
+ {
|
|
|
+ nickName: '甲',
|
|
|
+ status: 'rejected',
|
|
|
+ commentText: '不同意',
|
|
|
+ timestamp: '2026-07-23 11:00:00'
|
|
|
+ },
|
|
|
+ { nickName: '乙', status: 'done' }
|
|
|
+ ])
|
|
|
+ assert.equal(rejected[0].text, '甲 不同意 2026-07-23 11:00:00')
|
|
|
+ assert.equal(rejected[0].tone, 'danger')
|
|
|
+ assert.equal(rejected[1].text, '乙 无需处理')
|
|
|
+
|
|
|
+ // 空 people
|
|
|
+ assert.deepEqual(formatProgressPersonLines([]), [])
|
|
|
+ assert.deepEqual(formatProgressPersonLines(undefined), [])
|
|
|
+
|
|
|
+ console.log('oa-progress-person-lines: all assertions passed')
|
|
|
+}
|
|
|
+
|
|
|
+run()
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Run check — expect FAIL (module missing)**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && pnpm exec tsx scripts/check-oa-progress-person-lines.ts
|
|
|
+```
|
|
|
+
|
|
|
+Expected: error like `Cannot find module '../src/utils/oa-progress-person-lines'`
|
|
|
+
|
|
|
+- [ ] **Step 3: Implement the util**
|
|
|
+
|
|
|
+Create `web-admin/src/utils/oa-progress-person-lines.ts`:
|
|
|
+
|
|
|
+```ts
|
|
|
+/**
|
|
|
+ * 或签/会签进度人员行格式化
|
|
|
+ * Spec: docs/superpowers/specs/2026-07-23-oa-or-and-sign-display-design.md
|
|
|
+ */
|
|
|
+
|
|
|
+export type ProgressPersonInput = {
|
|
|
+ id?: string
|
|
|
+ nickName?: string
|
|
|
+ status?: string
|
|
|
+ timestamp?: string
|
|
|
+ commentText?: string
|
|
|
+}
|
|
|
+
|
|
|
+export type PersonLineTone = 'ok' | 'pending' | 'danger' | 'mute' | ''
|
|
|
+
|
|
|
+export type ProgressPersonLine = {
|
|
|
+ text: string
|
|
|
+ tone: PersonLineTone
|
|
|
+}
|
|
|
+
|
|
|
+const ACTED_STATUS = new Set(['approved', 'rejected', 'returned', 'completed'])
|
|
|
+
|
|
|
+function displayName(p: ProgressPersonInput): string {
|
|
|
+ return String(p.nickName || p.id || '').trim()
|
|
|
+}
|
|
|
+
|
|
|
+function statusFallbackLabel(status: string): string {
|
|
|
+ const s = (status || '').trim()
|
|
|
+ if (s === 'approved') return '已同意'
|
|
|
+ if (s === 'rejected') return '已拒绝'
|
|
|
+ if (s === 'returned') return '已打回'
|
|
|
+ if (s === 'completed') return '已完成'
|
|
|
+ return s || '已处理'
|
|
|
+}
|
|
|
+
|
|
|
+function toneForStatus(status: string): PersonLineTone {
|
|
|
+ const s = (status || '').trim()
|
|
|
+ if (s === 'approved' || s === 'completed') return 'ok'
|
|
|
+ if (s === 'pending' || s === 'running' || s === 'not_reached') return 'pending'
|
|
|
+ if (s === 'rejected' || s === 'returned' || s === 'cancelled') return 'danger'
|
|
|
+ if (s === 'done') return 'mute'
|
|
|
+ return ''
|
|
|
+}
|
|
|
+
|
|
|
+/** 将 progress.people 格式化为「姓名 意见/状态 时间」纯文本行 */
|
|
|
+export function formatProgressPersonLines(
|
|
|
+ people: ProgressPersonInput[] | null | undefined
|
|
|
+): ProgressPersonLine[] {
|
|
|
+ const list = Array.isArray(people) ? people : []
|
|
|
+ const out: ProgressPersonLine[] = []
|
|
|
+ for (const p of list) {
|
|
|
+ const name = displayName(p)
|
|
|
+ if (!name) continue
|
|
|
+ const status = String(p.status || '').trim()
|
|
|
+ const comment = String(p.commentText || '').trim()
|
|
|
+ const ts = String(p.timestamp || '').trim()
|
|
|
+ const tone = toneForStatus(status)
|
|
|
+
|
|
|
+ if (status === 'done') {
|
|
|
+ out.push({ text: `${name} 无需处理`, tone: 'mute' })
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if (status === 'pending' || status === 'running' || status === 'not_reached' || !status) {
|
|
|
+ // 无 status 时:若有意见/时间仍按已处理拼;否则待处理
|
|
|
+ if (!status && (comment || ts)) {
|
|
|
+ const mid = comment || statusFallbackLabel('approved')
|
|
|
+ out.push({
|
|
|
+ text: ts ? `${name} ${mid} ${ts}` : `${name} ${mid}`,
|
|
|
+ tone: 'ok'
|
|
|
+ })
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ out.push({ text: `${name} 待处理`, tone: 'pending' })
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if (ACTED_STATUS.has(status) || comment || ts) {
|
|
|
+ const mid = comment || statusFallbackLabel(status)
|
|
|
+ out.push({
|
|
|
+ text: ts ? `${name} ${mid} ${ts}` : `${name} ${mid}`,
|
|
|
+ tone: tone || 'ok'
|
|
|
+ })
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ out.push({ text: `${name} 待处理`, tone: 'pending' })
|
|
|
+ }
|
|
|
+ return out
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: Run check — expect PASS**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && pnpm exec tsx scripts/check-oa-progress-person-lines.ts
|
|
|
+```
|
|
|
+
|
|
|
+Expected: `oa-progress-person-lines: all assertions passed`
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/utils/oa-progress-person-lines.ts web-admin/scripts/check-oa-progress-person-lines.ts
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): add OA progress person-line formatter for or/and sign
|
|
|
+
|
|
|
+Provide shared plain-text lines (name opinion time) used by progress UI and PDF.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: 审批进度 UI(OaProgressChain)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/components/oa-approval/OaProgressChain.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: Import formatter and render person-lines**
|
|
|
+
|
|
|
+In `<script setup>` add:
|
|
|
+
|
|
|
+```ts
|
|
|
+import {
|
|
|
+ formatProgressPersonLines,
|
|
|
+ type ProgressPersonLine
|
|
|
+} from '@/utils/oa-progress-person-lines'
|
|
|
+
|
|
|
+function personLines(item: OaProgressChainItem): ProgressPersonLine[] {
|
|
|
+ if (item.kind === 'terminal' || item.kind === 'submit') return []
|
|
|
+ if (item.taskType === 'cc' || item.signMode === 'cc') return []
|
|
|
+ const list = displayPeople(item)
|
|
|
+ if (!list.length) return []
|
|
|
+ return formatProgressPersonLines(list)
|
|
|
+}
|
|
|
+
|
|
|
+function showNodeComment(item: OaProgressChainItem): boolean {
|
|
|
+ // 有按人行时不再展示节点级拼接意见,避免双份
|
|
|
+ if (personLines(item).length) return false
|
|
|
+ return Boolean(item.commentText)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+In template, **replace** the comment block and add lines after the card row (inside `.oa-flow-card`):
|
|
|
+
|
|
|
+```vue
|
|
|
+ <div v-if="personLines(item).length" class="oa-person-lines">
|
|
|
+ <p
|
|
|
+ v-for="(line, li) in personLines(item)"
|
|
|
+ :key="`${item.key}-pl-${li}`"
|
|
|
+ class="oa-person-line"
|
|
|
+ :class="{
|
|
|
+ 'is-ok': line.tone === 'ok',
|
|
|
+ 'is-pending': line.tone === 'pending',
|
|
|
+ 'is-danger': line.tone === 'danger',
|
|
|
+ 'is-mute': line.tone === 'mute'
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ {{ line.text }}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ <div v-else-if="showNodeComment(item)" class="oa-flow-card__comment">
|
|
|
+ 意见:{{ item.commentText }}
|
|
|
+ </div>
|
|
|
+```
|
|
|
+
|
|
|
+Keep existing chips in the header row (高亮/灰显已由 `personChipClass` 覆盖 `done`→`is-mute`、`approved`→`is-ok`)。
|
|
|
+
|
|
|
+- [ ] **Step 2: Add scoped styles**
|
|
|
+
|
|
|
+```scss
|
|
|
+ .oa-person-lines {
|
|
|
+ margin-top: 8px;
|
|
|
+ display: grid;
|
|
|
+ gap: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .oa-person-line {
|
|
|
+ margin: 0;
|
|
|
+ color: var(--mint-text, #334155);
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.55;
|
|
|
+ word-break: break-word;
|
|
|
+
|
|
|
+ &.is-ok {
|
|
|
+ color: #059669;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-pending {
|
|
|
+ color: #d97706;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-danger {
|
|
|
+ color: #dc2626;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-mute {
|
|
|
+ color: #94a3b8;
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Smoke-check in browser**
|
|
|
+
|
|
|
+1. `cd web-admin && pnpm dev`
|
|
|
+2. 打开含或签多人节点的实例详情,确认:未批见「姓名 待处理」;一人同意后甲高亮行 + 乙丙「无需处理」灰显;节点下不再出现「意见:甲:…」拼接块。
|
|
|
+3. 会签部分完成:已批行含意见时间,未批「待处理」。
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/components/oa-approval/OaProgressChain.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): show per-person approval lines on OA progress chain
|
|
|
+
|
|
|
+Render name/opinion/time under multi-approver nodes; hide duplicate node comment.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: PDF buildFlowItems + PrintSheet
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/utils/oa-approval-print.ts`
|
|
|
+- Modify: `web-admin/src/components/oa-approval/OaApprovalPrintSheet.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: Extend type and wire formatter in `buildFlowItems`**
|
|
|
+
|
|
|
+In `oa-approval-print.ts`:
|
|
|
+
|
|
|
+1. Import:
|
|
|
+
|
|
|
+```ts
|
|
|
+import { formatProgressPersonLines } from '@/utils/oa-progress-person-lines'
|
|
|
+```
|
|
|
+
|
|
|
+2. Change type:
|
|
|
+
|
|
|
+```ts
|
|
|
+export type OaPrintFlowItem = {
|
|
|
+ left: string
|
|
|
+ right: string
|
|
|
+ /** @deprecated 多人节点改用 personLines;单人/发起人可保留 */
|
|
|
+ comment?: string
|
|
|
+ /** 或签/会签按人纯文本行 */
|
|
|
+ personLines?: string[]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+3. Add helper near `peopleDisplayNames`:
|
|
|
+
|
|
|
+```ts
|
|
|
+function progressPersonLinesFromItem(p: any): string[] {
|
|
|
+ const people = Array.isArray(p?.people) ? p.people : []
|
|
|
+ if (!people.length) return []
|
|
|
+ return formatProgressPersonLines(people).map((x) => x.text)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+4. In the `kind === 'pending_node'` branch, after building names, prefer person lines:
|
|
|
+
|
|
|
+```ts
|
|
|
+ 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
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+5. Replace the task branch (comment `// task(或签/会签…)`) with:
|
|
|
+
|
|
|
+```ts
|
|
|
+ // 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,
|
|
|
+ comment: comment || undefined
|
|
|
+ })
|
|
|
+```
|
|
|
+
|
|
|
+Keep submit / terminal / cc behavior unchanged(submit 仍可用单行 `comment`)。
|
|
|
+
|
|
|
+- [ ] **Step 2: Update PrintSheet template**
|
|
|
+
|
|
|
+In `OaApprovalPrintSheet.vue`, inside `.oa-print-flow__cell`, after the left/right row:
|
|
|
+
|
|
|
+```vue
|
|
|
+ <div v-if="f.personLines?.length" class="oa-print-flow__person-lines">
|
|
|
+ <div
|
|
|
+ v-for="(line, li) in f.personLines"
|
|
|
+ :key="li"
|
|
|
+ class="oa-print-flow__person-line"
|
|
|
+ >
|
|
|
+ {{ line }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-else-if="f.comment"
|
|
|
+ class="oa-print-flow__comment"
|
|
|
+ >
|
|
|
+ 意见:{{ f.comment }}
|
|
|
+ </div>
|
|
|
+```
|
|
|
+
|
|
|
+Add styles:
|
|
|
+
|
|
|
+```scss
|
|
|
+.oa-print-flow__person-lines {
|
|
|
+ margin-top: 6px;
|
|
|
+ display: grid;
|
|
|
+ gap: 3px;
|
|
|
+}
|
|
|
+.oa-print-flow__person-line {
|
|
|
+ line-height: 1.55;
|
|
|
+ word-break: break-word;
|
|
|
+ white-space: pre-wrap;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Extend check script for buildFlowItems or签样例(可选但推荐)**
|
|
|
+
|
|
|
+Append to `web-admin/scripts/check-oa-progress-person-lines.ts` (or create second assert block in same file):
|
|
|
+
|
|
|
+```ts
|
|
|
+import { buildFlowItems } from '../src/utils/oa-approval-print'
|
|
|
+
|
|
|
+ const detail = {
|
|
|
+ progress: [
|
|
|
+ {
|
|
|
+ kind: 'submit',
|
|
|
+ title: '发起人',
|
|
|
+ status: 'submitted',
|
|
|
+ assigneeUserName: '张三',
|
|
|
+ timestamp: '2026-07-23 09:00:00',
|
|
|
+ people: []
|
|
|
+ },
|
|
|
+ {
|
|
|
+ kind: 'task',
|
|
|
+ title: '部门审批',
|
|
|
+ status: 'approved',
|
|
|
+ signMode: 'or',
|
|
|
+ timestamp: '2026-07-23 10:05:00',
|
|
|
+ commentText: '甲:同意,材料齐全',
|
|
|
+ people: [
|
|
|
+ {
|
|
|
+ nickName: '甲',
|
|
|
+ status: 'approved',
|
|
|
+ commentText: '同意,材料齐全',
|
|
|
+ timestamp: '2026-07-23 10:05:00'
|
|
|
+ },
|
|
|
+ { nickName: '乙', status: 'done' },
|
|
|
+ { nickName: '丙', status: 'done' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ { kind: 'terminal', title: '流程结束', status: 'done', timestamp: '2026-07-23 10:05:00' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ const flow = buildFlowItems(detail)
|
|
|
+ const taskFlow = flow.find((x) => x.left.includes('部门审批'))
|
|
|
+ assert.ok(taskFlow)
|
|
|
+ assert.ok(taskFlow!.left.includes('或签'))
|
|
|
+ assert.deepEqual(taskFlow!.personLines, [
|
|
|
+ '甲 同意,材料齐全 2026-07-23 10:05:00',
|
|
|
+ '乙 无需处理',
|
|
|
+ '丙 无需处理'
|
|
|
+ ])
|
|
|
+ assert.equal(taskFlow!.comment, undefined)
|
|
|
+```
|
|
|
+
|
|
|
+Note: `buildFlowItems` 当前文件有 Vue/`html2canvas` 副作用依赖;若 `tsx` 导入失败,可只保留 Task 1 纯函数断言,本步改为手动打印验收,**不要**为测 PDF 强行拆文件除非导入报错。
|
|
|
+
|
|
|
+若导入报错,跳过本脚本扩展,改为 Step 4 手工验收。
|
|
|
+
|
|
|
+- [ ] **Step 4: Manual PDF check**
|
|
|
+
|
|
|
+1. 在「我发起的」对或签已通过实例点「打印」,打开 PDF「审批流程」。
|
|
|
+2. 期望类似:
|
|
|
+
|
|
|
+```
|
|
|
+【部门审批·或签】 已同意
|
|
|
+甲 同意,材料齐全 2026-07-23 10:05:00
|
|
|
+乙 无需处理
|
|
|
+丙 无需处理
|
|
|
+```
|
|
|
+
|
|
|
+3. 会签完成实例:每人一行「姓名 意见 时间」,无表头三列。
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/utils/oa-approval-print.ts \
|
|
|
+ web-admin/src/components/oa-approval/OaApprovalPrintSheet.vue \
|
|
|
+ web-admin/scripts/check-oa-progress-person-lines.ts
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): print OA or/and-sign nodes as per-person text lines
|
|
|
+
|
|
|
+Align PDF flow section with progress UI person-line format.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: 回归与收尾
|
|
|
+
|
|
|
+**Files:** none required(可选:在 print PDF design 文首加一句「多人节点见 2026-07-23 spec」——仅当发现文档仍写「姓名逗号拼接」且易误导时再改)
|
|
|
+
|
|
|
+- [ ] **Step 1: Re-run formatter check**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && pnpm exec tsx scripts/check-oa-progress-person-lines.ts
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 2: Backend regression (engine unchanged)**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_oa_engine.py::TestProgressItems::test_or_sign_same_node_merged_into_one_card -v
|
|
|
+```
|
|
|
+
|
|
|
+(若类名路径不同,用:`pytest tests/test_oa_engine.py -k or_sign_same_node_merged -v`)
|
|
|
+
|
|
|
+Expected: PASS — `people` 仍含三人含 `done`。
|
|
|
+
|
|
|
+- [ ] **Step 3: Acceptance checklist (spec §7)**
|
|
|
+
|
|
|
+- [ ] 或签未批:进度 + PDF 见全部候选人(待处理)
|
|
|
+- [ ] 或签一人同意:仅操作人高亮;乙丙「无需处理」仍在
|
|
|
+- [ ] 会签部分/全部:每人一行意见时间
|
|
|
+- [ ] 无表头三列表格
|
|
|
+- [ ] 单人节点、抄送、终态无回归
|
|
|
+- [ ] 节点级「意见:甲:…」在多人时不再与 person-lines 双显
|
|
|
+
|
|
|
+- [ ] **Step 4: Final commit only if docs tweaked; else done**
|
|
|
+
|
|
|
+若修改了 `docs/superpowers/specs/2026-07-15-oa-approval-print-pdf-design.md` 交叉引用:
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/superpowers/specs/2026-07-15-oa-approval-print-pdf-design.md
|
|
|
+git commit -m "docs(oa): point print flow multi-approver display to 2026-07-23 spec"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec coverage (self-review)
|
|
|
+
|
|
|
+| Spec 要求 | Task |
|
|
|
+|-----------|------|
|
|
|
+| §3 统一行格式 | Task 1 |
|
|
|
+| §4.1 或签高亮 + 无需处理仍列出 | Task 1–2(tone/chip)+ Task 3 PDF |
|
|
|
+| §4.2 会签每人一行 | Task 1–3 |
|
|
|
+| §5.1 OaProgressChain | Task 2 |
|
|
|
+| §5.2 buildFlowItems + PrintSheet | Task 3 |
|
|
|
+| §5.3 隐藏节点级双份意见 | Task 2–3 |
|
|
|
+| §7 验收 | Task 4 |
|
|
|
+| 不改引擎 | 无后端改动任务 |
|
|
|
+
|
|
|
+**Placeholder scan:** 无 TBD;`tsx` 导入 `buildFlowItems` 失败时的降级路径已写明。
|
|
|
+
|
|
|
+**Type consistency:** `ProgressPersonLine.text/tone`;`OaPrintFlowItem.personLines: string[]`(PDF 不需要 tone,避免模板复杂化)。
|