17 KiB
统一协议单管理 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: 细胞存储「采集协议单」与科研捐赠「捐赠协议单」共用一个壳页面(两个来源 Tab),日常只从细胞存储进入;科研捐赠侧协议单菜单(含一级空目录)隐藏,旧 audit 路由仍可进统一页。
Architecture: 对齐统一样本信息:UnifiedProtocolPage 用 panel-pill-tabs 切换来源;细胞存储 Tab 复用现有 ProtocolAuditTab;科研捐赠 Tab 从 research-donation/audit/index.vue 抽出为 DonationProtocolTab。两套 API/表不变。进页只认细胞存储采集协议单权限,两 Tab 始终可见。query.tab = cell_storage | research_donation。
Tech Stack: Vue 3 + Element Plus + vue-router(hash)/ FastAPI 固定菜单 menu.py / MySQL sys_menu 可选迁移
Design Doc: docs/superpowers/specs/2026-10-07-unified-protocol-design.md
Reuse: UnifiedSampleInfoPage.vue 的 pill tabs + query.tab 写法;ProtocolAuditTab.vue 的 Tab 根结构;ClinicalApplication 父级 meta.isHide: true 隐藏空一级菜单的先例。
File Structure Map
New files
| File | Responsibility |
|---|---|
web-admin/src/views/cell-storage/protocol-audit/UnifiedProtocolPage.vue |
双 Tab 壳;切换写 query.tab;按来源挂载子 Tab |
web-admin/src/views/cell-storage/protocol-audit/DonationProtocolTab.vue |
原捐赠协议单列表/工具栏/弹窗逻辑(自 audit 页抽出) |
docs/sql/migrations/2026-10-07-hide-research-donation-protocol-menus.sql |
DB 菜单模式:隐藏捐赠协议相关菜单与空一级目录 |
Modified files
| File | Change |
|---|---|
web-admin/src/views/cell-storage/protocol-audit/index.vue |
挂载 UnifiedProtocolPage;保留旧 query.tab 重定向 |
web-admin/src/views/research-donation/audit/index.vue |
薄包装 → 同一壳,default-source=research_donation |
web-admin/src/router/modules/sqxp.ts |
捐赠 list/audit isHide: true;一级 ResearchDonation isHide: true |
backend/app/api/menu.py |
与 sqxp.ts 同步隐藏 |
Unchanged (explicit)
ProtocolAuditTab.vue、捐赠/采集 Form/View/Audit/Status 弹窗、两端 API CRUD、库表- 隐藏录入路由
CellStorageProtocol/ResearchDonationList仍保留可直开 - 样本信息统一页逻辑不改
Task 1: 抽出 DonationProtocolTab
Files:
-
Create:
web-admin/src/views/cell-storage/protocol-audit/DonationProtocolTab.vue -
Modify: (本 Task 不改路由页;audit 薄化在 Task 3)
-
Step 1: 复制并改造组件外壳
从 web-admin/src/views/research-donation/audit/index.vue 整文件复制到
web-admin/src/views/cell-storage/protocol-audit/DonationProtocolTab.vue,然后按下列规则改(不要改业务逻辑):
- 去掉外层:
<div class="page-container"><section class="panel">/</section>
- 根节点改为(与
ProtocolAuditTab对齐):
<!-- 采集协议单管理 · 科研捐赠 Tab -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="donation-protocol-tab">
<!-- 原 filter-block → list-body → dialogs,内容原样保留 -->
</div>
</template>
defineOptions:
defineOptions({ name: 'DonationProtocolTab' })
- 弹窗 import 路径从相对
../list/改为别名(因文件已迁到protocol-audit/):
import DonationFormDialog from '@/views/research-donation/list/DonationFormDialog.vue'
import DonationViewDialog from '@/views/research-donation/list/DonationViewDialog.vue'
import DonationAuditDialog, {
type DonationAuditTarget
} from '@/views/research-donation/list/DonationAuditDialog.vue'
import DonationStatusChangeDialog, {
type DonationStatusChangeAction,
type DonationStatusChangeTarget
} from '@/views/research-donation/list/DonationStatusChangeDialog.vue'
其余 import(API、icons、status-tag-registry、fullscreen-loading 等)保持不变。
- 原
<style scoped>中卡片样式原样保留;不必新增页面级page-container样式。
- Step 2: 本地确认无坏 import
在 web-admin/:
pnpm exec vue-tsc --noEmit 2>&1 | head -80
Expected: 无 DonationProtocolTab 相关报错(若仓库原有无关 tsc 错误可忽略,但本文件路径解析须通过)。或用 IDE 打开该文件确认 import 全绿。
- Step 3: Commit
git add web-admin/src/views/cell-storage/protocol-audit/DonationProtocolTab.vue
git commit -m "$(cat <<'EOF'
refactor(web-admin): extract DonationProtocolTab from research-donation audit page.
EOF
)"
Task 2: 新建 UnifiedProtocolPage 壳
Files:
-
Create:
web-admin/src/views/cell-storage/protocol-audit/UnifiedProtocolPage.vue -
Step 1: 写入完整壳组件
<template>
<div class="unified-protocol-page">
<div class="panel-pill-tabs panel-pill-tabs--lg">
<div class="panel-pill-tabs__track" role="tablist" aria-label="协议来源">
<button
v-for="item in tabs"
:key="item.key"
type="button"
role="tab"
class="panel-pill-tabs__btn"
:class="{ 'is-active': activeSource === item.key }"
:aria-selected="activeSource === item.key"
@click="switchSource(item.key)"
>
{{ item.title }}
</button>
</div>
</div>
<!-- v-if:仅挂载当前 Tab,避免双列表同时请求 -->
<ProtocolAuditTab v-if="activeSource === 'cell_storage'" />
<DonationProtocolTab v-else />
</div>
</template>
<script setup lang="ts">
import ProtocolAuditTab from './ProtocolAuditTab.vue'
import DonationProtocolTab from './DonationProtocolTab.vue'
defineOptions({ name: 'UnifiedProtocolPage' })
const props = defineProps<{
defaultSource: 'cell_storage' | 'research_donation'
}>()
type SourceKey = 'cell_storage' | 'research_donation'
const tabs: Array<{ key: SourceKey; title: string }> = [
{ key: 'cell_storage', title: '细胞存储' },
{ key: 'research_donation', title: '科研捐赠' }
]
const route = useRoute()
const router = useRouter()
const activeSource = ref<SourceKey>(props.defaultSource)
function switchSource(key: SourceKey) {
if (activeSource.value === key) return
activeSource.value = key
router.replace({ query: { ...route.query, tab: key } })
}
onMounted(() => {
const tab = String(route.query.tab || '')
if (tab === 'cell_storage' || tab === 'research_donation') {
activeSource.value = tab
return
}
activeSource.value = props.defaultSource
})
</script>
说明(与样本信息差异,按设计方案 B):
-
不用
useMenuPerm裁剪 Tab;两 Tab 写死始终展示。 -
旧
payment-info/storage-cert等 query 不在本组件处理(留给 Task 3 薄页重定向,避免与来源 tab 冲突)。 -
Step 2: 快速目视
打开文件确认:pill tabs 类名与 UnifiedSampleInfoPage / app.scss 一致;v-if 分支正确。
- Step 3: Commit
git add web-admin/src/views/cell-storage/protocol-audit/UnifiedProtocolPage.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add UnifiedProtocolPage shell with cell-storage and research-donation tabs.
EOF
)"
Task 3: 薄化双路由入口并保留旧重定向
Files:
-
Modify:
web-admin/src/views/cell-storage/protocol-audit/index.vue -
Modify:
web-admin/src/views/research-donation/audit/index.vue -
Step 1: 重写细胞存储
protocol-audit/index.vue
完整替换为:
<template>
<div class="page-container">
<section class="panel">
<UnifiedProtocolPage v-if="showUnified" default-source="cell_storage" />
</section>
</div>
</template>
<script setup lang="ts">
import UnifiedProtocolPage from './UnifiedProtocolPage.vue'
defineOptions({ name: 'CellStorageProtocolAudit' })
const route = useRoute()
const router = useRouter()
const showUnified = ref(false)
onMounted(() => {
const tab = String(route.query.tab || '')
if (tab === 'payment-info') {
router.replace({
name: 'CellStoragePaymentInfo',
query: { ...route.query, tab: 'dfj' }
})
return
}
if (
['storage-cert', 'fullmoon-todo', 'sample-reception', 'storage-entry', 'sample-qc'].includes(
tab
)
) {
router.replace({
name: 'CellStorageSampleInfo',
query: { ...route.query, tab: 'cell_storage' }
})
return
}
showUnified.value = true
})
</script>
要点:先判旧 tab 再挂载统一页,避免闪一下错误 Tab。
- Step 2: 重写科研捐赠
audit/index.vue为薄包装
完整替换为:
<template>
<div class="page-container">
<section class="panel">
<UnifiedProtocolPage default-source="research_donation" />
</section>
</div>
</template>
<script setup lang="ts">
import UnifiedProtocolPage from '@/views/cell-storage/protocol-audit/UnifiedProtocolPage.vue'
defineOptions({ name: 'ResearchDonationAudit' })
</script>
原 ~1000 行列表逻辑已在 DonationProtocolTab,本文件不得再保留一份副本。
- Step 3: 手动冒烟(dev)
cd web-admin && pnpm dev
浏览器验证:
/#/market/cell-storage/protocol-audit→ 默认「细胞存储」Tab,列表与改前一致。- 点「科研捐赠」→ 捐赠列表出现;URL 含
tab=research_donation;刷新仍停在捐赠 Tab。 /#/market/research-donation/audit→ 统一页 + 默认捐赠 Tab。/#/market/cell-storage/protocol-audit?tab=storage-cert→ 跳到样本信息(不卡在协议壳)。
- Step 4: Commit
git add \
web-admin/src/views/cell-storage/protocol-audit/index.vue \
web-admin/src/views/research-donation/audit/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): wire protocol-audit and donation audit routes to UnifiedProtocolPage.
EOF
)"
Task 4: 隐藏科研捐赠协议菜单(前端固定菜单)
Files:
-
Modify:
web-admin/src/router/modules/sqxp.ts(ResearchDonation 块,约 L182–219) -
Modify:
backend/app/api/menu.py(科研捐赠块,约 L99–127) -
Step 1: 改
sqxp.ts
将一级与两个子项都设 isHide: true(路由保留)。对齐 ClinicalApplication 父级隐藏写法:
{
id: 'RSC00000000000000000000000000001',
name: 'ResearchDonation',
path: 'research-donation',
component: '',
meta: {
title: '科研捐赠',
icon: 'ri:hand-heart-line',
keepAlive: false,
isHide: true,
roles: ['admin']
},
children: [
{
id: '60000000000000000000000000000002',
name: 'ResearchDonationList',
path: 'list',
component: '/research-donation/list/index',
meta: {
title: '捐赠协议单录入',
keepAlive: true,
isHide: true,
menuCode: 'biz:researchDonation:list'
}
},
{
id: '60000000000000000000000000000003',
name: 'ResearchDonationAudit',
path: 'audit',
component: '/research-donation/audit/index',
meta: {
title: '捐赠协议单',
keepAlive: true,
isHide: true,
menuCode: 'biz:researchDonationAudit:list'
}
}
]
}
- Step 2: 同步
menu.py
同一结构:父级 meta 增加 "isHide": True;audit 子项补 "isHide": True(list 已有)。
- Step 3: 验证侧栏
刷新管理端:侧栏无「科研捐赠」;「细胞存储 → 采集协议单」仍在。直接访问 /#/market/research-donation/audit 仍可打开统一页。
- Step 4: Commit
git add web-admin/src/router/modules/sqxp.ts backend/app/api/menu.py
git commit -m "$(cat <<'EOF'
chore(menu): hide research-donation protocol menus; keep routes for unified page deep links.
EOF
)"
Task 5: DB 菜单迁移脚本(可选同步)
Files:
-
Create:
docs/sql/migrations/2026-10-07-hide-research-donation-protocol-menus.sql -
Step 1: 写入迁移
-- =============================================================================
-- 下线「科研捐赠」协议单侧栏菜单(含一级空目录)(2026-10-07)
-- =============================================================================
--
-- 【主路径 — 前端固定菜单】
-- VITE_ACCESS_MODE=frontend 时已在以下文件隐藏:
-- - web-admin/src/router/modules/sqxp.ts
-- - backend/app/api/menu.py
-- 协议单从「细胞存储 → 采集协议单」进入(统一页含科研捐赠 Tab)。
-- 前端模式环境 **无需执行** 本脚本;保留供 DB 菜单模式同步。
--
-- 科研捐赠目录 parent_id / id: RSC00000000000000000000000000001
-- 捐赠协议单录入 id: 60000000000000000000000000000002
-- perms: biz:researchDonation:list
-- 捐赠协议单 id: 60000000000000000000000000000003
-- perms: biz:researchDonationAudit:list
-- visible: '0'=隐藏(权限码可保留;统一页不依赖捐赠菜单控 Tab)
-- =============================================================================
UPDATE sys_menu
SET visible = '0',
update_time = NOW()
WHERE del_flag = 0
AND (
id IN (
'RSC00000000000000000000000000001',
'60000000000000000000000000000002',
'60000000000000000000000000000003'
)
OR perms IN (
'biz:researchDonation:list',
'biz:researchDonationAudit:list'
)
OR (
parent_id = 'RSC00000000000000000000000000001'
AND path IN ('list', 'audit')
)
);
- Step 2: Commit
git add docs/sql/migrations/2026-10-07-hide-research-donation-protocol-menus.sql
git commit -m "$(cat <<'EOF'
docs(sql): add migration to hide research-donation protocol menus for DB menu mode.
EOF
)"
Task 6: 权限冒烟、回归清单与图谱
Files:
-
无必须代码改动;若捐赠 API 出现细粒度 403,再改对应
Depends(当前research_donation/collection_protocol管理端为require_admin_or_super_admin,预期无需改后端)。 -
Step 1: 权限冒烟
用仅具备细胞存储「采集协议单」菜单权限的账号(或 admin):
- 打开采集协议单 → 两 Tab 可见。
- 捐赠 Tab:列表加载、打开查看、(若允许)新增弹窗能打开;Network 中捐赠 list 接口 非 403。
- 若 403:在
backend/app/api/research_donation.py查require_menu_code/require_any_menu_code,按设计改为接受biz:cellStorageProtocolAudit:list(或与 admin 门禁一致)。改完补测再单独 commit。
代码级结论(2026-10-07): research_donation.py 全部端点为 require_admin_or_super_admin,无 require_menu_code;UnifiedProtocolPage 不按 useMenuPerm 裁 Tab。无需改 API。浏览器 Network 冒烟仍建议人工点一次。
-
Step 2: 完整回归勾选(静态代码核对已完成;CRUD 交互需浏览器)
-
细胞存储 Tab:筛选、新增、编辑、审核、生效/过期/作废、转科研、删除(浏览器)
-
科研捐赠 Tab:筛选、新增、编辑、审核、生效/过期/作废、转细胞存储、删除(浏览器)
-
Tab 切换与刷新保持
query.tab(switchSource+onMounted读query.tab) -
旧
protocol-audit?tab=storage-cert(及 payment-info / sample-*)重定向仍有效(protocol-audit/index.vue) -
侧栏无科研捐赠 / 捐赠协议单(
sqxp.ts+menu.pyparent/list/auditisHide: true) -
直开
research-donation/audit进统一页捐赠 Tab(薄包装default-source="research_donation") -
细胞存储 Tab 挂载
ProtocolAuditTab;捐赠 Tab 挂载DonationProtocolTab -
迁移 SQL:
docs/sql/migrations/2026-10-07-hide-research-donation-protocol-menus.sql -
Step 3: 更新前端图谱
./scripts/update-graph.sh frontend
- Step 4: Commit 图谱(若有 diff)
git add web-admin/graphify-out/
git commit -m "$(cat <<'EOF'
chore(graphify): refresh frontend graph after unified protocol page.
EOF
)"
若 graphify-out 无变化可跳过本 commit。
Spec coverage(自检)
| Spec 要求 | Task |
|---|---|
| 壳 + 两 Tab(细胞存储 / 科研捐赠) | Task 2 |
| 日常入口仅细胞存储采集协议单 | Task 3 + 4 |
| 隐藏捐赠协议单/录入;一级科研捐赠隐藏 | Task 4 + 5 |
旧 audit 路由 + ?tab= |
Task 2 + 3 |
| 进页只认细胞存储权限,两 Tab 都可见 | Task 2(不裁剪)+ Task 6 |
| 不合并表/API;业务能力保留 | Task 1 原样搬迁 |
| 旧 protocol-audit tab 重定向 | Task 3 |
| DB 迁移可选 | Task 5 |
| 图谱更新 | Task 6 |
无占位符 TBD;DonationProtocolTab 路径与设计一致;权限方案 B 与壳实现一致。