From c40feab76a610740fb18a816d9402d02ade8d406 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Thu, 5 Nov 2026 21:53:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(web-admin):=20=E6=A0=B7=E6=9C=AC=E6=8E=A5?= =?UTF-8?q?=E6=94=B6=E5=B1=95=E7=A4=BA=E5=8D=8F=E8=AE=AE=E7=9F=A5=E6=83=85?= =?UTF-8?q?=E5=90=8C=E6=84=8F=E4=B9=A6=E7=AD=89=E9=99=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 质检样本接收列表与弹窗需查看采集协议侧附件。后端待接收/已接收列表补齐协议附件字段;前端列表增加知情同意书缩略图,接收弹窗右侧改为上运输、下协议附件分组展示。 关联 https://gitea.aichengyan.com/cykj/zkxw/issues/43 Co-authored-by: Cursor --- backend/app/crud/sample_reception.py | 12 ++ ...t_sample_reception_protocol_attachments.py | 129 ++++++++++++++++++ .../utils/protocol-source-attachment.test.ts | 62 +++++++++ .../src/utils/protocol-source-attachment.ts | 53 ++++++- .../SampleReceptionCardList.vue | 19 ++- .../sample-reception/index.vue | 121 +++++++++++----- 6 files changed, 362 insertions(+), 34 deletions(-) create mode 100644 backend/tests/test_sample_reception_protocol_attachments.py create mode 100644 web-admin/src/utils/protocol-source-attachment.test.ts diff --git a/backend/app/crud/sample_reception.py b/backend/app/crud/sample_reception.py index 24e3f5b8..c3fc8472 100644 --- a/backend/app/crud/sample_reception.py +++ b/backend/app/crud/sample_reception.py @@ -287,9 +287,15 @@ def _donation_rows_to_sample_list_items( ), "sourceType": "research_donation", "sourceTypeLabel": "科研捐赠", + "consentAttachmentFiles": urls_to_attachment_list( + donation.consent_form_urls if donation else None, db, tenant_no + ), "infectiousDiseaseReportAttachmentFiles": urls_to_attachment_list( donation.infectious_disease_report_urls if donation else None, db, tenant_no ), + "attachmentFiles": urls_to_attachment_list( + donation.attachment_urls if donation else None, db, tenant_no + ), "etaTime": _fmt_dt(transport.eta_time) if transport and transport.eta_time else "", "transportType": (transport.transport_type if transport else "") or "", "waybillNo": (transport.waybill_no if transport else "") or "", @@ -462,6 +468,12 @@ def _rows_to_sample_list_items( **sample_special_to_dict(sample), "sourceType": "cell_storage", "sourceTypeLabel": "细胞存储", + "protocolAttachmentFiles": urls_to_attachment_list( + protocol.protocol_attachments if protocol else None, db, tenant_no + ), + "consentAttachmentFiles": urls_to_attachment_list( + protocol.consent_form_urls if protocol else None, db, tenant_no + ), "healthSurveyAttachmentFiles": urls_to_attachment_list( protocol.health_survey_form_urls if protocol else None, db, tenant_no ), diff --git a/backend/tests/test_sample_reception_protocol_attachments.py b/backend/tests/test_sample_reception_protocol_attachments.py new file mode 100644 index 00000000..a8056865 --- /dev/null +++ b/backend/tests/test_sample_reception_protocol_attachments.py @@ -0,0 +1,129 @@ +"""样本接收列表项应携带协议侧附件字段,供质检查看。""" + +from __future__ import annotations + +import unittest +from types import SimpleNamespace +from unittest.mock import patch + + +class _FakeQuery: + def filter(self, *args, **kwargs): + return self + + def all(self): + return [] + + def first(self): + return None + + +class _FakeDb: + def query(self, *args, **kwargs): + return _FakeQuery() + + +class TestSampleReceptionProtocolAttachments(unittest.TestCase): + def test_cell_storage_item_includes_protocol_consent_health_files(self): + from app.crud import sample_reception as svc + + protocol = SimpleNamespace( + id="proto-1", + protocol_no="P1", + protocol_status="effective", + protocol_attachments="http://a/p.pdf", + consent_form_urls="http://a/c.pdf", + health_survey_form_urls="http://a/h.pdf", + ) + sample = SimpleNamespace( + id="sample-1", + sample_code="S001", + sample_type_id="t1", + collect_amount=None, + sample_status="", + ) + party = SimpleNamespace(mother_name="妈妈", newborn_name="宝宝") + transport = SimpleNamespace( + eta_time=None, + transport_type="", + waybill_no="", + express_company_id="", + carrier_name="", + carrier_phone="", + ) + + with patch.object( + svc, + "urls_to_attachment_list", + side_effect=lambda urls, *_a, **_k: [{"fileUrl": urls}] if urls else [], + ), patch.object(svc, "sample_special_to_dict", return_value={}), patch.object( + svc.prod_reception_svc, "get_map_by_reception_ids", return_value={} + ), patch( + "app.services.qc_work_order_fee_summary.build_protocol_payment_summaries", + return_value={}, + ): + items = svc._rows_to_sample_list_items( + _FakeDb(), + [(sample, protocol, party, transport)], + "t1", + reception_map={}, + ) + + self.assertEqual(len(items), 1) + item = items[0] + self.assertEqual(item["protocolAttachmentFiles"][0]["fileUrl"], "http://a/p.pdf") + self.assertEqual(item["consentAttachmentFiles"][0]["fileUrl"], "http://a/c.pdf") + self.assertEqual(item["healthSurveyAttachmentFiles"][0]["fileUrl"], "http://a/h.pdf") + + def test_donation_item_includes_consent_infectious_other_files(self): + from app.crud import sample_reception as svc + + donation = SimpleNamespace( + id="don-1", + donation_no="D1", + donation_status="effective", + consent_form_urls="http://a/c.pdf", + infectious_disease_report_urls="http://a/i.pdf", + attachment_urls="http://a/o.pdf", + ) + sample = SimpleNamespace( + id="ds-1", + sample_code="D001", + sample_type_id="t1", + collect_amount=None, + ) + party = SimpleNamespace(mother_name="妈妈", newborn_name="宝宝") + transport = SimpleNamespace( + eta_time=None, + transport_type="", + waybill_no="", + express_company_id="", + carrier_name="", + carrier_phone="", + ) + + with patch.object( + svc, + "urls_to_attachment_list", + side_effect=lambda urls, *_a, **_k: [{"fileUrl": urls}] if urls else [], + ), patch.object(svc, "sample_special_to_dict", return_value={}), patch.object( + svc.prod_reception_svc, "get_map_by_reception_ids", return_value={} + ): + items = svc._donation_rows_to_sample_list_items( + _FakeDb(), + [(sample, donation, party, transport)], + "t1", + reception_map={}, + ) + + self.assertEqual(len(items), 1) + item = items[0] + self.assertEqual(item["consentAttachmentFiles"][0]["fileUrl"], "http://a/c.pdf") + self.assertEqual( + item["infectiousDiseaseReportAttachmentFiles"][0]["fileUrl"], "http://a/i.pdf" + ) + self.assertEqual(item["attachmentFiles"][0]["fileUrl"], "http://a/o.pdf") + + +if __name__ == "__main__": + unittest.main() diff --git a/web-admin/src/utils/protocol-source-attachment.test.ts b/web-admin/src/utils/protocol-source-attachment.test.ts new file mode 100644 index 00000000..57e7c500 --- /dev/null +++ b/web-admin/src/utils/protocol-source-attachment.test.ts @@ -0,0 +1,62 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { + getConsentProtocolAttachment, + getSourceProtocolAttachment, + getSourceProtocolAttachmentGroups +} from './protocol-source-attachment' + +describe('protocol-source-attachment', () => { + it('returns health survey as primary for cell storage', () => { + const meta = getSourceProtocolAttachment({ + sourceType: 'cell_storage', + healthSurveyAttachmentFiles: [{ id: '1', fileUrl: '/a.png' }] + }) + assert.equal(meta.label, '孕妇健康调查表') + assert.equal(meta.files.length, 1) + }) + + it('returns infectious disease report as primary for research donation', () => { + const meta = getSourceProtocolAttachment({ + sourceType: 'research_donation', + infectiousDiseaseReportAttachmentFiles: [{ id: '2', fileUrl: '/b.png' }] + }) + assert.equal(meta.label, '传染病检测报告') + assert.equal(meta.files.length, 1) + }) + + it('returns consent attachment from shared field', () => { + const meta = getConsentProtocolAttachment({ + consentAttachmentFiles: [{ id: '3', fileUrl: '/c.png' }] + }) + assert.equal(meta.label, '知情同意书') + assert.equal(meta.files.length, 1) + }) + + it('groups cell storage attachments for reception dialog', () => { + const groups = getSourceProtocolAttachmentGroups({ + sourceType: 'cell_storage', + protocolAttachmentFiles: [{ id: 'p' }], + consentAttachmentFiles: [{ id: 'c' }], + healthSurveyAttachmentFiles: [{ id: 'h' }] + }) + assert.deepEqual( + groups.map((g) => g.label), + ['协议附件', '知情同意书', '孕妇健康调查表'] + ) + assert.ok(groups.every((g) => g.files.length === 1)) + }) + + it('groups research donation attachments for reception dialog', () => { + const groups = getSourceProtocolAttachmentGroups({ + sourceType: 'research_donation', + consentAttachmentFiles: [{ id: 'c' }], + infectiousDiseaseReportAttachmentFiles: [{ id: 'i' }], + attachmentFiles: [{ id: 'o' }] + }) + assert.deepEqual( + groups.map((g) => g.label), + ['知情同意书', '传染病检测报告', '其他附件'] + ) + }) +}) diff --git a/web-admin/src/utils/protocol-source-attachment.ts b/web-admin/src/utils/protocol-source-attachment.ts index b68a25ba..571f53a8 100644 --- a/web-admin/src/utils/protocol-source-attachment.ts +++ b/web-admin/src/utils/protocol-source-attachment.ts @@ -8,7 +8,7 @@ function normalizeAttachmentFiles(raw: unknown): Array> return raw.filter((item) => item && typeof item === 'object') as Array> } -/** 按来源返回协议级附件标签与文件列表(细胞存储=孕妇健康调查表,科研捐赠=传染病检测报告) */ +/** 按来源返回协议级主附件标签与文件列表(细胞存储=孕妇健康调查表,科研捐赠=传染病检测报告) */ export function getSourceProtocolAttachment( row: Record ): ProtocolSourceAttachmentMeta { @@ -24,3 +24,54 @@ export function getSourceProtocolAttachment( files: normalizeAttachmentFiles(row.healthSurveyAttachmentFiles) } } + +/** 知情同意书(细胞存储与科研捐赠共用字段) */ +export function getConsentProtocolAttachment( + row: Record +): ProtocolSourceAttachmentMeta { + return { + label: '知情同意书', + files: normalizeAttachmentFiles(row.consentAttachmentFiles) + } +} + +/** + * 样本接收弹窗:按来源返回质检需查看的协议附件分组。 + * 细胞存储:协议附件 / 知情同意书 / 孕妇健康调查表 + * 科研捐赠:知情同意书 / 传染病检测报告 / 其他附件 + */ +export function getSourceProtocolAttachmentGroups( + row: Record +): ProtocolSourceAttachmentMeta[] { + const source = String(row.sourceType || 'cell_storage').trim() + if (source === 'research_donation') { + return [ + { + label: '知情同意书', + files: normalizeAttachmentFiles(row.consentAttachmentFiles) + }, + { + label: '传染病检测报告', + files: normalizeAttachmentFiles(row.infectiousDiseaseReportAttachmentFiles) + }, + { + label: '其他附件', + files: normalizeAttachmentFiles(row.attachmentFiles) + } + ] + } + return [ + { + label: '协议附件', + files: normalizeAttachmentFiles(row.protocolAttachmentFiles) + }, + { + label: '知情同意书', + files: normalizeAttachmentFiles(row.consentAttachmentFiles) + }, + { + label: '孕妇健康调查表', + files: normalizeAttachmentFiles(row.healthSurveyAttachmentFiles) + } + ] +} diff --git a/web-admin/src/views/quality-inspection/sample-reception/SampleReceptionCardList.vue b/web-admin/src/views/quality-inspection/sample-reception/SampleReceptionCardList.vue index 42ae4b37..d9289a5e 100644 --- a/web-admin/src/views/quality-inspection/sample-reception/SampleReceptionCardList.vue +++ b/web-admin/src/views/quality-inspection/sample-reception/SampleReceptionCardList.vue @@ -80,6 +80,16 @@ /> +
+
{{ consentAttachmentMeta(row).label }}
+
+ +
+
实验室入库编码
@@ -242,7 +252,10 @@ import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue' import QcWorkOrderProtocolPaymentPanel from '@/views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue' import ProtocolAttachmentMiniReadonly from '@/components/business/protocol-attachment-mini-readonly/index.vue' - import { getSourceProtocolAttachment } from '@/utils/protocol-source-attachment' + import { + getConsentProtocolAttachment, + getSourceProtocolAttachment + } from '@/utils/protocol-source-attachment' import { useAttachmentActions } from '@/composables/useAttachmentActions' defineOptions({ name: 'SampleReceptionCardList' }) @@ -284,6 +297,10 @@ return getSourceProtocolAttachment(row) } + function consentAttachmentMeta(row: Record) { + return getConsentProtocolAttachment(row) + } + function protocolStatusLabelBySource(status: string, source: string, voidType?: string): string { return source === 'research_donation' ? donationStatusDisplayLabel(status, voidType) diff --git a/web-admin/src/views/quality-inspection/sample-reception/index.vue b/web-admin/src/views/quality-inspection/sample-reception/index.vue index 940b9c21..beb12cc5 100644 --- a/web-admin/src/views/quality-inspection/sample-reception/index.vue +++ b/web-admin/src/views/quality-inspection/sample-reception/index.vue @@ -204,41 +204,62 @@
-
-
-

运输信息

+
+
+
+

运输信息

+
+
+
+
运输方式
+
+ {{ transportTypeLabel(String(currentRow.transportType || '')) }} +
+
+
+
预计到达
+
{{ formatDisplayOrDash(currentRow.etaTime) }}
+
+ + +
-
-
-
运输方式
-
- {{ transportTypeLabel(String(currentRow.transportType || '')) }} +
+
+

协议附件

+
+
+
+
{{ group.label }}
+
-
-
预计到达
-
{{ formatDisplayOrDash(currentRow.etaTime) }}
-
- -
@@ -745,6 +766,7 @@ buildPrefilledLabStorageCode, parseLabStorageReferenceDate } from '@/utils/sample-reception-lab-storage-code' + import { getSourceProtocolAttachmentGroups } from '@/utils/protocol-source-attachment' import type { ProductionReceptionRecord } from '@/utils/sample-production-reception-fields' import { formatProductionFieldValue, @@ -955,6 +977,12 @@ .filter(Boolean) as Array> }) + const receiveProtocolAttachmentGroups = computed(() => { + const row = currentRow.value + if (!row) return [] + return getSourceProtocolAttachmentGroups(row) + }) + function formatMetric(v: unknown) { if (v === null || v === undefined || v === '') return '-' const n = Number(v) @@ -1653,6 +1681,35 @@ padding-left: 20px; border-left: 1px solid #e2e8f0; } + + &--stack { + display: flex; + flex-direction: column; + gap: 14px; + } + } + + .sample-transport-stack-block { + min-width: 0; + } + + .sample-protocol-attachments { + background: #ecfdf5; + border-radius: 8px; + padding: 12px 14px; + border: 1px solid #a7f3d0; + } + + .sample-protocol-attachments__list { + display: flex; + flex-direction: column; + gap: 12px; + } + + .sample-protocol-attachments__item { + .data-label { + margin-bottom: 6px; + } } .section-title { -- 2.54.0