This commit is contained in:
zlwu committed 2026-06-16 07:38:27 +08:00
1 parent a1c2511eb7
commit eaf7d490ec
382 files changed
+44146 -34245

No files matched your search

@@ -198,6 +198,8 @@
width="560px"
class="dialog-form-layout"
align-center
append-to-body
modal-class="protocol-dialog-mask"
destroy-on-close
:close-on-click-modal="false"
>
@@ -420,7 +422,31 @@
</el-select>
</el-form-item>
<el-form-item label="设备名称">
<el-input v-model="selectedStep.equipmentName" @change="saveStepBasic" />
<el-select
v-model="selectedStep.equipmentIds"
placeholder="请选择设备"
clearable
filterable
multiple
collapse-tags
collapse-tags-tooltip
style="width: 100%"
@change="onStepEquipmentChange"
>
<el-option
v-for="eq in equipmentOptions"
:key="eq.id"
:label="equipmentOptionLabel(eq)"
:value="eq.id"
:disabled="eq.status === '0'"
/>
</el-select>
<p
v-if="!(selectedStep.equipmentIds || []).length && selectedStep.equipmentName"
class="form-tip"
>
历史文本:{{ selectedStep.equipmentName }},请重新选择设备
</p>
</el-form-item>
<el-form-item label="预计时长(min)">
<el-input
@@ -464,9 +490,17 @@
</div>
</el-form>
<el-tabs v-model="activeSubTab" class="route-config-tabs">
<el-tabs
v-if="SHOW_PROCESS_REAGENT_CONSUMABLE"
v-model="activeSubTab"
class="route-config-tabs"
>
<!-- 试剂 -->
<el-tab-pane label="试剂" name="reagent">
<el-tab-pane
v-if="SHOW_PROCESS_REAGENT_CONSUMABLE"
label="试剂"
name="reagent"
>
<div class="route-config-tab-toolbar">
<el-button
class="btn-main"
@@ -645,7 +679,11 @@
</el-tab-pane>
<!-- 耗材 -->
<el-tab-pane label="耗材" name="consumable">
<el-tab-pane
v-if="SHOW_PROCESS_REAGENT_CONSUMABLE"
label="耗材"
name="consumable"
>
<div class="route-config-tab-toolbar">
<el-button
class="btn-main"
@@ -822,233 +860,237 @@
:image-size="48"
/>
</el-tab-pane>
</el-tabs>
<div class="route-config-step-sections">
<!-- 产出物 -->
<el-tab-pane label="产出物" name="output">
<div class="route-config-tab-toolbar">
<el-button class="btn-main" type="primary" size="small" @click="addOutputRow">
<el-icon><Plus /></el-icon>
添加产出物
</el-button>
<section class="route-config-step-section">
<h3 class="route-config-step-section-title">产出物</h3>
<div class="route-config-tab-list">
<div class="list-toolbar">
<div class="list-toolbar-row">
<el-button class="btn-main" type="primary" @click="addOutputRow">
<el-icon><Plus /></el-icon>
添加产出物
</el-button>
</div>
</div>
<div class="list-body list-wrap inner-table-wrap">
<el-table
v-if="selectedStep.outputs?.length"
:data="selectedStep.outputs"
stripe
class="list-table inner-table"
max-height="240"
>
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(i: number) => i + 1"
/>
<el-table-column
label="产出类型"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-select
v-model="row.outputTypeId"
placeholder="选择类型"
filterable
class="inner-cell-select"
@change="saveOutputs"
>
<el-option
v-for="ot in outputTypeOptions"
:key="ot.id"
:label="ot.typeName || ot.name"
:value="ot.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="名称"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-input
v-model="row.outputName"
placeholder="名称"
class="inner-cell-input"
@change="saveOutputs"
/>
</template>
</el-table-column>
<el-table-column
label="条码"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-input
v-model="row.outputBarcode"
placeholder="条码"
class="inner-cell-input"
@change="saveOutputs"
/>
</template>
</el-table-column>
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-input
v-model="row.remark"
placeholder="备注"
class="inner-cell-input"
@change="saveOutputs"
/>
</template>
</el-table-column>
<el-table-column
label="操作"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ $index }">
<el-button link type="danger" @click="removeOutputRow($index)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无产出物" :image-size="48" />
</div>
</div>
<el-table
:data="selectedStep.outputs"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(i: number) => i + 1"
/>
<el-table-column
label="产出类型"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-select
v-model="row.outputTypeId"
placeholder="选择类型"
filterable
class="inner-cell-select"
@change="saveOutputs"
>
<el-option
v-for="ot in outputTypeOptions"
:key="ot.id"
:label="ot.typeName || ot.name"
:value="ot.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="名称"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.outputName"
placeholder="名称"
class="inner-cell-input"
@change="saveOutputs"
/>
</template>
</el-table-column>
<el-table-column
label="条码"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.outputBarcode"
placeholder="条码"
class="inner-cell-input"
@change="saveOutputs"
/>
</template>
</el-table-column>
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.remark"
placeholder="备注"
class="inner-cell-input"
@change="saveOutputs"
/>
</template>
</el-table-column>
<el-table-column
label="操作"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ $index }">
<el-button
link
type="danger"
size="small"
@click="removeOutputRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty
v-if="!selectedStep.outputs || selectedStep.outputs.length === 0"
description="暂无产出物"
:image-size="48"
/>
</el-tab-pane>
</section>
<!-- 数据采集 -->
<el-tab-pane label="数据采集" name="dataCollection">
<div class="route-config-tab-toolbar">
<el-button
class="btn-main"
type="primary"
size="small"
@click="addDataCollectionRow"
>
<el-icon><Plus /></el-icon>
添加采集项
</el-button>
<section class="route-config-step-section">
<h3 class="route-config-step-section-title">数据采集</h3>
<div class="route-config-tab-list">
<div class="list-toolbar">
<div class="list-toolbar-row">
<el-button class="btn-main" type="primary" @click="addDataCollectionRow">
<el-icon><Plus /></el-icon>
添加采集项
</el-button>
</div>
</div>
<div class="list-body list-wrap inner-table-wrap">
<el-table
v-if="selectedStep.dataCollections?.length"
:data="selectedStep.dataCollections"
stripe
class="list-table inner-table"
max-height="240"
>
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(i: number) => i + 1"
/>
<el-table-column
label="采集名称"
min-width="180"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-input
v-model="row.collectionName"
placeholder="采集项名称"
class="inner-cell-input"
@change="saveDataCollections"
/>
</template>
</el-table-column>
<el-table-column
label="单位"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-select
v-model="row.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="itemUnitOptionsLoading"
:remote-method="searchItemUnitOptions"
@visible-change="(visible) => visible && searchItemUnitOptions('')"
@change="saveDataCollections"
>
<el-option
v-for="item in itemUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-input
v-model="row.remark"
placeholder="备注"
class="inner-cell-input"
@change="saveDataCollections"
/>
</template>
</el-table-column>
<el-table-column
label="操作"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ $index }">
<el-button link type="danger" @click="removeDataCollectionRow($index)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无采集项" :image-size="48" />
</div>
</div>
<el-table
:data="selectedStep.dataCollections"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(i: number) => i + 1"
/>
<el-table-column
label="采集名称"
min-width="180"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.collectionName"
placeholder="采集项名称"
class="inner-cell-input"
@change="saveDataCollections"
/>
</template>
</el-table-column>
<el-table-column
label="单位"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-select
v-model="row.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="itemUnitOptionsLoading"
:remote-method="searchItemUnitOptions"
@visible-change="(visible) => visible && searchItemUnitOptions('')"
@change="saveDataCollections"
>
<el-option
v-for="item in itemUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.remark"
placeholder="备注"
class="inner-cell-input"
@change="saveDataCollections"
/>
</template>
</el-table-column>
<el-table-column
label="操作"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ $index }">
<el-button
link
type="danger"
size="small"
@click="removeDataCollectionRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty
v-if="
!selectedStep.dataCollections || selectedStep.dataCollections.length === 0
"
description="暂无采集项"
:image-size="48"
/>
</el-tab-pane>
</el-tabs>
</section>
</div>
</div>
</template>
<template v-else>
@@ -1101,9 +1143,15 @@
fetchAllProcesses,
fetchAllProcessCategories,
fetchAllSopDocuments,
fetchAllOutputTypes
fetchAllOutputTypes,
fetchAllEquipments
} from '@/api/production'
import { fetchCellProductTypeList, fetchItemUnitList } from '@/api/admin'
import { equipmentOptionLabel, parseEquipmentIds, syncEquipmentNamesFromOptions } from '@/utils/equipment-select'
import {
SHOW_PROCESS_REAGENT_CONSUMABLE,
PROCESS_ROUTE_DEFAULT_SUB_TAB
} from '@/utils/process-material-ui'
defineOptions({ name: 'ProcessRouteTab' })
@@ -1323,7 +1371,7 @@
const stepList = ref<Record<string, any>[]>([])
const selectedStepId = ref('')
const stepListLoading = ref(false)
const activeSubTab = ref('reagent')
const activeSubTab = ref(PROCESS_ROUTE_DEFAULT_SUB_TAB)
const processSearchKeyword = ref('')
/** 接口 isKeyMaterial / isRequired 为 "0" | "1" 字符串 */
@@ -1339,6 +1387,7 @@
function normalizeStepSubTables(step: Record<string, any>) {
return {
...step,
equipmentIds: parseEquipmentIds(step.equipmentIds || step.equipmentName),
reagents: (step.reagents || []).map((r: Record<string, any>) => ({
...r,
isKeyMaterial: fromApiFlag(r.isKeyMaterial)
@@ -1379,6 +1428,7 @@
]
const categoryOptions = ref<Record<string, any>[]>([])
const sopOptions = ref<Record<string, any>[]>([])
const equipmentOptions = ref<Record<string, any>[]>([])
const outputTypeOptions = ref<Record<string, any>[]>([])
const itemUnitOptions = ref<{ label: string; value: string }[]>([])
const itemUnitOptionsLoading = ref(false)
@@ -1780,15 +1830,17 @@
async function loadConfigReferenceData() {
try {
const [processes, categories, sops, outputTypes] = await Promise.all([
const [processes, categories, sops, outputTypes, equipments] = await Promise.all([
fetchAllProcesses(),
fetchAllProcessCategories(),
fetchAllSopDocuments(),
fetchAllOutputTypes()
fetchAllOutputTypes(),
fetchAllEquipments()
])
categoryOptions.value = categories || []
sopOptions.value = sops || []
outputTypeOptions.value = outputTypes || []
equipmentOptions.value = equipments || []
// 按分类分组
const catMap: Record<
@@ -1834,7 +1886,7 @@
configRouteName.value = row.routeName || ''
configRouteVersion.value = row.version || ''
selectedStepId.value = ''
activeSubTab.value = 'reagent'
activeSubTab.value = PROCESS_ROUTE_DEFAULT_SUB_TAB
processSearchKeyword.value = ''
configSuppressSave.value = true
stepList.value = []
@@ -1961,7 +2013,9 @@
processName: selectedStep.value.processName,
categoryId: selectedStep.value.categoryId,
description: selectedStep.value.description,
equipmentName: selectedStep.value.equipmentName,
equipmentIds: selectedStep.value.equipmentIds?.length
? selectedStep.value.equipmentIds
: null,
estimatedDuration: selectedStep.value.estimatedDuration,
sopIds: selectedStep.value.sopIds
})
@@ -1970,6 +2024,13 @@
}
}
function onStepEquipmentChange(val: string[]) {
if (!selectedStep.value) return
selectedStep.value.equipmentIds = val || []
selectedStep.value.equipmentName = syncEquipmentNamesFromOptions(val, equipmentOptions.value)
saveStepBasic()
}
// ---------------------------------------------------------------------------
// 子表操作
// ---------------------------------------------------------------------------
@@ -2526,12 +2587,89 @@
}
}
.route-config-tabs + .route-config-step-sections {
margin-top: 12px;
}
.route-config-step-sections {
display: flex;
flex-direction: column;
gap: 16px;
}
.route-config-step-section-title {
margin: 0 0 8px;
padding-left: 10px;
border-left: 3px solid #10b981;
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
.route-config-tab-toolbar {
margin-bottom: 8px;
display: flex;
justify-content: flex-end;
}
/* 步骤详情 Tab 内嵌列表:弹窗在 body 下,须本地复刻 page-container 列表视觉 */
.route-config-tab-list {
border: 1px solid var(--mint-border, #e2e8f0);
border-radius: var(--mint-radius, 10px);
overflow: hidden;
background: var(--mint-surface, #fff);
.list-toolbar {
padding: 0;
background: var(--mint-table-head, #f8fafc);
border-bottom: none;
}
.list-toolbar-row {
padding: 8px 16px;
gap: 8px;
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: flex-start;
:deep(.el-button + .el-button) {
margin-left: 0 !important;
}
}
.list-body.list-wrap.inner-table-wrap {
border-top: 1px solid var(--mint-border, #e2e8f0);
border-radius: 0;
overflow: hidden;
: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;
}
}
}
/* 步骤详情内嵌表格:单元格控件随列宽收缩 */
.inner-table {
width: 100%;
+329 -207
View File
@@ -205,7 +205,25 @@
</el-select>
</el-form-item>
<el-form-item label="设备名称">
<el-input v-model="formData.equipmentName" placeholder="请输入设备名称" />
<el-select
v-model="formData.equipmentIds"
placeholder="请选择设备"
clearable
filterable
multiple
collapse-tags
collapse-tags-tooltip
style="width: 100%"
>
<el-option
v-for="eq in equipmentOptions"
:key="eq.id"
:label="equipmentOptionLabel(eq)"
:value="eq.id"
:disabled="eq.status === '0'"
/>
</el-select>
<p v-if="legacyEquipmentHint" class="form-tip">{{ legacyEquipmentHint }}</p>
</el-form-item>
<el-form-item>
<template #label>预估耗时<span class="form-label-tip">(min)</span></template>
@@ -248,7 +266,11 @@
</div>
</div>
<div id="form-section-reagents" class="form-tab-pane">
<div
v-if="SHOW_PROCESS_REAGENT_CONSUMABLE"
id="form-section-reagents"
class="form-tab-pane"
>
<div class="section-card">
<div class="section-title">
<div class="section-title-left"><h3>试剂关联</h3></div>
@@ -353,7 +375,11 @@
</div>
</div>
<div id="form-section-consumables" class="form-tab-pane">
<div
v-if="SHOW_PROCESS_REAGENT_CONSUMABLE"
id="form-section-consumables"
class="form-tab-pane"
>
<div class="section-card">
<div class="section-title">
<div class="section-title-left"><h3>耗材关联</h3></div>
@@ -459,89 +485,103 @@
<div class="section-card">
<div class="section-title">
<div class="section-title-left"><h3>产出物</h3></div>
<el-button class="btn-main" type="primary" size="small" @click="addOutputRow">
<el-icon><Plus /></el-icon>
添加产出物
</el-button>
</div>
<el-table :data="outputList" stripe class="inner-table">
<el-table-column
label="产出物类型"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-select
v-model="row.outputTypeId"
placeholder="选择类型"
filterable
class="inner-cell-select"
<div class="route-config-tab-list">
<div class="list-toolbar">
<div class="list-toolbar-row">
<el-button class="btn-main" type="primary" @click="addOutputRow">
<el-icon><Plus /></el-icon>
添加产出物
</el-button>
</div>
</div>
<div class="list-body list-wrap inner-table-wrap">
<el-table
v-if="outputList.length"
:data="outputList"
stripe
class="list-table inner-table"
>
<el-table-column
label="产出物类型"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
>
<el-option
v-for="ot in outputTypeOptions"
:key="ot.id"
:label="ot.typeName || ot.name"
:value="ot.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="产出物名称"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.outputName"
placeholder="名称"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column
label="产出物条码"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.outputBarcode"
placeholder="条码"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button
link
type="danger"
size="small"
@click="outputList.splice($index, 1)"
>删除</el-button
<template #default="{ row }">
<el-select
v-model="row.outputTypeId"
placeholder="选择类型"
filterable
class="inner-cell-select"
>
<el-option
v-for="ot in outputTypeOptions"
:key="ot.id"
:label="ot.typeName || ot.name"
:value="ot.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="产出物名称"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
>
</template>
</el-table-column>
</el-table>
<el-empty
v-if="outputList.length === 0"
description="暂无产出物,点击上方按钮添加"
:image-size="48"
/>
<template #default="{ row }">
<el-input
v-model="row.outputName"
placeholder="名称"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column
label="产出物条码"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-input
v-model="row.outputBarcode"
placeholder="条码"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" @click="outputList.splice($index, 1)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty
v-else
description="暂无产出物,点击上方按钮添加"
:image-size="48"
/>
</div>
</div>
</div>
</div>
@@ -550,125 +590,129 @@
<div class="section-title">
<div class="section-title-left">
<h3>数据采集</h3>
<span class="inner-table-sort-tip">拖拽左侧手柄可调整排序</span>
</div>
<el-button
class="btn-main"
type="primary"
size="small"
@click="addDataCollectionRow"
>
<el-icon><Plus /></el-icon>
添加采集项
</el-button>
</div>
<el-table
ref="dataCollectionTableRef"
:data="dataCollectionList"
row-key="_clientKey"
stripe
class="inner-table data-collection-sort-table"
>
<el-table-column
label=""
min-width="52"
align="center"
header-align="center"
class-name="drag-col"
>
<template #default>
<el-icon class="data-collection-drag-handle" title="拖拽排序"
><Rank
/></el-icon>
</template>
</el-table-column>
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column
label="采集项名称"
min-width="180"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.collectionName"
placeholder="请输入采集项名称"
class="inner-cell-input"
<div class="route-config-tab-list">
<div class="list-toolbar">
<div class="list-toolbar-row">
<el-button class="btn-main" type="primary" @click="addDataCollectionRow">
<el-icon><Plus /></el-icon>
添加采集项
</el-button>
<span class="inner-table-sort-tip">拖拽左侧手柄可调整排序</span>
</div>
</div>
<div class="list-body list-wrap inner-table-wrap">
<el-table
v-if="dataCollectionList.length"
ref="dataCollectionTableRef"
:data="dataCollectionList"
row-key="_clientKey"
stripe
class="list-table inner-table data-collection-sort-table"
>
<el-table-column
label=""
min-width="52"
align="center"
header-align="center"
class-name="drag-col"
>
<template #default>
<el-icon class="data-collection-drag-handle" title="拖拽排序"
><Rank
/></el-icon>
</template>
</el-table-column>
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(index) => index + 1"
/>
</template>
</el-table-column>
<el-table-column
label="单位"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-select
v-model="row.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="itemUnitOptionsLoading"
:remote-method="searchItemUnitOptions"
@visible-change="(visible) => visible && searchItemUnitOptions('')"
<el-table-column
label="采集项名称"
min-width="180"
align="center"
header-align="center"
show-overflow-tooltip
>
<el-option
v-for="item in itemUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="排序号"
min-width="88"
align="center"
header-align="center"
>
<template #default="{ $index }">
<span class="data-collection-order-num">{{ $index + 1 }}</span>
</template>
</el-table-column>
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button
link
type="danger"
size="small"
@click="removeDataCollectionRow($index)"
>删除</el-button
<template #default="{ row }">
<el-input
v-model="row.collectionName"
placeholder="请输入采集项名称"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column
label="单位"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
>
</template>
</el-table-column>
</el-table>
<el-empty
v-if="dataCollectionList.length === 0"
description="暂无采集项,点击上方按钮添加"
:image-size="48"
/>
<template #default="{ row }">
<el-select
v-model="row.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="itemUnitOptionsLoading"
:remote-method="searchItemUnitOptions"
@visible-change="(visible) => visible && searchItemUnitOptions('')"
>
<el-option
v-for="item in itemUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="排序号"
min-width="88"
align="center"
header-align="center"
>
<template #default="{ $index }">
<span class="data-collection-order-num">{{ $index + 1 }}</span>
</template>
</el-table-column>
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" @click="removeDataCollectionRow($index)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty
v-else
description="暂无采集项,点击上方按钮添加"
:image-size="48"
/>
</div>
</div>
</div>
</div>
</el-form>
@@ -688,6 +732,7 @@
<!-- 选择试剂内弹窗 -->
<el-dialog
v-if="SHOW_PROCESS_REAGENT_CONSUMABLE"
v-model="selectReagentVisible"
title="选择试剂"
width="1000px"
@@ -791,6 +836,7 @@
<!-- 选择耗材内弹窗 -->
<el-dialog
v-if="SHOW_PROCESS_REAGENT_CONSUMABLE"
v-model="selectConsumableVisible"
title="选择耗材"
width="1000px"
@@ -894,6 +940,7 @@
<!-- 选择耗材包导入 -->
<el-dialog
v-if="SHOW_PROCESS_REAGENT_CONSUMABLE"
v-model="importPackVisible"
title="从耗材包导入"
width="960px"
@@ -991,7 +1038,7 @@
</template>
<script setup lang="ts">
import { Plus, Close, Check, Rank, RefreshLeft, Search } from '@element-plus/icons-vue'
import { Plus, Close, Check, Rank, RefreshLeft, Search, Delete } from '@element-plus/icons-vue'
import Sortable from 'sortablejs'
import type { SortableEvent } from 'sortablejs'
import {
@@ -1006,9 +1053,12 @@
fetchAllConsumables,
fetchAllOutputTypes,
fetchAllConsumablePacks,
fetchConsumablePackDetail
fetchConsumablePackDetail,
fetchAllEquipments
} from '@/api/production'
import { fetchItemUnitList } from '@/api/admin'
import { equipmentOptionLabel, parseEquipmentIds } from '@/utils/equipment-select'
import { SHOW_PROCESS_REAGENT_CONSUMABLE } from '@/utils/process-material-ui'
import { packTypeLabel, packTypeTagType } from '@/utils/status-tag-registry'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import DialogFormBody from '@/components/business/dialog-form-body/index.vue'
@@ -1038,8 +1088,12 @@
const formTabs = [
{ key: 'basic', label: '基本信息' },
{ key: 'reagents', label: '试剂关联' },
{ key: 'consumables', label: '耗材关联' },
...(SHOW_PROCESS_REAGENT_CONSUMABLE
? [
{ key: 'reagents', label: '试剂关联' },
{ key: 'consumables', label: '耗材关联' }
]
: []),
{ key: 'outputs', label: '产出物' },
{ key: 'data-collections', label: '数据采集' }
] as const
@@ -1055,6 +1109,8 @@
// 下拉选项
const categoryOptions = ref<any[]>([])
const sopOptions = ref<any[]>([])
const equipmentOptions = ref<any[]>([])
const legacyEquipmentHint = ref('')
const outputTypeOptions = ref<any[]>([])
const itemUnitOptions = ref<{ label: string; value: string }[]>([])
const itemUnitOptionsLoading = ref(false)
@@ -1077,7 +1133,7 @@
processName: '',
categoryId: '',
description: '',
equipmentName: '',
equipmentIds: [] as string[],
estimatedDuration: '',
sopIds: [] as string[],
remark: ''
@@ -1224,13 +1280,15 @@
// 加载所有下拉参考数据
async function loadReferenceData() {
try {
const [cats, sops, reagents, consumables, outputTypes, packs] = await Promise.all([
const [cats, sops, reagents, consumables, outputTypes, packs, equipments] =
await Promise.all([
fetchAllProcessCategories(),
fetchAllSopDocuments(),
fetchAllReagents(),
fetchAllConsumables(),
fetchAllOutputTypes(),
fetchAllConsumablePacks()
fetchAllConsumablePacks(),
fetchAllEquipments()
])
categoryOptions.value = cats || []
sopOptions.value = sops || []
@@ -1238,6 +1296,7 @@
allConsumableOptions.value = consumables || []
outputTypeOptions.value = outputTypes || []
packOptions.value = packs || []
equipmentOptions.value = equipments || []
} catch {
/* ignore */
}
@@ -1259,7 +1318,8 @@
formData.processName = ''
formData.categoryId = ''
formData.description = ''
formData.equipmentName = ''
formData.equipmentIds = []
legacyEquipmentHint.value = ''
formData.estimatedDuration = ''
formData.sopIds = []
formData.remark = ''
@@ -1376,7 +1436,11 @@
formData.processName = detail.processName || ''
formData.categoryId = detail.categoryId || ''
formData.description = detail.description || ''
formData.equipmentName = detail.equipmentName || ''
formData.equipmentIds = parseEquipmentIds(detail.equipmentIds || detail.equipmentName)
legacyEquipmentHint.value =
formData.equipmentIds.length === 0 && detail.equipmentName
? `历史文本:${detail.equipmentName},请重新选择设备`
: ''
formData.estimatedDuration = detail.estimatedDuration || ''
formData.sopIds = parseSopIds(detail.sopIds)
formData.remark = detail.remark || ''
@@ -1419,7 +1483,7 @@
processName: formData.processName.trim(),
categoryId: formData.categoryId || undefined,
description: formData.description?.trim() || undefined,
equipmentName: formData.equipmentName?.trim() || undefined,
equipmentIds: formData.equipmentIds.length ? formData.equipmentIds : null,
estimatedDuration: formData.estimatedDuration?.trim() || undefined,
sopIds: serializeSopIds(formData.sopIds),
remark: formData.remark?.trim() || undefined,
@@ -1849,6 +1913,64 @@
color: var(--mint-text-muted, #64748b);
}
/* 弹窗内嵌列表:append-to-body 不在 page-container,须本地复刻标准列表视觉 */
.route-config-tab-list {
border: 1px solid var(--mint-border, #e2e8f0);
border-radius: var(--mint-radius, 10px);
overflow: hidden;
background: var(--mint-surface, #fff);
.list-toolbar {
padding: 0;
background: var(--mint-table-head, #f8fafc);
border-bottom: none;
}
.list-toolbar-row {
padding: 8px 16px;
gap: 8px;
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: flex-start;
:deep(.el-button + .el-button) {
margin-left: 0 !important;
}
}
.list-body.list-wrap.inner-table-wrap {
border-top: 1px solid var(--mint-border, #e2e8f0);
border-radius: 0;
overflow: hidden;
: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;
}
}
}
.data-collection-drag-handle {
cursor: move;
color: #909399;