diff --git a/docs/mockups/oa-related-approval-nested-demo.html b/docs/mockups/oa-related-approval-nested-demo.html
new file mode 100644
index 00000000..76b1c2d8
--- /dev/null
+++ b/docs/mockups/oa-related-approval-nested-demo.html
@@ -0,0 +1,417 @@
+
+
+
+
+
关联审批单 · 嵌套展示方案
+
+ 场景:发起/详情里选中一张历史审批单 A;A 自己的表单里也有「关联审批单」字段(指向 B)。
+ 当前 V1 规格把嵌套字段压成「已关联 M 条」,导致看不到缩略信息与展开。请点选你更倾向的方案(可点卡片内「展开」试交互)。
+
+
+
+ 数据链:当前单 → 关联 差旅报销 · 07-18(单 A)→ 单 A 内又关联了 外出申请 · 07-10(单 B)
+
+
+
+
+
+
+
+
关联审批单
+
+
+
+
+
+
+ 事由客户拜访
+ 金额¥1,280.00
+ 关联审批单已关联 1 条
+ 备注含打车费
+
+
↑ 嵌套字段只有计数文案;即使点「展开」也看不到单 B 的业务字段
+
+
+ 优点:实现简单、无递归性能风险。缺点:信息不足,正是你遇到的问题。
+
+
+
+
+
+
+
关联审批单
+
+
+
+
+
+
+
+
+ 优点:改动小、无嵌套 UI。缺点:仍无缩略预览,要跳页才能看清。
+
+
+
+
+
+
+
关联审批单
+
+
+
+
+
+
+ 事由客户拜访
+ 金额¥1,280.00
+ 备注含打车费
+
+
+
内含关联审批单 · 1 条
+
+
+
+
+
+
+ 目的地上海
+ 起止07-10 ~ 07-12
+ 同行人张三
+ 关联审批单已关联 0 条
+
+
+
+
+
+ 优点:有缩略与展开,信息完整可控。缺点:enrichment 需多取一层;UI 略复杂。
+
+
+
+
+
+
+
关联审批单
+
+
+
+ 已展开
+
+
+ 事由客户拜访
+
+
+
+
+
+
+
+
+
+
+
+
…可能继续嵌套
+
+
+
+
+
+
+ 优点:最完整。缺点:性能、权限、循环引用、UI 噪声都更大,一般不建议 V1.x。
+
+
+
+
+
+
+ 当前选择:尚未选择
+ 也可直接在对话里回复 A / B / C / D
+
+
+
+
+
+
diff --git a/docs/mockups/oa-related-approval-print-demo.html b/docs/mockups/oa-related-approval-print-demo.html
new file mode 100644
index 00000000..945abe43
--- /dev/null
+++ b/docs/mockups/oa-related-approval-print-demo.html
@@ -0,0 +1,280 @@
+
+
+
+
+
关联审批单 · 打印(PDF)如何展示?
+
+ 屏幕端你已选 方案 B:嵌套关联显示「已关联 M 条」,可点进详情。
+ 但「打印」导出的是静态 PDF,无法点击跳转。请选择打印行里该字段怎么写。
+
+
+ 现状问题:打印逻辑把 relatedApproval 的值当普通数组格式化,
+ 会直接打出一串 instanceId(UUID),既不可读,也和屏幕端「已关联 M 条」不一致。
+
+
+
+
+
+
+
+
+
差旅报销
+
安徽中科兴皖医学研究有限公司创建时间:2026-07-20
+
+ | 审批编号 | 57F72ADA… |
+ | 事由 | 客户拜访 |
+ | 关联审批单 | B6244B87…、C1D2E3F4… |
+
+
+
+ 问题:纸面无法读,也不符合方案 B 语义。
+
+
+
+
+
+
+
+
差旅报销
+
安徽中科兴皖医学研究有限公司创建时间:2026-07-20
+
+ | 审批编号 | 57F72ADA… |
+ | 事由 | 客户拜访 |
+ | 关联审批单 | 已关联 2 条 |
+
+
+
+ 优点:实现最简单、与屏幕文案一致。缺点:打印归档时看不出关联了哪几单。
+
+
+
+
+
+
+
+
差旅报销
+
安徽中科兴皖医学研究有限公司创建时间:2026-07-20
+
+ | 审批编号 | 57F72ADA… |
+ | 事由 | 客户拜访 |
+ | 关联审批单 | 外出申请 · 07-10、立项申请 · 06-01 |
+
+
+
+ 优点:纸面可辨认关联对象;仍不递归嵌套内容。缺点:打印前需 enrichment 取应用名/时间。
+
+
+
+
+
+
+
+
差旅报销
+
安徽中科兴皖医学研究有限公司创建时间:2026-07-20
+
+ | 审批编号 | 57F72ADA… |
+ | 事由 | 客户拜访 |
+
+ | 关联审批单 |
+
+ 1. 外出申请 · 2026-07-10
+ 2. 立项申请 · 2026-06-01
+ |
+
+
+
+
+ 优点:多条时更清晰。缺点:表格略占高;仍不打印对方表单正文。
+
+
+
+
+ 关于「嵌套」:若关联单 A 里还有关联单 B,打印侧建议与屏幕 B 一致 ——
+ 不递归打印 B 的字段,只打印当前单直接关联的那一层可读清单(方案 2/3)。
+
+
+
+ 当前选择:尚未选择
+ 也可回复:1 / 2 / 3(0 为需废弃的现状)
+
+
+
+
+
diff --git a/docs/superpowers/plans/2026-07-22-oa-related-approval-nested-print.md b/docs/superpowers/plans/2026-07-22-oa-related-approval-nested-print.md
new file mode 100644
index 00000000..10097045
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-22-oa-related-approval-nested-print.md
@@ -0,0 +1,741 @@
+# OA Related Approval Nested + Print 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:** Make nested「已关联 M 条」clickable on screen, and make PDF print render related approvals as multi-line field blocks with up to 3 levels of recursion (deduped).
+
+**Architecture:** Keep screen enrichment flat (`value` + optional `relatedIds`). Do not return a summary tree from the API. Print path asynchronously batches `fetchOaRelatedSummaries`, then a pure recursive builder (`maxDepth=3`, `visited`) produces `RelatedPrintBlock[]` for `OaApprovalPrintSheet`.
+
+**Tech Stack:** FastAPI · pytest · Vue 3 · TypeScript · existing `oa-approval-print.ts` / html2canvas / jspdf · Element Plus
+
+**Spec:** `docs/superpowers/specs/2026-07-22-oa-related-approval-nested-print-design.md`
+
+---
+
+## File map
+
+| Path | Responsibility |
+|------|----------------|
+| Modify: `backend/app/services/oa_related_approval.py` | Flatten relatedApproval fields with `relatedIds` |
+| Modify: `backend/tests/test_oa_related_approval.py` | Assert `relatedIds` on nested/related flatten |
+| Modify: `web-admin/src/api/oaApproval.ts` | `OaRelatedSummaryField.relatedIds?: string[]` |
+| Modify: `web-admin/src/components/oa-approval/OaRelatedApproval.vue` | Clickable「已关联 M 条」→ detail |
+| Create: `web-admin/src/utils/oa-approval-print-related.ts` | Pure recursive block builder + async fetch wrapper |
+| Modify: `web-admin/src/utils/oa-approval-print.ts` | Async form rows; special-case `relatedApproval` |
+| Modify: `web-admin/src/components/oa-approval/OaApprovalPrintSheet.vue` | Render `blocks` with indent |
+| Verify: V1 spec cross-ref (already updated in brainstorm) | No further doc task unless drift |
+
+**Locked rules:**
+- Screen: nested = count text + jump (`M=1` → that id; `M>1` → first id). No nested cards.
+- Print: multi-line blocks; recurse on `relatedIds` while `depth < 3`; at depth 3 keep count text only; duplicate id → title `(已在上文出现)`.
+
+---
+
+### Task 1: Backend flatten emits `relatedIds` (TDD)
+
+**Files:**
+- Modify: `backend/tests/test_oa_related_approval.py`
+- Modify: `backend/app/services/oa_related_approval.py`
+
+- [ ] **Step 1: Write failing test for `relatedIds`**
+
+Add to `TestFlatten` in `backend/tests/test_oa_related_approval.py`:
+
+```python
+ def test_related_approval_field_includes_related_ids(self):
+ schema = {
+ "rule": [
+ {"type": "input", "field": "reason", "title": "事由"},
+ {"type": "relatedApproval", "field": "prev", "title": "关联审批单"},
+ ]
+ }
+ data = {"reason": "拜访", "prev": ["id1", "", "id2", "id1"]}
+ fields = flatten_form_fields_for_summary(schema, data)
+ rel = next(f for f in fields if f["label"] == "关联审批单")
+ self.assertEqual(rel["value"], "已关联 2 条")
+ self.assertEqual(rel["relatedIds"], ["id1", "id2"])
+
+ def test_related_approval_empty_ids(self):
+ schema = {"rule": [{"type": "relatedApproval", "field": "prev", "title": "关联"}]}
+ fields = flatten_form_fields_for_summary(schema, {"prev": []})
+ self.assertEqual(fields[0]["value"], "已关联 0 条")
+ self.assertEqual(fields[0]["relatedIds"], [])
+```
+
+Also update existing `test_skips_layout_and_summarizes_upload` assertion for the related field — after change it should allow `relatedIds`:
+
+```python
+ self.assertEqual(fields[2]["value"], "已关联 2 条")
+ self.assertEqual(fields[2].get("relatedIds"), ["id1", "id2"])
+```
+
+- [ ] **Step 2: Run test — expect FAIL**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_oa_related_approval.py::TestFlatten::test_related_approval_field_includes_related_ids -v
+```
+
+Expected: `AssertionError` / `KeyError` — no `relatedIds`.
+
+- [ ] **Step 3: Implement flatten change**
+
+In `backend/app/services/oa_related_approval.py`, add helper and change `flatten_form_fields_for_summary`:
+
+```python
+def _normalize_related_ids(raw: Any) -> List[str]:
+ if isinstance(raw, list):
+ items = raw
+ elif raw is None or raw == "":
+ items = []
+ else:
+ items = [raw]
+ out: List[str] = []
+ seen = set()
+ for x in items:
+ t = str(x or "").strip()
+ if not t or t in seen:
+ continue
+ seen.add(t)
+ out.append(t)
+ return out
+
+
+def flatten_form_fields_for_summary(schema: Any, form_data: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ data = form_data or {}
+ out: List[Dict[str, Any]] = []
+ for rule in _schema_rules(schema):
+ typ = str(rule.get("type") or "")
+ if not typ or typ in LAYOUT_SKIP:
+ continue
+ field = str(rule.get("field") or "").strip()
+ if not field:
+ continue
+ label = str(rule.get("title") or rule.get("label") or field)
+ raw = data.get(field)
+ if typ == "relatedApproval":
+ ids = _normalize_related_ids(raw)
+ out.append(
+ {
+ "label": label,
+ "value": f"已关联 {len(ids)} 条",
+ "relatedIds": ids,
+ }
+ )
+ continue
+ text, full = _format_value(typ, raw)
+ item: Dict[str, Any] = {"label": label, "value": text}
+ if full:
+ item["fullWidth"] = True
+ out.append(item)
+ return out
+```
+
+Keep `_format_value` relatedApproval branch for any other callers, or leave as-is (dead for flatten path).
+
+- [ ] **Step 4: Run full related-approval tests — expect PASS**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_oa_related_approval.py -v
+```
+
+Expected: all PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add backend/app/services/oa_related_approval.py backend/tests/test_oa_related_approval.py
+git commit -m "$(cat <<'EOF'
+feat(oa): emit relatedIds when flattening nested relatedApproval fields
+
+Screen jump and print recursion need the instance id list alongside the
+「已关联 M 条」summary text without changing form_data shape.
+EOF
+)"
+```
+
+---
+
+### Task 2: Frontend types + clickable nested count
+
+**Files:**
+- Modify: `web-admin/src/api/oaApproval.ts`
+- Modify: `web-admin/src/components/oa-approval/OaRelatedApproval.vue`
+
+- [ ] **Step 1: Extend field type**
+
+In `web-admin/src/api/oaApproval.ts`:
+
+```ts
+export type OaRelatedSummaryField = {
+ label: string
+ value: string
+ fullWidth?: boolean
+ /** 嵌套关联审批单的 instanceId 列表(可跳转 / 打印递归) */
+ relatedIds?: string[]
+}
+```
+
+- [ ] **Step 2: Render clickable count in card fields**
+
+In `OaRelatedApproval.vue` field value area, replace the plain `{{ f.value }}` branch with:
+
+```vue
+