feat(web-admin): add storage-cert QC view on cell-storage sample-info.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-09 12:40:16 +08:00
1 parent f1dcafd33d
commit 500a85465f
1 file changed
+576
@@ -0,0 +1,576 @@
<template>
<div class="unified-sample-info-page">
<div v-if="visibleTabs.length > 1" class="panel-pill-tabs panel-pill-tabs--lg">
<div class="panel-pill-tabs__track" role="tablist" aria-label="来源">
<button
v-for="item in visibleTabs"
: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>
<div class="filter-block">
<el-form
:model="searchForm"
class="filter-form sample-info-filter-form"
label-width="auto"
@submit.prevent
>
<div
class="filter-grid sample-info-filter-grid"
:class="{ 'is-cell-storage': isCellStorage }"
>
<el-form-item label="关键词">
<el-input v-model="searchForm.keyword" placeholder="姓名/样本编码/入库编码" clearable />
</el-form-item>
<el-form-item label="采集">
<el-select v-model="searchForm.collectionFlag" placeholder="全部" clearable style="width: 100%">
<el-option label="未采集" value="uncollected" />
<el-option label="已采集" value="collected" />
<el-option label="已作废" value="voided" />
</el-select>
</el-form-item>
<el-form-item label="接收">
<el-select v-model="searchForm.receptionFlag" placeholder="全部" clearable style="width: 100%">
<el-option label="未接收" value="none" />
<el-option label="已接收" value="js" />
<el-option label="拒收" value="bj" />
</el-select>
</el-form-item>
<el-form-item label="质检">
<el-select v-model="searchForm.qcFlag" placeholder="全部" clearable style="width: 100%">
<el-option label="未完成" value="incomplete" />
<el-option label="已完成" value="complete" />
<el-option label="已作废" value="voided" />
</el-select>
</el-form-item>
<el-form-item label="入库">
<el-select v-model="searchForm.storageFlag" placeholder="全部" clearable style="width: 100%">
<el-option label="未入库" value="none" />
<el-option label="已入库" value="yrk" />
<el-option label="拒绝入库" value="rejected" />
</el-select>
</el-form-item>
<el-form-item v-if="isCellStorage" label="储存证">
<el-select
v-model="searchForm.storageCertHandled"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="已办理" value="1" />
<el-option label="未办理" value="0" />
</el-select>
</el-form-item>
<el-form-item v-if="isCellStorage" label="保险">
<el-select
v-model="searchForm.insuranceHandled"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="已办理" value="1" />
<el-option label="未办理" value="0" />
</el-select>
</el-form-item>
</div>
<div class="filter-actions-row">
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button
type="primary"
class="btn-main"
:disabled="!selectedRows.length"
@click="openQuickRegister('storage')"
>
<el-icon><Box /></el-icon>
登记入库
</el-button>
<el-button
v-if="isCellStorage"
class="btn-ghost"
:disabled="!selectedRows.length"
@click="openQuickRegister('storage_cert')"
>
<el-icon><Ticket /></el-icon>
登记储存证
</el-button>
<el-button
v-if="isCellStorage"
class="btn-ghost"
:disabled="!selectedRows.length"
@click="openQuickRegister('insurance')"
>
<el-icon><Collection /></el-icon>
登记保险
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
ref="tableRef"
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="rowKey"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column label="协议状态" min-width="100" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row)" size="small">{{ statusLabel(row) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="expectedDeliveryDate" label="预产期" min-width="120" align="center" header-align="center" />
<el-table-column prop="motherName" label="母亲姓名" min-width="100" align="center" header-align="center" />
<el-table-column
prop="newbornName"
label="新生儿姓名"
min-width="110"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column prop="sampleCode" label="样本编码" min-width="160" align="center" header-align="center">
<template #default="{ row }">
<SampleCodeWithSpecialDisplay
:sample-code="row.sampleCode"
:is-special="!!row.isSpecial"
:special-remark="row.specialRemark"
monospace
/>
</template>
</el-table-column>
<el-table-column prop="labStorageCode" label="实验室入库编码" min-width="150" align="center" header-align="center">
<template #default="{ row }">{{ row.labStorageCode || '—' }}</template>
</el-table-column>
<el-table-column prop="sampleTypeName" label="样本类型" min-width="110" align="center" header-align="center" />
<el-table-column label="采集" min-width="90" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="sampleCollectionFlagTagType(String(row.collectionFlag || ''))" size="small">
{{ row.collectionFlagLabel || sampleCollectionFlagLabel(String(row.collectionFlag || '')) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="接收" min-width="90" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="sampleReceptionFlagTagType(String(row.receptionFlag || ''))" size="small">
{{ row.receptionFlagLabel || sampleReceptionFlagLabel(String(row.receptionFlag || '')) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="质检" min-width="90" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="sampleQcFlagTagType(String(row.qcFlag || ''))" size="small">
{{ row.qcFlagLabel || sampleQcFlagLabel(String(row.qcFlag || '')) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="入库" min-width="100" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="sampleStorageFlagTagType(String(row.storageFlag || ''))" size="small">
{{ row.storageFlagLabel || sampleStorageFlagLabel(String(row.storageFlag || '')) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isCellStorage"
label="储存证"
min-width="90"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="handleDoneFlagTagType(row.storageCertHandled ? '1' : '0')" size="small">
{{ handleDoneFlagLabel(row.storageCertHandled ? '1' : '0') }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isCellStorage"
label="保险"
min-width="90"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="handleDoneFlagTagType(row.insuranceHandled ? '1' : '0')" size="small">
{{ handleDoneFlagLabel(row.insuranceHandled ? '1' : '0') }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="280" fixed="right" align="center" header-align="center">
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleView(row)">
<el-icon><View /></el-icon>查看
</el-button>
<el-button
v-if="isCellStorage && supportsStorageCertQc(row.sampleTypeName)"
class="btn-op-item"
@click="openStorageCertQcView(row)"
>
<el-icon><Document /></el-icon>查看储存证的质检信息
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
</div>
<ProtocolViewDialog
v-if="isCellStorage"
v-model="protocolViewVisible"
:protocol-id="viewProtocolId"
:list-row="viewProtocolRow"
:highlight-sample-code="highlightSampleCode"
show-audit-history
show-handle-panels
/>
<DonationViewDialog
v-else
v-model="donationViewVisible"
:donation-id="viewDonationId"
:list-row="viewDonationRow"
:highlight-sample-code="highlightSampleCode"
show-audit-history
/>
<SampleInfoQuickRegisterDialog
v-model="quickRegisterVisible"
:kind="quickRegisterKind"
:reception-source="activeSource"
:rows="quickRegisterRows"
@saved="onQuickRegisterSaved"
/>
<StorageCertQcDialog
v-model:visible="storageCertQcVisible"
mode="view"
:sample-code="storageCertQcSampleCode"
:sample-type-name="storageCertQcSampleTypeName"
/>
</div>
</template>
<script setup lang="ts">
import { Box, Collection, Document, RefreshLeft, Search, Ticket, View } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import StorageCertQcDialog from '@/components/business/storage-cert-qc-dialog/index.vue'
import { supportsStorageCertQc } from '@/utils/storage-cert-qc-fields'
import { fetchProtocolSampleCollectionList } from '@/api/protocol-sample-collection'
import { fetchResearchDonationSampleCollectionList } from '@/api/research-donation-sample-collection'
import type { CollectionProtocolRow } from '@/api/collection-protocol'
import type { ResearchDonationRow } from '@/api/research-donation'
import ProtocolViewDialog from '@/views/cell-storage/protocol/ProtocolViewDialog.vue'
import DonationViewDialog from '@/views/research-donation/list/DonationViewDialog.vue'
import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
import SampleInfoQuickRegisterDialog, {
type QuickRegisterKind
} from './SampleInfoQuickRegisterDialog.vue'
import { useMenuPerm } from '@/hooks/core/useMenuPerm'
import {
donationStatusDisplayLabel,
donationStatusTagType,
handleDoneFlagLabel,
handleDoneFlagTagType,
protocolStatusDisplayLabel,
protocolStatusTagType,
sampleCollectionFlagLabel,
sampleCollectionFlagTagType,
sampleQcFlagLabel,
sampleQcFlagTagType,
sampleReceptionFlagLabel,
sampleReceptionFlagTagType,
sampleStorageFlagLabel,
sampleStorageFlagTagType
} from '@/utils/status-tag-registry'
defineOptions({ name: 'UnifiedSampleInfoPage' })
const props = defineProps<{
defaultSource: 'cell_storage' | 'research_donation'
}>()
const { hasMenuPerm } = useMenuPerm()
const route = useRoute()
const router = useRouter()
type SourceKey = 'cell_storage' | 'research_donation'
const allTabs: Array<{ key: SourceKey; title: string; perm: string }> = [
{ key: 'cell_storage', title: '细胞存储', perm: 'cell-storage:sample-info:list' },
{
key: 'research_donation',
title: '科研捐赠',
perm: 'research-donation:sample-info:list'
}
]
const visibleTabs = computed(() => allTabs.filter((t) => hasMenuPerm(t.perm)))
const activeSource = ref<SourceKey>(props.defaultSource)
const isCellStorage = computed(() => activeSource.value === 'cell_storage')
const searchForm = reactive({
keyword: '',
collectionFlag: '',
receptionFlag: '',
qcFlag: '',
storageFlag: '',
storageCertHandled: '',
insuranceHandled: ''
})
const tableData = ref<Record<string, unknown>[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const selectedRows = ref<Record<string, unknown>[]>([])
const tableRef = ref<{ clearSelection?: () => void } | null>(null)
const quickRegisterVisible = ref(false)
const quickRegisterKind = ref<QuickRegisterKind>('storage')
const quickRegisterRows = ref<Record<string, unknown>[]>([])
const protocolViewVisible = ref(false)
const donationViewVisible = ref(false)
const viewProtocolId = ref<string | null>(null)
const viewDonationId = ref<string | null>(null)
const viewProtocolRow = ref<CollectionProtocolRow | null>(null)
const viewDonationRow = ref<ResearchDonationRow | null>(null)
const highlightSampleCode = ref('')
const storageCertQcVisible = ref(false)
const storageCertQcSampleCode = ref('')
const storageCertQcSampleTypeName = ref('')
function openStorageCertQcView(row: Record<string, unknown>) {
storageCertQcSampleCode.value = String(row.sampleCode || '')
storageCertQcSampleTypeName.value = String(row.sampleTypeName || '')
if (!storageCertQcSampleCode.value) {
ElMessage.warning('缺少样本编码,无法查看')
return
}
storageCertQcVisible.value = true
}
function statusLabel(row: Record<string, unknown>) {
if (isCellStorage.value) {
return protocolStatusDisplayLabel(String(row.protocolStatus || ''), String(row.voidType || ''))
}
return donationStatusDisplayLabel(String(row.donationStatus || ''), String(row.voidType || ''))
}
function statusTagType(row: Record<string, unknown>) {
if (isCellStorage.value) {
return protocolStatusTagType(String(row.protocolStatus || ''))
}
return donationStatusTagType(String(row.donationStatus || ''))
}
function switchSource(key: SourceKey) {
activeSource.value = key
protocolViewVisible.value = false
donationViewVisible.value = false
selectedRows.value = []
router.replace({ query: { ...route.query, tab: key } })
pagination.page = 1
loadData()
}
async function loadData() {
loading.value = true
try {
const params: Record<string, unknown> = {
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
collectionFlag: searchForm.collectionFlag || undefined,
receptionFlag: searchForm.receptionFlag || undefined,
qcFlag: searchForm.qcFlag || undefined,
storageFlag: searchForm.storageFlag || undefined
}
if (isCellStorage.value) {
params.storageCertHandled = searchForm.storageCertHandled || undefined
params.insuranceHandled = searchForm.insuranceHandled || undefined
const res = (await fetchProtocolSampleCollectionList(params as any)) as Record<string, unknown>
const list = ((res.list as Record<string, unknown>[]) || []).map((row) => ({
...row,
rowKey: String(row.protocolSampleId || row.sampleCode || Math.random())
}))
tableData.value = list
pagination.total = Number(res.total) || 0
} else {
const res = (await fetchResearchDonationSampleCollectionList(
params as any
)) as Record<string, unknown>
const list = ((res.list as Record<string, unknown>[]) || []).map((row) => ({
...row,
rowKey: String(row.donationSampleId || row.sampleCode || Math.random())
}))
tableData.value = list
pagination.total = Number(res.total) || 0
}
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
Object.assign(searchForm, {
keyword: '',
collectionFlag: '',
receptionFlag: '',
qcFlag: '',
storageFlag: '',
storageCertHandled: '',
insuranceHandled: ''
})
handleSearch()
}
function handleSelectionChange(rows: Record<string, unknown>[]) {
selectedRows.value = rows
}
function openQuickRegister(kind: QuickRegisterKind) {
if (!selectedRows.value.length) {
ElMessage.warning('请先选择样本')
return
}
if ((kind === 'storage_cert' || kind === 'insurance') && !isCellStorage.value) {
ElMessage.warning('储存证与保险仅支持细胞存储样本')
return
}
quickRegisterKind.value = kind
quickRegisterRows.value = [...selectedRows.value]
quickRegisterVisible.value = true
}
function onQuickRegisterSaved() {
tableRef.value?.clearSelection?.()
selectedRows.value = []
loadData()
}
function handleView(row: Record<string, unknown>) {
highlightSampleCode.value = String(row.sampleCode || '')
if (isCellStorage.value) {
const pid = String(row.collectionProtocolId || '')
if (!pid) {
ElMessage.warning('缺少关联协议单,无法查看')
return
}
viewProtocolId.value = pid
viewProtocolRow.value = {
id: pid,
motherName: row.motherName,
newbornName: row.newbornName
} as CollectionProtocolRow
protocolViewVisible.value = true
return
}
const did = String(row.researchDonationId || '')
if (!did) {
ElMessage.warning('缺少关联捐赠协议单,无法查看')
return
}
viewDonationId.value = did
viewDonationRow.value = {
id: did,
motherName: row.motherName,
newbornName: row.newbornName
} as ResearchDonationRow
donationViewVisible.value = true
}
watchDebounced(
searchForm,
() => {
pagination.page = 1
loadData()
},
{ debounce: 300, deep: true }
)
onMounted(() => {
const tab = String(route.query.tab || '')
const allowed = visibleTabs.value.map((t) => t.key)
if (allowed.includes(tab as SourceKey)) {
activeSource.value = tab as SourceKey
} else if (allowed.includes(props.defaultSource)) {
activeSource.value = props.defaultSource
} else if (allowed.length) {
activeSource.value = allowed[0]
}
loadData()
})
</script>
<style scoped lang="scss">
.sample-info-filter-form {
min-width: 0;
}
.unified-sample-info-page .filter-grid.sample-info-filter-grid {
margin-bottom: 0;
align-items: center;
grid-template-columns: minmax(0, 1.5fr) repeat(4, minmax(0, 1fr));
}
.unified-sample-info-page .filter-grid.sample-info-filter-grid.is-cell-storage {
grid-template-columns: minmax(0, 1.5fr) repeat(6, minmax(0, 1fr));
}
.sample-info-filter-form :deep(.el-form-item) {
margin-bottom: 0;
min-width: 0;
}
.sample-info-filter-form :deep(.el-select),
.sample-info-filter-form :deep(.el-input) {
min-width: 0;
}
</style>