This commit is contained in:
zlwu committed 2026-05-28 00:15:29 +08:00
1 parent c137b46939
commit b6c63591ea
126 files changed
+41593 -22990

No files matched your search

@@ -36,8 +36,8 @@
</div>
<div class="filter-actions-row">
<span class="filter-hint"
>支持协议号、客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索;默认展示近 30
天内应缴费的明细。</span
>支持协议号、客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索;默认展示应缴费时间前
15 天至后 15 天内的明细。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
@@ -418,9 +418,11 @@
}
function getDefaultInstallmentDueDateRange(): [string, string] {
const end = new Date()
const start = new Date()
start.setDate(start.getDate() - 30)
const today = new Date()
const start = new Date(today)
const end = new Date(today)
start.setDate(start.getDate() - 15)
end.setDate(end.getDate() + 15)
return [formatLocalDate(start), formatLocalDate(end)]
}
@@ -69,8 +69,8 @@
</div>
<div class="filter-actions-row">
<span class="filter-hint"
>支持协议号、客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索;默认展示近 30
天内应缴费的明细。</span
>支持协议号、客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索;默认展示应缴费时间前
15 天至后 15 天内的明细。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
@@ -436,9 +436,11 @@
}
function getDefaultInstallmentDueDateRange(): [string, string] {
const end = new Date()
const start = new Date()
start.setDate(start.getDate() - 30)
const today = new Date()
const start = new Date(today)
const end = new Date(today)
start.setDate(start.getDate() - 15)
end.setDate(end.getDate() + 15)
return [formatLocalDate(start), formatLocalDate(end)]
}
@@ -15,7 +15,7 @@
</el-tabs>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" label-position="top" @submit.prevent>
<el-form :model="searchForm" class="filter-form filter-form--inline-label" @submit.prevent>
<div class="filter-grid">
<el-form-item label="关键词">
<el-input
@@ -47,7 +47,9 @@
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持按协议号模糊检索;默认展示近 30 天内应缴费的明细。</span>
<span class="filter-hint"
>支持按协议号模糊检索;默认展示应缴费时间为当天前 10 天至后 20 天的明细。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -458,9 +460,10 @@
}
function getDefaultInstallmentDueDateRange(): [string, string] {
const end = new Date()
const start = new Date()
start.setDate(start.getDate() - 30)
const end = new Date()
start.setDate(start.getDate() - 10)
end.setDate(end.getDate() + 20)
return [formatLocalDate(start), formatLocalDate(end)]
}
@@ -736,17 +739,24 @@
margin-bottom: 4px;
}
.filter-form :deep(.el-form-item) {
.filter-form--inline-label :deep(.el-form-item) {
margin-bottom: 0;
min-width: 0;
align-items: center;
}
.filter-form :deep(.el-form-item__label) {
.filter-form--inline-label :deep(.el-form-item__label) {
font-size: 13px;
font-weight: 500;
color: var(--mint-text-muted, #64748b);
padding: 0 0 4px;
line-height: 1.2;
padding-right: 8px;
line-height: 36px;
height: 36px;
}
.filter-form--inline-label :deep(.el-form-item__content) {
flex: 1;
min-width: 0;
}
.attachment-upload {
@@ -37,28 +37,13 @@
<div class="section-title-left"><h3>基本信息</h3></div>
</div>
<div class="form-grid form-grid--col4">
<el-form-item label="协议号" prop="protocolNo">
<el-select
v-model="formData.protocolNoId"
<el-form-item label="协议号">
<el-input
:model-value="isEdit ? formData.protocolNo : ''"
class="w-full"
filterable
remote
clearable
reserve-keyword
:remote-method="remoteSearchProtocolNos"
:loading="protocolNoLoading"
placeholder="输入协议号检索"
@change="onProtocolNoChange"
@clear="onProtocolNoClear"
>
<el-option
v-for="p in protocolNoOptions"
:key="p.id"
:label="protocolNoOptionLabel(p)"
:value="p.id"
:disabled="isProtocolNoOptionDisabled(p)"
/>
</el-select>
disabled
:placeholder="isEdit ? '' : '保存后自动生成'"
/>
</el-form-item>
<el-form-item label="协议状态" prop="protocolStatus">
<el-select
@@ -1171,7 +1156,6 @@
fetchItemUnitList,
fetchCollectionRegionList,
fetchHospitalList,
fetchCollectionProtocolNoList,
fetchSampleCodeList,
fetchExpressCompanyList,
fetchSiteList
@@ -1236,8 +1220,6 @@
}>
>([])
const unitOptions = ref<Array<{ id: string; unitName: string; unitCode: string }>>([])
const protocolNoOptions = ref<Api.BizCollectionProtocolNo.ProtocolNoItem[]>([])
const protocolNoLoading = ref(false)
const sampleCodeOptions = ref<Api.BizSampleCode.SampleCodeItem[]>([])
const sampleCodeLoading = ref(false)
const expressCompanyOptions = ref<Api.ExpressCompany.ExpressCompanyItem[]>([])
@@ -1480,7 +1462,6 @@
})
const formRules = {
protocolNo: [{ required: true, message: '请输入协议号', trigger: 'blur' }],
protocolStatus: [{ required: true, message: '请选择协议状态', trigger: 'change' }]
}
@@ -1539,7 +1520,6 @@
salesSnapshot.value = null
regionOptions.value = []
hospitalOptions.value = []
protocolNoOptions.value = []
selectedSamples.value = []
batchTransportVisible.value = false
Object.assign(batchTransport, createEmptyTransport())
@@ -1799,27 +1779,9 @@
if (prot) {
formData.collectorUserId = String(prot.collectorUserId || formData.collectorUserId || '')
formData.protocolNo = String(prot.protocolNo || formData.protocolNo || '')
formData.protocolNoId = String(prot.protocolNoId || formData.protocolNoId || '')
formData.protocolStatus = String(prot.protocolStatus || formData.protocolStatus || 'dsx')
formData.registerDate = String(prot.registerDate || formData.registerDate || '')
formData.remark = String(prot.remark ?? formData.remark ?? '')
if (formData.protocolNoId) {
protocolNoOptions.value = [
{
id: formData.protocolNoId,
protocolNo: formData.protocolNo,
issued: 0,
issuedUserId: '',
issuedUserName: '',
issuedTime: '',
tenantNo: '',
orderNum: 0,
remark: '',
createTime: '',
updateTime: ''
}
]
}
}
const pc = bundle.partyCustomer as Record<string, any> | null
@@ -1905,27 +1867,9 @@
resetForm()
formData.protocolNo = String(row.protocolNo || '')
formData.protocolNoId = String((row as any).protocolNoId || '')
formData.protocolStatus = String(row.protocolStatus || 'dsx')
formData.registerDate = String(row.registerDate || '')
formData.collectorUserId = String(row.collectorUserId || '')
if (formData.protocolNoId) {
protocolNoOptions.value = [
{
id: formData.protocolNoId,
protocolNo: formData.protocolNo,
issued: 0,
issuedUserId: '',
issuedUserName: '',
issuedTime: '',
tenantNo: '',
orderNum: 0,
remark: '',
createTime: '',
updateTime: ''
}
]
}
formData.protocolAttachments = String((row as any).protocolAttachments || '')
formData.remark = String(row.remark || '')
@@ -1982,13 +1926,6 @@
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate()
if (!isEdit.value && formData.protocolNoId) {
const picked = protocolNoOptions.value.find((x) => x.id === formData.protocolNoId)
if (picked?.usedInProtocol) {
ElMessage.warning(`协议号「${formData.protocolNo}」已被占用,请更换其他协议号`)
return
}
}
if (!isEdit.value) {
for (const sampleRow of formData.samples) {
if (!sampleRow.sampleCodeId) continue
@@ -2007,9 +1944,7 @@
.map((a) => a.fileUrl)
.filter(Boolean)
.join(',')
const payload = {
protocolNo: formData.protocolNo,
protocolNoId: formData.protocolNoId || undefined,
const payload: Record<string, unknown> = {
protocolStatus: formData.protocolStatus,
registerDate: formData.registerDate || undefined,
collectorUserId: formData.collectorUserId || undefined,
@@ -2585,52 +2520,6 @@
syncContractAmount()
}
/* ====== 协议号下拉检索 ====== */
function protocolNoOptionLabel(p: Api.BizCollectionProtocolNo.ProtocolNoItem) {
if (p.usedInProtocol) return `${p.protocolNo}(已占用)`
if (p.issued) return `${p.protocolNo}(已下发)`
return p.protocolNo
}
function isProtocolNoOptionDisabled(p: Api.BizCollectionProtocolNo.ProtocolNoItem) {
if (!p.usedInProtocol) return false
return !(isEdit.value && p.id === formData.protocolNoId)
}
async function remoteSearchProtocolNos(query: string) {
protocolNoLoading.value = true
try {
const res = await fetchCollectionProtocolNoList({
page: 1,
pageSize: 100,
keyword: query || undefined
})
protocolNoOptions.value = res.list || []
} catch {
protocolNoOptions.value = []
} finally {
protocolNoLoading.value = false
}
}
function onProtocolNoChange(val: string | null | undefined) {
if (!val) {
formData.protocolNo = ''
formData.protocolNoId = ''
return
}
const p = protocolNoOptions.value.find((x) => x.id === val)
if (p) {
formData.protocolNo = p.protocolNo
formData.protocolNoId = p.id
}
}
function onProtocolNoClear() {
formData.protocolNo = ''
formData.protocolNoId = ''
}
/* ====== 样本编码选择 ====== */
function sampleCodeOptionLabel(sc: Api.BizSampleCode.SampleCodeItem) {
if (sc.usedInProtocol) return `${sc.sampleCode}(已占用)`
@@ -1436,7 +1436,8 @@
}
.storage-tree-search {
margin-bottom: 12px;
margin: 10px auto 12px;
width: calc(100% - 32px);
flex-shrink: 0;
}
@@ -20,6 +20,10 @@
<span class="label-k">工单</span>
<span class="label-v wo" :title="workOrderLabel">{{ workOrderLabel }}</span>
</div>
<div v-if="workOrderTypeLabel" class="label-line">
<span class="label-k">类型</span>
<span class="label-v wo-type" :title="workOrderTypeLabel">{{ workOrderTypeLabel }}</span>
</div>
<div v-if="taskLabel" class="label-line">
<span class="label-k">任务</span>
<span class="label-v task" :title="taskLabel">{{ taskLabel }}</span>
@@ -84,8 +88,11 @@
const labelY = computed(() => path.value[2])
const workOrderLabel = computed(() => props.data?.workOrderNo?.trim() || '')
const workOrderTypeLabel = computed(() => props.data?.workOrderTypeLabel?.trim() || '')
const taskLabel = computed(() => props.data?.taskNo?.trim() || '')
const hasLabel = computed(() => Boolean(workOrderLabel.value || taskLabel.value))
const hasLabel = computed(
() => Boolean(workOrderLabel.value || workOrderTypeLabel.value || taskLabel.value)
)
const labelStyle = computed(() => ({
transform: `translate(-50%, -50%) translate(${labelX.value}px, ${labelY.value}px)`
@@ -155,6 +162,10 @@
color: #059669;
}
&.wo-type {
color: #0f766e;
}
&.task {
color: #64748b;
font-weight: 500;
@@ -26,7 +26,9 @@ export interface ChainNode {
export interface ChainEdgeMeta {
workOrderNo: string
/** inbound / temporary / outbound */
workOrderType: string
workOrderTypeLabel?: string
taskNo: string
taskName: string
taskId?: string
@@ -57,9 +59,10 @@ export interface ChainNodeData {
export interface ChainEdgeData {
workOrderNo?: string
workOrderTypeLabel?: string
taskNo?: string
taskId?: string
taskName?: string
/** 同源分叉子节点连线使用贝塞尔曲线 */
/** 同源分叉 / 多源汇聚连线使用贝塞尔曲线 */
curved?: boolean
}
@@ -13,12 +13,6 @@ import type {
export const CHAIN_NODE_WIDTH = 148
export const CHAIN_NODE_HEIGHT = 100
export function nextAncestorLink(parent: AncestorItem['parent']): AncestorItem | null {
if (!parent) return null
if (Array.isArray(parent)) return parent[0] ?? null
return parent
}
function makeFlowNode(n: ChainNode, isCurrent: boolean): Node<ChainNodeData> {
return {
id: n.id,
@@ -41,6 +35,7 @@ function makeFlowEdge(
type: 'chainLink',
data: {
workOrderNo: edge.workOrderNo || undefined,
workOrderTypeLabel: edge.workOrderTypeLabel || undefined,
taskNo: edge.taskNo || undefined,
taskId: edge.taskId || undefined,
taskName: edge.taskName || undefined
@@ -48,6 +43,30 @@ function makeFlowEdge(
}
}
function walkAncestorParents(
parent: AncestorItem | AncestorItem[] | null | undefined,
downstreamId: string,
addNode: (n: ChainNode, isCurrent?: boolean) => void,
addEdge: (source: string, target: string, meta: ChainEdgeMeta) => void
) {
if (!parent) return
const items = Array.isArray(parent) ? parent : [parent]
for (const link of items) {
walkAncestorBranch(link, downstreamId, addNode, addEdge)
}
}
function walkAncestorBranch(
link: AncestorItem,
downstreamId: string,
addNode: (n: ChainNode, isCurrent?: boolean) => void,
addEdge: (source: string, target: string, meta: ChainEdgeMeta) => void
) {
addNode(link.node)
addEdge(link.node.id, downstreamId, link.edge)
walkAncestorParents(link.parent, link.node.id, addNode, addEdge)
}
/** 将 /chain API 数据转为 Vue Flow 节点与边(未布局) */
export function chainApiToElements(data: ChainApiData): {
nodes: Node<ChainNodeData>[]
@@ -73,16 +92,8 @@ export function chainApiToElements(data: ChainApiData): {
const current = data.current
addNode(current, true)
if (data.ancestors.length) {
let link: AncestorItem | null = data.ancestors[0]
let downstreamId = current.id
while (link) {
addNode(link.node)
addEdge(link.node.id, downstreamId, link.edge)
downstreamId = link.node.id
link = nextAncestorLink(link.parent)
}
for (const link of data.ancestors) {
walkAncestorBranch(link, current.id, addNode, addEdge)
}
const walkDescendants = (items: DescendantItem[], sourceId: string) => {
@@ -103,14 +114,18 @@ export function chainApiToElements(data: ChainApiData): {
}
}
/** 同一源节点连出 2 条及以上边时标记为分叉曲线 */
/** 同源分叉 / 多源汇聚时使用贝塞尔曲线 */
function annotateForkEdges(edges: Edge<ChainEdgeData>[]): Edge<ChainEdgeData>[] {
const outCount = new Map<string, number>()
const inCount = new Map<string, number>()
for (const e of edges) {
outCount.set(e.source, (outCount.get(e.source) || 0) + 1)
inCount.set(e.target, (inCount.get(e.target) || 0) + 1)
}
return edges.map((e) => {
if ((outCount.get(e.source) || 0) < 2) {
const fork = (outCount.get(e.source) || 0) >= 2
const merge = (inCount.get(e.target) || 0) >= 2
if (!fork && !merge) {
return e
}
return {
@@ -66,6 +66,29 @@
:index="(i: number) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="taskNo" label="任务号" min-width="150" align="center" />
<el-table-column prop="taskType" label="任务类型" min-width="120" align="center">
<template #default="{ row }">
<el-tag :type="productionTaskTypeTag(row.taskType, row.orderSource).type" size="small">
{{ productionTaskTypeTag(row.taskType, row.orderSource).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="taskName"
label="任务名称"
min-width="160"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="routeName"
label="工艺路线"
min-width="140"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.routeName || '—' }}</template>
</el-table-column>
<el-table-column prop="workOrderNo" label="所属工单号" min-width="140" align="center" />
<el-table-column prop="orderSource" label="来源" min-width="100" align="center">
<template #default="{ row }">
@@ -74,6 +97,14 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="receptionSource" label="来源类型" min-width="110" align="center">
<template #default="{ row }">
<span v-if="!showReceptionSourceType(row)">—</span>
<el-tag v-else :type="receptionSourceTagType(row.receptionSource)" size="small">
{{ row.receptionSourceLabel || receptionSourceLabel(row.receptionSource) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="protocolNo" label="协议号" min-width="140" align="center">
<template #default="{ row }">{{ row.protocolNo || '—' }}</template>
</el-table-column>
@@ -104,29 +135,6 @@
<el-table-column prop="sampleTypeName" label="样本类型" min-width="110" align="center">
<template #default="{ row }">{{ row.sampleTypeName || '—' }}</template>
</el-table-column>
<el-table-column prop="taskType" label="任务类型" min-width="120" align="center">
<template #default="{ row }">
<el-tag :type="productionTaskTypeTag(row.taskType, row.orderSource).type" size="small">
{{ productionTaskTypeTag(row.taskType, row.orderSource).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="taskName"
label="任务名称"
min-width="160"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="routeName"
label="工艺路线"
min-width="140"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.routeName || '—' }}</template>
</el-table-column>
<el-table-column prop="receiverName" label="接收人" min-width="100" align="center">
<template #default="{ row }">{{ row.receiverName || '—' }}</template>
</el-table-column>
@@ -328,7 +336,9 @@
protocolStatusLabel,
protocolStatusTagType,
donationStatusLabel,
donationStatusTagType
donationStatusTagType,
receptionSourceLabel,
receptionSourceTagType
} from '@/utils/status-tag-registry'
import type { TagType } from '@/utils/status-tag-registry'
import TaskExecutionDialog from './TaskExecutionDialog.vue'
@@ -358,6 +368,13 @@
return row.orderSource === 'temporary' && !String(value || '').trim()
}
/** 入库任务展示工单来源类型(细胞存储 / 科研捐赠) */
function showReceptionSourceType(row: { orderSource?: string; receptionSource?: string }) {
if (row.orderSource === 'temporary' || row.orderSource === 'outbound') return false
const src = (row.receptionSource || '').trim()
return src === 'cell_storage' || src === 'research_donation'
}
defineOptions({ name: 'ProductionTaskTab' })
// ── 新增任务(工单选择 → 复用 DispatchTaskDialog) ──
@@ -41,9 +41,8 @@
@tab-change="handleTypeTabChange"
>
<el-tab-pane label="入库生产工单" name="inbound" />
<el-tab-pane label="临时生产工单" name="temporary" />
<el-tab-pane label="出库生产工单" name="outbound" />
<el-tab-pane label="全部生产工单" name="all" />
<el-tab-pane label="临时生产工单" name="temporary" />
</el-tabs>
<div class="filter-block">
@@ -91,7 +90,7 @@
</el-form>
</div>
<div v-if="activeTypeTab !== 'all'" class="list-toolbar">
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
@@ -124,7 +123,7 @@
<div class="list-body list-wrap">
<el-table
ref="workOrderListTableRef"
:class="['list-table', { 'list-table--no-action-col list-table--all-tab': activeTypeTab === 'all' }]"
class="list-table"
:data="tableData"
v-loading="loading"
stripe
@@ -133,7 +132,6 @@
@row-dblclick="handleView"
>
<el-table-column
v-if="activeTypeTab !== 'all'"
type="selection"
min-width="55"
align="center"
@@ -160,19 +158,6 @@
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="activeTypeTab === 'all'"
prop="workOrderType"
label="工单类型"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="woTypeTag(row.workOrderType).type" size="small">
{{ woTypeTag(row.workOrderType).label }}
</el-tag>
</template>
</el-table-column>
<!-- 出库工单专属列 -->
<el-table-column
v-if="activeTypeTab === 'outbound'"
@@ -309,7 +294,6 @@
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
<el-table-column
v-if="activeTypeTab !== 'all'"
label="操作"
min-width="160"
fixed="right"
@@ -690,7 +674,7 @@
const route = useRoute()
const mainTab = ref('work-order')
type WorkOrderTypeTab = 'inbound' | 'temporary' | 'outbound' | 'all'
type WorkOrderTypeTab = 'inbound' | 'temporary' | 'outbound'
const activeTypeTab = ref<WorkOrderTypeTab>('inbound')
const showReceptionCols = computed(
() => activeTypeTab.value !== 'temporary' && activeTypeTab.value !== 'outbound'
@@ -813,19 +797,10 @@
const sourceTag = (source: string) => {
const map: Record<string, { label: string; type: string }> = {
cell_storage: { label: '细胞存储', type: 'success' },
research_donation: { label: '科研捐赠', type: 'primary' }
research_donation: { label: '科研捐赠', type: 'warning' }
}
return map[source] || { label: source, type: 'info' }
}
const woTypeTag = (type: string) => {
const map: Record<string, { label: string; type: string }> = {
inbound: { label: '入库', type: 'success' },
temporary: { label: '临时', type: 'warning' },
outbound: { label: '出库', type: 'primary' }
}
return map[type] || { label: type, type: 'info' }
}
function formatLocalDate(d: Date): string {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
@@ -933,7 +908,7 @@
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
workOrderStatus: searchForm.workOrderStatus || undefined,
workOrderType: activeTypeTab.value === 'all' ? undefined : activeTypeTab.value,
workOrderType: activeTypeTab.value,
createTimeStart: createTimeRange.value?.[0] || undefined,
createTimeEnd: createTimeRange.value?.[1] || undefined
})
@@ -1077,7 +1052,7 @@
r.sourceTypeLabel ||
d.receptionSourceLabel ||
receptionSourceLabel(d.receptionSource || ''),
tagType: 'primary' as const
tagType: sourceTag(d.receptionSource || '').type as TagType
},
{ label: '样本类型', value: r.sampleTypeName || d.sampleTypeName || '—' }
)
@@ -1388,14 +1363,6 @@
min-width: 0;
}
/* 全部工单 Tab 无操作列,略增高行高便于阅读多列信息 */
:deep(.list-table.list-table--all-tab) {
td.el-table__cell > .cell {
min-height: 36px;
line-height: 36px;
}
}
.work-order-type-tabs {
padding: 0 16px;
margin-bottom: 4px;
@@ -45,6 +45,42 @@
/>
</div>
</template>
<template v-for="task in labTasksWithMaterials" #[taskMaterialSlotName(task.id)]>
<div v-if="task.materialUsages?.length" :key="task.id" 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="materialName"
label="物料名称"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column label="规格型号" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{ row.specModel || '—' }}</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 :key="`${task.id}-empty`" class="view-empty-hint">暂无物料使用记录</p>
</template>
</SystemViewDialog>
</template>
@@ -56,7 +92,13 @@
type OutboundQcDetail,
type QcWorkOrderDetail
} from '@/api/qc-work-order'
import { fetchQcTaskResults, type QcTaskResultItem } from '@/api/qc-task-result'
import {
fetchQcTaskResults,
fetchQcTaskMaterialUsages,
type QcTaskResultItem,
type QcTaskMaterialUsageItem
} from '@/api/qc-task-result'
import { fetchQcMaterialOptions, type QcMaterialOption } from '@/api/qc-material'
import {
workOrderStatusLabel,
taskStatusLabel,
@@ -88,6 +130,15 @@
const loading = ref(false)
const detail = ref<OutboundQcDetail | null>(null)
type ViewMaterialUsageRow = {
materialId: string
materialName: string
specModel: string
unit: string
qty: number | undefined
remark: string
}
type TaskView = {
id: string
taskNo: string
@@ -101,6 +152,7 @@
completedTime?: string
attachmentFiles?: Array<{ id?: string; fileUrl?: string; fileName?: string }>
results?: QcTaskResultItem[]
materialUsages?: ViewMaterialUsageRow[]
companyName?: string
feeAmount?: number
}
@@ -109,6 +161,40 @@
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
function materialRowsFromApi(items: QcTaskMaterialUsageItem[]): ViewMaterialUsageRow[] {
const merged = new Map<string, ViewMaterialUsageRow>()
for (const u of items || []) {
const mid = (u.materialId || '').trim()
if (!mid) continue
const qtyRaw = Math.abs(Number(u.changeQty) || 0)
const prev = merged.get(mid)
if (prev) {
prev.qty = (Number(prev.qty) || 0) + qtyRaw
} else {
merged.set(mid, {
materialId: mid,
materialName: u.materialName || '',
specModel: '',
unit: '',
qty: qtyRaw > 0 ? qtyRaw : undefined,
remark: u.remark || ''
})
}
}
return Array.from(merged.values())
}
function enrichMaterialUsageRows(rows: ViewMaterialUsageRow[], options: QcMaterialOption[]) {
for (const row of rows) {
const opt = options.find((o) => o.id === row.materialId)
if (opt) {
row.materialName = opt.materialName || row.materialName
row.specModel = opt.specModel || ''
row.unit = opt.unit || ''
}
}
}
watch(
() => props.visible,
async (v) => {
@@ -119,17 +205,30 @@
const d = await fetchOutboundQcDetail(props.workOrderId)
detail.value = d
const rawTasks = (d.workOrder?.tasks || d.tasks || []) as TaskView[]
const materialOptions = await fetchQcMaterialOptions().catch(() => [] as QcMaterialOption[])
const enriched = await Promise.all(
rawTasks.map(async (task) => {
if (task.taskType !== 'sysz' || task.taskStatus !== 'ywc') {
return { ...task, results: [] as QcTaskResultItem[] }
const base: TaskView = { ...task, results: [], materialUsages: [] }
if (task.taskStatus !== 'ywc') {
return base
}
try {
const results = await fetchQcTaskResults(task.id)
return { ...task, results: results || [] }
base.results = results || []
} catch {
return { ...task, results: [] as QcTaskResultItem[] }
base.results = []
}
if (task.taskType === 'sysz') {
try {
const savedUsages = await fetchQcTaskMaterialUsages(task.id)
const rows = materialRowsFromApi(savedUsages || [])
enrichMaterialUsageRows(rows, materialOptions || [])
base.materialUsages = rows
} catch {
base.materialUsages = []
}
}
return base
})
)
tasksWithResults.value = enriched
@@ -178,11 +277,18 @@
const tasksWithAttachments = computed(() =>
tasksWithResults.value.filter((t) => (t.attachmentFiles?.length || 0) > 0)
)
const labTasksWithMaterials = computed(() =>
tasksWithResults.value.filter((t) => t.taskType === 'sysz')
)
function taskAttachmentSlotName(taskId: string) {
return `task-att-${taskId}`
}
function taskMaterialSlotName(taskId: string) {
return `task-mat-${taskId}`
}
function handleTaskPreview(file: Record<string, unknown>) {
const url = fullUrl(String(file.fileUrl || ''))
if (url) window.open(url, '_blank')
@@ -230,9 +336,9 @@
const filtered = tasks.filter((t) => t.taskType === taskType)
if (!filtered.length) return []
return filtered.map((task, index) => {
return filtered.flatMap((task, index) => {
const title = filtered.length > 1 ? `${sectionTitle}(${index + 1})` : sectionTitle
const fields: ViewField[] = [
const taskInfoFields: ViewField[] = [
{ label: '检测项目', value: task.itemName || '—' },
{ label: '项目编码', value: task.itemCode || '—' },
{
@@ -249,34 +355,65 @@
{ label: '完成时间', value: task.completedTime || '—' }
]
if (taskType === 'dsfjc') {
fields.splice(2, 0, { label: '第三方公司', value: task.companyName || '—' })
taskInfoFields.splice(2, 0, { label: '第三方公司', value: task.companyName || '—' })
if (task.feeAmount != null) {
fields.push({ label: '费用(元)', value: String(task.feeAmount) })
taskInfoFields.push({ label: '费用(元)', value: String(task.feeAmount) })
}
}
if (task.results?.length) {
for (const item of task.results) {
fields.push({
label: item.paramName + (item.paramUnit ? `(${item.paramUnit})` : ''),
value: item.paramValue || '—'
})
}
} else if (taskType === 'sysz') {
fields.push({ label: '检测参数', value: '—' })
}
fields.push({ label: '备注', value: task.remark || '—', span: true })
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 = { title, fields }
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') {
fields.push({ label: '检测材料', value: '—' })
section.fields.push({ label: '检测材料', value: '—' })
} else if (taskType === 'sysz') {
section.fieldGroups?.push({ fields: [{ label: '检测附件', value: '—' }] })
} else {
fields.push({ label: '检测附件', value: '—' })
section.fields.push({ label: '检测附件', value: '—' })
}
return section
const sections: ViewSection[] = [section]
if (taskType === 'sysz') {
sections.push({
title: '',
fields: [],
slotTitle: '物料使用',
slotTitleStyle: 'field',
slot: taskMaterialSlotName(task.id)
})
}
return sections
})
}
@@ -41,9 +41,8 @@
@tab-change="handleTypeTabChange"
>
<el-tab-pane label="入库质检工单" name="inbound" />
<el-tab-pane label="临时质检工单" name="temporary" />
<el-tab-pane label="出库质检工单" name="outbound" />
<el-tab-pane label="全部质检工单" name="all" />
<el-tab-pane label="临时质检工单" name="temporary" />
</el-tabs>
<div class="filter-block">
@@ -460,7 +459,7 @@
<el-dialog
v-model="configureVisible"
:title="configureIsEdit ? '调整检测' : '配置检测'"
width="800px"
width="900px"
class="dialog-form-layout"
align-center
destroy-on-close
@@ -553,6 +552,42 @@
/>
</div>
</template>
<template v-for="task in viewLabTasks" #[taskMaterialSlotName(task.id)]>
<div v-if="task.materialUsages?.length" :key="task.id" 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="materialName"
label="物料名称"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column label="规格型号" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{ row.specModel || '—' }}</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 :key="`${task.id}-empty`" class="view-empty-hint">暂无物料使用记录</p>
</template>
</SystemViewDialog>
<el-dialog
@@ -694,7 +729,13 @@
type QcWorkOrderDetail
} from '@/api/qc-work-order'
import { fetchCellProductList } from '@/api/cell-product'
import { fetchQcTaskResults, type QcTaskResultItem } from '@/api/qc-task-result'
import {
fetchQcTaskResults,
fetchQcTaskMaterialUsages,
type QcTaskResultItem,
type QcTaskMaterialUsageItem
} from '@/api/qc-task-result'
import { fetchQcMaterialOptions, type QcMaterialOption } from '@/api/qc-material'
import type { QcTask } from '@/api/qc-task'
import {
workOrderStatusLabel,
@@ -1048,18 +1089,68 @@
const viewVisible = ref(false)
const viewLoading = ref(false)
const detail = ref<QcWorkOrderDetail | null>(null)
type QcWorkOrderTaskView = QcTask & { results?: QcTaskResultItem[] }
type ViewMaterialUsageRow = {
materialId: string
materialName: string
specModel: string
unit: string
qty: number | undefined
remark: string
}
type QcWorkOrderTaskView = QcTask & {
results?: QcTaskResultItem[]
materialUsages?: ViewMaterialUsageRow[]
}
const viewTasks = ref<QcWorkOrderTaskView[]>([])
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
const viewTasksWithAttachments = computed(() =>
viewTasks.value.filter((t) => (t.attachmentFiles?.length || 0) > 0)
)
const viewLabTasks = computed(() => viewTasks.value.filter((t) => t.taskType === 'sysz'))
function taskAttachmentSlotName(taskId: string) {
return `task-att-${taskId}`
}
function taskMaterialSlotName(taskId: string) {
return `task-mat-${taskId}`
}
function materialRowsFromApi(items: QcTaskMaterialUsageItem[]): ViewMaterialUsageRow[] {
const merged = new Map<string, ViewMaterialUsageRow>()
for (const u of items || []) {
const mid = (u.materialId || '').trim()
if (!mid) continue
const qtyRaw = Math.abs(Number(u.changeQty) || 0)
const prev = merged.get(mid)
if (prev) {
prev.qty = (Number(prev.qty) || 0) + qtyRaw
} else {
merged.set(mid, {
materialId: mid,
materialName: u.materialName || '',
specModel: '',
unit: '',
qty: qtyRaw > 0 ? qtyRaw : undefined,
remark: u.remark || ''
})
}
}
return Array.from(merged.values())
}
function enrichMaterialUsageRows(rows: ViewMaterialUsageRow[], options: QcMaterialOption[]) {
for (const row of rows) {
const opt = options.find((o) => o.id === row.materialId)
if (opt) {
row.materialName = opt.materialName || row.materialName
row.specModel = opt.specModel || ''
row.unit = opt.unit || ''
}
}
}
const viewReceptionAttachmentFiles = computed(() => {
const r = detail.value?.reception
if (!r) return []
@@ -1148,9 +1239,9 @@
const filtered = tasks.filter((t) => t.taskType === taskType)
if (!filtered.length) return []
return filtered.map((task, index) => {
return filtered.flatMap((task, index) => {
const title = filtered.length > 1 ? `${sectionTitle}(${index + 1})` : sectionTitle
const fields: ViewField[] = [
const taskInfoFields: ViewField[] = [
{ label: '检测项目', value: task.itemName || '—' },
{ label: '项目编码', value: task.itemCode || '—' },
{
@@ -1167,34 +1258,65 @@
{ label: '完成时间', value: task.completedTime || '—' }
]
if (taskType === 'dsfjc') {
fields.splice(2, 0, { label: '第三方公司', value: task.companyName || '—' })
taskInfoFields.splice(2, 0, { label: '第三方公司', value: task.companyName || '—' })
if (task.feeAmount != null) {
fields.push({ label: '费用(元)', value: String(task.feeAmount) })
taskInfoFields.push({ label: '费用(元)', value: String(task.feeAmount) })
}
}
if (task.results?.length) {
for (const item of task.results) {
fields.push({
label: item.paramName + (item.paramUnit ? `(${item.paramUnit})` : ''),
value: item.paramValue || '—'
})
}
} else if (taskType === 'sysz') {
fields.push({ label: '检测参数', value: '—' })
}
fields.push({ label: '备注', value: task.remark || '—', span: true })
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 = { title, fields }
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') {
fields.push({ label: '检测材料', value: '—' })
section.fields.push({ label: '检测材料', value: '—' })
} else if (taskType === 'sysz') {
section.fieldGroups?.push({ fields: [{ label: '检测附件', value: '—' }] })
} else {
fields.push({ label: '检测附件', value: '—' })
section.fields.push({ label: '检测附件', value: '—' })
}
return section
const sections: ViewSection[] = [section]
if (taskType === 'sysz') {
sections.push({
title: '',
fields: [],
slotTitle: '物料使用',
slotTitleStyle: 'field',
slot: taskMaterialSlotName(task.id)
})
}
return sections
})
}
@@ -1306,10 +1428,12 @@
{
label: '来源类型',
value:
r.sourceTypeLabel ||
d.receptionSourceLabel ||
receptionSourceLabel(d.receptionSource || ''),
tagType: 'primary' as const
tagType:
d.receptionSource === 'research_donation'
? ('warning' as const)
: ('success' as const)
},
{ label: '样本类型', value: r.sampleTypeName || d.sampleTypeName || '—' }
]),
@@ -1485,17 +1609,34 @@
const d = await fetchQcWorkOrderDetail(row.id)
detail.value = d
const rawTasks = (d.tasks || []) as QcWorkOrderTaskView[]
const materialOptions = await fetchQcMaterialOptions().catch(() => [] as QcMaterialOption[])
const enriched = await Promise.all(
rawTasks.map(async (task) => {
if (task.taskType !== 'sysz' || task.taskStatus !== 'ywc') {
return { ...task, results: [] as QcTaskResultItem[] }
const base: QcWorkOrderTaskView = {
...task,
results: [],
materialUsages: []
}
if (task.taskStatus !== 'ywc') {
return base
}
try {
const results = await fetchQcTaskResults(task.id)
return { ...task, results: results || [] }
base.results = results || []
} catch {
return { ...task, results: [] as QcTaskResultItem[] }
base.results = []
}
if (task.taskType === 'sysz') {
try {
const savedUsages = await fetchQcTaskMaterialUsages(task.id)
const rows = materialRowsFromApi(savedUsages || [])
enrichMaterialUsageRows(rows, materialOptions || [])
base.materialUsages = rows
} catch {
base.materialUsages = []
}
}
return base
})
)
viewTasks.value = enriched
@@ -1626,6 +1767,9 @@
}
.qc-company-group {
display: flex;
flex-direction: column;
gap: 10px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
@@ -1637,9 +1781,6 @@
font-size: 13px;
font-weight: 500;
color: #334155;
padding-bottom: 6px;
border-bottom: 1px dashed #e2e8f0;
margin-bottom: 6px;
}
.qc-company-group-items {
@@ -2,27 +2,20 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<div class="region-tabs region-tab-strip">
<div class="tab-button-group region-tab-group">
<el-button
v-for="tab in tabs"
:key="tab.key"
class="region-tab-btn"
:type="activeTab === tab.key ? 'primary' : 'default'"
@click="switchTab(tab.key)"
>
<span class="region-tab-inner">
<span class="region-tab-name">{{ tab.label }}</span>
</span>
</el-button>
</div>
</div>
<section class="panel">
<div class="panel-top">
<h2>样本接收列表</h2>
</div>
<el-tabs
v-model="activeTab"
class="work-order-type-tabs"
@tab-change="handleStatusTabChange"
>
<el-tab-pane label="待接收" name="pending" />
<el-tab-pane label="已接收" name="received" />
</el-tabs>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
@@ -66,7 +59,7 @@
<span class="filter-hint">
{{
activeTab === 'pending'
? '勾选多个样本后可批量接收;预计到达时间默认近 30 天。'
? '勾选多个样本后可批量接收;预计到达时间默认为当天前 15 天至后 15 天。'
: '双击行可查看接收详情;接收时间默认近 30 天。'
}}
</span>
@@ -856,11 +849,6 @@
defineOptions({ name: 'QualityInspectionSampleReception' })
const userStore = useUserStore()
const tabs = [
{ key: 'pending', label: '待接收' },
{ key: 'received', label: '已接收' }
]
const activeTab = ref('pending')
const searchForm = reactive({ keyword: '', sourceType: '' })
@@ -878,7 +866,16 @@
return [formatLocalDate(start), formatLocalDate(end)]
}
const etaDateRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
/** 预计到达时间:当天前 15 天至后 15 天 */
function getDefaultEtaDateRange(): [string, string] {
const start = new Date()
const end = new Date()
start.setDate(start.getDate() - 15)
end.setDate(end.getDate() + 15)
return [formatLocalDate(start), formatLocalDate(end)]
}
const etaDateRange = ref<[string, string] | null>(getDefaultEtaDateRange())
const receptionTimeRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
const tableData = ref<Record<string, unknown>[]>([])
const loading = ref(false)
@@ -1319,9 +1316,7 @@
}
}
function switchTab(key: string) {
if (activeTab.value === key) return
activeTab.value = key
function handleStatusTabChange() {
selectedPendingRows.value = []
pagination.page = 1
loadData()
@@ -1336,7 +1331,7 @@
searchForm.keyword = ''
searchForm.sourceType = ''
if (activeTab.value === 'pending') {
etaDateRange.value = getDefaultLast30DaysRange()
etaDateRange.value = getDefaultEtaDateRange()
} else {
receptionTimeRange.value = getDefaultLast30DaysRange()
}
@@ -1493,8 +1488,9 @@
</script>
<style scoped lang="scss">
.region-tabs {
margin-bottom: 16px;
.work-order-type-tabs {
padding: 0 16px;
margin-bottom: 4px;
}
.sample-reception-dialog {
@@ -37,29 +37,13 @@
<div class="section-title-left"><h3>基本信息</h3></div>
</div>
<div class="form-grid form-grid--col4">
<el-form-item label="捐赠协议号" prop="donationNoId">
<el-select
v-model="formData.donationNoId"
<el-form-item label="捐赠协议号">
<el-input
:model-value="isEdit ? formData.donationNo : ''"
class="w-full"
filterable
remote
clearable
reserve-keyword
:disabled="isEdit"
:remote-method="remoteSearchDonationProtocolNos"
:loading="donationProtocolNoLoading"
placeholder="输入捐赠协议号检索"
@change="onDonationProtocolNoChange"
@clear="onDonationProtocolNoClear"
>
<el-option
v-for="p in donationProtocolNoOptions"
:key="p.id"
:label="donationProtocolNoOptionLabel(p)"
:value="p.id"
:disabled="isDonationProtocolNoOptionDisabled(p)"
/>
</el-select>
disabled
:placeholder="isEdit ? '' : '保存后自动生成'"
/>
</el-form-item>
<el-form-item label="捐赠状态" prop="donationStatus">
<el-select
@@ -952,7 +936,6 @@
fetchItemUnitList,
fetchCollectionRegionList,
fetchHospitalList,
fetchCollectionProtocolNoList,
fetchSampleCodeList,
fetchExpressCompanyList,
fetchSiteList
@@ -999,8 +982,6 @@
}>
>([])
const unitOptions = ref<Array<{ id: string; unitName: string; unitCode: string }>>([])
const donationProtocolNoOptions = ref<Api.BizCollectionProtocolNo.ProtocolNoItem[]>([])
const donationProtocolNoLoading = ref(false)
const sampleCodeOptions = ref<Api.BizSampleCode.SampleCodeItem[]>([])
const expressCompanyOptions = ref<Api.ExpressCompany.ExpressCompanyItem[]>([])
const siteOptions = ref<Api.Site.SiteItem[]>([])
@@ -1162,9 +1143,6 @@
})
const formRules = computed(() => ({
donationNoId: isEdit.value
? []
: [{ required: true, message: '请选择捐赠协议号', trigger: 'change' }],
donationStatus: [{ required: true, message: '请选择捐赠状态', trigger: 'change' }]
}))
@@ -1219,7 +1197,6 @@
selectedSamples.value = []
batchTransportVisible.value = false
Object.assign(batchTransport, createEmptyTransport())
donationProtocolNoOptions.value = []
activeFormTab.value = 'basic'
}
@@ -1296,6 +1273,7 @@
function handleOpenForAdd() {
isEdit.value = false
editingId.value = null
dialogTitle.value = '新增捐赠协议单'
resetForm()
formData.motherEthnic = DEFAULT_MOTHER_ETHNIC
@@ -1303,7 +1281,6 @@
loadSampleOptions()
void loadEthnicOptions()
void remoteSearchCustomers('')
void remoteSearchDonationProtocolNos('')
// 默认采集专员、销售人员为当前登录人
const currentUser = userStore.getUserInfo
@@ -1347,7 +1324,6 @@
if (donation) {
formData.collectorUserId = String(donation.collectorUserId || formData.collectorUserId || '')
formData.donationNo = String(donation.donationNo || formData.donationNo || '')
ensureDonationProtocolNoInOptions(formData.donationNo)
formData.donationStatus = String(donation.donationStatus || formData.donationStatus || 'dsx')
formData.donationDate = String(donation.donationDate || formData.donationDate || '')
formData.remark = String(donation.remark ?? formData.remark ?? '')
@@ -1416,7 +1392,6 @@
resetForm()
formData.donationNo = String(row.donationNo || '')
ensureDonationProtocolNoInOptions(formData.donationNo)
formData.donationStatus = String(row.donationStatus || 'dsx')
formData.donationDate = String(row.donationDate || '')
formData.collectorUserId = String(row.collectorUserId || '')
@@ -1475,17 +1450,6 @@
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate()
if (!isEdit.value && formData.donationNoId) {
const picked = donationProtocolNoOptions.value.find((x) => x.id === formData.donationNoId)
if (picked?.usedInProtocol) {
ElMessage.warning(`捐赠协议号「${formData.donationNo}」已被占用,请更换其他协议号`)
return
}
}
if (!isEdit.value && !formData.donationNo?.trim()) {
ElMessage.warning('请选择捐赠协议号')
return
}
if (!isEdit.value) {
for (const sampleRow of formData.samples) {
if (!sampleRow.sampleCodeId) continue
@@ -1498,8 +1462,7 @@
}
submitLoading.value = true
try {
const payload = {
donationNo: formData.donationNo || undefined,
const payload: Record<string, unknown> = {
donationStatus: formData.donationStatus,
donationDate: formData.donationDate || undefined,
collectorUserId: formData.collectorUserId || undefined,
@@ -1991,86 +1954,6 @@
}
}
/* ====== 捐赠协议号下拉检索(采集协议号池) ====== */
function donationProtocolNoOptionLabel(p: Api.BizCollectionProtocolNo.ProtocolNoItem) {
if (p.usedInProtocol) return `${p.protocolNo}(已占用)`
if (p.issued) return `${p.protocolNo}(已下发)`
return p.protocolNo
}
function isDonationProtocolNoOptionDisabled(p: Api.BizCollectionProtocolNo.ProtocolNoItem) {
if (!p.usedInProtocol) return false
return !(isEdit.value && p.id === formData.donationNoId)
}
function ensureDonationProtocolNoInOptions(protocolNo: string) {
const no = (protocolNo || '').trim()
if (!no) {
formData.donationNoId = ''
return
}
const matched = donationProtocolNoOptions.value.find((x) => x.protocolNo === no)
if (matched) {
formData.donationNoId = matched.id
return
}
const stubId = `stub-${no}`
formData.donationNoId = stubId
donationProtocolNoOptions.value = [
{
id: stubId,
protocolNo: no,
issued: 0,
issuedUserId: '',
issuedUserName: '',
issuedTime: '',
tenantNo: '',
orderNum: 0,
remark: '',
createTime: '',
updateTime: ''
},
...donationProtocolNoOptions.value
]
}
async function remoteSearchDonationProtocolNos(query: string) {
donationProtocolNoLoading.value = true
try {
const res = await fetchCollectionProtocolNoList({
page: 1,
pageSize: 100,
keyword: query || undefined
})
donationProtocolNoOptions.value = res.list || []
if (formData.donationNo) {
ensureDonationProtocolNoInOptions(formData.donationNo)
}
} catch {
donationProtocolNoOptions.value = []
} finally {
donationProtocolNoLoading.value = false
}
}
function onDonationProtocolNoChange(val: string | null | undefined) {
if (!val) {
formData.donationNo = ''
formData.donationNoId = ''
return
}
const p = donationProtocolNoOptions.value.find((x) => x.id === val)
if (p) {
formData.donationNo = p.protocolNo
formData.donationNoId = p.id
}
}
function onDonationProtocolNoClear() {
formData.donationNo = ''
formData.donationNoId = ''
}
/* ====== 样本编码选择 ====== */
function sampleCodeOptionLabel(sc: Api.BizSampleCode.SampleCodeItem) {
if (sc.usedInProtocol) return `${sc.sampleCode}(已占用)`