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、一条样本一行),列表只读展示采集/接收/质检/入库标识,细胞存储另展示证/保险已办理,详情里轻量办理证与保险;采集协议单去掉储存证、满月待办 Tab。
Architecture: 进度标识由现有接收单、入库质检工单、入库放行审核按 tenant_no + sample_code 派生,不建总表。证/保险仍写 biz_protocol_storage_cert / biz_protocol_fullmoon_todo(协议级),补 handle_time、handler_user_id。列表扩展现有两个 sample-collections 接口;办理走独立 PUT。前端两路由共用同一列表页。
Tech Stack: MySQL 5.7 / FastAPI + SQLAlchemy / pytest unittest / Vue 3 + Element Plus / pnpm
Design Doc: docs/superpowers/specs/2026-10-05-unified-sample-info-design.md
Reuse: backend/app/services/protocol_sample_list_summary.py(接收/质检摘要)、qc_sms_notification._qc_status_label 语义、storage_entry_sms_notification._storage_status_label、协议详情里已有的接收/质检/入库只读组件。
| File | Responsibility |
|---|---|
docs/sql/migrations/2026-10-05-sample-info-handle-fields.sql |
两表 ALTER 增加办理时间、办理人 |
backend/app/services/sample_info_progress.py |
采集/接收/质检/入库展示码、证/保险已办理判定、同步 status 日期 |
backend/tests/test_sample_info_progress.py |
上述纯函数单测 |
backend/tests/test_sample_info_handle.py |
办理 upsert 同步字段单测(mock db 或纯函数 + crud 辅助) |
web-admin/src/views/cell-storage/sample-info/UnifiedSampleInfoPage.vue |
两来源 Tab + 筛选 + 列表 + 打开详情 |
web-admin/src/views/cell-storage/sample-info/SampleProgressTags.ts |
列表 Tag 的 type/label 映射(可与 registry 合并,见 Task 7) |
web-admin/src/components/business/protocol-handle-panel/index.vue |
证/保险办理表单(时间、人员、附件、备注、保存) |
| File | Change |
|---|---|
backend/app/models/models.py |
BizProtocolStorageCert、BizProtocolFullmoonTodo 加列 |
backend/app/crud/collection_protocol.py |
dict/upsert 支持新字段;PUT 办理辅助 |
backend/app/crud/protocol_sample_collection.py |
列表富化 + 进度筛选 |
backend/app/crud/research_donation_sample_collection.py |
同上(无证/保险) |
backend/app/api/protocol_sample_collection.py |
新 query 参数 |
backend/app/api/research_donation_sample_collection.py |
新 query 参数 |
backend/app/api/collection_protocol.py |
PUT /storage-certs/{id}、PUT /fullmoon-todos/{id}/handle;bundle 返回办理字段 |
web-admin/src/api/protocol-sample-collection.ts |
筛选参数 |
web-admin/src/api/research-donation-sample-collection.ts |
筛选参数 |
web-admin/src/api/admin.ts |
办理 PUT(或新 sample-info-handle.ts) |
web-admin/src/views/cell-storage/sample-info/index.vue |
改为挂载统一页,默认细胞存储 |
web-admin/src/views/research-donation/sample-info/index.vue |
挂载同一页,默认科研捐赠 |
web-admin/src/views/cell-storage/protocol-audit/index.vue |
只留协议单 Tab |
web-admin/src/views/cell-storage/storage-cert/index.vue |
跳转样本信息 |
web-admin/src/views/cell-storage/fullmoon-todo/index.vue |
跳转样本信息 |
web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue |
高亮样本;证/保险改为办理面板 |
web-admin/src/views/research-donation/list/DonationViewDialog.vue |
高亮样本(进度组件已有则只加 highlight) |
web-admin/src/utils/status-tag-registry.ts |
进度/办理 Tag |
protocol-audit/StorageCertTab.vue、FullmoonTodoTab.vueSampleCollectionInfoTab.vue、SampleReceptionInfoTab.vue、SampleQcInfoTab.vue、StorageEntryInfoTab.vueresearch-donation/sample-info/DonationSampleCollectionInfoTab.vue短信 API 不要删。
Files:
docs/sql/migrations/2026-10-05-sample-info-handle-fields.sqlModify: backend/app/models/models.py(BizProtocolStorageCert 约 L1597、BizProtocolFullmoonTodo 约 L3613)
[ ] Step 1: 写迁移 SQL
MySQL 5.7,列加在业务字段末、公共字段前。handle_date 已存在于满月表,新列名为 handle_time(DATETIME)。
-- 储存证 / 满月保险轻量办理字段(2026-10-05)
ALTER TABLE `biz_protocol_storage_cert`
ADD COLUMN `handle_time` datetime DEFAULT NULL COMMENT '办理时间' AFTER `make_date`,
ADD COLUMN `handler_user_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '办理人用户ID' AFTER `handle_time`;
ALTER TABLE `biz_protocol_fullmoon_todo`
ADD COLUMN `handle_time` datetime DEFAULT NULL COMMENT '办理时间' AFTER `handle_date`,
ADD COLUMN `handler_user_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '办理人用户ID' AFTER `handle_time`;
在 make_date 后、tenant_no 前:
handle_time = Column(DateTime, nullable=True, comment="办理时间")
handler_user_id = Column(String(64), nullable=True, comment="办理人用户ID")
满月表:handle_date 列之后同样两列。不要删除保险公司旧列。
git add docs/sql/migrations/2026-10-05-sample-info-handle-fields.sql backend/app/models/models.py
git commit -m "$(cat <<'EOF'
feat(db): add handle_time and handler_user_id for storage cert and fullmoon todo.
EOF
)"
Files:
backend/app/services/sample_info_progress.pybackend/tests/test_sample_info_progress.py在仓库根目录 backend/ 下跑 pytest(已有 tests/test_protocol_list_sample_reception_qc.py 风格:unittest + 直接 import 纯函数)。
"""样本信息管理:进度展示码与证/保险已办理判定。"""
import unittest
from datetime import datetime
from types import SimpleNamespace
from app.services.sample_info_progress import (
collection_progress,
insurance_handled,
qc_progress,
reception_progress,
storage_cert_handled,
storage_progress,
sync_handle_status_fields,
)
class TestCollectionProgress(unittest.TestCase):
def test_empty(self):
self.assertEqual(collection_progress(""), ("uncollected", "未采集"))
def test_collected_includes_mid_states(self):
for st in ("ycj", "jcz", "yrk"):
self.assertEqual(collection_progress(st)[0], "collected")
self.assertEqual(collection_progress("ycj")[1], "已采集")
def test_void(self):
self.assertEqual(collection_progress("yzf"), ("voided", "已作废"))
class TestReceptionProgress(unittest.TestCase):
def test_missing_or_pending(self):
self.assertEqual(reception_progress(None)[0], "none")
self.assertEqual(reception_progress({"acceptConclusion": "dqr"})[0], "none")
self.assertEqual(reception_progress({"acceptConclusion": ""})[0], "none")
def test_accept_reject(self):
self.assertEqual(reception_progress({"acceptConclusion": "js"}), ("js", "已接收"))
self.assertEqual(reception_progress({"acceptConclusion": "bj"}), ("bj", "拒收"))
class TestQcProgress(unittest.TestCase):
def test_none(self):
self.assertEqual(qc_progress(None)[0], "incomplete")
def test_complete_pass_or_fail(self):
self.assertEqual(qc_progress({"qcConclusion": "tg", "qcWorkOrderStatus": "ywc"})[0], "complete")
self.assertEqual(qc_progress({"qcConclusion": "btg", "qcWorkOrderStatus": "ywc"})[0], "complete")
def test_void(self):
self.assertEqual(qc_progress({"qcWorkOrderStatus": "yzf"})[0], "voided")
def test_in_progress(self):
self.assertEqual(qc_progress({"qcWorkOrderStatus": "ypz", "qcConclusion": ""})[0], "incomplete")
class TestStorageProgress(unittest.TestCase):
def test_none_or_pending(self):
self.assertEqual(storage_progress(None)[0], "none")
self.assertEqual(storage_progress({"storageStatus": "drk"})[0], "none")
def test_in_and_reject(self):
self.assertEqual(storage_progress({"storageStatus": "yrk"})[0], "yrk")
self.assertEqual(
storage_progress({"storageStatus": "drk", "reviewConclusion": "btg"})[0],
"rejected",
)
class TestHandleFlags(unittest.TestCase):
def test_cert_handle_time_wins(self):
row = SimpleNamespace(handle_time=datetime(2026, 10, 1, 8, 0), make_status="0", make_date="")
self.assertTrue(storage_cert_handled(row))
def test_cert_legacy_make_status(self):
row = SimpleNamespace(handle_time=None, make_status="1", make_date="")
self.assertTrue(storage_cert_handled(row))
def test_cert_legacy_make_date(self):
row = SimpleNamespace(handle_time=None, make_status="0", make_date="20260101")
self.assertTrue(storage_cert_handled(row))
def test_cert_empty(self):
self.assertFalse(storage_cert_handled(None))
row = SimpleNamespace(handle_time=None, make_status="0", make_date="")
self.assertFalse(storage_cert_handled(row))
def test_insurance_legacy_todo_or_date(self):
self.assertTrue(
insurance_handled(SimpleNamespace(handle_time=None, todo_status="1", handle_date=""))
)
self.assertTrue(
insurance_handled(SimpleNamespace(handle_time=None, todo_status="0", handle_date="20260131"))
)
self.assertFalse(
insurance_handled(SimpleNamespace(handle_time=None, todo_status="0", handle_date=""))
)
class TestSyncHandleStatus(unittest.TestCase):
def test_set_and_clear(self):
ht = datetime(2026, 10, 5, 15, 0, 0)
st, ymd = sync_handle_status_fields(ht)
self.assertEqual(st, "1")
self.assertEqual(ymd, "20261005")
st2, ymd2 = sync_handle_status_fields(None)
self.assertEqual(st2, "0")
self.assertEqual(ymd2, "")
Run: cd backend && python -m pytest tests/test_sample_info_progress.py -v
Expected: FAIL(sample_info_progress 未定义)
sample_info_progress.py约定展示码(列表 xxxFlag / xxxFlagLabel):
| 领域 | code | 文案 |
|---|---|---|
| 采集 | uncollected / collected / voided |
未采集 / 已采集 / 已作废 |
| 接收 | none / js / bj |
未接收 / 已接收 / 拒收 |
| 质检 | incomplete / complete / voided |
未完成 / 已完成 / 已作废 |
| 入库 | none / yrk / rejected |
未入库 / 已入库 / 拒绝入库 |
入库拒绝:与短信列表一致——storage_status != yrk 且 review_conclusion == btg。
def collection_progress(sample_status: str) -> tuple:
st = (sample_status or "").strip()
if not st:
return "uncollected", "未采集"
if st == "yzf":
return "voided", "已作废"
if st in ("ycj", "jcz", "yrk"):
return "collected", "已采集"
return "uncollected", "未采集"
def reception_progress(reception: dict | None) -> tuple:
if not reception:
return "none", "未接收"
c = (reception.get("acceptConclusion") or "").strip()
if c == "js":
return "js", "已接收"
if c == "bj":
return "bj", "拒收"
return "none", "未接收"
def qc_progress(qc: dict | None) -> tuple:
if not qc:
return "incomplete", "未完成"
if (qc.get("qcWorkOrderStatus") or "").strip() == "yzf":
return "voided", "已作废"
conc = (qc.get("qcConclusion") or "").strip()
if conc in ("tg", "btg"):
return "complete", "已完成"
return "incomplete", "未完成"
def storage_progress(review: dict | None) -> tuple:
if not review:
return "none", "未入库"
if (review.get("storageStatus") or "").strip() == "yrk":
return "yrk", "已入库"
if (review.get("reviewConclusion") or "").strip() == "btg":
return "rejected", "拒绝入库"
return "none", "未入库"
def storage_cert_handled(row) -> bool:
if row is None:
return False
if getattr(row, "handle_time", None):
return True
if (getattr(row, "make_status", None) or "").strip() == "1":
return True
return bool((getattr(row, "make_date", None) or "").strip())
def insurance_handled(row) -> bool:
if row is None:
return False
if getattr(row, "handle_time", None):
return True
if (getattr(row, "todo_status", None) or "").strip() == "1":
return True
return bool((getattr(row, "handle_date", None) or "").strip())
def sync_handle_status_fields(handle_time):
if handle_time is None:
return "0", ""
return "1", handle_time.strftime("%Y%m%d")
把 sync_handle_status_fields 写清楚:handle_time is None → ("0", ""),否则 ("1", YYYYMMDD)。
Run: cd backend && python -m pytest tests/test_sample_info_progress.py -v
Expected: PASS
git add backend/app/services/sample_info_progress.py backend/tests/test_sample_info_progress.py
git commit -m "$(cat <<'EOF'
feat(backend): add sample info progress flag helpers.
EOF
)"
Files:
backend/app/crud/collection_protocol.py(_storage_cert_to_dict L607、upsert_storage_cert L3348、_fullmoon_todo_to_dict L3434、upsert_fullmoon_todo L3476)Test: backend/tests/test_sample_info_handle.py
[ ] Step 1: 测试 apply_handle_payload 纯逻辑(可放在 sample_info_progress.py)
from datetime import datetime
from app.services.sample_info_progress import apply_handle_payload
def test_apply_sets_status():
out = apply_handle_payload(
handle_time=datetime(2026, 10, 5, 9, 0, 0),
handler_user_id="u1",
attachments="http://a.png",
remark="ok",
)
assert out["handle_time"].year == 2026
assert out["handler_user_id"] == "u1"
assert out["make_or_todo_status"] == "1"
assert out["legacy_date"] == "20261005"
def test_apply_clear():
out = apply_handle_payload(handle_time=None, handler_user_id="u1", attachments="", remark="")
assert out["handle_time"] is None
assert out["handler_user_id"] is None
assert out["make_or_todo_status"] == "0"
assert out["legacy_date"] == ""
规则:清空办理时间时 同时清空 handler_user_id(与 spec 一致)。有办理时间时若 handler_user_id 为空,upsert 层抛 ValueError("填写办理时间时必须指定办理人员")。
apply_handle_payload,再接到 upsertupsert_storage_cert 增加字段:handle_time、handler_user_id;写入时调用 sync_handle_status_fields 设置 make_status、make_date。
upsert_fullmoon_todo:写入 handle_time、handler_user_id,同步 todo_status、handle_date。调用方 不要 传入 insurance_company_* 等键(exclude)。
_storage_cert_to_dict / _fullmoon_todo_to_dict 增加:
"handleTime": _fmt_dt(s.handle_time),
"handlerUserId": s.handler_user_id or "",
"handled": storage_cert_handled(s), # 保险用 insurance_handled
办理人姓名在 API 层批量查 sys_user.nick_name,CRUD dict 可只带 id。
git commit -m "$(cat <<'EOF'
feat(backend): sync storage cert and insurance handle time with legacy status.
EOF
)"
Files:
backend/app/crud/protocol_sample_collection.pybackend/app/crud/research_donation_sample_collection.pybackend/app/api/protocol_sample_collection.pybackend/app/api/research_donation_sample_collection.pybackend/app/services/protocol_sample_list_summary.py(接收多条时按 reception_time 取最晚)列表每行增加(camelCase):
collectionFlag, collectionFlagLabelreceptionFlag, receptionFlagLabel, receptionTime(可空)qcFlag, qcFlagLabel, qcCompleteTime(可空)storageFlag, storageFlagLabel, storageTime(可空)storageCertHandled, insuranceHandled(bool)科研捐赠 禁止 返回证/保险 handled 字段。
Query(两接口): collectionFlag, receptionFlag, qcFlag, storageFlag;细胞存储另加 storageCertHandled、insuranceHandled(1/0 或 true/false)。
实现要点:
sample_code、collection_protocol_id。get_list_reception_summary_map_by_sample_codes 中同码多条时保留 reception_time 最大的一条(改循环:不要 if code in out: continue 先到先得;改为比较时间)。同时返回 receptionTime。get_list_qc_conclusion_map_by_sample_codes(..., reception_source=...)。tenant_no + sample_code + reception_source 查 BizStorageReleaseReview,del_flag=0,同码多条取 update_time/storage_time 最晚。字段 storageStatus、reviewConclusion、storageTime。protocol_id 批量查两表,用 Task 2 的 handled。采集筛选 SQL:
uncollected:sample_status IS NULL OR ''collected:IN ('ycj','jcz','yrk')voided:= 'yzf'保留旧参数 sampleStatus 时:若传入 ycj 等原始码仍过滤该码;新前端只传 collectionFlag。
在 tests/test_protocol_list_sample_reception_qc.py 增加:两条 mock reception_time 不同,断言留下较晚且 js。
细胞存储 list_protocol_sample_collections 签名增加上述 filter。捐赠 crud 同样,无 cert 参数。
cd backend && python -m pytest tests/test_protocol_list_sample_reception_qc.py tests/test_sample_info_progress.py -vExpected: PASS
git commit -m "$(cat <<'EOF'
feat(backend): enrich sample collection lists with progress and handle flags.
EOF
)"
Files:
Modify: backend/app/api/collection_protocol.py
[ ] Step 1: Pydantic body
class StorageCertHandleBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
handle_time: Optional[datetime] = Field(None, alias="handleTime")
handler_user_id: Optional[str] = Field(None, alias="handlerUserId")
cert_image_urls: Optional[str] = Field(None, alias="certImageUrls")
remark: Optional[str] = None
class FullmoonHandleBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
handle_time: Optional[datetime] = Field(None, alias="handleTime")
handler_user_id: Optional[str] = Field(None, alias="handlerUserId")
insurance_attachments: Optional[str] = Field(None, alias="insuranceAttachments")
handle_remark: Optional[str] = Field(None, alias="handleRemark")
[ ] Step 2: 路由
PUT /web/admin/storage-certs/{protocol_id}:鉴权与协议更新相同(require_admin_or_super_admin 或样本信息 menu code 并集,与现有 storage-certs GET 对齐)。校验协议存在、租户、数据权限。handle_time 有值则 handler_user_id 必填。调用 upsert_storage_cert。
PUT /web/admin/fullmoon-todos/{protocol_id}/handle:同样。model_dump 只含 上述四字段,禁止 spread 进保险公司字段。
不删除 PUT .../insurance。
协议详情 bundle 里 storageCert、fullmoonTodo 带上 handleTime、handlerUserId、handlerUserName、handled。
[ ] Step 3: 无 DB 的校验测(handler 缺失 → ValueError)放 test_sample_info_handle.py。
[ ] Step 4: Commit
git commit -m "$(cat <<'EOF'
feat(api): add storage cert and fullmoon lightweight handle endpoints.
EOF
)"
Files:
web-admin/src/api/protocol-sample-collection.tsweb-admin/src/api/research-donation-sample-collection.tsweb-admin/src/api/admin.ts(或新建 web-admin/src/api/sample-info-handle.ts)Modify: web-admin/src/utils/status-tag-registry.ts
[ ] Step 1: list params 增加 flag 字段(与后端 alias 一致)。
[ ] Step 2: 办理函数
export function fetchUpdateStorageCertHandle(protocolId: string, data: {
handleTime?: string | null
handlerUserId?: string | null
certImageUrls?: string
remark?: string
}) {
return request.put({ url: `/web/admin/storage-certs/${protocolId}`, params: data })
}
export function fetchUpdateFullmoonHandle(protocolId: string, data: {
handleTime?: string | null
handlerUserId?: string | null
insuranceAttachments?: string
handleRemark?: string
}) {
return request.put({ url: `/web/admin/fullmoon-todos/${protocolId}/handle`, params: data })
}
export const sampleCollectionFlag: StatusRegistry = {
uncollected: { label: '未采集', tagType: 'info' },
collected: { label: '已采集', tagType: 'success' },
voided: { label: '已作废', tagType: 'danger' }
}
export const sampleReceptionFlag: StatusRegistry = {
none: { label: '未接收', tagType: 'info' },
js: { label: '已接收', tagType: 'success' },
bj: { label: '拒收', tagType: 'danger' }
}
export const sampleQcFlag: StatusRegistry = {
incomplete: { label: '未完成', tagType: 'warning' },
complete: { label: '已完成', tagType: 'success' },
voided: { label: '已作废', tagType: 'danger' }
}
export const sampleStorageFlag: StatusRegistry = {
none: { label: '未入库', tagType: 'info' },
yrk: { label: '已入库', tagType: 'success' },
rejected: { label: '拒绝入库', tagType: 'danger' }
}
export const handleDoneFlag: StatusRegistry = {
'1': { label: '已办理', tagType: 'success' },
'0': { label: '未办理', tagType: 'info' }
}
导出 label/tagType getter,与文件内现有工厂一致。
git commit -m "$(cat <<'EOF'
feat(web-admin): add sample info list params, handle APIs, and progress tags.
EOF
)"
Files:
web-admin/src/views/cell-storage/sample-info/UnifiedSampleInfoPage.vueweb-admin/src/views/cell-storage/sample-info/index.vueweb-admin/src/views/research-donation/sample-info/index.vue对照:SampleCollectionInfoTab.vue 的表格骨架 + protocol-audit/index.vue 的 pill Tab。UI:Fresh Mint、筛选无重复标题、按钮带 el-icon。
Props:defaultSource: 'cell_storage' | 'research_donation'。
两 Tab 文案:细胞存储 / 科研捐赠。query.tab 同步。用当前路由 meta.menuCode 与用户菜单树判断 Tab 可见性(无科研捐赠权限则只显示细胞存储,反之亦然)。从细胞存储菜单进入默认 cell_storage。
关键词 placeholder:姓名/样本编码/入库编码。采集/接收/质检/入库下拉用 Task 2 的 code。细胞存储 Tab 增加储存证、保险:已办理 / 未办理。防抖 300ms 与现页一致。
与 spec §4.2:状态 Tag、样本编码用 SampleCodeWithSpecialDisplay。四个进度 +(存储 Tab)两列办理。操作列仅「查看」,fixed="right",min-width ≥ 260。双击查看。分页 10/20/50。
细胞存储 fetchProtocolSampleCollectionList;捐赠 fetchResearchDonationSampleCollectionList。
细胞存储:ProtocolViewDialog,protocol-id = collectionProtocolId,传入 highlight-sample-code。捐赠:DonationViewDialog + researchDonationId。show-audit-history 可保留;不要再靠 show-storage-cert 只读拼盘作为办理入口。
<UnifiedSampleInfoPage default-source="cell_storage" />
科研捐赠:default-source="research_donation"。科研捐赠页的 panel-top「样本信息管理」标题:统一页自带 pill Tab 后,按 Fresh Mint 单标题——标题放统一页 h2 一次即可,子 index 不要再套一个 h2。
git commit -m "$(cat <<'EOF'
feat(web-admin): unify sample info management into two source tabs.
EOF
)"
Files:
web-admin/src/components/business/protocol-handle-panel/index.vueweb-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vueModify: web-admin/src/views/research-donation/list/DonationViewDialog.vue(高亮当前样本)
[ ] Step 1: protocol-handle-panel
label-position="top",.dialog-form-layout 内嵌即可(本身是 section 不是独立 dialog)。字段:el-date-picker datetime value-format="YYYY-MM-DD HH:mm:ss";办理人 el-select filterable,远程 fetchUserList({ page: 1, pageSize: 50, keyword, status: '1' }),空表单默认 userStore 当前用户 id/名。附件卡片对齐 ProtocolFormDialog 协议附件(fetchUploadAttachment,folder 用 storage-cert / fullmoon-todo)。备注 textarea。底栏「保存」Check 图标。有办理时间无办理人前端拦截。保存成功 ElMessage.success 并 emit('saved')。
两种 mode:storage-cert | insurance,调用对应 PUT。
[ ] Step 2: ProtocolViewDialog
props:highlightSampleCode?: string。匹配的 .sample-card 加高亮 class(主色描边)。onMounted/watch bundle 就绪后 scrollIntoView 该卡片。
替换基本信息里旧储存证只读行、客户下「新生儿保险信息」只读大段:改为两个 protocol-handle-panel(仅 source 为细胞存储详情时显示)。打开后用 bundle 预填;保存后 reload bundle。
样本卡片已有接收/质检/入库只读,不重复造四段表单;确保入库组件在捐赠详情同样存在。
[ ] Step 3: DonationViewDialog
同样 highlightSampleCode + 滚动。不要加证/保险面板。
git commit -m "$(cat <<'EOF'
feat(web-admin): add cert and insurance handle panels on protocol detail.
EOF
)"
Files:
web-admin/src/views/cell-storage/protocol-audit/index.vueweb-admin/src/views/cell-storage/storage-cert/index.vueweb-admin/src/views/cell-storage/fullmoon-todo/index.vueweb-admin/src/views/cell-storage/sample-reception-info/index.vue(若仍指向旧 tab)Modify: web-admin/src/views/cell-storage/storage-entry-info/index.vue
[ ] Step 1: protocol-audit/index.vue 的 entityTabs 只留 protocol;删除 StorageCertTab/FullmoonTodoTab 引用。?tab=storage-cert|fullmoon-todo 时 replace 到 CellStorageSampleInfo。
[ ] Step 2: 储存证/满月隐藏页:
router.replace({ name: 'CellStorageSampleInfo', query: { ...route.query, tab: 'cell_storage' } })
[ ] Step 3: 确认样本信息页不再 import 四个旧 Tab。删除或停止引用 StorageCertTab.vue、FullmoonTodoTab.vue、四个 Sample*InfoTab.vue、DonationSampleCollectionInfoTab.vue(无引用后再删文件,避免死代码)。
[ ] Step 4: Commit
git commit -m "$(cat <<'EOF'
refactor(web-admin): remove storage cert and fullmoon tabs from protocol audit.
EOF
)"
[ ] Step 1: ./scripts/update-graph.sh all(或分别 backend/frontend)。
[ ] Step 2: cd backend && python -m pytest tests/test_sample_info_progress.py tests/test_sample_info_handle.py tests/test_protocol_list_sample_reception_qc.py -v
[ ] Step 3: 浏览器(有环境时):细胞存储样本信息四标识、多样本改证后两行已办理一致、清空办理时间变未办理、科研捐赠无证列、采集协议单无证/满月 Tab、旧储存证 URL 跳转。无浏览器则注明未做 UI 点验。
[ ] Step 4: Commit 图谱(若有 diff)
git commit -m "$(cat <<'EOF'
chore: update code graphs after unified sample info.
EOF
)"
| Spec | Task |
|---|---|
| 一页两 Tab、默认来源、权限 | 7 |
| 样本行、列、只读进度 | 2, 4, 7 |
| 证/保险已办理兼容旧数据 | 2, 3, 4 |
| 详情进度 + 办理四字段 | 5, 8(进度复用已有只读组件) |
| 独立 PUT、不同步保险公司字段 | 3, 5 |
| 协议单撤 Tab、旧路由 | 9 |
| 不发短信、不建总表 | 全程禁止 |
| 测试要点 | 2–5, 10 |
uncollected|collected|voided、none|js|bj、incomplete|complete|voided、none|yrk|rejected。