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 | 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 菜单模式:隐藏捐赠协议相关菜单与空一级目录 |
| 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 同步隐藏 |
ProtocolAuditTab.vue、捐赠/采集 Form/View/Audit/Status 弹窗、两端 API CRUD、库表CellStorageProtocol / ResearchDonationList 仍保留可直开DonationProtocolTabFiles:
web-admin/src/views/cell-storage/protocol-audit/DonationProtocolTab.vueModify: (本 Task 不改路由页;audit 薄化在 Task 3)
[x] 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' })
../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 样式。在 web-admin/:
pnpm exec vue-tsc --noEmit 2>&1 | head -80
Expected: 无 DonationProtocolTab 相关报错(若仓库原有无关 tsc 错误可忽略,但本文件路径解析须通过)。或用 IDE 打开该文件确认 import 全绿。
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
)"
UnifiedProtocolPage 壳Files:
Create: web-admin/src/views/cell-storage/protocol-audit/UnifiedProtocolPage.vue
[x] 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 冲突)。
[x] Step 2: 快速目视
打开文件确认:pill tabs 类名与 UnifiedSampleInfoPage / app.scss 一致;v-if 分支正确。
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
)"
Files:
web-admin/src/views/cell-storage/protocol-audit/index.vueModify: web-admin/src/views/research-donation/audit/index.vue
[x] 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。
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,本文件不得再保留一份副本。
cd web-admin && pnpm dev
浏览器验证:
/#/market/cell-storage/protocol-audit → 默认「细胞存储」Tab,列表与改前一致。tab=research_donation;刷新仍停在捐赠 Tab。/#/market/research-donation/audit → 统一页 + 默认捐赠 Tab。/#/market/cell-storage/protocol-audit?tab=storage-cert → 跳到样本信息(不卡在协议壳)。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
)"
Files:
web-admin/src/router/modules/sqxp.ts(ResearchDonation 块,约 L182–219)Modify: backend/app/api/menu.py(科研捐赠块,约 L99–127)
[x] 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'
}
}
]
}
menu.py同一结构:父级 meta 增加 "isHide": True;audit 子项补 "isHide": True(list 已有)。
刷新管理端:侧栏无「科研捐赠」;「细胞存储 → 采集协议单」仍在。直接访问 /#/market/research-donation/audit 仍可打开统一页。
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
)"
Files:
Create: docs/sql/migrations/2026-10-07-hide-research-donation-protocol-menus.sql
[x] 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')
)
);
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
)"
Files:
无必须代码改动;若捐赠 API 出现细粒度 403,再改对应 Depends(当前 research_donation / collection_protocol 管理端为 require_admin_or_super_admin,预期无需改后端)。
[x] Step 1: 权限冒烟
用仅具备细胞存储「采集协议单」菜单权限的账号(或 admin):
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 冒烟仍建议人工点一次。
[x] Step 2: 完整回归勾选(静态代码核对已完成;CRUD 交互需浏览器)
[ ] 细胞存储 Tab:筛选、新增、编辑、审核、生效/过期/作废、转科研、删除(浏览器)
[ ] 科研捐赠 Tab:筛选、新增、编辑、审核、生效/过期/作废、转细胞存储、删除(浏览器)
[x] Tab 切换与刷新保持 query.tab(switchSource + onMounted 读 query.tab)
[x] 旧 protocol-audit?tab=storage-cert(及 payment-info / sample-*)重定向仍有效(protocol-audit/index.vue)
[x] 侧栏无科研捐赠 / 捐赠协议单(sqxp.ts + menu.py parent/list/audit isHide: true)
[x] 直开 research-donation/audit 进统一页捐赠 Tab(薄包装 default-source="research_donation")
[x] 细胞存储 Tab 挂载 ProtocolAuditTab;捐赠 Tab 挂载 DonationProtocolTab
[x] 迁移 SQL:docs/sql/migrations/2026-10-07-hide-research-donation-protocol-menus.sql
[x] Step 3: 更新前端图谱
./scripts/update-graph.sh frontend
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 要求 | 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 与壳实现一致。