688 lines
21 KiB
Markdown
688 lines
21 KiB
Markdown
# OA 办理中心列表整单进度字段 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:** 四类办理列表展示流程名称、申请人、申请时间、当前节点、当前审批人、流程状态;进度语义为整单此刻;前端四页独立实现。
|
||
|
||
**Architecture:** 后端在 `list_tasks` 内批量 enrichment(实例 / 应用 / 用户 / pending 审批任务),写入统一摘要字段;前端 `pending`/`done`/`mine`/`cc` 各写完整页面,删除 `RuntimeList` 依赖。
|
||
|
||
**Tech Stack:** FastAPI / SQLAlchemy / Vue 3 / Element Plus;单测 `backend/tests/test_oa_engine.py`
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-07-15-oa-task-list-columns-design.md`
|
||
|
||
---
|
||
|
||
## File map
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `backend/app/services/oa_engine.py` | `_INSTANCE_STATUS_NODE_LABEL`、`_enrich_list_rows`、改写 `list_tasks` |
|
||
| `backend/tests/test_oa_engine.py` | enrichment 单测(running / terminal / mine / done 整单进度) |
|
||
| `web-admin/src/views/oa-approval/pending/index.vue` | 待处理独立列表 |
|
||
| `web-admin/src/views/oa-approval/done/index.vue` | 已处理独立列表 |
|
||
| `web-admin/src/views/oa-approval/mine/index.vue` | 我发起的独立列表 + 复制并重新发起 |
|
||
| `web-admin/src/views/oa-approval/cc/index.vue` | 抄送我的独立列表 |
|
||
| `web-admin/src/views/oa-approval/_placeholder/RuntimeList.vue` | 删除 |
|
||
|
||
---
|
||
|
||
### Task 1: 后端 — list_tasks enrichment + 单测
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/services/oa_engine.py`
|
||
- Modify: `backend/tests/test_oa_engine.py`
|
||
|
||
- [ ] **Step 1: 写失败单测**
|
||
|
||
在 `backend/tests/test_oa_engine.py` 末尾新增:
|
||
|
||
```python
|
||
class TestListTasksEnrichment(unittest.TestCase):
|
||
"""list_tasks 返回整单此刻进度摘要字段。"""
|
||
|
||
def setUp(self):
|
||
from sqlalchemy import create_engine
|
||
from sqlalchemy.orm import sessionmaker
|
||
from app.models.models import OaApp, OaAppVersion, OaInstance, OaTask, SysUser
|
||
|
||
self.engine = create_engine("sqlite:///:memory:")
|
||
OaApp.__table__.create(self.engine)
|
||
OaAppVersion.__table__.create(self.engine)
|
||
OaInstance.__table__.create(self.engine)
|
||
OaTask.__table__.create(self.engine)
|
||
SysUser.__table__.create(self.engine)
|
||
Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
|
||
self.db = Session()
|
||
self.OaTask = OaTask
|
||
now = datetime(2026, 7, 15, 10, 0, 0)
|
||
self.db.add(
|
||
OaApp(
|
||
id="app1",
|
||
app_name="请假审批",
|
||
initiator_scope="all",
|
||
admin_scope="all_oa_admin",
|
||
status="1",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaAppVersion(
|
||
id="v1",
|
||
app_id="app1",
|
||
version_no=1,
|
||
status="published",
|
||
is_latest_published="1",
|
||
form_schema_json="{}",
|
||
flow_tree_json="{}",
|
||
advanced_settings_json="{}",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
SysUser(
|
||
id="u0",
|
||
user_no="initiator",
|
||
nick_name="发起人甲",
|
||
password="x",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
SysUser(
|
||
id="u1",
|
||
user_no="approver1",
|
||
nick_name="审批人乙",
|
||
password="x",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
SysUser(
|
||
id="u2",
|
||
user_no="approver2",
|
||
nick_name="审批人丙",
|
||
password="x",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaInstance(
|
||
id="i1",
|
||
app_id="app1",
|
||
app_version_id="v1",
|
||
title="请假-忽略",
|
||
initiator_user_id="u0",
|
||
initiator_dept_id="d1",
|
||
form_data_json="{}",
|
||
status="running",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
# u1 已处理;同单仍有 u2 pending
|
||
self.db.add(
|
||
OaTask(
|
||
id="t-done",
|
||
instance_id="i1",
|
||
node_key="a1",
|
||
node_name="一级审批",
|
||
task_type="approval",
|
||
assignee_user_id="u1",
|
||
status="approved",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.add(
|
||
OaTask(
|
||
id="t-pending",
|
||
instance_id="i1",
|
||
node_key="a2",
|
||
node_name="二级审批",
|
||
task_type="approval",
|
||
assignee_user_id="u2",
|
||
status="pending",
|
||
tenant_no="zkxw",
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
self.db.commit()
|
||
|
||
def tearDown(self):
|
||
self.db.close()
|
||
self.engine.dispose()
|
||
|
||
def test_done_list_shows_instance_current_progress(self):
|
||
from app.services.oa_engine import list_tasks
|
||
|
||
done = list_tasks(self.db, tenant_no="zkxw", user_id="u1", kind="done")
|
||
self.assertEqual(len(done), 1)
|
||
row = done[0]
|
||
self.assertEqual(row["appName"], "请假审批")
|
||
self.assertEqual(row["initiatorUserName"], "发起人甲")
|
||
self.assertEqual(row["applyTime"], "2026-07-15 10:00:00")
|
||
self.assertEqual(row["instanceStatus"], "running")
|
||
self.assertEqual(row["currentNodeName"], "二级审批")
|
||
self.assertEqual(row["currentApproverNames"], "审批人丙")
|
||
# 任务级 status 仍保留
|
||
self.assertEqual(row["status"], "approved")
|
||
|
||
def test_mine_list_enrichment(self):
|
||
from app.services.oa_engine import list_tasks
|
||
|
||
mine = list_tasks(self.db, tenant_no="zkxw", user_id="u0", kind="mine")
|
||
self.assertEqual(len(mine), 1)
|
||
row = mine[0]
|
||
self.assertEqual(row["appName"], "请假审批")
|
||
self.assertEqual(row["instanceStatus"], "running")
|
||
self.assertEqual(row["currentNodeName"], "二级审批")
|
||
self.assertEqual(row["currentApproverNames"], "审批人丙")
|
||
|
||
def test_terminal_instance_labels(self):
|
||
from app.services.oa_engine import list_tasks
|
||
|
||
inst = self.db.query(
|
||
__import__("app.models.models", fromlist=["OaInstance"]).OaInstance
|
||
).filter_by(id="i1").first()
|
||
inst.status = "returned"
|
||
pending = self.db.query(self.OaTask).filter_by(id="t-pending").first()
|
||
pending.status = "cancelled"
|
||
self.db.commit()
|
||
|
||
mine = list_tasks(self.db, tenant_no="zkxw", user_id="u0", kind="mine")
|
||
row = mine[0]
|
||
self.assertEqual(row["instanceStatus"], "returned")
|
||
self.assertEqual(row["currentNodeName"], "已打回")
|
||
self.assertEqual(row["currentApproverNames"], "-")
|
||
```
|
||
|
||
若 `SysUser` 必填字段与上不同,按 `backend/app/models/models.py` 中 `SysUser` 实际列调整 setUp(保持能 insert 即可)。
|
||
|
||
- [ ] **Step 2: 跑测确认失败**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cd backend && source venv/bin/activate && pytest tests/test_oa_engine.py::TestListTasksEnrichment -q
|
||
```
|
||
|
||
Expected: FAIL(缺 `appName` / `currentNodeName` 等)
|
||
|
||
- [ ] **Step 3: 实现 enrichment**
|
||
|
||
在 `oa_engine.py` 中 `_task_to_dict` 之后新增(字段名与 spec 一致):
|
||
|
||
```python
|
||
_INSTANCE_STATUS_NODE_LABEL = {
|
||
"completed": "已完成",
|
||
"rejected": "已拒绝",
|
||
"returned": "已打回",
|
||
"cancelled": "已撤销",
|
||
}
|
||
|
||
|
||
def _instance_progress_summary(
|
||
inst: OaInstance,
|
||
*,
|
||
app_name: str,
|
||
initiator_user_name: str,
|
||
pending_approval_tasks: List[OaTask],
|
||
name_map: Dict[str, str],
|
||
) -> Dict[str, Any]:
|
||
status = (inst.status or "").strip()
|
||
apply_time = (
|
||
inst.create_time.strftime("%Y-%m-%d %H:%M:%S") if inst.create_time else ""
|
||
)
|
||
if status == "running":
|
||
node_names: List[str] = []
|
||
seen_nodes = set()
|
||
approver_names: List[str] = []
|
||
for t in pending_approval_tasks:
|
||
nn = (t.node_name or t.node_key or "").strip()
|
||
if nn and nn not in seen_nodes:
|
||
seen_nodes.add(nn)
|
||
node_names.append(nn)
|
||
uid = (t.assignee_user_id or "").strip()
|
||
if uid:
|
||
approver_names.append(name_map.get(uid, uid))
|
||
current_node = "、".join(node_names) if node_names else "-"
|
||
current_approvers = "、".join(approver_names) if approver_names else "-"
|
||
else:
|
||
current_node = _INSTANCE_STATUS_NODE_LABEL.get(status, status or "-")
|
||
current_approvers = "-"
|
||
return {
|
||
"appName": app_name or (inst.title or ""),
|
||
"initiatorUserId": inst.initiator_user_id or "",
|
||
"initiatorUserName": initiator_user_name
|
||
or name_map.get(inst.initiator_user_id or "", inst.initiator_user_id or ""),
|
||
"applyTime": apply_time,
|
||
"instanceStatus": status,
|
||
"currentNodeName": current_node,
|
||
"currentApproverNames": current_approvers,
|
||
}
|
||
|
||
|
||
def _enrich_list_rows(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
rows: List[Dict[str, Any]],
|
||
instances: List[OaInstance],
|
||
) -> List[Dict[str, Any]]:
|
||
if not rows:
|
||
return []
|
||
inst_by_id = {i.id: i for i in instances}
|
||
app_ids = list({i.app_id for i in instances if i.app_id})
|
||
apps = (
|
||
db.query(OaApp).filter(OaApp.id.in_(app_ids), OaApp.del_flag == 0).all()
|
||
if app_ids
|
||
else []
|
||
)
|
||
app_name_by_id = {a.id: (a.app_name or "") for a in apps}
|
||
|
||
inst_ids = list(inst_by_id.keys())
|
||
pending_tasks = (
|
||
db.query(OaTask)
|
||
.filter(
|
||
OaTask.tenant_no == tenant_no,
|
||
OaTask.del_flag == 0,
|
||
OaTask.instance_id.in_(inst_ids),
|
||
OaTask.task_type == "approval",
|
||
OaTask.status == "pending",
|
||
)
|
||
.order_by(OaTask.create_time.asc())
|
||
.all()
|
||
if inst_ids
|
||
else []
|
||
)
|
||
pending_by_inst: Dict[str, List[OaTask]] = {}
|
||
for t in pending_tasks:
|
||
pending_by_inst.setdefault(t.instance_id, []).append(t)
|
||
|
||
user_ids: List[str] = []
|
||
for i in instances:
|
||
if i.initiator_user_id:
|
||
user_ids.append(i.initiator_user_id)
|
||
for t in pending_tasks:
|
||
if t.assignee_user_id:
|
||
user_ids.append(t.assignee_user_id)
|
||
name_map = _user_name_map(db, user_ids)
|
||
|
||
out: List[Dict[str, Any]] = []
|
||
for row in rows:
|
||
iid = row.get("instanceId") or row.get("id")
|
||
inst = inst_by_id.get(iid)
|
||
if not inst:
|
||
out.append(row)
|
||
continue
|
||
summary = _instance_progress_summary(
|
||
inst,
|
||
app_name=app_name_by_id.get(inst.app_id, ""),
|
||
initiator_user_name=name_map.get(inst.initiator_user_id or "", ""),
|
||
pending_approval_tasks=pending_by_inst.get(inst.id, []),
|
||
name_map=name_map,
|
||
)
|
||
merged = dict(row)
|
||
merged.update(summary)
|
||
# mine 行:保留 instance 自身 status,同时带 instanceStatus
|
||
if "instanceId" not in row and "status" in row:
|
||
# 实例行的 status 本就是 instance status,保持
|
||
pass
|
||
out.append(merged)
|
||
return out
|
||
```
|
||
|
||
改写 `list_tasks` 尾部:
|
||
|
||
```python
|
||
def list_tasks(...):
|
||
...
|
||
elif kind == "mine":
|
||
insts = (
|
||
db.query(OaInstance)
|
||
.filter(
|
||
OaInstance.tenant_no == tenant_no,
|
||
OaInstance.del_flag == 0,
|
||
OaInstance.initiator_user_id == user_id,
|
||
)
|
||
.order_by(OaInstance.create_time.desc())
|
||
.all()
|
||
)
|
||
rows = [_instance_to_dict(i) for i in insts]
|
||
return _enrich_list_rows(db, tenant_no=tenant_no, rows=rows, instances=insts)
|
||
|
||
rows_orm = q.order_by(OaTask.create_time.desc()).all()
|
||
rows = [_task_to_dict(t) for t in rows_orm]
|
||
inst_ids = list({t.instance_id for t in rows_orm if t.instance_id})
|
||
insts = (
|
||
db.query(OaInstance)
|
||
.filter(
|
||
OaInstance.tenant_no == tenant_no,
|
||
OaInstance.del_flag == 0,
|
||
OaInstance.id.in_(inst_ids),
|
||
)
|
||
.all()
|
||
if inst_ids
|
||
else []
|
||
)
|
||
return _enrich_list_rows(db, tenant_no=tenant_no, rows=rows, instances=insts)
|
||
```
|
||
|
||
确认文件顶部已 import `OaApp`(与现有 `OaInstance`/`OaTask` 一并)。
|
||
|
||
- [ ] **Step 4: 跑测通过**
|
||
|
||
```bash
|
||
cd backend && source venv/bin/activate && pytest tests/test_oa_engine.py::TestListTasksEnrichment tests/test_oa_engine.py::TestCompleteTaskOrSign::test_approve_keeps_approved_not_done -q
|
||
```
|
||
|
||
Expected: PASS
|
||
|
||
- [ ] **Step 5: Commit(仅用户明确要求时再执行;本步可跳过)**
|
||
|
||
```bash
|
||
git add backend/app/services/oa_engine.py backend/tests/test_oa_engine.py
|
||
git commit -m "$(cat <<'EOF'
|
||
feat(oa): enrich list_tasks with instance progress summary fields
|
||
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: 前端 — 待处理独立页
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/views/oa-approval/pending/index.vue`
|
||
|
||
- [ ] **Step 1: 用完整页面替换 RuntimeList 引用**
|
||
|
||
将 `pending/index.vue` 写为自包含列表(勿 import RuntimeList):
|
||
|
||
```vue
|
||
<!-- OA 待处理 -->
|
||
<template>
|
||
<div class="page-container">
|
||
<section class="panel">
|
||
<div class="panel-top">
|
||
<h2 class="title-bar-heading">待处理</h2>
|
||
</div>
|
||
<div class="list-body list-wrap">
|
||
<el-table class="list-table" :data="rows" v-loading="loading" stripe>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
min-width="68"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="appName"
|
||
label="流程名称"
|
||
min-width="160"
|
||
align="left"
|
||
header-align="left"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">{{ row.appName || row.title || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="initiatorUserName"
|
||
label="申请人"
|
||
min-width="120"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">{{ row.initiatorUserName || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="applyTime"
|
||
label="申请时间"
|
||
min-width="170"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">{{ row.applyTime || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="currentNodeName"
|
||
label="当前节点"
|
||
min-width="140"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">{{ row.currentNodeName || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="currentApproverNames"
|
||
label="当前审批人"
|
||
min-width="140"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">{{ row.currentApproverNames || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="instanceStatus" label="流程状态" min-width="110" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag size="small" :type="statusType(row.instanceStatus)">
|
||
{{ statusText(row.instanceStatus) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" min-width="260" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<el-button class="btn-op-item primary" @click="openDetail(row)">
|
||
<el-icon><View /></el-icon>
|
||
查看
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onActivated, onMounted, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { View } from '@element-plus/icons-vue'
|
||
import { fetchOaTasks } from '@/api/oaApproval'
|
||
|
||
defineOptions({ name: 'OaApprovalPending' })
|
||
|
||
const router = useRouter()
|
||
const loading = ref(false)
|
||
const rows = ref<any[]>([])
|
||
|
||
function statusType(s: string) {
|
||
if (s === 'pending' || s === 'running') return 'warning'
|
||
if (s === 'approved' || s === 'completed' || s === 'done') return 'success'
|
||
if (s === 'rejected' || s === 'cancelled' || s === 'returned') return 'danger'
|
||
return 'info'
|
||
}
|
||
|
||
function statusText(s: string) {
|
||
if (!s) return '-'
|
||
const map: Record<string, string> = {
|
||
pending: '待处理',
|
||
running: '进行中',
|
||
approved: '已同意',
|
||
rejected: '已拒绝',
|
||
completed: '已完成',
|
||
cancelled: '已撤销',
|
||
done: '已结束',
|
||
returned: '已打回'
|
||
}
|
||
return map[s] || s
|
||
}
|
||
|
||
function openDetail(row: any) {
|
||
const id = row.instanceId
|
||
if (!id) return
|
||
router.push({ path: `/system-manage/oa-approval/instances/${id}` })
|
||
}
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
const res = await fetchOaTasks('pending')
|
||
rows.value = res?.list || []
|
||
} catch {
|
||
rows.value = []
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(load)
|
||
onActivated(load)
|
||
</script>
|
||
```
|
||
|
||
- [ ] **Step 2: 目视确认无 RuntimeList import;列齐全**
|
||
|
||
- [ ] **Step 3: Commit(可选,用户要求时)**
|
||
|
||
---
|
||
|
||
### Task 3: 前端 — 已处理独立页
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/views/oa-approval/done/index.vue`
|
||
|
||
- [ ] **Step 1: 复制 Task 2 结构,改为「已处理」**
|
||
|
||
与 pending 相同列与逻辑,差异:
|
||
|
||
- `defineOptions({ name: 'OaApprovalDone' })`
|
||
- 标题 `已处理`
|
||
- `fetchOaTasks('done')`
|
||
- 详情仍用 `row.instanceId`
|
||
|
||
**注意:** 流程状态列必须用 `instanceStatus`(整单),不要用任务 `status`。
|
||
|
||
- [ ] **Step 2: Commit(可选)**
|
||
|
||
---
|
||
|
||
### Task 4: 前端 — 我发起的独立页
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/views/oa-approval/mine/index.vue`
|
||
|
||
- [ ] **Step 1: 独立页 + 复制并重新发起**
|
||
|
||
在 Task 2 列基础上:
|
||
|
||
- 标题 `我发起的`;`fetchOaTasks('mine')`
|
||
- `openDetail` 用 `row.id`(实例主键)
|
||
- 操作列增加:
|
||
|
||
```vue
|
||
<el-button
|
||
v-if="row.appId && ['returned', 'cancelled', 'rejected'].includes(row.instanceStatus || row.status)"
|
||
class="btn-op-item"
|
||
@click="openCopyRestart(row)"
|
||
>
|
||
<el-icon><RefreshRight /></el-icon>
|
||
复制并重新发起
|
||
</el-button>
|
||
```
|
||
|
||
```ts
|
||
import { RefreshRight, View } from '@element-plus/icons-vue'
|
||
|
||
function openDetail(row: any) {
|
||
if (!row?.id) return
|
||
router.push({ path: `/system-manage/oa-approval/instances/${row.id}` })
|
||
}
|
||
|
||
function openCopyRestart(row: any) {
|
||
if (!row?.appId || !row?.id) return
|
||
router.push({
|
||
path: '/system-manage/oa-approval/start',
|
||
query: { appId: row.appId, copyFromInstanceId: row.id }
|
||
})
|
||
}
|
||
```
|
||
|
||
`defineOptions({ name: 'OaApprovalMine' })`
|
||
|
||
- [ ] **Step 2: Commit(可选)**
|
||
|
||
---
|
||
|
||
### Task 5: 前端 — 抄送我的 + 删除 RuntimeList
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/views/oa-approval/cc/index.vue`
|
||
- Delete: `web-admin/src/views/oa-approval/_placeholder/RuntimeList.vue`
|
||
|
||
- [ ] **Step 1: 按 Task 2 写 `cc/index.vue`**
|
||
|
||
- 标题 `抄送我的`;`fetchOaTasks('cc')`;详情 `row.instanceId`
|
||
- `defineOptions({ name: 'OaApprovalCc' })`
|
||
|
||
- [ ] **Step 2: 全局确认无 RuntimeList 引用后删除文件**
|
||
|
||
```bash
|
||
rg "RuntimeList" web-admin/src
|
||
```
|
||
|
||
Expected: 无匹配(或仅残留注释)。然后删除 `_placeholder/RuntimeList.vue`。若 `_placeholder` 目录空,可一并删除空目录。
|
||
|
||
- [ ] **Step 3: Commit(可选)**
|
||
|
||
---
|
||
|
||
### Task 6: 验收 + 图谱
|
||
|
||
- [x] **Step 1: 后端全量相关测** — `pytest tests/test_oa_engine.py -q` → 21 passed
|
||
|
||
- [x] **Step 2: 更新图谱** — backend 增量更新;frontend force 覆盖(RuntimeList 已移除)
|
||
|
||
- [x] **Step 3: 勾选 spec §7 验收项**
|
||
|
||
---
|
||
|
||
## Spec coverage (self-review)
|
||
|
||
| Spec 要求 | Task |
|
||
|-----------|------|
|
||
| 摘要字段 appName/initiator/applyTime/current*/instanceStatus | Task 1 |
|
||
| 整单此刻进度;done 不展示本人任务快照 | Task 1 `test_done_list_shows_instance_current_progress` |
|
||
| 终态节点文案 + 审批人 `-` | Task 1 `test_terminal_instance_labels` |
|
||
| 批量无 N+1 | Task 1 `_enrich_list_rows` |
|
||
| 四页独立列 | Task 2–5 |
|
||
| 删除 RuntimeList | Task 5 |
|
||
| 我发起终态复制再发 | Task 4 |
|
||
| 图谱 | Task 6 |
|