提交
This commit is contained in:
1 parent
4f24d3b687
commit
1ff73b9a74
202 files changed
+11989
-10045
No files matched your search
@@ -1044,61 +1044,63 @@
|
||||
class="section-card section-card--white"
|
||||
>
|
||||
<div class="section-title section-title--banner"><h3>审核记录</h3></div>
|
||||
<el-table
|
||||
v-loading="auditLoading"
|
||||
class="list-table sub-view-table"
|
||||
:data="auditRecords"
|
||||
stripe
|
||||
empty-text="暂无审核记录"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="审核时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="auditStatus"
|
||||
label="审核状态"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
<div class="list-body list-wrap view-inner-table-wrap">
|
||||
<el-table
|
||||
v-loading="auditLoading"
|
||||
class="list-table inner-table"
|
||||
:data="auditRecords"
|
||||
stripe
|
||||
empty-text="暂无审核记录"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="String(row.auditStatus) === 'shtg' ? 'success' : 'warning'"
|
||||
size="small"
|
||||
>
|
||||
{{ auditStatusLabel(String(row.auditStatus || '')) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="auditRemark"
|
||||
label="审核备注"
|
||||
min-width="200"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="auditorUserName"
|
||||
label="审核人"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ auditorDisplay(row) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="审核时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="auditStatus"
|
||||
label="审核状态"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="String(row.auditStatus) === 'shtg' ? 'success' : 'warning'"
|
||||
size="small"
|
||||
>
|
||||
{{ auditStatusLabel(String(row.auditStatus || '')) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="auditRemark"
|
||||
label="审核备注"
|
||||
min-width="200"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="auditorUserName"
|
||||
label="审核人"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ auditorDisplay(row) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1851,26 +1853,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAuditRecords(protocolNoHint?: string) {
|
||||
async function loadAuditRecords() {
|
||||
if (!props.showAuditHistory || !props.protocolId) {
|
||||
auditRecords.value = []
|
||||
return
|
||||
}
|
||||
auditLoading.value = true
|
||||
try {
|
||||
const pno =
|
||||
String(
|
||||
protocolNoHint || protocol.value?.protocolNo || props.listRow?.protocolNo || ''
|
||||
).trim() || undefined
|
||||
const res = await fetchCollectionProtocolAuditList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: pno
|
||||
collectionProtocolId: props.protocolId
|
||||
})
|
||||
const pid = props.protocolId
|
||||
auditRecords.value = (res.list || []).filter(
|
||||
(row) => String(row.collectionProtocolId || '') === pid
|
||||
)
|
||||
auditRecords.value = res.list || []
|
||||
} catch {
|
||||
auditRecords.value = []
|
||||
} finally {
|
||||
auditLoading.value = false
|
||||
}
|
||||
@@ -1883,10 +1880,8 @@
|
||||
bundle.value = null
|
||||
auditRecords.value = []
|
||||
|
||||
const protocolNoHint = String(props.listRow?.protocolNo || '').trim()
|
||||
const baseLookupsPromise = loadBaseLookups().catch(() => {})
|
||||
const auditEarlyPromise =
|
||||
props.showAuditHistory && protocolNoHint ? loadAuditRecords(protocolNoHint) : null
|
||||
const auditPromise = props.showAuditHistory ? loadAuditRecords() : null
|
||||
|
||||
let data: Record<string, any> | null = null
|
||||
try {
|
||||
@@ -1902,10 +1897,7 @@
|
||||
}
|
||||
|
||||
void finishLookups(data, baseLookupsPromise)
|
||||
|
||||
if (props.showAuditHistory && !auditEarlyPromise) {
|
||||
void loadAuditRecords()
|
||||
}
|
||||
void auditPromise
|
||||
}
|
||||
|
||||
function scrollToSection(key: string) {
|
||||
@@ -2292,9 +2284,38 @@
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* ---- Sub View Table ---- */
|
||||
.sub-view-table {
|
||||
margin-bottom: 8px;
|
||||
/* ---- 审核记录内嵌列表(对齐 OpenSpec 嵌套列表 14px 标准) ---- */
|
||||
.view-inner-table-wrap.list-body.list-wrap {
|
||||
margin-top: 4px;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: var(--mint-radius, 10px);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-table) {
|
||||
--el-table-header-bg-color: var(--mint-table-head, #f8fafc);
|
||||
--el-table-border-color: var(--mint-border, #e2e8f0);
|
||||
--el-table-row-hover-bg-color: var(--mint-row-hover, #f0fdf4);
|
||||
--el-table-text-color: var(--mint-text, #0f172a);
|
||||
--el-table-header-text-color: var(--mint-text-muted, #64748b);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-table th.el-table__cell) {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
:deep(.el-table td.el-table__cell) {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text, #0f172a);
|
||||
}
|
||||
|
||||
:deep(.el-empty) {
|
||||
padding: 24px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Sample Cards ---- */
|
||||
|
||||
@@ -429,116 +429,195 @@
|
||||
<el-dialog
|
||||
v-model="occupancyDialogVisible"
|
||||
:title="occupancyDialogTitle"
|
||||
width="920px"
|
||||
top="5vh"
|
||||
class="dialog-form-layout"
|
||||
width="1280px"
|
||||
class="dialog-form-layout storage-occupancy-dialog"
|
||||
align-center
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-tabs v-model="occupancyActiveTab">
|
||||
<el-tabs v-model="occupancyActiveTab" class="storage-occupancy-tabs">
|
||||
<el-tab-pane label="在库物品" name="stored">
|
||||
<div class="occupancy-toolbar">
|
||||
<el-button
|
||||
v-if="occupancyTotal === 0"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleOpenStoreDialog"
|
||||
>
|
||||
入库
|
||||
</el-button>
|
||||
<span v-else class="occupancy-full-tip">该储物格已有物品在库,一格只能存放一个物品</span>
|
||||
<div class="list-toolbar occupancy-list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<el-button
|
||||
v-if="occupancyTotal === 0"
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
@click="handleOpenStoreDialog"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
入库
|
||||
</el-button>
|
||||
<span v-else class="occupancy-full-tip">该储物格已有物品在库,一格只能存放一个物品</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
:data="occupancyList"
|
||||
v-loading="occupancyLoading"
|
||||
size="small"
|
||||
max-height="360"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="60" />
|
||||
<el-table-column label="物品类型" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.itemType === 'cell_product' ? 'primary' : 'info'" size="small">
|
||||
{{ row.itemType === 'cell_product' ? '细胞产品' : '其他' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="itemCode" label="物品编码" min-width="140" />
|
||||
<el-table-column prop="itemName" label="物品名称" min-width="140" />
|
||||
<el-table-column label="来源" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{
|
||||
{ production: '生产入库', manual: '手动入库', transfer: '转移' }[
|
||||
row.itemSource
|
||||
] || row.itemSource
|
||||
}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="storedAt" label="入库时间" min-width="170" />
|
||||
<el-table-column label="操作" min-width="80" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" link size="small" @click="handleRemoveOccupancy(row)">
|
||||
出库
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-if="occupancyTotal > occupancyPageSize" class="occupancy-pagination">
|
||||
<el-pagination
|
||||
small
|
||||
layout="total, prev, pager, next"
|
||||
:total="occupancyTotal"
|
||||
:page-size="occupancyPageSize"
|
||||
v-model:current-page="occupancyPage"
|
||||
@current-change="loadOccupancies"
|
||||
/>
|
||||
<div class="list-body list-wrap view-inner-table-wrap">
|
||||
<el-table
|
||||
class="list-table inner-table"
|
||||
:data="occupancyList"
|
||||
v-loading="occupancyLoading"
|
||||
stripe
|
||||
row-key="id"
|
||||
empty-text="暂无在库物品"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (occupancyPage - 1) * occupancyPageSize + index + 1"
|
||||
/>
|
||||
<el-table-column label="物品类型" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.itemType === 'cell_product' ? 'primary' : 'info'" size="small">
|
||||
{{ row.itemType === 'cell_product' ? '细胞产品' : '其他' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="itemCode"
|
||||
label="物品编码"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="itemName"
|
||||
label="物品名称"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="来源" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{
|
||||
{ production: '生产入库', manual: '手动入库', transfer: '转移' }[
|
||||
row.itemSource
|
||||
] || row.itemSource
|
||||
}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="storedAt"
|
||||
label="入库时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="120"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item" @click="handleRemoveOccupancy(row)">
|
||||
<el-icon><Top /></el-icon>出库
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="occupancyPage"
|
||||
:page-size="occupancyPageSize"
|
||||
:total="occupancyTotal"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="(val) => ((occupancyPage = val), loadOccupancies())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="出入库历史" name="history">
|
||||
<el-table :data="historyList" v-loading="historyLoading" size="small" max-height="400">
|
||||
<el-table-column type="index" label="序号" min-width="60" />
|
||||
<el-table-column label="物品类型" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.itemType === 'cell_product' ? 'primary' : 'info'" size="small">
|
||||
{{ row.itemType === 'cell_product' ? '细胞产品' : '其他' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="itemCode" label="物品编码" min-width="120" />
|
||||
<el-table-column prop="itemName" label="物品名称" min-width="120" />
|
||||
<el-table-column label="来源" min-width="100">
|
||||
<template #default="{ row }">
|
||||
{{
|
||||
{ production: '生产入库', manual: '手动入库', transfer: '转移' }[
|
||||
row.itemSource
|
||||
] || row.itemSource
|
||||
}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="storedAt" label="入库时间" min-width="170" />
|
||||
<el-table-column prop="removedAt" label="出库时间" min-width="170" />
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.occupancyStatus === 'stored' ? 'success' : 'warning'"
|
||||
size="small"
|
||||
>
|
||||
{{ row.occupancyStatus === 'stored' ? '在库' : '已出库' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-if="historyTotal > historyPageSize" class="occupancy-pagination">
|
||||
<el-pagination
|
||||
small
|
||||
layout="total, prev, pager, next"
|
||||
:total="historyTotal"
|
||||
:page-size="historyPageSize"
|
||||
v-model:current-page="historyPage"
|
||||
@current-change="loadHistory"
|
||||
/>
|
||||
<div class="list-body list-wrap view-inner-table-wrap">
|
||||
<el-table
|
||||
class="list-table inner-table"
|
||||
:data="historyList"
|
||||
v-loading="historyLoading"
|
||||
stripe
|
||||
row-key="id"
|
||||
empty-text="暂无出入库历史"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (historyPage - 1) * historyPageSize + index + 1"
|
||||
/>
|
||||
<el-table-column label="物品类型" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.itemType === 'cell_product' ? 'primary' : 'info'" size="small">
|
||||
{{ row.itemType === 'cell_product' ? '细胞产品' : '其他' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="itemCode"
|
||||
label="物品编码"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="itemName"
|
||||
label="物品名称"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="来源" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{
|
||||
{ production: '生产入库', manual: '手动入库', transfer: '转移' }[
|
||||
row.itemSource
|
||||
] || row.itemSource
|
||||
}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="storedAt"
|
||||
label="入库时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="removedAt"
|
||||
label="出库时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="状态" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.occupancyStatus === 'stored' ? 'success' : 'warning'"
|
||||
size="small"
|
||||
>
|
||||
{{ row.occupancyStatus === 'stored' ? '在库' : '已出库' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="historyPage"
|
||||
:page-size="historyPageSize"
|
||||
:total="historyTotal"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="(val) => ((historyPage = val), loadHistory())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -576,11 +655,12 @@
|
||||
</el-form>
|
||||
<div class="occupancy-slot-save-bar">
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="occupancySlotSaving"
|
||||
@click="handleOccupancySlotSave"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -635,7 +715,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check } from '@element-plus/icons-vue'
|
||||
import { Plus, Close, Check, Top } from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchStorageFacilityList,
|
||||
fetchCreateStorageFacility,
|
||||
@@ -1678,14 +1758,14 @@
|
||||
}
|
||||
|
||||
.shelf-label {
|
||||
width: 28px;
|
||||
width: 36px;
|
||||
flex-shrink: 0;
|
||||
background: #ea580c;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
@@ -1693,12 +1773,12 @@
|
||||
.shelf-cells {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.shelf-cell {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 1px solid #fed7aa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1739,7 +1819,7 @@
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 20px;
|
||||
font-size: 32px;
|
||||
color: #fb923c;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -1754,14 +1834,14 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: none;
|
||||
border-radius: 0 4px 0 4px;
|
||||
background: rgba(245, 108, 108, 0.85);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
@@ -1779,24 +1859,25 @@
|
||||
}
|
||||
|
||||
.cell-code {
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 34px;
|
||||
max-width: 72px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.cell-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
font-size: 10px;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
background: #f56c6c;
|
||||
@@ -1804,8 +1885,10 @@
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.occupancy-toolbar {
|
||||
.occupancy-list-toolbar {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.occupancy-full-tip {
|
||||
@@ -1813,15 +1896,59 @@
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.occupancy-pagination {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
:deep(.el-dialog.storage-occupancy-dialog) {
|
||||
min-height: min(720px, calc(100vh - 100px));
|
||||
}
|
||||
|
||||
.storage-occupancy-tabs {
|
||||
:deep(.el-tabs__content) {
|
||||
min-height: 520px;
|
||||
}
|
||||
}
|
||||
|
||||
.view-inner-table-wrap.list-body.list-wrap {
|
||||
margin-top: 0;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: var(--mint-radius, 10px);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-table) {
|
||||
--el-table-header-bg-color: var(--mint-table-head, #f8fafc);
|
||||
--el-table-border-color: var(--mint-border, #e2e8f0);
|
||||
--el-table-row-hover-bg-color: var(--mint-row-hover, #f0fdf4);
|
||||
--el-table-text-color: var(--mint-text, #0f172a);
|
||||
--el-table-header-text-color: var(--mint-text-muted, #64748b);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-table th.el-table__cell) {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
:deep(.el-table td.el-table__cell) {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text, #0f172a);
|
||||
}
|
||||
|
||||
:deep(.el-empty) {
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.pagination.list-pager-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.occupancy-slot-save-bar {
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -553,11 +553,112 @@
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewRow?.productName || '产品详情'"
|
||||
:icon="Box"
|
||||
:meta="viewRow?.productCode || ''"
|
||||
:code="viewRow?.productCode"
|
||||
code-label="产品编码"
|
||||
:sections="viewSections"
|
||||
width="1200px"
|
||||
:grid-columns="4"
|
||||
:grid-columns="3"
|
||||
>
|
||||
<template #basicInfo>
|
||||
<div v-if="viewRow" class="cell-product-basic">
|
||||
<div class="cell-product-basic__content">
|
||||
<div class="cell-product-basic__summary">
|
||||
<div class="cell-product-basic__headline">
|
||||
<span class="cell-product-basic__name">{{ viewCellVal(viewRow.productName) }}</span>
|
||||
<el-tag
|
||||
:type="productStatusTag(String(viewRow.productStatus || '')).type"
|
||||
size="small"
|
||||
>
|
||||
{{ productStatusTag(String(viewRow.productStatus || '')).label }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="cell-product-basic__meta">
|
||||
<span>{{ viewCellVal(viewRow.productTypeName) }}</span>
|
||||
<span class="cell-product-basic__meta-sep">·</span>
|
||||
<span>{{ viewCellVal(viewRow.specName) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cell-product-basic__panels">
|
||||
<div class="cell-product-basic__panel">
|
||||
<div class="cell-product-basic__panel-title">批次与存储</div>
|
||||
<div class="cell-product-basic__grid">
|
||||
<div class="cell-product-basic__field">
|
||||
<div class="cell-product-basic__field-label">代次</div>
|
||||
<div class="cell-product-basic__field-value">{{ viewCellVal(viewRow.passage) }}</div>
|
||||
</div>
|
||||
<div class="cell-product-basic__field">
|
||||
<div class="cell-product-basic__field-label">批次号</div>
|
||||
<div class="cell-product-basic__field-value">{{ viewCellVal(viewRow.batchNo) }}</div>
|
||||
</div>
|
||||
<div class="cell-product-basic__field cell-product-basic__field--wide">
|
||||
<div class="cell-product-basic__field-label">储物格位置</div>
|
||||
<div class="cell-product-basic__field-value">{{
|
||||
viewCellVal(viewRow.storageSlotName)
|
||||
}}</div>
|
||||
</div>
|
||||
<div class="cell-product-basic__field">
|
||||
<div class="cell-product-basic__field-label">冻存日期</div>
|
||||
<div class="cell-product-basic__field-value">{{ viewCellVal(viewRow.freezeDate) }}</div>
|
||||
</div>
|
||||
<div class="cell-product-basic__field">
|
||||
<div class="cell-product-basic__field-label">有效期</div>
|
||||
<div class="cell-product-basic__field-value">{{ viewCellVal(viewRow.expiryDate) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cell-product-basic__panel">
|
||||
<div class="cell-product-basic__panel-title">质量与数量</div>
|
||||
<div class="cell-product-basic__grid">
|
||||
<div class="cell-product-basic__field">
|
||||
<div class="cell-product-basic__field-label">预计细胞数量(亿)</div>
|
||||
<div class="cell-product-basic__field-value">{{
|
||||
viewRow.expectedCellCount != null ? String(viewRow.expectedCellCount) : '—'
|
||||
}}</div>
|
||||
</div>
|
||||
<div class="cell-product-basic__field">
|
||||
<div class="cell-product-basic__field-label">活性(%)</div>
|
||||
<div class="cell-product-basic__field-value">{{
|
||||
viewRow.viability != null ? String(viewRow.viability) : '—'
|
||||
}}</div>
|
||||
</div>
|
||||
<div class="cell-product-basic__field">
|
||||
<div class="cell-product-basic__field-label">数量</div>
|
||||
<div class="cell-product-basic__field-value">{{
|
||||
viewRow.quantity != null ? String(viewRow.quantity) : '—'
|
||||
}}</div>
|
||||
</div>
|
||||
<div class="cell-product-basic__field">
|
||||
<div class="cell-product-basic__field-label">单位</div>
|
||||
<div class="cell-product-basic__field-value">{{ viewCellVal(viewRow.unit) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="viewRow.remark" class="cell-product-basic__panel cell-product-basic__panel--remark">
|
||||
<div class="cell-product-basic__panel-title">备注</div>
|
||||
<p class="cell-product-basic__remark">{{ viewRow.remark }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside v-if="viewRow.productCode" class="cell-product-basic__aside">
|
||||
<div class="cell-product-qr-card">
|
||||
<QrcodeVue
|
||||
:value="String(viewRow.productCode)"
|
||||
:size="120"
|
||||
level="M"
|
||||
render-as="svg"
|
||||
foreground="#065f46"
|
||||
background="#ffffff"
|
||||
/>
|
||||
<p class="cell-product-qr-card__label">扫码识别产品编码</p>
|
||||
<p class="cell-product-qr-card__code">{{ viewRow.productCode }}</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
<template #images>
|
||||
<div class="attachment-upload attachment-upload--readonly">
|
||||
<div v-if="viewProductImageFiles.length" class="attachment-list">
|
||||
@@ -617,6 +718,7 @@
|
||||
} from '@element-plus/icons-vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import SourceChainGraph from './SourceChainGraph.vue'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
import {
|
||||
@@ -676,6 +778,11 @@
|
||||
return map[status] || { label: status, type: 'info' }
|
||||
}
|
||||
|
||||
function viewCellVal(val: unknown): string {
|
||||
if (val === null || val === undefined || val === '') return '—'
|
||||
return String(val)
|
||||
}
|
||||
|
||||
/* ---- 搜索 & 分页 ---- */
|
||||
const searchForm = reactive({ keyword: '', productStatus: '', passage: '', sourceType: '' })
|
||||
const tableData = ref<any[]>([])
|
||||
@@ -886,43 +993,11 @@
|
||||
/* ---- 查看弹窗 sections ---- */
|
||||
const viewSections = computed((): ViewSection[] => {
|
||||
if (!viewRow.value) return []
|
||||
const row = viewRow.value
|
||||
return [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
{ label: '产品编码', value: row.productCode || '—' },
|
||||
{ label: '产品名称', value: row.productName || '—' },
|
||||
{ label: '产品类型', value: row.productTypeName || '—' },
|
||||
{ label: '规格', value: row.specName || '—' },
|
||||
{ label: '代次', value: row.passage || '—' },
|
||||
{ label: '批次号', value: row.batchNo || '—' },
|
||||
{ label: '储物格位置', value: row.storageSlotName || '—' },
|
||||
{
|
||||
label: '预计细胞数量(亿)',
|
||||
value: row.expectedCellCount != null ? String(row.expectedCellCount) : '—'
|
||||
},
|
||||
{
|
||||
label: '活性(%)',
|
||||
value: row.viability != null ? String(row.viability) : '—'
|
||||
},
|
||||
{ label: '数量', value: row.quantity != null ? String(row.quantity) : '—' },
|
||||
{ label: '单位', value: row.unit || '—' },
|
||||
{ label: '冻存日期', value: row.freezeDate || '—' },
|
||||
{ label: '有效期', value: row.expiryDate || '—' },
|
||||
{
|
||||
label: '状态',
|
||||
value: productStatusTag(row.productStatus).label,
|
||||
tagType: productStatusTag(row.productStatus).type as
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'info'
|
||||
| ''
|
||||
| 'primary'
|
||||
},
|
||||
{ label: '备注', value: row.remark || '—', span: true }
|
||||
]
|
||||
slot: 'basicInfo',
|
||||
fields: []
|
||||
},
|
||||
{
|
||||
title: '产品图片',
|
||||
@@ -1338,6 +1413,172 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cell-product-basic {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.cell-product-basic__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.cell-product-basic__summary {
|
||||
padding: 16px 18px;
|
||||
border: 1px solid #c5e0d4;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #ecfdf5 0%, #ffffff 72%);
|
||||
}
|
||||
|
||||
.cell-product-basic__headline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cell-product-basic__name {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
line-height: 1.35;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.cell-product-basic__meta {
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.cell-product-basic__meta-sep {
|
||||
margin: 0 6px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.cell-product-basic__panels {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.cell-product-basic__panel {
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cell-product-basic__panel--remark {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.cell-product-basic__panel-title {
|
||||
margin-bottom: 12px;
|
||||
padding-left: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1;
|
||||
border-left: 2px solid var(--primary, #10b981);
|
||||
}
|
||||
|
||||
.cell-product-basic__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 16px;
|
||||
}
|
||||
|
||||
.cell-product-basic__field {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cell-product-basic__field--wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.cell-product-basic__field-label {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cell-product-basic__field-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cell-product-basic__remark {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #334155;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cell-product-basic__aside {
|
||||
flex-shrink: 0;
|
||||
width: 156px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.cell-product-qr-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 12px;
|
||||
border: 1px solid #c5e0d4;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, #f0fdf4 0%, #ffffff 100%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cell-product-qr-card__label {
|
||||
margin: 12px 0 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cell-product-qr-card__code {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #065f46;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.cell-product-basic {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cell-product-basic__aside {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
margin: 0 auto;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.cell-product-basic__panels {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-product-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
>
|
||||
<div class="route-config-body">
|
||||
<!-- 左侧:工艺库 -->
|
||||
<div class="route-config-left">
|
||||
<div class="route-config-left" v-loading="processLibraryLoading">
|
||||
<div class="route-config-left-header">
|
||||
<h3>工艺库</h3>
|
||||
<el-input
|
||||
@@ -318,7 +318,7 @@
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<el-empty
|
||||
v-if="filteredProcessLibrary.length === 0"
|
||||
v-if="filteredProcessLibrary.length === 0 && !processLibraryLoading"
|
||||
description="暂无工艺数据"
|
||||
:image-size="48"
|
||||
/>
|
||||
@@ -1403,6 +1403,7 @@
|
||||
|
||||
// 工艺库数据
|
||||
const processLibrary = ref<Record<string, any>[]>([])
|
||||
const processLibraryLoading = ref(false)
|
||||
const expandedCategories = ref<string[]>([])
|
||||
const chineseOrdinals = [
|
||||
'一',
|
||||
@@ -1829,6 +1830,7 @@
|
||||
})
|
||||
|
||||
async function loadConfigReferenceData() {
|
||||
processLibraryLoading.value = true
|
||||
try {
|
||||
const [processes, categories, sops, outputTypes, equipments] = await Promise.all([
|
||||
fetchAllProcesses(),
|
||||
@@ -1871,6 +1873,13 @@
|
||||
// 展开所有分类
|
||||
expandedCategories.value = Object.keys(catMap)
|
||||
processLibrary.value = Object.values(catMap)
|
||||
} catch {
|
||||
processLibrary.value = []
|
||||
expandedCategories.value = []
|
||||
} finally {
|
||||
processLibraryLoading.value = false
|
||||
}
|
||||
try {
|
||||
await Promise.all([
|
||||
searchItemUnitOptions(''),
|
||||
searchReagentOptions(''),
|
||||
|
||||
@@ -101,16 +101,18 @@
|
||||
|
||||
<template #footer>
|
||||
<div v-if="phase === 'form'">
|
||||
<el-button class="btn-ghost" @click="handleClose">取消</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleCreateTask"
|
||||
>生成任务</el-button
|
||||
>
|
||||
<el-button class="btn-ghost" @click="handleClose">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleCreateTask">
|
||||
<el-icon><Check /></el-icon>生成任务
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 阶段二:三栏编辑 -->
|
||||
<div v-if="phase === 'edit'" class="route-config-body">
|
||||
<div class="route-config-left">
|
||||
<div class="route-config-left" v-loading="processLibraryLoading">
|
||||
<div class="route-config-left-header">
|
||||
<h3>工艺库</h3>
|
||||
<el-input
|
||||
@@ -151,7 +153,7 @@
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<el-empty
|
||||
v-if="!filteredProcessLibrary.length"
|
||||
v-if="!filteredProcessLibrary.length && !processLibraryLoading"
|
||||
description="暂无工艺数据"
|
||||
:image-size="48"
|
||||
/>
|
||||
@@ -223,7 +225,7 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Rank, Plus, CopyDocument, Delete } from '@element-plus/icons-vue'
|
||||
import { Rank, Plus, CopyDocument, Delete, Close, Check } from '@element-plus/icons-vue'
|
||||
import { VueDraggable as draggable } from 'vue-draggable-plus'
|
||||
import { fetchCreateProductionTask } from '@/api/production-task'
|
||||
import { fetchUserList } from '@/api/admin'
|
||||
@@ -278,6 +280,7 @@
|
||||
stepList,
|
||||
selectedStepId,
|
||||
stepListLoading,
|
||||
processLibraryLoading,
|
||||
processSearchKeyword,
|
||||
expandedCategories,
|
||||
filteredProcessLibrary
|
||||
|
||||
@@ -79,7 +79,11 @@
|
||||
<!-- 可折叠工艺库侧栏(只读时隐藏) -->
|
||||
<template v-if="!readonly">
|
||||
<transition name="slide-lib">
|
||||
<div v-show="editor.processLibraryExpanded.value" class="execution-lib-panel">
|
||||
<div
|
||||
v-show="editor.processLibraryExpanded.value"
|
||||
v-loading="editor.processLibraryLoading.value"
|
||||
class="execution-lib-panel"
|
||||
>
|
||||
<div class="execution-lib-header">
|
||||
<h3>工艺库</h3>
|
||||
<el-input
|
||||
@@ -124,7 +128,7 @@
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<el-empty
|
||||
v-if="!editor.filteredProcessLibrary.value.length"
|
||||
v-if="!editor.filteredProcessLibrary.value.length && !editor.processLibraryLoading.value"
|
||||
description="暂无工艺数据"
|
||||
:image-size="48"
|
||||
/>
|
||||
@@ -514,6 +518,7 @@
|
||||
|
||||
/* 工艺库折叠侧栏 */
|
||||
.execution-lib-panel {
|
||||
position: relative;
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
min-height: 0;
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ export function useProductionViewDialog(buildSections: ProductionViewSectionBuil
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
const isOutboundContextView = computed(() => isOutboundProductionView(detail.value))
|
||||
const viewDialogWidth = computed(() => (isOutboundContextView.value ? '1200px' : '720px'))
|
||||
const viewDialogWidth = computed(() => (isOutboundContextView.value ? '1280px' : '1120px'))
|
||||
const viewDialogGridColumns = computed<2 | 3 | 4>(() => (isOutboundContextView.value ? 4 : 2))
|
||||
const viewDialogCompact = computed(() => isOutboundContextView.value)
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<component
|
||||
:is="activeTabComponent"
|
||||
<ProductionWorkOrderTabPanel
|
||||
ref="activeTabRef"
|
||||
:key="activeTabKey"
|
||||
:tab-key="activeTabKey"
|
||||
page-mode="manage"
|
||||
@dispatch="handleOpenDispatch"
|
||||
@dispatch-qc="handleOpenDispatchQc"
|
||||
@@ -115,7 +115,7 @@
|
||||
parseProductionWorkOrderTabKey,
|
||||
type ProductionWorkOrderTabKey
|
||||
} from '@/utils/production-work-order-tabs'
|
||||
import { getProductionWorkOrderTabComponent } from './tabs/tab-registry'
|
||||
import ProductionWorkOrderTabPanel from './tabs/ProductionWorkOrderTabPanel.vue'
|
||||
import DispatchTaskDialog from './DispatchTaskDialog.vue'
|
||||
import DispatchQcWorkOrderDialog from './DispatchQcWorkOrderDialog.vue'
|
||||
import WorkOrderRemarkDialog from '@/components/business/work-order-remark-dialog/index.vue'
|
||||
@@ -135,7 +135,6 @@
|
||||
const activeTabKey = ref<ProductionWorkOrderTabKey>(
|
||||
parseProductionWorkOrderTabKey(route.query.tab as string)
|
||||
)
|
||||
const activeTabComponent = computed(() => getProductionWorkOrderTabComponent(activeTabKey.value))
|
||||
|
||||
async function refreshActiveTab() {
|
||||
activeTabRef.value?.bumpTaskRefresh()
|
||||
|
||||
@@ -88,6 +88,7 @@ export function useTaskStepEditor() {
|
||||
const stepList = ref<Record<string, any>[]>([])
|
||||
const selectedStepId = ref('')
|
||||
const stepListLoading = ref(false)
|
||||
const processLibraryLoading = ref(false)
|
||||
const activeSubTab = ref(PROCESS_ROUTE_DEFAULT_SUB_TAB)
|
||||
const processSearchKeyword = ref('')
|
||||
const configSuppressSave = ref(false)
|
||||
@@ -674,39 +675,47 @@ export function useTaskStepEditor() {
|
||||
})
|
||||
|
||||
async function loadConfigReferenceData() {
|
||||
const [processes, categories, sops, outputTypes, equipments] = await Promise.all([
|
||||
fetchAllProcesses(),
|
||||
fetchAllProcessCategories(),
|
||||
fetchAllSopDocuments(),
|
||||
fetchAllOutputTypes(),
|
||||
fetchAllEquipments()
|
||||
])
|
||||
categoryOptions.value = categories || []
|
||||
sopOptions.value = sops || []
|
||||
outputTypeOptions.value = outputTypes || []
|
||||
equipmentOptions.value = equipments || []
|
||||
processLibraryLoading.value = true
|
||||
try {
|
||||
const [processes, categories, sops, outputTypes, equipments] = await Promise.all([
|
||||
fetchAllProcesses(),
|
||||
fetchAllProcessCategories(),
|
||||
fetchAllSopDocuments(),
|
||||
fetchAllOutputTypes(),
|
||||
fetchAllEquipments()
|
||||
])
|
||||
categoryOptions.value = categories || []
|
||||
sopOptions.value = sops || []
|
||||
outputTypeOptions.value = outputTypes || []
|
||||
equipmentOptions.value = equipments || []
|
||||
|
||||
const catMap: Record<
|
||||
string,
|
||||
{ categoryId: string; categoryName: string; processes: Record<string, any>[] }
|
||||
> = {}
|
||||
const uKey = '__uncategorized__'
|
||||
catMap[uKey] = { categoryId: uKey, categoryName: '未分类', processes: [] }
|
||||
;(categories || []).forEach((cat: Record<string, any>) => {
|
||||
catMap[cat.id] = {
|
||||
categoryId: cat.id,
|
||||
categoryName: cat.categoryName || cat.name || '未命名分类',
|
||||
processes: []
|
||||
}
|
||||
})
|
||||
;(processes || []).forEach((proc: Record<string, any>) => {
|
||||
const catId = proc.categoryId || uKey
|
||||
if (!catMap[catId])
|
||||
catMap[catId] = { categoryId: catId, categoryName: '未知分类', processes: [] }
|
||||
catMap[catId].processes.push(proc)
|
||||
})
|
||||
expandedCategories.value = Object.keys(catMap)
|
||||
processLibrary.value = Object.values(catMap)
|
||||
const catMap: Record<
|
||||
string,
|
||||
{ categoryId: string; categoryName: string; processes: Record<string, any>[] }
|
||||
> = {}
|
||||
const uKey = '__uncategorized__'
|
||||
catMap[uKey] = { categoryId: uKey, categoryName: '未分类', processes: [] }
|
||||
;(categories || []).forEach((cat: Record<string, any>) => {
|
||||
catMap[cat.id] = {
|
||||
categoryId: cat.id,
|
||||
categoryName: cat.categoryName || cat.name || '未命名分类',
|
||||
processes: []
|
||||
}
|
||||
})
|
||||
;(processes || []).forEach((proc: Record<string, any>) => {
|
||||
const catId = proc.categoryId || uKey
|
||||
if (!catMap[catId])
|
||||
catMap[catId] = { categoryId: catId, categoryName: '未知分类', processes: [] }
|
||||
catMap[catId].processes.push(proc)
|
||||
})
|
||||
expandedCategories.value = Object.keys(catMap)
|
||||
processLibrary.value = Object.values(catMap)
|
||||
} catch {
|
||||
processLibrary.value = []
|
||||
expandedCategories.value = []
|
||||
} finally {
|
||||
processLibraryLoading.value = false
|
||||
}
|
||||
await Promise.all([
|
||||
searchItemUnitOptions(''),
|
||||
searchReagentOptions(''),
|
||||
@@ -1653,6 +1662,7 @@ export function useTaskStepEditor() {
|
||||
selectedStepId,
|
||||
selectedStep,
|
||||
stepListLoading,
|
||||
processLibraryLoading,
|
||||
activeSubTab,
|
||||
processSearchKeyword,
|
||||
configSuppressSave,
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
</template>
|
||||
<template #product-table>
|
||||
<div v-if="detail?.products?.length" class="view-subtable-wrap">
|
||||
<el-table :data="detail?.products || []" stripe size="small" class="list-table view-subtable">
|
||||
<el-table :data="detail?.products || []" stripe class="list-table view-subtable">
|
||||
<el-table-column type="index" label="序号" min-width="60" align="center" />
|
||||
<el-table-column
|
||||
prop="projectCode"
|
||||
|
||||
+70
-42
@@ -24,6 +24,21 @@
|
||||
<span class="qc-protocol-payment__compact-item">
|
||||
待收 {{ formatAmount(summary.pendingAmount) }}
|
||||
</span>
|
||||
<template v-if="isLumpSum && primaryDetail">
|
||||
<span
|
||||
v-if="primaryDetail.installmentDueDate"
|
||||
class="qc-protocol-payment__compact-item"
|
||||
>
|
||||
应缴 {{ primaryDetail.installmentDueDate }}
|
||||
</span>
|
||||
<el-tag
|
||||
v-if="hideOverallPayStatus && primaryDetail.payStatus"
|
||||
:type="payStatusTagType(primaryDetail.payStatus)"
|
||||
size="small"
|
||||
>
|
||||
{{ primaryDetail.payStatusLabel || payStatusLabel(primaryDetail.payStatus || '') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<span v-if="summary.statusHint" class="qc-protocol-payment__compact-hint">
|
||||
{{ summary.statusHint }}
|
||||
</span>
|
||||
@@ -36,12 +51,15 @@
|
||||
{{ summary.overallPayStatusLabel }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-if="detailRows.length" class="qc-protocol-payment__compact-details">
|
||||
<div v-if="showCompactDetailChips" class="qc-protocol-payment__compact-details">
|
||||
<div
|
||||
v-for="(row, index) in detailRows"
|
||||
:key="row.installmentNo ?? index"
|
||||
class="qc-protocol-payment__detail-chip"
|
||||
:class="{ 'qc-protocol-payment__detail-chip--head-only': hideInstallmentDetailAmounts && isInstallment }"
|
||||
:class="{
|
||||
'qc-protocol-payment__detail-chip--head-only': hideInstallmentDetailAmounts && isInstallment,
|
||||
'qc-protocol-payment__detail-chip--inline': isLumpSum
|
||||
}"
|
||||
>
|
||||
<div class="qc-protocol-payment__detail-chip-head">
|
||||
<span v-if="isInstallment && row.installmentLabel" class="qc-protocol-payment__detail-chip-label">
|
||||
@@ -113,42 +131,39 @@
|
||||
<div class="qc-protocol-payment__table-title">
|
||||
{{ isInstallment ? `缴费明细(共 ${detailRows.length} 期)` : '缴费明细' }}
|
||||
</div>
|
||||
<el-table
|
||||
:data="detailRows"
|
||||
stripe
|
||||
size="small"
|
||||
class="list-table qc-protocol-payment__table"
|
||||
>
|
||||
<el-table-column
|
||||
v-if="isInstallment"
|
||||
prop="installmentLabel"
|
||||
label="期次"
|
||||
min-width="88"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isInstallment"
|
||||
prop="installmentDueDate"
|
||||
label="应缴日期"
|
||||
min-width="110"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ row.installmentDueDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="应缴金额" min-width="110" align="center">
|
||||
<template #default="{ row }">{{ formatAmount(row.dueAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实收金额" min-width="110" align="center">
|
||||
<template #default="{ row }">{{ formatAmount(row.transferAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="缴费状态" min-width="108" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="payStatusTagType(row.payStatus)" size="small">
|
||||
{{ row.payStatusLabel || payStatusLabel(row.payStatus || '') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="view-subtable-wrap">
|
||||
<el-table :data="detailRows" stripe class="list-table view-subtable">
|
||||
<el-table-column
|
||||
v-if="isInstallment"
|
||||
prop="installmentLabel"
|
||||
label="期次"
|
||||
min-width="88"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isInstallment"
|
||||
prop="installmentDueDate"
|
||||
label="应缴日期"
|
||||
min-width="110"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ row.installmentDueDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="应缴金额" min-width="110" align="center">
|
||||
<template #default="{ row }">{{ formatAmount(row.dueAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实收金额" min-width="110" align="center">
|
||||
<template #default="{ row }">{{ formatAmount(row.transferAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="缴费状态" min-width="108" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="payStatusTagType(row.payStatus)" size="small">
|
||||
{{ row.payStatusLabel || payStatusLabel(row.payStatus || '') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="qc-protocol-payment__empty">暂无缴费明细</p>
|
||||
</template>
|
||||
@@ -184,8 +199,19 @@
|
||||
|
||||
const isInstallment = computed(() => props.summary?.payMode === 'fq')
|
||||
|
||||
const isLumpSum = computed(() => props.summary?.payMode === 'dj')
|
||||
|
||||
const detailRows = computed(() => props.summary?.paymentDetails || [])
|
||||
|
||||
const primaryDetail = computed(() => detailRows.value[0] ?? null)
|
||||
|
||||
/** 趸交仅 1 条明细时信息已并入摘要行,不再占满整行的明细区 */
|
||||
const showCompactDetailChips = computed(() => {
|
||||
if (!detailRows.value.length) return false
|
||||
if (isLumpSum.value) return detailRows.value.length > 1
|
||||
return true
|
||||
})
|
||||
|
||||
const payModeTagType = computed((): TagType =>
|
||||
payModeTagTypeGetter(props.summary?.payMode || '')
|
||||
)
|
||||
@@ -259,10 +285,6 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.qc-protocol-payment__table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.qc-protocol-payment--embedded {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
@@ -319,6 +341,12 @@
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.qc-protocol-payment__detail-chip--inline {
|
||||
flex: 0 1 auto;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.qc-protocol-payment__detail-chip-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -22,7 +22,11 @@
|
||||
<template v-for="field in visibleMetaFields" :key="field.label">
|
||||
<div
|
||||
class="qc-work-order-card__meta-item"
|
||||
:class="{ 'qc-work-order-card__meta-item--wide': field.wide }"
|
||||
:class="{
|
||||
'qc-work-order-card__meta-item--wide': field.wide,
|
||||
'qc-work-order-card__meta-item--span-2': field.colSpan === 2,
|
||||
'qc-work-order-card__meta-item--span-3': field.colSpan === 3
|
||||
}"
|
||||
>
|
||||
<dt>{{ field.label }}</dt>
|
||||
<dd>
|
||||
@@ -294,6 +298,14 @@
|
||||
&--wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
&--span-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
&--span-3 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.qc-work-order-card__fee-in-head) {
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
<!-- 质检工单查看弹窗 — 实验室 / 第三方项目卡片面板 -->
|
||||
<template>
|
||||
<div class="qc-wo-task-result-panel">
|
||||
<template v-if="!tasks.length && configuredItems.length">
|
||||
<div class="qc-wo-configured-list">
|
||||
<article
|
||||
v-for="(item, idx) in configuredItems"
|
||||
:key="`${item.primary}-${idx}`"
|
||||
class="qc-wo-configured-item"
|
||||
>
|
||||
<span class="qc-wo-configured-item__name">{{ item.primary }}</span>
|
||||
<span v-if="item.secondary" class="qc-wo-configured-item__sub">{{ item.secondary }}</span>
|
||||
<span v-if="item.meta" class="qc-wo-configured-item__meta">{{ item.meta }}</span>
|
||||
</article>
|
||||
</div>
|
||||
<p class="qc-wo-task-result-panel__hint">已配置检测项目,任务待派发</p>
|
||||
</template>
|
||||
|
||||
<div v-else-if="tasks.length" class="qc-wo-task-cards">
|
||||
<article v-for="(task, idx) in tasks" :key="task.id || idx" class="qc-wo-task-card">
|
||||
<header class="qc-wo-task-card__head">
|
||||
<div class="qc-wo-task-card__title">
|
||||
<span v-if="tasks.length > 1" class="qc-wo-task-card__index">{{ idx + 1 }}</span>
|
||||
<span class="qc-wo-task-card__name">{{ task.itemName || '—' }}</span>
|
||||
<span v-if="task.itemCode" class="qc-wo-task-card__code">{{ task.itemCode }}</span>
|
||||
</div>
|
||||
<div class="qc-wo-task-card__tags">
|
||||
<el-tag :type="taskStatusTagType(task.taskStatus)" size="small">
|
||||
{{ taskStatusLabel(task.taskStatus || '') }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="task.conclusion === 'tg' || task.conclusion === 'btg'"
|
||||
:type="conclusionTagType(task.conclusion)"
|
||||
size="small"
|
||||
>
|
||||
{{ task.conclusionLabel || qcTaskConclusionLabel(task.conclusion) }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="info" size="small">待结论</el-tag>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="qc-wo-task-card__body">
|
||||
<dl class="qc-wo-task-card__meta">
|
||||
<div class="qc-wo-task-card__meta-item">
|
||||
<dt>任务号</dt>
|
||||
<dd>{{ task.taskNo || '—' }}</dd>
|
||||
</div>
|
||||
<div v-if="taskType === 'dsfjc'" class="qc-wo-task-card__meta-item">
|
||||
<dt>第三方公司</dt>
|
||||
<dd>{{ task.companyName || '—' }}</dd>
|
||||
</div>
|
||||
<div v-if="taskType === 'dsfjc' && task.feeAmount != null" class="qc-wo-task-card__meta-item">
|
||||
<dt>费用(元)</dt>
|
||||
<dd>¥{{ formatMoneyDisplay(task.feeAmount) }}</dd>
|
||||
</div>
|
||||
<div class="qc-wo-task-card__meta-item">
|
||||
<dt>完成时间</dt>
|
||||
<dd>{{ task.completedTime || '—' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="qc-wo-task-card__block">
|
||||
<div class="qc-wo-task-card__block-label">检测参数</div>
|
||||
<dl v-if="task.results?.length" class="qc-wo-task-card__params">
|
||||
<div v-for="(param, pi) in task.results" :key="pi" class="qc-wo-task-card__param">
|
||||
<dt>
|
||||
{{ param.paramName
|
||||
}}<span v-if="param.paramUnit" class="qc-wo-task-card__param-unit"
|
||||
>({{ param.paramUnit }})</span
|
||||
>
|
||||
</dt>
|
||||
<dd>{{ param.paramValue || '—' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p v-else class="qc-wo-task-result-panel__empty">—</p>
|
||||
</div>
|
||||
|
||||
<div v-if="task.remark" class="qc-wo-task-card__block">
|
||||
<div class="qc-wo-task-card__block-label">备注</div>
|
||||
<p class="qc-wo-task-card__remark">{{ task.remark }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="taskType === 'sysz' && showMaterialUsage" class="qc-wo-task-card__block">
|
||||
<div class="qc-wo-task-card__block-label">物料使用</div>
|
||||
<div v-if="task.materialUsages?.length" class="view-subtable-wrap">
|
||||
<el-table :data="task.materialUsages" stripe class="list-table view-subtable">
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="56"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="stockInNo"
|
||||
label="入库编号"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="materialName"
|
||||
label="物料名称"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="批号" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.batchNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="领用人" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.receiverName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用量" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.qty != null && row.qty !== '' ? row.qty : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" min-width="72" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.unit || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.remark || '—' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<p v-else class="qc-wo-task-result-panel__empty">暂无物料使用记录</p>
|
||||
</div>
|
||||
|
||||
<div class="qc-wo-task-card__block">
|
||||
<div class="qc-wo-task-card__block-label">{{ attachmentLabel }}</div>
|
||||
<ProtocolAttachmentReadonly
|
||||
v-if="task.attachmentFiles?.length"
|
||||
:files="task.attachmentFiles"
|
||||
@preview="onAttachmentPreview"
|
||||
@download="onAttachmentDownload"
|
||||
/>
|
||||
<p v-else class="qc-wo-task-result-panel__empty">—</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p v-else class="qc-wo-task-result-panel__empty">—</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import type { QcWorkOrderDetail } from '@/api/qc-work-order'
|
||||
import { taskStatusLabel, qcTaskConclusionLabel } from '@/utils/qc-labels'
|
||||
import { SHOW_QC_TASK_MATERIAL_USAGE } from '@/utils/qc-material-ui'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import type { QcWorkOrderTaskView } from './composables/useQcWorkOrderTaskViewSections'
|
||||
import { qcPreviewAttachment, qcDownloadAttachment } from './composables/useQcViewAttachmentHelpers'
|
||||
|
||||
defineOptions({ name: 'QcWorkOrderTaskResultPanel' })
|
||||
|
||||
type ConfiguredItem = {
|
||||
primary: string
|
||||
secondary?: string
|
||||
meta?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
taskType: 'sysz' | 'dsfjc'
|
||||
tasks: QcWorkOrderTaskView[]
|
||||
labItems?: QcWorkOrderDetail['labItems']
|
||||
thirdPartyItems?: QcWorkOrderDetail['thirdPartyItems']
|
||||
}>()
|
||||
|
||||
const showMaterialUsage = SHOW_QC_TASK_MATERIAL_USAGE
|
||||
|
||||
const attachmentLabel = computed(() => (props.taskType === 'dsfjc' ? '检测材料' : '检测附件'))
|
||||
|
||||
const configuredItems = computed<ConfiguredItem[]>(() => {
|
||||
if (props.taskType === 'sysz') {
|
||||
return (props.labItems || []).map((li) => ({
|
||||
primary: li.itemName || '—',
|
||||
secondary: li.itemCode ? `编码 ${li.itemCode}` : undefined
|
||||
}))
|
||||
}
|
||||
return (props.thirdPartyItems || []).map((li) => ({
|
||||
primary: li.itemName || '—',
|
||||
secondary: [li.companyName, li.itemCode ? `编码 ${li.itemCode}` : '']
|
||||
.filter(Boolean)
|
||||
.join(' · '),
|
||||
meta: li.feeAmount != null ? `¥${formatMoneyDisplay(li.feeAmount)}` : undefined
|
||||
}))
|
||||
})
|
||||
|
||||
function conclusionTagType(conclusion?: string) {
|
||||
if (conclusion === 'tg') return 'success' as const
|
||||
if (conclusion === 'btg') return 'danger' as const
|
||||
return 'info' as const
|
||||
}
|
||||
|
||||
function taskStatusTagType(status?: string) {
|
||||
if (status === 'ywc') return 'success' as const
|
||||
if (status === 'jxz') return 'warning' as const
|
||||
return 'info' as const
|
||||
}
|
||||
|
||||
const onAttachmentPreview = qcPreviewAttachment
|
||||
const onAttachmentDownload = qcDownloadAttachment
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.qc-wo-task-result-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.qc-wo-task-result-panel__hint,
|
||||
.qc-wo-task-result-panel__empty {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.qc-wo-task-result-panel__hint {
|
||||
margin-top: 10px;
|
||||
padding-left: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.qc-wo-configured-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.qc-wo-configured-item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.qc-wo-configured-item__name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.qc-wo-configured-item__sub {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.qc-wo-configured-item__meta {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.qc-wo-task-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.qc-wo-task-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qc-wo-task-card__head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px 12px;
|
||||
padding: 12px 14px;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #fff 100%);
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.qc-wo-task-card__title {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 6px 10px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.qc-wo-task-card__index {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #047857;
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.qc-wo-task-card__name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.qc-wo-task-card__code {
|
||||
font-size: 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.qc-wo-task-card__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qc-wo-task-card__body {
|
||||
padding: 12px 14px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.qc-wo-task-card__meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 10px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.qc-wo-task-card__meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
|
||||
dt {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: #1e293b;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.qc-wo-task-card__block {
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.qc-wo-task-card__block-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.qc-wo-task-card__params {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px 12px;
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.qc-wo-task-card__param {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
|
||||
dt {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
color: #1e293b;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.qc-wo-task-card__param-unit {
|
||||
font-weight: 400;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.qc-wo-task-card__remark {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #1e293b;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.view-subtable-wrap {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -353,7 +353,6 @@
|
||||
<el-table
|
||||
:data="viewOutboundDetail?.products || []"
|
||||
stripe
|
||||
size="small"
|
||||
class="list-table view-subtable"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="60" align="center" />
|
||||
@@ -516,6 +515,7 @@
|
||||
import { SHOW_QC_TASK_MATERIAL_USAGE } from '@/utils/qc-material-ui'
|
||||
import {
|
||||
taskTypeLabel,
|
||||
taskTypeTagType,
|
||||
taskStatusLabel,
|
||||
qcBizTypeLabel,
|
||||
qcBizTypeTagType,
|
||||
@@ -755,10 +755,6 @@
|
||||
return 'info' as const
|
||||
}
|
||||
|
||||
function taskTypeTagType(type: string) {
|
||||
return type === 'sysz' ? ('success' as const) : ('info' as const)
|
||||
}
|
||||
|
||||
function conclusionTagType(conclusion: string) {
|
||||
if (conclusion === 'tg') return 'success' as const
|
||||
if (conclusion === 'btg') return 'danger' as const
|
||||
@@ -1128,7 +1124,7 @@
|
||||
{
|
||||
label: '检测类型',
|
||||
value: taskTypeLabel(d.taskType),
|
||||
tagType: d.taskType === 'sysz' ? 'success' : 'info'
|
||||
tagType: taskTypeTagType(d.taskType)
|
||||
},
|
||||
{ label: '检测项目', value: d.itemName },
|
||||
{ label: '项目编码', value: d.itemCode || '—' },
|
||||
|
||||
-11
@@ -3,9 +3,6 @@ import { fetchQcWorkOrderDetail, type QcWorkOrderDetail } from '@/api/qc-work-or
|
||||
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import {
|
||||
loadEnrichedQcWorkOrderTasks,
|
||||
useQcWorkOrderTaskSlotHelpers,
|
||||
taskAttachmentSlotName,
|
||||
taskMaterialSlotName,
|
||||
type QcWorkOrderTaskView
|
||||
} from './useQcWorkOrderTaskViewSections'
|
||||
import {
|
||||
@@ -26,8 +23,6 @@ export function useInboundQcViewDialog(
|
||||
return buildSections(detail.value, viewTasks.value)
|
||||
})
|
||||
|
||||
const { viewTasksWithAttachments, viewLabTasks } = useQcWorkOrderTaskSlotHelpers(viewTasks)
|
||||
|
||||
const viewReceptionAttachmentFiles = computed(() => {
|
||||
const r = detail.value?.reception
|
||||
if (!r) return []
|
||||
@@ -55,15 +50,9 @@ export function useInboundQcViewDialog(
|
||||
detail,
|
||||
sections,
|
||||
viewTasks,
|
||||
viewTasksWithAttachments,
|
||||
viewLabTasks,
|
||||
viewReceptionAttachmentFiles,
|
||||
taskAttachmentSlotName,
|
||||
taskMaterialSlotName,
|
||||
handleReceptionAttachmentPreview: qcPreviewAttachment,
|
||||
handleReceptionAttachmentDownload: qcDownloadAttachment,
|
||||
handleTaskAttachmentPreview: qcPreviewAttachment,
|
||||
handleTaskAttachmentDownload: qcDownloadAttachment,
|
||||
load
|
||||
}
|
||||
}
|
||||
+3
-14
@@ -14,11 +14,8 @@ import {
|
||||
} from '@/utils/outbound-qc-pilot-view-sections'
|
||||
import type { ViewField, ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import {
|
||||
buildTaskTypeSections,
|
||||
appendTaskResultSections,
|
||||
loadEnrichedQcWorkOrderTasks,
|
||||
useQcWorkOrderTaskSlotHelpers,
|
||||
taskAttachmentSlotName,
|
||||
taskMaterialSlotName,
|
||||
type QcWorkOrderTaskView
|
||||
} from './useQcWorkOrderTaskViewSections'
|
||||
import {
|
||||
@@ -35,8 +32,6 @@ export function useOutboundQcViewDialog(options: {
|
||||
const detail = ref<OutboundQcDetail | null>(null)
|
||||
const viewTasks = ref<QcWorkOrderTaskView[]>([])
|
||||
|
||||
const { viewTasksWithAttachments, viewLabTasks } = useQcWorkOrderTaskSlotHelpers(viewTasks)
|
||||
|
||||
const diagnosisAttachmentFiles = computed(() => {
|
||||
const urls = detail.value?.patient?.diagnosisAttachments || ''
|
||||
if (!urls.trim()) return []
|
||||
@@ -131,8 +126,7 @@ export function useOutboundQcViewDialog(options: {
|
||||
})
|
||||
}
|
||||
|
||||
result.push(...buildTaskTypeSections(viewTasks.value, 'sysz', '实验室项目'))
|
||||
result.push(...buildTaskTypeSections(viewTasks.value, 'dsfjc', '第三方项目'))
|
||||
result.push(...appendTaskResultSections(wo, viewTasks.value))
|
||||
return result
|
||||
})
|
||||
|
||||
@@ -155,15 +149,10 @@ export function useOutboundQcViewDialog(options: {
|
||||
loading,
|
||||
detail,
|
||||
sections,
|
||||
viewTasksWithAttachments,
|
||||
viewLabTasks,
|
||||
viewTasks,
|
||||
diagnosisAttachmentFiles,
|
||||
taskAttachmentSlotName,
|
||||
taskMaterialSlotName,
|
||||
handlePreview: qcPreviewAttachment,
|
||||
handleDownload: qcDownloadAttachment,
|
||||
handleTaskPreview: qcPreviewAttachment,
|
||||
handleTaskDownload: qcDownloadAttachment,
|
||||
load
|
||||
}
|
||||
}
|
||||
+23
-155
@@ -1,5 +1,4 @@
|
||||
import { computed, type Ref } from 'vue'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import type { QcWorkOrderDetail } from '@/api/qc-work-order'
|
||||
import type { QcTask } from '@/api/qc-task'
|
||||
import {
|
||||
@@ -9,8 +8,7 @@ import {
|
||||
type QcTaskMaterialUsageItem
|
||||
} from '@/api/qc-task-result'
|
||||
import { SHOW_QC_TASK_MATERIAL_USAGE } from '@/utils/qc-material-ui'
|
||||
import { taskStatusLabel, qcTaskConclusionLabel } from '@/utils/qc-labels'
|
||||
import type { ViewField, ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
|
||||
export type ViewMaterialUsageRow = {
|
||||
stockInNo: string
|
||||
@@ -35,6 +33,14 @@ export function taskMaterialSlotName(taskId: string) {
|
||||
return `task-mat-${taskId}`
|
||||
}
|
||||
|
||||
export const LAB_TASKS_PANEL_SLOT = 'lab-tasks-panel'
|
||||
export const THIRD_TASKS_PANEL_SLOT = 'third-tasks-panel'
|
||||
|
||||
export type QcTaskPanelContext = Pick<
|
||||
QcWorkOrderDetail,
|
||||
'hasLab' | 'hasThirdParty' | 'labItems' | 'thirdPartyItems'
|
||||
>
|
||||
|
||||
function materialRowsFromApi(items: QcTaskMaterialUsageItem[]): ViewMaterialUsageRow[] {
|
||||
return (items || [])
|
||||
.filter((u) => (u.requisitionUsageId || u.parentUsageId || '').trim())
|
||||
@@ -73,174 +79,36 @@ export async function loadEnrichedQcWorkOrderTasks(
|
||||
)
|
||||
}
|
||||
|
||||
function conclusionTagType(conclusion?: string) {
|
||||
if (conclusion === 'tg') return 'success' as const
|
||||
if (conclusion === 'btg') return 'danger' as const
|
||||
return 'info' as const
|
||||
}
|
||||
|
||||
function taskStatusTagType(status?: string) {
|
||||
if (status === 'ywc') return 'success' as const
|
||||
if (status === 'jxz') return 'warning' as const
|
||||
return 'info' as const
|
||||
}
|
||||
|
||||
function formatConfiguredLabItems(d: QcWorkOrderDetail) {
|
||||
return d.labItems?.map((li) => `${li.itemName || ''}(${li.itemCode || ''})`).join('、') || '无'
|
||||
}
|
||||
|
||||
function formatConfiguredThirdItems(d: QcWorkOrderDetail) {
|
||||
return (
|
||||
d.thirdPartyItems
|
||||
?.map(
|
||||
(li) =>
|
||||
`${li.companyName || ''} — ${li.itemName || ''}(${li.feeAmount != null ? '¥' + formatMoneyDisplay(li.feeAmount) : '—'})`
|
||||
)
|
||||
.join('、') || '无'
|
||||
)
|
||||
}
|
||||
|
||||
function buildConfiguredOnlySection(
|
||||
title: string,
|
||||
configuredLabel: string,
|
||||
configuredValue: string,
|
||||
attachmentLabel: string
|
||||
slot: string
|
||||
): ViewSection {
|
||||
return {
|
||||
title,
|
||||
fields: [
|
||||
{ label: configuredLabel, value: configuredValue, span: true },
|
||||
{ label: '检测结论', value: '—' },
|
||||
{ label: '检测参数', value: '—' },
|
||||
{ label: '备注', value: '—' },
|
||||
{ label: attachmentLabel, value: '—' }
|
||||
]
|
||||
fields: [],
|
||||
slot
|
||||
}
|
||||
}
|
||||
|
||||
export function buildTaskTypeSections(
|
||||
tasks: QcWorkOrderTaskView[],
|
||||
taskType: 'sysz' | 'dsfjc',
|
||||
sectionTitle: string
|
||||
): ViewSection[] {
|
||||
const filtered = tasks.filter((t) => t.taskType === taskType)
|
||||
if (!filtered.length) return []
|
||||
|
||||
return filtered.flatMap((task, index) => {
|
||||
const title = filtered.length > 1 ? `${sectionTitle}(${index + 1})` : sectionTitle
|
||||
const taskInfoFields: ViewField[] = [
|
||||
{ label: '检测项目', value: task.itemName || '—' },
|
||||
{ label: '项目编码', value: task.itemCode || '—' },
|
||||
{
|
||||
label: '任务状态',
|
||||
value: taskStatusLabel(task.taskStatus || ''),
|
||||
tagType: taskStatusTagType(task.taskStatus)
|
||||
},
|
||||
{
|
||||
label: '检测结论',
|
||||
value: task.conclusionLabel || qcTaskConclusionLabel(task.conclusion || '') || '—',
|
||||
tagType: conclusionTagType(task.conclusion)
|
||||
},
|
||||
{ label: '任务号', value: task.taskNo || '—' },
|
||||
{ label: '完成时间', value: task.completedTime || '—' }
|
||||
]
|
||||
if (taskType === 'dsfjc') {
|
||||
taskInfoFields.splice(2, 0, { label: '第三方公司', value: task.companyName || '—' })
|
||||
if (task.feeAmount != null) {
|
||||
taskInfoFields.push({ label: '费用(元)', value: String(task.feeAmount) })
|
||||
}
|
||||
}
|
||||
const paramFields: ViewField[] = (task.results || []).map((item) => ({
|
||||
label: item.paramName + (item.paramUnit ? `(${item.paramUnit})` : ''),
|
||||
value: item.paramValue || '—'
|
||||
}))
|
||||
const remarkField: ViewField = { label: '备注', value: task.remark || '—', span: true }
|
||||
|
||||
const section: ViewSection =
|
||||
taskType === 'sysz'
|
||||
? {
|
||||
title,
|
||||
fields: [],
|
||||
fieldGroups: [
|
||||
{ fields: taskInfoFields },
|
||||
{
|
||||
title: '检测参数',
|
||||
titleStyle: 'field',
|
||||
fields: paramFields,
|
||||
emptyHint: '—'
|
||||
},
|
||||
{ fields: [remarkField] }
|
||||
]
|
||||
}
|
||||
: {
|
||||
title,
|
||||
fields: [
|
||||
...taskInfoFields,
|
||||
...(paramFields.length ? paramFields : [{ label: '检测参数', value: '—' as const }]),
|
||||
remarkField
|
||||
]
|
||||
}
|
||||
if (task.attachmentFiles?.length) {
|
||||
section.slot = taskAttachmentSlotName(task.id)
|
||||
section.slotTitle = taskType === 'dsfjc' ? '检测材料' : '检测附件'
|
||||
section.slotTitleStyle = 'field'
|
||||
} else if (taskType === 'dsfjc') {
|
||||
section.fields!.push({ label: '检测材料', value: '—' })
|
||||
} else if (taskType === 'sysz') {
|
||||
section.fieldGroups?.push({ fields: [{ label: '检测附件', value: '—' }] })
|
||||
} else {
|
||||
section.fields!.push({ label: '检测附件', value: '—' })
|
||||
}
|
||||
const sections: ViewSection[] = [section]
|
||||
if (SHOW_QC_TASK_MATERIAL_USAGE && taskType === 'sysz') {
|
||||
sections.push({
|
||||
title: '',
|
||||
fields: [],
|
||||
slotTitle: '物料使用',
|
||||
slotTitleStyle: 'field',
|
||||
slot: taskMaterialSlotName(task.id)
|
||||
})
|
||||
}
|
||||
return sections
|
||||
})
|
||||
}
|
||||
|
||||
export function appendTaskResultSections(
|
||||
detail: QcWorkOrderDetail,
|
||||
detail: QcTaskPanelContext,
|
||||
tasks: QcWorkOrderTaskView[]
|
||||
): ViewSection[] {
|
||||
const result: ViewSection[] = []
|
||||
if (tasks.some((t) => t.taskType === 'sysz')) {
|
||||
result.push(...buildTaskTypeSections(tasks, 'sysz', '实验室项目'))
|
||||
} else if (detail.hasLab) {
|
||||
result.push(
|
||||
buildConfiguredOnlySection(
|
||||
'实验室项目',
|
||||
'已配置项目',
|
||||
formatConfiguredLabItems(detail),
|
||||
'检测附件'
|
||||
)
|
||||
)
|
||||
const hasLabTasks = tasks.some((t) => t.taskType === 'sysz')
|
||||
const hasThirdTasks = tasks.some((t) => t.taskType === 'dsfjc')
|
||||
|
||||
if (hasLabTasks || detail.hasLab) {
|
||||
result.push(buildConfiguredOnlySection('实验室项目', LAB_TASKS_PANEL_SLOT))
|
||||
}
|
||||
if (tasks.some((t) => t.taskType === 'dsfjc')) {
|
||||
result.push(...buildTaskTypeSections(tasks, 'dsfjc', '第三方项目'))
|
||||
} else if (detail.hasThirdParty) {
|
||||
result.push(
|
||||
buildConfiguredOnlySection(
|
||||
'第三方项目',
|
||||
'已配置项目',
|
||||
formatConfiguredThirdItems(detail),
|
||||
'检测材料'
|
||||
)
|
||||
)
|
||||
if (hasThirdTasks || detail.hasThirdParty) {
|
||||
result.push(buildConfiguredOnlySection('第三方项目', THIRD_TASKS_PANEL_SLOT))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function useQcWorkOrderTaskSlotHelpers(viewTasks: Ref<QcWorkOrderTaskView[]>) {
|
||||
const viewTasksWithAttachments = computed(() =>
|
||||
viewTasks.value.filter((t) => (t.attachmentFiles?.length || 0) > 0)
|
||||
)
|
||||
const viewLabTasks = computed(() => viewTasks.value.filter((t) => t.taskType === 'sysz'))
|
||||
return { viewTasksWithAttachments, viewLabTasks }
|
||||
const labTasks = computed(() => viewTasks.value.filter((t) => t.taskType === 'sysz'))
|
||||
const thirdPartyTasks = computed(() => viewTasks.value.filter((t) => t.taskType === 'dsfjc'))
|
||||
return { labTasks, thirdPartyTasks }
|
||||
}
|
||||
@@ -20,10 +20,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<component
|
||||
:is="activeTabComponent"
|
||||
<QcWorkOrderTabPanel
|
||||
ref="activeTabRef"
|
||||
:key="activeTabKey"
|
||||
:tab-key="activeTabKey"
|
||||
page-mode="manage"
|
||||
@configure="handleConfigure"
|
||||
@command="handleDropdownCommand"
|
||||
@@ -305,7 +305,7 @@
|
||||
import { fetchCellProductList } from '@/api/cell-product'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import WorkOrderRemarkDialog from '@/components/business/work-order-remark-dialog/index.vue'
|
||||
import { getQcWorkOrderTabComponent } from './tabs/tab-registry'
|
||||
import QcWorkOrderTabPanel from './tabs/QcWorkOrderTabPanel.vue'
|
||||
import {
|
||||
QC_WORK_ORDER_VISIBLE_TABS,
|
||||
DEFAULT_QC_WORK_ORDER_TAB,
|
||||
@@ -327,7 +327,6 @@
|
||||
const activeTabRef = ref<QcWorkOrderTabPanelExpose | null>(null)
|
||||
|
||||
const activeTabKey = ref<QcWorkOrderTabKey>(parseQcWorkOrderTabKey(route.query.tab as string))
|
||||
const activeTabComponent = computed(() => getQcWorkOrderTabComponent(activeTabKey.value))
|
||||
|
||||
async function refreshActiveTab() {
|
||||
activeTabRef.value?.bumpTaskRefresh()
|
||||
|
||||
+3
-4
@@ -12,8 +12,8 @@
|
||||
:meta="detail?.workOrderNo || ''"
|
||||
:sections="sections"
|
||||
modal-class="qc-work-order-view-dialog-mask"
|
||||
:tasks-with-attachments="viewTasksWithAttachments"
|
||||
:lab-tasks="viewLabTasks"
|
||||
:detail="detail"
|
||||
:view-tasks="viewTasks"
|
||||
>
|
||||
<template #protocol-payment-details>
|
||||
<QcWorkOrderProtocolPaymentPanel :summary="detail?.paymentSummary" embedded />
|
||||
@@ -64,8 +64,7 @@
|
||||
loading,
|
||||
detail,
|
||||
sections,
|
||||
viewTasksWithAttachments,
|
||||
viewLabTasks,
|
||||
viewTasks,
|
||||
viewReceptionAttachmentFiles,
|
||||
handleReceptionAttachmentPreview,
|
||||
handleReceptionAttachmentDownload,
|
||||
|
||||
+3
-4
@@ -12,8 +12,8 @@
|
||||
:meta="detail?.workOrderNo || ''"
|
||||
:sections="sections"
|
||||
modal-class="qc-work-order-view-dialog-mask"
|
||||
:tasks-with-attachments="viewTasksWithAttachments"
|
||||
:lab-tasks="viewLabTasks"
|
||||
:detail="detail"
|
||||
:view-tasks="viewTasks"
|
||||
>
|
||||
<template #reception-extra>
|
||||
<div class="view-reception-attachments">
|
||||
@@ -60,8 +60,7 @@
|
||||
loading,
|
||||
detail,
|
||||
sections,
|
||||
viewTasksWithAttachments,
|
||||
viewLabTasks,
|
||||
viewTasks,
|
||||
viewReceptionAttachmentFiles,
|
||||
handleReceptionAttachmentPreview,
|
||||
handleReceptionAttachmentDownload,
|
||||
|
||||
+3
-3
@@ -11,8 +11,8 @@
|
||||
:icon="Document"
|
||||
:meta="detail?.workOrder?.workOrderNo || ''"
|
||||
:sections="sections"
|
||||
:tasks-with-attachments="viewTasksWithAttachments"
|
||||
:lab-tasks="viewLabTasks"
|
||||
:detail="detail?.workOrder"
|
||||
:view-tasks="viewTasks"
|
||||
>
|
||||
<template #product-table>
|
||||
<OutboundQcProductTable :products="detail?.products" />
|
||||
@@ -42,7 +42,7 @@
|
||||
set: (v) => emit('update:visible', v)
|
||||
})
|
||||
|
||||
const { Document, loading, detail, sections, viewTasksWithAttachments, viewLabTasks, load } =
|
||||
const { Document, loading, detail, sections, viewTasks, load } =
|
||||
useOutboundQcViewDialog({ variant: 'basic_experiment', showFeeSection: true })
|
||||
|
||||
watch(
|
||||
|
||||
+3
-4
@@ -11,8 +11,8 @@
|
||||
:icon="Document"
|
||||
:meta="detail?.workOrder?.workOrderNo || ''"
|
||||
:sections="sections"
|
||||
:tasks-with-attachments="viewTasksWithAttachments"
|
||||
:lab-tasks="viewLabTasks"
|
||||
:detail="detail?.workOrder"
|
||||
:view-tasks="viewTasks"
|
||||
>
|
||||
<template #diagnosis-attachments>
|
||||
<OutboundQcDiagnosisAttachments :files="diagnosisAttachmentFiles" />
|
||||
@@ -51,8 +51,7 @@
|
||||
loading,
|
||||
detail,
|
||||
sections,
|
||||
viewTasksWithAttachments,
|
||||
viewLabTasks,
|
||||
viewTasks,
|
||||
diagnosisAttachmentFiles,
|
||||
load
|
||||
} = useOutboundQcViewDialog({ variant: 'clinical_application', showFeeSection: true })
|
||||
|
||||
+3
-4
@@ -11,8 +11,8 @@
|
||||
:icon="Document"
|
||||
:meta="detail?.workOrder?.workOrderNo || ''"
|
||||
:sections="sections"
|
||||
:tasks-with-attachments="viewTasksWithAttachments"
|
||||
:lab-tasks="viewLabTasks"
|
||||
:detail="detail?.workOrder"
|
||||
:view-tasks="viewTasks"
|
||||
>
|
||||
<template #diagnosis-attachments>
|
||||
<OutboundQcDiagnosisAttachments :files="diagnosisAttachmentFiles" />
|
||||
@@ -51,8 +51,7 @@
|
||||
loading,
|
||||
detail,
|
||||
sections,
|
||||
viewTasksWithAttachments,
|
||||
viewLabTasks,
|
||||
viewTasks,
|
||||
diagnosisAttachmentFiles,
|
||||
load
|
||||
} = useOutboundQcViewDialog({ variant: 'pilot_service', showFeeSection: false })
|
||||
|
||||
+1
-28
@@ -1,11 +1,6 @@
|
||||
<template>
|
||||
<div v-if="products?.length" class="view-subtable-wrap">
|
||||
<el-table
|
||||
:data="products"
|
||||
stripe
|
||||
size="small"
|
||||
class="list-table view-subtable outbound-product-table"
|
||||
>
|
||||
<el-table :data="products" stripe class="list-table view-subtable">
|
||||
<el-table-column type="index" label="序号" min-width="60" align="center" />
|
||||
<el-table-column
|
||||
prop="projectCode"
|
||||
@@ -60,28 +55,6 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.outbound-product-table {
|
||||
margin-top: 0;
|
||||
|
||||
:deep(.el-table__header th.el-table__cell),
|
||||
:deep(.el-table__body td.el-table__cell) {
|
||||
font-size: 12px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
:deep(.el-table__header th.el-table__cell .cell),
|
||||
:deep(.el-table__body td.el-table__cell .cell) {
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
:deep(.el-tag) {
|
||||
--el-tag-font-size: 11px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.view-subtable-wrap {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
|
||||
+30
-86
@@ -1,93 +1,44 @@
|
||||
<!-- SystemViewDialog 包装:转发业务 slot + 内置任务附件/物料 slot -->
|
||||
<!-- SystemViewDialog 包装:任务结果面板 + 业务 slot 转发 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<SystemViewDialog v-model:visible="dialogVisible" v-bind="attrs">
|
||||
<template v-for="name in extraSlotNames" #[name]>
|
||||
<slot :name="name" />
|
||||
</template>
|
||||
<template
|
||||
v-for="task in tasksWithAttachments"
|
||||
:key="task.id"
|
||||
#[taskAttachmentSlotName(task.id)]
|
||||
>
|
||||
<div class="qc-work-order-task-attachments">
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="task.attachmentFiles || []"
|
||||
@preview="onTaskPreview"
|
||||
@download="onTaskDownload"
|
||||
/>
|
||||
</div>
|
||||
<template #[LAB_TASKS_PANEL_SLOT]>
|
||||
<QcWorkOrderTaskResultPanel
|
||||
task-type="sysz"
|
||||
:tasks="labTasks"
|
||||
:lab-items="detail?.labItems"
|
||||
/>
|
||||
</template>
|
||||
<template v-for="task in materialLabTasks" :key="task.id" #[taskMaterialSlotName(task.id)]>
|
||||
<div v-if="task.materialUsages?.length" class="view-subtable-wrap">
|
||||
<el-table :data="task.materialUsages" stripe class="list-table view-subtable">
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="56"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="stockInNo"
|
||||
label="入库编号"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="materialName"
|
||||
label="物料名称"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="批号" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.batchNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="领用人" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.receiverName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用量" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.qty != null && row.qty !== '' ? row.qty : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" min-width="72" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.unit || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.remark || '—' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<p v-else class="view-empty-hint">暂无物料使用记录</p>
|
||||
<template #[THIRD_TASKS_PANEL_SLOT]>
|
||||
<QcWorkOrderTaskResultPanel
|
||||
task-type="dsfjc"
|
||||
:tasks="thirdPartyTasks"
|
||||
:third-party-items="detail?.thirdPartyItems"
|
||||
/>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useAttrs, useSlots } from 'vue'
|
||||
import type { QcWorkOrderDetail } from '@/api/qc-work-order'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import { SHOW_QC_TASK_MATERIAL_USAGE } from '@/utils/qc-material-ui'
|
||||
import QcWorkOrderTaskResultPanel from '../QcWorkOrderTaskResultPanel.vue'
|
||||
import {
|
||||
taskAttachmentSlotName,
|
||||
taskMaterialSlotName,
|
||||
LAB_TASKS_PANEL_SLOT,
|
||||
THIRD_TASKS_PANEL_SLOT,
|
||||
type QcWorkOrderTaskView
|
||||
} from '../composables/useQcWorkOrderTaskViewSections'
|
||||
import {
|
||||
qcPreviewAttachment,
|
||||
qcDownloadAttachment
|
||||
} from '../composables/useQcViewAttachmentHelpers'
|
||||
|
||||
defineOptions({ name: 'QcWorkOrderSystemViewDialog', inheritAttrs: false })
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
tasksWithAttachments: QcWorkOrderTaskView[]
|
||||
labTasks: QcWorkOrderTaskView[]
|
||||
detail?: Partial<QcWorkOrderDetail> | null
|
||||
viewTasks?: QcWorkOrderTaskView[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -96,28 +47,21 @@
|
||||
|
||||
const attrs = useAttrs()
|
||||
const slots = useSlots()
|
||||
const extraSlotNames = computed(() => Object.keys(slots))
|
||||
const extraSlotNames = computed(() =>
|
||||
Object.keys(slots).filter(
|
||||
(name) => name !== LAB_TASKS_PANEL_SLOT && name !== THIRD_TASKS_PANEL_SLOT
|
||||
)
|
||||
)
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (v) => emit('update:visible', v)
|
||||
})
|
||||
|
||||
const materialLabTasks = computed(() => (SHOW_QC_TASK_MATERIAL_USAGE ? props.labTasks : []))
|
||||
|
||||
const onTaskPreview = qcPreviewAttachment
|
||||
const onTaskDownload = qcDownloadAttachment
|
||||
const labTasks = computed(() =>
|
||||
(props.viewTasks || []).filter((t) => t.taskType === 'sysz')
|
||||
)
|
||||
const thirdPartyTasks = computed(() =>
|
||||
(props.viewTasks || []).filter((t) => t.taskType === 'dsfjc')
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.view-subtable-wrap {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.view-empty-hint {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
</style>
|
||||
+3
-3
@@ -12,8 +12,8 @@
|
||||
:meta="detail?.workOrderNo || ''"
|
||||
:sections="sections"
|
||||
modal-class="qc-work-order-view-dialog-mask"
|
||||
:tasks-with-attachments="viewTasksWithAttachments"
|
||||
:lab-tasks="viewLabTasks"
|
||||
:detail="detail"
|
||||
:view-tasks="viewTasks"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
set: (v) => emit('update:visible', v)
|
||||
})
|
||||
|
||||
const { loading, detail, sections, viewTasksWithAttachments, viewLabTasks, load } =
|
||||
const { loading, detail, sections, viewTasks, load } =
|
||||
useInboundQcViewDialog(buildTemporaryViewSections)
|
||||
|
||||
watch(
|
||||
|
||||
@@ -116,220 +116,176 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@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="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="donationStatus"
|
||||
label="状态"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="donationStatusTagType(String(row.donationStatus || ''))"
|
||||
size="small"
|
||||
>{{ donationStatusLabel(String(row.donationStatus || '')) }}</el-tag
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="expectedDeliveryDate"
|
||||
label="预产期"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="sampleCollectSummary"
|
||||
label="是否已采集"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="donationDate"
|
||||
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="motherPhone"
|
||||
label="母亲电话"
|
||||
min-width="130"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="motherIdCard"
|
||||
label="母亲身份证号"
|
||||
min-width="190"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="fatherName"
|
||||
label="父亲姓名"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="newbornSex"
|
||||
label="新生儿性别"
|
||||
min-width="110"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{
|
||||
newbornSexLabel(String(row.newbornSex || ''))
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="deliveryMode"
|
||||
label="分娩方式"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{
|
||||
deliveryModeLabel(String(row.deliveryMode || ''))
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="newbornWeightGram"
|
||||
label="新生儿体重(克)"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="sampleTypeNames"
|
||||
label="样本类型"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="sampleCodes"
|
||||
label="样本编码"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="labStorageCodes"
|
||||
label="实验室入库编码"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.labStorageCodes || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="regionName"
|
||||
label="采集地区"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="hospitalName"
|
||||
label="采集医院"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="collectorUserName"
|
||||
label="采集专员"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="salesUserName"
|
||||
label="销售人员"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="sampleCollectTime"
|
||||
label="样本采集时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="160"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item" @click="openAuditDialog(row)">
|
||||
<el-icon><CircleCheck /></el-icon>审核
|
||||
</el-button>
|
||||
<el-dropdown trigger="hover" @command="(cmd: string) => handleDropdownCommand(cmd, row)">
|
||||
<el-button class="btn-op-item">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="view">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<div v-loading="loading" class="protocol-card-section">
|
||||
<div v-if="tableData.length" class="protocol-card-list">
|
||||
<article
|
||||
v-for="(row, index) in tableData"
|
||||
:key="String(row.id)"
|
||||
class="protocol-card"
|
||||
:class="{ 'is-selected': isRowSelected(String(row.id)) }"
|
||||
@dblclick="handleView(row)"
|
||||
>
|
||||
<div class="protocol-card__select" @click.stop>
|
||||
<el-checkbox
|
||||
:model-value="isRowSelected(String(row.id))"
|
||||
@change="(val) => toggleRowSelection(row, !!val)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="protocol-card__aside">
|
||||
<div class="protocol-card__name-row">
|
||||
<span class="protocol-card__index">{{
|
||||
(pagination.page - 1) * pagination.pageSize + index + 1
|
||||
}}</span>
|
||||
<span class="protocol-card__name">{{ row.motherName || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__contact">
|
||||
<div class="protocol-card__sub">{{ row.motherPhone || '—' }}</div>
|
||||
<div class="protocol-card__sub protocol-card__sub--id">
|
||||
{{ row.motherIdCard ? maskIdCard(String(row.motherIdCard)) : '—' }}
|
||||
</div>
|
||||
</div>
|
||||
<el-tag
|
||||
:type="donationStatusTagType(String(row.donationStatus || ''))"
|
||||
size="small"
|
||||
class="protocol-card__status"
|
||||
>{{ donationStatusLabel(String(row.donationStatus || '')) }}</el-tag
|
||||
>
|
||||
<div v-if="row.createTime" class="protocol-card__create-time">
|
||||
创建 {{ row.createTime }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="protocol-card__sections">
|
||||
<section class="protocol-card__block protocol-card__block--basic">
|
||||
<div class="protocol-card__block-title">基础信息</div>
|
||||
<div class="protocol-card__kv-grid">
|
||||
<div class="protocol-card__kv">
|
||||
<label>捐赠单号</label><span>{{ row.donationNo || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>捐赠日期</label><span>{{ row.donationDate || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>采集地区</label><span>{{ row.regionName || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>采集医院</label
|
||||
><span :title="String(row.hospitalName || '')">{{
|
||||
row.hospitalName || '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>采集专员</label><span>{{ row.collectorUserName || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>销售人员</label><span>{{ row.salesUserName || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>样本采集</label><span>{{ row.sampleCollectSummary || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>采集时间</label><span>{{ row.sampleCollectTime || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="protocol-card__block protocol-card__block--newborn">
|
||||
<div class="protocol-card__block-title">新生儿</div>
|
||||
<div class="protocol-card__kv-grid">
|
||||
<div class="protocol-card__kv">
|
||||
<label>姓名</label><span>{{ row.newbornName || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>父亲</label><span>{{ row.fatherName || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>性别</label
|
||||
><span>{{ newbornSexLabel(String(row.newbornSex || '')) || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>预产期</label><span>{{ row.expectedDeliveryDate || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>分娩时间</label><span>{{ row.newbornDeliveryTime || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>分娩方式</label
|
||||
><span>{{ deliveryModeLabel(String(row.deliveryMode || '')) || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>体重(克)</label
|
||||
><span>{{
|
||||
row.newbornWeightGram != null && row.newbornWeightGram !== ''
|
||||
? row.newbornWeightGram
|
||||
: '—'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="protocol-card__block protocol-card__block--samples">
|
||||
<div class="protocol-card__block-title">
|
||||
样本信息
|
||||
<span v-if="getSampleItems(row).length" class="protocol-card__block-count">
|
||||
{{ getSampleItems(row).length }} 项
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="getSampleItems(row).length" class="protocol-card__sample-list">
|
||||
<div
|
||||
v-for="(item, sIdx) in getSampleItems(row)"
|
||||
:key="sIdx"
|
||||
class="protocol-card__sample-item"
|
||||
>
|
||||
<span class="protocol-card__sample-seq">{{ sIdx + 1 }}</span>
|
||||
<div class="protocol-card__sample-main">
|
||||
<div class="protocol-card__sample-line">
|
||||
<span class="protocol-card__sample-label">类型</span>
|
||||
<span class="protocol-card__sample-value">{{
|
||||
item.sampleTypeName || '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="protocol-card__sample-line">
|
||||
<span class="protocol-card__sample-label">编码</span>
|
||||
<span class="protocol-card__sample-value protocol-card__sample-value--code">
|
||||
{{ item.sampleCode || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="protocol-card__sample-line">
|
||||
<span class="protocol-card__sample-label">入库</span>
|
||||
<span class="protocol-card__sample-value protocol-card__sample-value--code">
|
||||
{{ item.labStorageCode || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="protocol-card__empty">暂无样本</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="protocol-card__foot">
|
||||
<div class="protocol-card__actions">
|
||||
<el-button class="btn-op-item" @click.stop="openAuditDialog(row)">
|
||||
<el-icon><CircleCheck /></el-icon>审核
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click.stop="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click.stop="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<el-empty
|
||||
v-else-if="!loading"
|
||||
class="protocol-card-empty"
|
||||
description="暂无捐赠协议单"
|
||||
:image-size="80"
|
||||
/>
|
||||
</div>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
@@ -386,8 +342,7 @@
|
||||
Delete,
|
||||
Search,
|
||||
RefreshLeft,
|
||||
CircleCheck,
|
||||
More
|
||||
CircleCheck
|
||||
} from '@element-plus/icons-vue'
|
||||
import DonationFormDialog from '../list/DonationFormDialog.vue'
|
||||
import DonationViewDialog from '../list/DonationViewDialog.vue'
|
||||
@@ -412,9 +367,37 @@
|
||||
formatProtocolDocDeleteConfirm,
|
||||
formatProtocolDocBatchDeleteConfirm
|
||||
} from '@/utils/protocol-doc-label'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
|
||||
defineOptions({ name: 'ResearchDonationAudit' })
|
||||
|
||||
type DonationSampleItem = {
|
||||
sampleTypeName: string
|
||||
sampleCode: string
|
||||
labStorageCode: string
|
||||
}
|
||||
|
||||
function getSampleItems(row: ResearchDonationRow): DonationSampleItem[] {
|
||||
const types = String(row.sampleTypeNames || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
const codes = String(row.sampleCodes || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
const labCodes = String(row.labStorageCodes || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
const len = Math.max(types.length, codes.length)
|
||||
if (!len) return []
|
||||
return Array.from({ length: len }, (_, i) => ({
|
||||
sampleTypeName: types[i] || '',
|
||||
sampleCode: codes[i] || '',
|
||||
labStorageCode: labCodes[i] || ''
|
||||
}))
|
||||
}
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
@@ -441,8 +424,15 @@
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
|
||||
const handleSelectionChange = (rows: ResearchDonationRow[]) => {
|
||||
selectedRows.value = rows
|
||||
const isRowSelected = (id: string) => selectedRows.value.some((r) => String(r.id) === id)
|
||||
|
||||
const toggleRowSelection = (row: ResearchDonationRow, checked: boolean) => {
|
||||
const id = String(row.id)
|
||||
if (checked) {
|
||||
if (!isRowSelected(id)) selectedRows.value = [...selectedRows.value, row]
|
||||
return
|
||||
}
|
||||
selectedRows.value = selectedRows.value.filter((r) => String(r.id) !== id)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
@@ -460,6 +450,8 @@
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
const validIds = new Set(tableData.value.map((r) => String(r.id)))
|
||||
selectedRows.value = selectedRows.value.filter((r) => validIds.has(String(r.id)))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -502,7 +494,7 @@
|
||||
}
|
||||
|
||||
function handleEdit(row: ResearchDonationRow) {
|
||||
formDialogEditId.value = row.id
|
||||
formDialogEditId.value = String(row.id)
|
||||
formDialogRow.value = row
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
@@ -523,7 +515,7 @@
|
||||
}
|
||||
auditTargets.value = rows.map((r) => ({
|
||||
id: String(r.id),
|
||||
motherName: r.motherName
|
||||
motherName: r.motherName != null ? String(r.motherName) : undefined
|
||||
}))
|
||||
auditDialogVisible.value = true
|
||||
}
|
||||
@@ -547,7 +539,7 @@
|
||||
statusChangeAction.value = action
|
||||
statusChangeTargets.value = rows.map((r) => ({
|
||||
id: String(r.id),
|
||||
motherName: r.motherName
|
||||
motherName: r.motherName != null ? String(r.motherName) : undefined
|
||||
}))
|
||||
statusChangeDialogVisible.value = true
|
||||
}
|
||||
@@ -557,14 +549,6 @@
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleDropdownCommand(command: string, row: any) {
|
||||
switch (command) {
|
||||
case 'view': handleView(row); break
|
||||
case 'edit': handleEdit(row); break
|
||||
case 'delete': handleDelete(row); break
|
||||
}
|
||||
}
|
||||
|
||||
/* ====== 删除 ====== */
|
||||
function handleDelete(row: ResearchDonationRow) {
|
||||
ElMessageBox.confirm(formatProtocolDocDeleteConfirm('research_donation', row), '删除确认', {
|
||||
@@ -614,3 +598,334 @@
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.protocol-card-section {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.protocol-card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.protocol-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
padding: 12px 14px 12px 40px;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #c5e0d4;
|
||||
box-shadow: 0 2px 8px rgb(15 23 42 / 5%);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
border-color: var(--primary, #10b981);
|
||||
box-shadow: 0 0 0 1px rgb(16 185 129 / 25%);
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-card__select {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.protocol-card__aside {
|
||||
flex: 0 0 148px;
|
||||
min-width: 132px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-right: 12px;
|
||||
border-right: 1px solid #eef2f6;
|
||||
}
|
||||
|
||||
.protocol-card__status {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.protocol-card__create-time {
|
||||
margin-top: auto;
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.35;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.protocol-card__name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.protocol-card__index {
|
||||
flex-shrink: 0;
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
padding: 0 6px;
|
||||
border-radius: 6px;
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
color: var(--primary-dark, #059669);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.protocol-card__name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #1e293b);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.protocol-card__contact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.protocol-card__sub {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&--id {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-card__sections {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.protocol-card__block {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #eef2f6;
|
||||
|
||||
&--basic {
|
||||
flex: 1.15;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
&--newborn {
|
||||
flex: 0.85;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
&--samples {
|
||||
flex: 1.2;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-card__block-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark, #059669);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.protocol-card__block-count {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.protocol-card__kv-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.protocol-card__kv {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
|
||||
label {
|
||||
flex-shrink: 0;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
span {
|
||||
min-width: 0;
|
||||
color: var(--text, #1e293b);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-card__empty {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.protocol-card__sample-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.protocol-card__sample-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.protocol-card__sample-seq {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
color: var(--primary-dark, #059669);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.protocol-card__sample-main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.protocol-card__sample-line {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.protocol-card__sample-label {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.protocol-card__sample-value {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: var(--text, #1e293b);
|
||||
word-break: break-all;
|
||||
|
||||
&--code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-card__foot {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding-left: 12px;
|
||||
border-left: 1px solid #eef2f6;
|
||||
}
|
||||
|
||||
.protocol-card__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
min-width: 72px;
|
||||
|
||||
:deep(.el-button) {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-card-empty {
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.protocol-card {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.protocol-card__sections {
|
||||
flex-basis: 100%;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.protocol-card__foot {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border-left: none;
|
||||
border-top: 1px solid #eef2f6;
|
||||
padding-left: 0;
|
||||
padding-top: 10px;
|
||||
margin-left: 0;
|
||||
order: 4;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.protocol-card__sections {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.protocol-card__block {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -89,12 +89,6 @@
|
||||
><div class="data-label">捐赠日期</div
|
||||
><div class="data-value">{{ formatYmd(donation?.donationDate) }}</div></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">来源类型</div
|
||||
><div class="data-value">{{
|
||||
sourceTypeLabel(String(donation?.sourceType || ''))
|
||||
}}</div></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">采集专员</div
|
||||
><div class="data-value">{{ collectorDisplay }}</div></div
|
||||
@@ -243,7 +237,8 @@
|
||||
<div class="data-item">
|
||||
<div class="data-label">采集量</div>
|
||||
<div class="data-value"
|
||||
>{{ dash(row.collectAmount) }} {{ unitName(row.unitId) }}</div
|
||||
>{{ dash(row.collectAmount) }}
|
||||
{{ unitName(row.unitId, row.unitName) }}</div
|
||||
>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
@@ -523,61 +518,63 @@
|
||||
class="section-card section-card--white"
|
||||
>
|
||||
<div class="section-title section-title--banner"><h3>审核记录</h3></div>
|
||||
<el-table
|
||||
v-loading="auditLoading"
|
||||
class="list-table sub-view-table"
|
||||
:data="auditRecords"
|
||||
stripe
|
||||
empty-text="暂无审核记录"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="审核时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="auditStatus"
|
||||
label="审核状态"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
<div class="list-body list-wrap view-inner-table-wrap">
|
||||
<el-table
|
||||
v-loading="auditLoading"
|
||||
class="list-table inner-table"
|
||||
:data="auditRecords"
|
||||
stripe
|
||||
empty-text="暂无审核记录"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="String(row.auditStatus) === 'shtg' ? 'success' : 'warning'"
|
||||
size="small"
|
||||
>
|
||||
{{ auditStatusLabel(String(row.auditStatus || '')) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="auditRemark"
|
||||
label="审核备注"
|
||||
min-width="200"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="auditorUserName"
|
||||
label="审核人"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ auditorDisplay(row) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="审核时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="auditStatus"
|
||||
label="审核状态"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="String(row.auditStatus) === 'shtg' ? 'success' : 'warning'"
|
||||
size="small"
|
||||
>
|
||||
{{ auditStatusLabel(String(row.auditStatus || '')) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="auditRemark"
|
||||
label="审核备注"
|
||||
min-width="200"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="auditorUserName"
|
||||
label="审核人"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ auditorDisplay(row) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -642,7 +639,6 @@
|
||||
import {
|
||||
donationStatusLabel,
|
||||
donationStatusTagType,
|
||||
sourceTypeLabel,
|
||||
auditStatusLabel,
|
||||
auditStatusTagType,
|
||||
newbornSexLabel,
|
||||
@@ -824,8 +820,30 @@
|
||||
return sampleTypeOptions.value.find((x) => x.id === id)?.typeName || dash(id)
|
||||
}
|
||||
|
||||
function unitName(id: string) {
|
||||
return unitOptions.value.find((x) => x.id === id)?.unitName || dash(id)
|
||||
function unitName(id: string, fallbackName?: string) {
|
||||
const fromOptions = unitOptions.value.find((x) => x.id === id)?.unitName
|
||||
if (fromOptions) return fromOptions
|
||||
const name = String(fallbackName || '').trim()
|
||||
if (name) return name
|
||||
return dash(id)
|
||||
}
|
||||
|
||||
function ensureUnitsInOptions(samples: Array<Record<string, any>>) {
|
||||
const merged = [...unitOptions.value]
|
||||
for (const s of samples) {
|
||||
const uid = String(s.unitId || '')
|
||||
if (!uid) continue
|
||||
const name = String(s.unitName || '').trim()
|
||||
const existing = merged.find((x) => x.id === uid)
|
||||
if (existing) {
|
||||
if (name && (!existing.unitName || existing.unitName === existing.id)) {
|
||||
existing.unitName = name
|
||||
}
|
||||
continue
|
||||
}
|
||||
merged.push({ id: uid, unitName: name || uid })
|
||||
}
|
||||
unitOptions.value = merged
|
||||
}
|
||||
|
||||
function siteName(id: string) {
|
||||
@@ -923,6 +941,7 @@
|
||||
unitOptions.value = (uRes.list || []) as any
|
||||
expressCompanyOptions.value = ecRes.list || []
|
||||
siteOptions.value = siteRes.list || []
|
||||
ensureUnitsInOptions((data.samples as Array<Record<string, any>>) || [])
|
||||
|
||||
const coll = data.collection as Record<string, any> | null
|
||||
const don = data.donation as Record<string, any> | undefined
|
||||
@@ -964,12 +983,10 @@
|
||||
}
|
||||
auditLoading.value = true
|
||||
try {
|
||||
const no =
|
||||
String(donation.value?.donationNo || props.listRow?.donationNo || '').trim() || undefined
|
||||
const res = await fetchResearchDonationAuditList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: no
|
||||
researchDonationId: props.donationId
|
||||
})
|
||||
auditRecords.value = res.list || []
|
||||
} catch {
|
||||
@@ -1337,9 +1354,38 @@
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* ---- Sub View Table ---- */
|
||||
.sub-view-table {
|
||||
margin-bottom: 8px;
|
||||
/* ---- 审核记录内嵌列表(对齐 OpenSpec 嵌套列表 14px 标准) ---- */
|
||||
.view-inner-table-wrap.list-body.list-wrap {
|
||||
margin-top: 4px;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: var(--mint-radius, 10px);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-table) {
|
||||
--el-table-header-bg-color: var(--mint-table-head, #f8fafc);
|
||||
--el-table-border-color: var(--mint-border, #e2e8f0);
|
||||
--el-table-row-hover-bg-color: var(--mint-row-hover, #f0fdf4);
|
||||
--el-table-text-color: var(--mint-text, #0f172a);
|
||||
--el-table-header-text-color: var(--mint-text-muted, #64748b);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-table th.el-table__cell) {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
:deep(.el-table td.el-table__cell) {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text, #0f172a);
|
||||
}
|
||||
|
||||
:deep(.el-empty) {
|
||||
padding: 24px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Sample Cards ---- */
|
||||
|
||||
Reference in new issue
Block a user