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%;