docs(oa): add implementation plan for or/and-sign progress and PDF display
Break work into shared person-line formatter, OaProgressChain, and print flow wiring. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
569a13a3ea
commit
f951c12071
1 file changed
+629
@@ -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,避免模板复杂化)。
|
||||
Reference in new issue
Block a user