2026-10-07-unified-protocol.md 17 KB

统一协议单管理 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)

  • [x] Step 1: 复制并改造组件外壳

从 web-admin/src/views/research-donation/audit/index.vue 整文件复制到
web-admin/src/views/cell-storage/protocol-audit/DonationProtocolTab.vue,然后按下列规则改(不要改业务逻辑):

  1. 去掉外层:
    • <div class="page-container">
    • <section class="panel"> / </section>
  2. 根节点改为(与 ProtocolAuditTab 对齐):
<!-- 采集协议单管理 · 科研捐赠 Tab -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
  <div class="donation-protocol-tab">
    <!-- 原 filter-block → list-body → dialogs,内容原样保留 -->
  </div>
</template>
  1. defineOptions:
defineOptions({ name: 'DonationProtocolTab' })
  1. 弹窗 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 等)保持不变。

  1. 原 <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

  • [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 分支正确。

  • 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

  • [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。

  • 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

浏览器验证:

  1. /#/market/cell-storage/protocol-audit → 默认「细胞存储」Tab,列表与改前一致。
  2. 点「科研捐赠」→ 捐赠列表出现;URL 含 tab=research_donation;刷新仍停在捐赠 Tab。
  3. /#/market/research-donation/audit → 统一页 + 默认捐赠 Tab。
  4. /#/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)

  • [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'
      }
    }
  ]
}
  • 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

  • [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')
    )
  );
  • 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,预期无需改后端)。

  • [x] Step 1: 权限冒烟

用仅具备细胞存储「采集协议单」菜单权限的账号(或 admin):

  1. 打开采集协议单 → 两 Tab 可见。
  2. 捐赠 Tab:列表加载、打开查看、(若允许)新增弹窗能打开;Network 中捐赠 list 接口 非 403。
  3. 若 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 冒烟仍建议人工点一次。

  • [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
  • 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 与壳实现一致。