Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f37181478 | ||
|
|
00635740f1 | ||
|
|
76316fad7a | ||
|
|
d4617b4988 | ||
|
|
c40feab76a | ||
|
|
9cf1110495 |
No files matched your search
@@ -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
|
||||
),
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1,63 @@
|
||||
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))
|
||||
assert.equal(groups.length, 2)
|
||||
})
|
||||
|
||||
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),
|
||||
['知情同意书', '传染病检测报告', '其他附件']
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -8,7 +8,7 @@ function normalizeAttachmentFiles(raw: unknown): Array<Record<string, unknown>>
|
||||
return raw.filter((item) => item && typeof item === 'object') as Array<Record<string, unknown>>
|
||||
}
|
||||
|
||||
/** 按来源返回协议级附件标签与文件列表(细胞存储=孕妇健康调查表,科研捐赠=传染病检测报告) */
|
||||
/** 按来源返回协议级主附件标签与文件列表(细胞存储=孕妇健康调查表,科研捐赠=传染病检测报告) */
|
||||
export function getSourceProtocolAttachment(
|
||||
row: Record<string, unknown>
|
||||
): ProtocolSourceAttachmentMeta {
|
||||
@@ -24,3 +24,50 @@ export function getSourceProtocolAttachment(
|
||||
files: normalizeAttachmentFiles(row.healthSurveyAttachmentFiles)
|
||||
}
|
||||
}
|
||||
|
||||
/** 知情同意书(细胞存储与科研捐赠共用字段) */
|
||||
export function getConsentProtocolAttachment(
|
||||
row: Record<string, unknown>
|
||||
): ProtocolSourceAttachmentMeta {
|
||||
return {
|
||||
label: '知情同意书',
|
||||
files: normalizeAttachmentFiles(row.consentAttachmentFiles)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 样本接收弹窗:按来源返回质检需查看的协议附件分组。
|
||||
* 细胞存储:仅知情同意书 / 孕妇健康调查表(不展示「协议附件」项)
|
||||
* 科研捐赠:知情同意书 / 传染病检测报告 / 其他附件
|
||||
*/
|
||||
export function getSourceProtocolAttachmentGroups(
|
||||
row: Record<string, unknown>
|
||||
): 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 ?? row.otherAttachmentFiles)
|
||||
}
|
||||
]
|
||||
}
|
||||
return [
|
||||
{
|
||||
label: '知情同意书',
|
||||
files: normalizeAttachmentFiles(row.consentAttachmentFiles)
|
||||
},
|
||||
{
|
||||
label: '孕妇健康调查表',
|
||||
files: normalizeAttachmentFiles(row.healthSurveyAttachmentFiles)
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -44,6 +44,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Document, Download, VideoPlay } from '@element-plus/icons-vue'
|
||||
import { attachmentFullUrl } from '@/composables/useAttachmentActions'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
@@ -58,14 +59,8 @@
|
||||
(e: 'download', file: Record<string, any>): void
|
||||
}>()
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
function resolveFileUrl(suffix: string) {
|
||||
if (!suffix) return ''
|
||||
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/')) {
|
||||
return suffix
|
||||
}
|
||||
return `${minioBaseUrl}/${suffix}`
|
||||
return attachmentFullUrl(suffix)
|
||||
}
|
||||
|
||||
function isImageFile(file: { fileType?: string; fileName?: string; fileUrl?: string }) {
|
||||
|
||||
+18
-1
@@ -80,6 +80,16 @@
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="sample-reception-card__meta-item sample-reception-card__meta-item--attachment">
|
||||
<dt>{{ consentAttachmentMeta(row).label }}</dt>
|
||||
<dd>
|
||||
<ProtocolAttachmentMiniReadonly
|
||||
:files="consentAttachmentMeta(row).files"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="activeTab === 'received'" class="sample-reception-card__meta-item">
|
||||
<dt>实验室入库编码</dt>
|
||||
<dd>
|
||||
@@ -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<string, unknown>) {
|
||||
return getConsentProtocolAttachment(row)
|
||||
}
|
||||
|
||||
function protocolStatusLabelBySource(status: string, source: string, voidType?: string): string {
|
||||
return source === 'research_donation'
|
||||
? donationStatusDisplayLabel(status, voidType)
|
||||
|
||||
@@ -204,41 +204,63 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="form-section-transport" class="sample-transport-col">
|
||||
<div class="section-title section-title--compact">
|
||||
<div class="section-title-left"><h3>运输信息</h3></div>
|
||||
<div id="form-section-transport" class="sample-transport-col sample-transport-col--stack">
|
||||
<div class="sample-transport-stack-block">
|
||||
<div class="section-title section-title--compact">
|
||||
<div class="section-title-left"><h3>运输信息</h3></div>
|
||||
</div>
|
||||
<div class="data-grid data-grid--2">
|
||||
<div class="data-item">
|
||||
<div class="data-label">运输方式</div>
|
||||
<div class="data-value">
|
||||
{{ transportTypeLabel(String(currentRow.transportType || '')) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">预计到达</div>
|
||||
<div class="data-value">{{ formatDisplayOrDash(currentRow.etaTime) }}</div>
|
||||
</div>
|
||||
<template v-if="currentRow.transportType === 'kdys'">
|
||||
<div class="data-item">
|
||||
<div class="data-label">快递公司</div>
|
||||
<div class="data-value">{{ currentRow.expressCompanyName || '-' }}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">快递单号</div>
|
||||
<div class="data-value">{{ currentRow.waybillNo || '-' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="currentRow.transportType === 'ryxd'">
|
||||
<div class="data-item">
|
||||
<div class="data-label">携带人</div>
|
||||
<div class="data-value">{{ currentRow.carrierName || '-' }}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">携带人电话</div>
|
||||
<div class="data-value">{{ currentRow.carrierPhone || '-' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-grid data-grid--2">
|
||||
<div class="data-item">
|
||||
<div class="data-label">运输方式</div>
|
||||
<div class="data-value">
|
||||
{{ transportTypeLabel(String(currentRow.transportType || '')) }}
|
||||
<div class="sample-transport-stack-block sample-protocol-attachments">
|
||||
<div class="section-title section-title--compact">
|
||||
<div class="section-title-left"><h3>协议附件</h3></div>
|
||||
</div>
|
||||
<div class="sample-protocol-attachments__list">
|
||||
<div
|
||||
v-for="group in receiveProtocolAttachmentGroups"
|
||||
:key="group.label"
|
||||
class="sample-protocol-attachments__item"
|
||||
>
|
||||
<div class="data-label">{{ group.label }}</div>
|
||||
<ProtocolAttachmentMiniReadonly
|
||||
:files="group.files"
|
||||
empty-text="暂无附件"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">预计到达</div>
|
||||
<div class="data-value">{{ formatDisplayOrDash(currentRow.etaTime) }}</div>
|
||||
</div>
|
||||
<template v-if="currentRow.transportType === 'kdys'">
|
||||
<div class="data-item">
|
||||
<div class="data-label">快递公司</div>
|
||||
<div class="data-value">{{ currentRow.expressCompanyName || '-' }}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">快递单号</div>
|
||||
<div class="data-value">{{ currentRow.waybillNo || '-' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="currentRow.transportType === 'ryxd'">
|
||||
<div class="data-item">
|
||||
<div class="data-label">携带人</div>
|
||||
<div class="data-value">{{ currentRow.carrierName || '-' }}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">携带人电话</div>
|
||||
<div class="data-value">{{ currentRow.carrierPhone || '-' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -722,6 +744,7 @@
|
||||
import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
|
||||
import ProtocolFinanceConfirmFields from '@/components/business/protocol-finance-confirm-fields/index.vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import ProtocolAttachmentMiniReadonly from '@/components/business/protocol-attachment-mini-readonly/index.vue'
|
||||
import SampleReceptionCardList from './SampleReceptionCardList.vue'
|
||||
import SampleReceptionDualReadonlyPanel from '@/components/business/sample-reception-dual-readonly-panel/index.vue'
|
||||
import {
|
||||
@@ -745,6 +768,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 +979,12 @@
|
||||
.filter(Boolean) as Array<Record<string, unknown>>
|
||||
})
|
||||
|
||||
const receiveProtocolAttachmentGroups = computed(() => {
|
||||
const row = currentRow.value
|
||||
if (!row) return []
|
||||
return getSourceProtocolAttachmentGroups(row as Record<string, unknown>)
|
||||
})
|
||||
|
||||
function formatMetric(v: unknown) {
|
||||
if (v === null || v === undefined || v === '') return '-'
|
||||
const n = Number(v)
|
||||
@@ -1485,6 +1515,8 @@
|
||||
let detail: Record<string, unknown> = row
|
||||
try {
|
||||
detail = (await fetchSampleReceptionDetail(receptionId)) as Record<string, unknown>
|
||||
// 详情可能带更完整的协议附件字段,合并后供弹窗右侧展示
|
||||
currentRow.value = { ...row, ...detail }
|
||||
} catch {
|
||||
/* 列表行已含主要字段,详情失败时回退 */
|
||||
}
|
||||
@@ -1653,6 +1685,44 @@
|
||||
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;
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
.sample-protocol-attachments__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sample-protocol-attachments__item {
|
||||
min-width: 0;
|
||||
|
||||
.data-label {
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
:deep(.protocol-attachment-mini) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
|
||||
Reference in new issue
Block a user