1584 lines
55 KiB
Vue
1584 lines
55 KiB
Vue
<!-- 工艺路线管理 -->
|
||
<template>
|
||
<div class="page-container">
|
||
<header class="page-head">
|
||
<h1>工艺路线管理</h1>
|
||
<p class="page-desc">管理产品工艺路线,配置工序步骤及其关联的试剂、耗材与产出物。</p>
|
||
</header>
|
||
|
||
<section class="panel">
|
||
<div class="panel-top">
|
||
<h2 class="title-bar-heading">路线列表</h2>
|
||
</div>
|
||
|
||
<div class="filter-block">
|
||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||
<div class="filter-grid">
|
||
<el-form-item label="路线名称">
|
||
<el-input
|
||
v-model="searchForm.routeName"
|
||
placeholder="模糊搜索"
|
||
clearable
|
||
@keyup.enter="handleSearch"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="路线编码">
|
||
<el-input
|
||
v-model="searchForm.routeCode"
|
||
placeholder="精确搜索"
|
||
clearable
|
||
@keyup.enter="handleSearch"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="产品类型">
|
||
<el-select
|
||
v-model="searchForm.productTypeId"
|
||
placeholder="全部"
|
||
clearable
|
||
filterable
|
||
>
|
||
<el-option
|
||
v-for="pt in productTypeOptions"
|
||
:key="pt.id"
|
||
:label="pt.productTypeName"
|
||
:value="pt.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</div>
|
||
<div class="filter-actions-row">
|
||
<span class="filter-hint">按路线名称、编码、产品类型筛选。</span>
|
||
<div class="filter-actions-end">
|
||
<el-button class="btn-ghost" @click="handleReset">重置</el-button>
|
||
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</el-button>
|
||
</div>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
|
||
<div class="list-toolbar">
|
||
<div class="list-toolbar-row">
|
||
<div class="list-toolbar-selected">
|
||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||
</div>
|
||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||
<el-icon><Plus /></el-icon>
|
||
新增路线
|
||
</el-button>
|
||
<el-button
|
||
class="btn-ghost"
|
||
type="danger"
|
||
:disabled="selectedRows.length === 0"
|
||
@click="handleBatchDelete"
|
||
>
|
||
批量删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="list-body list-wrap">
|
||
<el-table
|
||
class="list-table"
|
||
:data="tableData"
|
||
v-loading="loading"
|
||
stripe
|
||
row-key="id"
|
||
@selection-change="handleSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="55" align="center" header-align="center" />
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
width="68"
|
||
align="center"
|
||
header-align="center"
|
||
:index="(index: number) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||
/>
|
||
<el-table-column prop="routeName" label="路线名称" min-width="160" align="center" header-align="center" show-overflow-tooltip />
|
||
<el-table-column prop="routeCode" label="路线编码" min-width="130" align="center" header-align="center" show-overflow-tooltip />
|
||
<el-table-column label="产品类型" min-width="130" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
{{ productTypeMap[row.productTypeId] || '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="version" label="版本号" min-width="90" align="center" header-align="center" />
|
||
<el-table-column label="当前版本" min-width="90" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.isCurrent" type="success" size="small">当前</el-tag>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="remark" label="备注" min-width="120" align="center" header-align="center" show-overflow-tooltip />
|
||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" header-align="center" />
|
||
<el-table-column label="操作" width="340" fixed="right" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-button class="btn-op-item primary" @click="handleOpenConfig(row)">
|
||
<el-icon><Rank /></el-icon>路线图配置
|
||
</el-button>
|
||
<el-button class="btn-op-item" @click="handleEdit(row)">编辑</el-button>
|
||
<el-button
|
||
v-if="!row.isCurrent"
|
||
class="btn-op-item"
|
||
@click="handleSetCurrent(row)"
|
||
>
|
||
设为当前
|
||
</el-button>
|
||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="pagination list-pager-bar">
|
||
<el-pagination
|
||
:current-page="pagination.page"
|
||
:page-size="pagination.pageSize"
|
||
:total="pagination.total"
|
||
:page-sizes="[10, 20, 50]"
|
||
layout="total, sizes, prev, pager, next"
|
||
@size-change="(val: number) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||
@current-change="(val: number) => ((pagination.page = val), loadData())"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增/编辑路线弹窗 -->
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
:title="dialogTitle"
|
||
width="560px"
|
||
class="dialog-form-layout"
|
||
align-center
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-form
|
||
ref="formRef"
|
||
:model="formData"
|
||
:rules="formRules"
|
||
label-position="top"
|
||
class="route-dialog-form"
|
||
scroll-to-error
|
||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||
>
|
||
<div class="form-grid-2">
|
||
<el-form-item label="路线编码" prop="routeCode">
|
||
<el-input v-model="formData.routeCode" placeholder="请输入路线编码" />
|
||
</el-form-item>
|
||
<el-form-item label="路线名称" prop="routeName">
|
||
<el-input v-model="formData.routeName" placeholder="请输入路线名称" />
|
||
</el-form-item>
|
||
<el-form-item label="产品类型">
|
||
<el-select v-model="formData.productTypeId" placeholder="请选择产品类型" clearable filterable>
|
||
<el-option
|
||
v-for="pt in productTypeOptions"
|
||
:key="pt.id"
|
||
:label="pt.productTypeName"
|
||
:value="pt.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="版本号">
|
||
<el-input v-model="formData.version" placeholder="如 V1.0" />
|
||
</el-form-item>
|
||
<el-form-item label="备注" class="form-col-span-2">
|
||
<el-input v-model="formData.remark" placeholder="可选" type="textarea" :rows="3" />
|
||
</el-form-item>
|
||
</div>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="dialogVisible = false">
|
||
<el-icon><Close /></el-icon>
|
||
取消
|
||
</el-button>
|
||
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="handleSubmit">
|
||
<el-icon><Check /></el-icon>
|
||
确定
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 路线图配置弹窗 -->
|
||
<el-dialog
|
||
v-model="configDialogVisible"
|
||
:title="`路线图配置 — ${configRouteName}${configRouteVersion ? ' ' + configRouteVersion : ''}`"
|
||
width="90vw"
|
||
class="dialog-form-layout route-config-dialog"
|
||
align-center
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<div class="route-config-body">
|
||
<!-- 左侧:工艺库 -->
|
||
<div class="route-config-left">
|
||
<div class="route-config-left-header">
|
||
<h3>工艺库</h3>
|
||
<el-input
|
||
v-model="processSearchKeyword"
|
||
placeholder="搜索工序"
|
||
clearable
|
||
size="small"
|
||
class="route-config-search"
|
||
/>
|
||
</div>
|
||
<el-collapse v-model="expandedCategories" class="route-config-collapse">
|
||
<el-collapse-item
|
||
v-for="(cat, catIndex) in filteredProcessLibrary"
|
||
:key="cat.categoryId"
|
||
:name="cat.categoryId"
|
||
>
|
||
<template #title>
|
||
<span class="route-config-cat-bar"></span>
|
||
<span>{{ cat.categoryName }}</span>
|
||
</template>
|
||
<div
|
||
v-for="(proc, procIndex) in cat.processes"
|
||
:key="proc.id"
|
||
class="route-config-process-card"
|
||
draggable="true"
|
||
@dragstart="onDragStart($event, proc)"
|
||
>
|
||
<div class="route-config-process-info">
|
||
<div class="route-config-process-name">
|
||
<span class="route-config-process-seq">{{ procIndex + 1 }}</span>{{ proc.processName }}
|
||
</div>
|
||
<div class="route-config-process-code">{{ proc.processCode }}</div>
|
||
</div>
|
||
<el-icon class="route-config-process-drag"><Rank /></el-icon>
|
||
</div>
|
||
<el-empty
|
||
v-if="cat.processes.length === 0"
|
||
description="该分类暂无工序"
|
||
:image-size="32"
|
||
/>
|
||
</el-collapse-item>
|
||
</el-collapse>
|
||
<el-empty
|
||
v-if="filteredProcessLibrary.length === 0"
|
||
description="暂无工艺数据"
|
||
:image-size="48"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 中间:步骤列表 -->
|
||
<div
|
||
class="route-config-center"
|
||
@dragover.prevent
|
||
@drop="onDropToCenter"
|
||
v-loading="stepListLoading"
|
||
element-loading-text="加载中..."
|
||
>
|
||
<div class="route-config-center-header">
|
||
<h3>工序步骤</h3>
|
||
<span class="route-config-step-count">共 {{ stepList.length }} 步</span>
|
||
</div>
|
||
<div class="route-config-center-dropzone">
|
||
<draggable
|
||
v-model="stepList"
|
||
handle=".route-config-step-handle"
|
||
:animation="200"
|
||
@end="onStepDragEnd"
|
||
>
|
||
<div
|
||
v-for="(element, index) in stepList"
|
||
:key="element.id"
|
||
class="route-config-step-card"
|
||
:class="{ 'is-active': selectedStepId === element.id }"
|
||
@click="selectStep(element)"
|
||
>
|
||
<div class="route-config-step-handle">
|
||
<el-icon><Rank /></el-icon>
|
||
</div>
|
||
<div class="route-config-step-order">{{ index + 1 }}</div>
|
||
<div class="route-config-step-info">
|
||
<div class="route-config-step-name">{{ element.processName || '未命名步骤' }}</div>
|
||
<div class="route-config-step-desc">{{ element.description || '' }}</div>
|
||
</div>
|
||
<el-button
|
||
link
|
||
type="primary"
|
||
size="small"
|
||
title="复制步骤"
|
||
@click.stop="handleCopyStep(element)"
|
||
>
|
||
<el-icon><CopyDocument /></el-icon>
|
||
</el-button>
|
||
<el-button
|
||
link
|
||
type="danger"
|
||
size="small"
|
||
@click.stop="handleDeleteStep(element, index)"
|
||
>
|
||
<el-icon><Delete /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
</draggable>
|
||
<div v-if="stepList.length === 0" class="route-config-center-empty">
|
||
<el-icon :size="32" color="#a7f3d0"><Plus /></el-icon>
|
||
<p>从左侧拖拽工序到此处,添加路线步骤</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:步骤详情 -->
|
||
<div class="route-config-right">
|
||
<template v-if="selectedStep">
|
||
<div class="route-config-right-header">
|
||
<h3>步骤详情</h3>
|
||
</div>
|
||
<div class="route-config-right-body">
|
||
<el-form label-position="top" class="route-config-step-form">
|
||
<div class="form-grid-2">
|
||
<el-form-item label="工序名称">
|
||
<el-input v-model="selectedStep.processName" @change="saveStepBasic" />
|
||
</el-form-item>
|
||
<el-form-item label="工序分类">
|
||
<el-select
|
||
v-model="selectedStep.categoryId"
|
||
placeholder="选择分类"
|
||
clearable
|
||
@change="saveStepBasic"
|
||
>
|
||
<el-option
|
||
v-for="cat in categoryOptions"
|
||
:key="cat.id"
|
||
:label="cat.categoryName || cat.name"
|
||
:value="cat.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="描述" class="form-col-span-2">
|
||
<el-input
|
||
v-model="selectedStep.description"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="工序描述"
|
||
@change="saveStepBasic"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="设备名称">
|
||
<el-input v-model="selectedStep.equipmentName" @change="saveStepBasic" />
|
||
</el-form-item>
|
||
<el-form-item label="预计时长(min)">
|
||
<el-input v-model="selectedStep.estimatedDuration" placeholder="如 2h" @change="saveStepBasic" />
|
||
</el-form-item>
|
||
<el-form-item label="关联SOP" class="form-col-span-2">
|
||
<el-select
|
||
v-model="sopIdsArr"
|
||
multiple
|
||
placeholder="选择SOP文档"
|
||
filterable
|
||
@change="onSopIdsChange"
|
||
>
|
||
<el-option
|
||
v-for="sop in sopOptions"
|
||
:key="sop.id"
|
||
:label="sop.sopName || sop.name"
|
||
:value="sop.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</div>
|
||
</el-form>
|
||
|
||
<el-tabs v-model="activeSubTab" class="route-config-tabs">
|
||
<!-- 试剂 -->
|
||
<el-tab-pane label="试剂" name="reagent">
|
||
<div class="route-config-tab-toolbar">
|
||
<el-button class="btn-main" type="primary" size="small" @click="addReagentRow">
|
||
<el-icon><Plus /></el-icon>
|
||
添加试剂
|
||
</el-button>
|
||
</div>
|
||
<el-table :data="selectedStep.reagents" stripe class="inner-table" max-height="300">
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
width="56"
|
||
align="center"
|
||
header-align="center"
|
||
:index="(i: number) => i + 1"
|
||
/>
|
||
<el-table-column label="试剂编码" min-width="110" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.reagentCode" placeholder="编码" class="inner-cell-input" @change="saveReagents" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="试剂名称" min-width="120" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.reagentName" placeholder="名称" class="inner-cell-input" @change="saveReagents" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="规格" min-width="100" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.spec" placeholder="规格" class="inner-cell-input" @change="saveReagents" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="标准用量" min-width="110" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.standardUsage" placeholder="用量" class="inner-cell-input" @change="saveReagents" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="单位" min-width="80" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.unit" placeholder="单位" class="inner-cell-input" @change="saveReagents" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="关键物料" width="90" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-switch v-model="row.isKeyMaterial" @change="saveReagents" />
|
||
</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="saveReagents" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80" align="center" header-align="center">
|
||
<template #default="{ $index }">
|
||
<el-button link type="danger" size="small" @click="removeReagentRow($index)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-empty v-if="!selectedStep.reagents || selectedStep.reagents.length === 0" description="暂无试剂" :image-size="48" />
|
||
</el-tab-pane>
|
||
|
||
<!-- 耗材 -->
|
||
<el-tab-pane label="耗材" name="consumable">
|
||
<div class="route-config-tab-toolbar">
|
||
<el-button class="btn-main" type="primary" size="small" @click="addConsumableRow">
|
||
<el-icon><Plus /></el-icon>
|
||
添加耗材
|
||
</el-button>
|
||
</div>
|
||
<el-table :data="selectedStep.consumables" stripe class="inner-table" max-height="300">
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
width="56"
|
||
align="center"
|
||
header-align="center"
|
||
:index="(i: number) => i + 1"
|
||
/>
|
||
<el-table-column label="耗材编码" min-width="110" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.consumableCode" placeholder="编码" class="inner-cell-input" @change="saveConsumables" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="耗材名称" min-width="120" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.consumableName" placeholder="名称" class="inner-cell-input" @change="saveConsumables" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="规格型号" min-width="110" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.specModel" placeholder="规格型号" class="inner-cell-input" @change="saveConsumables" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="标准用量" min-width="110" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.standardUsage" placeholder="用量" class="inner-cell-input" @change="saveConsumables" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="单位" min-width="80" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.unit" placeholder="单位" class="inner-cell-input" @change="saveConsumables" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="必需" width="80" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-switch v-model="row.isRequired" @change="saveConsumables" />
|
||
</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="saveConsumables" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80" align="center" header-align="center">
|
||
<template #default="{ $index }">
|
||
<el-button link type="danger" size="small" @click="removeConsumableRow($index)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-empty v-if="!selectedStep.consumables || selectedStep.consumables.length === 0" description="暂无耗材" :image-size="48" />
|
||
</el-tab-pane>
|
||
|
||
<!-- 产出物 -->
|
||
<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>
|
||
</div>
|
||
<el-table :data="selectedStep.outputs" stripe class="inner-table" max-height="300">
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
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="操作" 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>
|
||
|
||
<!-- 数据采集 -->
|
||
<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>
|
||
</div>
|
||
<el-table :data="selectedStep.dataCollections" stripe class="inner-table" max-height="300">
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
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-input v-model="row.unit" placeholder="如 °C、mL" class="inner-cell-input" @change="saveDataCollections" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="排序号" min-width="100" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-model="row.orderNum"
|
||
:min="0"
|
||
:max="9999"
|
||
controls-position="right"
|
||
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-input v-model="row.remark" placeholder="备注" class="inner-cell-input" @change="saveDataCollections" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" 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>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="route-config-right-empty">
|
||
<el-icon :size="48" color="#d1d5db"><Rank /></el-icon>
|
||
<p>请选择左侧步骤查看详情</p>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { Plus, Delete, Rank, Close, Check, CopyDocument } from '@element-plus/icons-vue'
|
||
import { VueDraggable as draggable } from 'vue-draggable-plus'
|
||
import {
|
||
fetchProcessRouteList,
|
||
fetchProcessRouteDetail,
|
||
fetchCreateProcessRoute,
|
||
fetchUpdateProcessRoute,
|
||
fetchSetCurrentProcessRoute,
|
||
fetchDeleteProcessRoute,
|
||
fetchAddRouteStep,
|
||
fetchCopyRouteStep,
|
||
fetchUpdateRouteStep,
|
||
fetchReorderRouteSteps,
|
||
fetchDeleteRouteStep,
|
||
fetchUpdateStepReagents,
|
||
fetchUpdateStepConsumables,
|
||
fetchUpdateStepOutputs,
|
||
fetchUpdateStepDataCollections,
|
||
fetchAllProcesses,
|
||
fetchAllProcessCategories,
|
||
fetchAllSopDocuments,
|
||
fetchAllOutputTypes
|
||
} from '@/api/production'
|
||
import { fetchCellProductTypeList } from '@/api/admin'
|
||
|
||
defineOptions({ name: 'ProductionProcessRoute' })
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 列表页
|
||
// ---------------------------------------------------------------------------
|
||
const searchForm = reactive({
|
||
routeName: '',
|
||
routeCode: '',
|
||
productTypeId: ''
|
||
})
|
||
const tableData = ref<Record<string, any>[]>([])
|
||
const selectedRows = ref<Record<string, any>[]>([])
|
||
const loading = ref(false)
|
||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||
const productTypeOptions = ref<Record<string, any>[]>([])
|
||
|
||
const productTypeMap = computed(() => {
|
||
const map: Record<string, string> = {}
|
||
productTypeOptions.value.forEach((pt: Record<string, any>) => {
|
||
map[pt.id] = pt.productTypeName || ''
|
||
})
|
||
return map
|
||
})
|
||
|
||
const handleSelectionChange = (rows: Record<string, any>[]) => {
|
||
selectedRows.value = rows
|
||
}
|
||
|
||
async function loadProductTypes() {
|
||
try {
|
||
const res = await fetchCellProductTypeList({ page: 1, pageSize: 100 })
|
||
productTypeOptions.value = res.list || res || []
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
async function loadData() {
|
||
loading.value = true
|
||
try {
|
||
const res = await fetchProcessRouteList({
|
||
page: pagination.page,
|
||
pageSize: pagination.pageSize,
|
||
routeName: searchForm.routeName || undefined,
|
||
routeCode: searchForm.routeCode || undefined,
|
||
productTypeId: searchForm.productTypeId || undefined
|
||
})
|
||
tableData.value = res.list || []
|
||
pagination.total = res.total ?? 0
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleSearch() {
|
||
pagination.page = 1
|
||
loadData()
|
||
}
|
||
|
||
function handleReset() {
|
||
searchForm.routeName = ''
|
||
searchForm.routeCode = ''
|
||
searchForm.productTypeId = ''
|
||
pagination.page = 1
|
||
loadData()
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 新增 / 编辑弹窗
|
||
// ---------------------------------------------------------------------------
|
||
const dialogVisible = ref(false)
|
||
const dialogTitle = ref('新增路线')
|
||
const isEdit = ref(false)
|
||
const submitLoading = ref(false)
|
||
const formRef = ref()
|
||
const editingId = ref<string | null>(null)
|
||
|
||
const formData = reactive({
|
||
routeCode: '',
|
||
routeName: '',
|
||
productTypeId: '',
|
||
version: '',
|
||
remark: ''
|
||
})
|
||
|
||
const formRules = {
|
||
routeCode: [{ required: true, message: '请输入路线编码', trigger: 'blur' }],
|
||
routeName: [{ required: true, message: '请输入路线名称', trigger: 'blur' }]
|
||
}
|
||
|
||
function resetForm() {
|
||
formData.routeCode = ''
|
||
formData.routeName = ''
|
||
formData.productTypeId = ''
|
||
formData.version = ''
|
||
formData.remark = ''
|
||
editingId.value = null
|
||
}
|
||
|
||
function handleAdd() {
|
||
isEdit.value = false
|
||
dialogTitle.value = '新增路线'
|
||
resetForm()
|
||
dialogVisible.value = true
|
||
nextTick(() => formRef.value?.clearValidate?.())
|
||
}
|
||
|
||
function handleEdit(row: Record<string, any>) {
|
||
isEdit.value = true
|
||
dialogTitle.value = '编辑路线'
|
||
editingId.value = row.id
|
||
formData.routeCode = row.routeCode || ''
|
||
formData.routeName = row.routeName || ''
|
||
formData.productTypeId = row.productTypeId || ''
|
||
formData.version = row.version || ''
|
||
formData.remark = row.remark || ''
|
||
dialogVisible.value = true
|
||
nextTick(() => formRef.value?.clearValidate?.())
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
await formRef.value?.validate?.()
|
||
submitLoading.value = true
|
||
try {
|
||
const payload = {
|
||
routeCode: formData.routeCode.trim(),
|
||
routeName: formData.routeName.trim(),
|
||
productTypeId: formData.productTypeId || undefined,
|
||
version: formData.version?.trim() || undefined,
|
||
remark: formData.remark?.trim() || undefined
|
||
}
|
||
if (isEdit.value && editingId.value) {
|
||
await fetchUpdateProcessRoute(editingId.value, payload)
|
||
} else {
|
||
await fetchCreateProcessRoute(payload)
|
||
}
|
||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||
dialogVisible.value = false
|
||
await loadData()
|
||
} catch {
|
||
/* 校验或接口错误已由全局处理 */
|
||
} finally {
|
||
submitLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function handleSetCurrent(row: Record<string, any>) {
|
||
await ElMessageBox.confirm('确定将该路线设为当前版本吗?', '提示', { type: 'warning' })
|
||
await fetchSetCurrentProcessRoute(row.id)
|
||
ElMessage.success('设置成功')
|
||
await loadData()
|
||
}
|
||
|
||
async function handleDelete(row: Record<string, any>) {
|
||
await ElMessageBox.confirm('确定要删除该路线吗?删除后不可恢复。', '提示', { type: 'warning' })
|
||
await fetchDeleteProcessRoute(row.id)
|
||
ElMessage.success('删除成功')
|
||
await loadData()
|
||
}
|
||
|
||
async function handleBatchDelete() {
|
||
const ids = selectedRows.value.map((r) => r.id)
|
||
if (!ids.length) return
|
||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条路线吗?`, '提示', { type: 'warning' })
|
||
for (const id of ids) {
|
||
await fetchDeleteProcessRoute(id)
|
||
}
|
||
ElMessage.success('批量删除成功')
|
||
selectedRows.value = []
|
||
await loadData()
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 路线图配置弹窗
|
||
// ---------------------------------------------------------------------------
|
||
const configDialogVisible = ref(false)
|
||
const configRouteId = ref('')
|
||
const configRouteName = ref('')
|
||
const configRouteVersion = ref('')
|
||
/** 加载路线图详情期间禁止子表自动保存(避免 el-switch 挂载误触发 @change) */
|
||
const configSuppressSave = ref(false)
|
||
const stepList = ref<Record<string, any>[]>([])
|
||
const selectedStepId = ref('')
|
||
const stepListLoading = ref(false)
|
||
const activeSubTab = ref('reagent')
|
||
const processSearchKeyword = ref('')
|
||
|
||
/** 接口 isKeyMaterial / isRequired 为 "0" | "1" 字符串 */
|
||
function toApiFlag(val: boolean | number | string | undefined): string {
|
||
if (val === true || val === 1 || val === '1') return '1'
|
||
return '0'
|
||
}
|
||
|
||
function fromApiFlag(val: boolean | number | string | undefined): boolean {
|
||
return val === true || val === 1 || val === '1'
|
||
}
|
||
|
||
function normalizeStepSubTables(step: Record<string, any>) {
|
||
return {
|
||
...step,
|
||
reagents: (step.reagents || []).map((r: Record<string, any>) => ({
|
||
...r,
|
||
isKeyMaterial: fromApiFlag(r.isKeyMaterial)
|
||
})),
|
||
consumables: (step.consumables || []).map((c: Record<string, any>) => ({
|
||
...c,
|
||
isRequired: fromApiFlag(c.isRequired)
|
||
})),
|
||
outputs: step.outputs || [],
|
||
dataCollections: step.dataCollections || []
|
||
}
|
||
}
|
||
|
||
// 工艺库数据
|
||
const processLibrary = ref<Record<string, any>[]>([])
|
||
const expandedCategories = ref<string[]>([])
|
||
const chineseOrdinals = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十']
|
||
const categoryOptions = ref<Record<string, any>[]>([])
|
||
const sopOptions = ref<Record<string, any>[]>([])
|
||
const outputTypeOptions = ref<Record<string, any>[]>([])
|
||
|
||
const selectedStep = computed(() => {
|
||
if (!selectedStepId.value) return null
|
||
return stepList.value.find((s) => s.id === selectedStepId.value) || null
|
||
})
|
||
|
||
const sopIdsArr = computed({
|
||
get() {
|
||
if (!selectedStep.value) return []
|
||
const raw = selectedStep.value.sopIds
|
||
if (!raw) return []
|
||
if (Array.isArray(raw)) return raw.filter(Boolean)
|
||
return String(raw)
|
||
.split(',')
|
||
.map((s: string) => s.trim())
|
||
.filter(Boolean)
|
||
},
|
||
set(val: string[]) {
|
||
if (!selectedStep.value) return
|
||
selectedStep.value.sopIds = val.join(',')
|
||
}
|
||
})
|
||
|
||
function onSopIdsChange(val: string[]) {
|
||
if (!selectedStep.value) return
|
||
selectedStep.value.sopIds = val.join(',')
|
||
saveStepBasic()
|
||
}
|
||
|
||
// 按分类分组的工艺库(支持搜索过滤)
|
||
const filteredProcessLibrary = computed(() => {
|
||
const keyword = processSearchKeyword.value?.trim().toLowerCase() || ''
|
||
return processLibrary.value
|
||
.map((group) => {
|
||
const filtered = keyword
|
||
? group.processes.filter(
|
||
(p: Record<string, any>) =>
|
||
(p.processName || '').toLowerCase().includes(keyword) ||
|
||
(p.processCode || '').toLowerCase().includes(keyword)
|
||
)
|
||
: group.processes
|
||
return {
|
||
categoryId: group.categoryId,
|
||
categoryName: group.categoryName,
|
||
processes: filtered
|
||
}
|
||
})
|
||
.filter((group) => group.processes.length > 0)
|
||
})
|
||
|
||
async function loadConfigReferenceData() {
|
||
try {
|
||
const [processes, categories, sops, outputTypes] = await Promise.all([
|
||
fetchAllProcesses(),
|
||
fetchAllProcessCategories(),
|
||
fetchAllSopDocuments(),
|
||
fetchAllOutputTypes()
|
||
])
|
||
categoryOptions.value = categories || []
|
||
sopOptions.value = sops || []
|
||
outputTypeOptions.value = outputTypes || []
|
||
|
||
// 按分类分组
|
||
const catMap: Record<string, { categoryId: string; categoryName: string; processes: Record<string, any>[] }> = {}
|
||
const uncategorizedKey = '__uncategorized__'
|
||
catMap[uncategorizedKey] = { categoryId: uncategorizedKey, 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 || uncategorizedKey
|
||
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 {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
async function handleOpenConfig(row: Record<string, any>) {
|
||
configRouteId.value = row.id
|
||
configRouteName.value = row.routeName || ''
|
||
configRouteVersion.value = row.version || ''
|
||
selectedStepId.value = ''
|
||
activeSubTab.value = 'reagent'
|
||
processSearchKeyword.value = ''
|
||
configSuppressSave.value = true
|
||
configDialogVisible.value = true
|
||
|
||
try {
|
||
await loadConfigReferenceData()
|
||
await loadRouteDetail()
|
||
} finally {
|
||
await nextTick()
|
||
configSuppressSave.value = false
|
||
}
|
||
}
|
||
|
||
async function loadRouteDetail() {
|
||
try {
|
||
const detail = await fetchProcessRouteDetail(configRouteId.value)
|
||
stepList.value = (detail.steps || []).map((s: Record<string, any>) => normalizeStepSubTables(s))
|
||
if (stepList.value.length > 0 && !selectedStepId.value) {
|
||
selectedStepId.value = stepList.value[0].id
|
||
}
|
||
} catch {
|
||
stepList.value = []
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 拖拽:从工艺库拖入步骤列表
|
||
// ---------------------------------------------------------------------------
|
||
function onDragStart(event: DragEvent, proc: Record<string, any>) {
|
||
event.dataTransfer?.setData('processId', proc.id)
|
||
event.dataTransfer!.effectAllowed = 'copy'
|
||
}
|
||
|
||
async function onDropToCenter(event: DragEvent) {
|
||
const processId = event.dataTransfer?.getData('processId')
|
||
if (!processId || !configRouteId.value) return
|
||
stepListLoading.value = true
|
||
try {
|
||
const newStep = await fetchAddRouteStep({
|
||
routeId: configRouteId.value,
|
||
processId
|
||
})
|
||
await loadRouteDetail()
|
||
if (newStep?.id) {
|
||
selectedStepId.value = newStep.id
|
||
}
|
||
ElMessage.success('步骤添加成功')
|
||
} catch {
|
||
/* 接口错误已由全局处理 */
|
||
} finally {
|
||
stepListLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 步骤排序
|
||
// ---------------------------------------------------------------------------
|
||
async function onStepDragEnd() {
|
||
if (!configRouteId.value) return
|
||
const stepIds = stepList.value.map((s) => s.id)
|
||
stepListLoading.value = true
|
||
try {
|
||
await fetchReorderRouteSteps({ routeId: configRouteId.value, stepIds })
|
||
} catch {
|
||
/* ignore */
|
||
} finally {
|
||
stepListLoading.value = false
|
||
}
|
||
}
|
||
|
||
function selectStep(step: Record<string, any>) {
|
||
selectedStepId.value = step.id
|
||
}
|
||
|
||
async function handleCopyStep(step: Record<string, any>) {
|
||
stepListLoading.value = true
|
||
try {
|
||
const newStep = await fetchCopyRouteStep({ stepId: step.id })
|
||
await loadRouteDetail()
|
||
if (newStep?.id) {
|
||
selectedStepId.value = newStep.id
|
||
}
|
||
ElMessage.success('步骤已复制')
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.message || '复制失败')
|
||
} finally {
|
||
stepListLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function handleDeleteStep(step: Record<string, any>, index: number) {
|
||
await ElMessageBox.confirm('确定删除该步骤吗?', '提示', { type: 'warning' })
|
||
stepListLoading.value = true
|
||
try {
|
||
await fetchDeleteRouteStep(step.id)
|
||
await loadRouteDetail()
|
||
if (selectedStepId.value === step.id) {
|
||
selectedStepId.value = stepList.value.length > 0 ? stepList.value[0].id : ''
|
||
}
|
||
ElMessage.success('步骤已删除')
|
||
} catch {
|
||
/* ignore */
|
||
} finally {
|
||
stepListLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 步骤基本信息实时保存
|
||
// ---------------------------------------------------------------------------
|
||
async function saveStepBasic() {
|
||
if (!selectedStep.value) return
|
||
try {
|
||
await fetchUpdateRouteStep({
|
||
stepId: selectedStep.value.id,
|
||
processName: selectedStep.value.processName,
|
||
categoryId: selectedStep.value.categoryId,
|
||
description: selectedStep.value.description,
|
||
equipmentName: selectedStep.value.equipmentName,
|
||
estimatedDuration: selectedStep.value.estimatedDuration,
|
||
sopIds: selectedStep.value.sopIds
|
||
})
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 子表操作
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// --- 试剂 ---
|
||
function addReagentRow() {
|
||
if (!selectedStep.value) return
|
||
if (!selectedStep.value.reagents) selectedStep.value.reagents = []
|
||
selectedStep.value.reagents.push({
|
||
reagentCode: '',
|
||
reagentName: '',
|
||
spec: '',
|
||
standardUsage: '',
|
||
unit: '',
|
||
isKeyMaterial: false,
|
||
remark: ''
|
||
})
|
||
saveReagents()
|
||
}
|
||
|
||
function removeReagentRow(index: number) {
|
||
selectedStep.value?.reagents?.splice(index, 1)
|
||
saveReagents()
|
||
}
|
||
|
||
async function saveReagents() {
|
||
if (!selectedStep.value || configSuppressSave.value) return
|
||
try {
|
||
await fetchUpdateStepReagents({
|
||
stepId: selectedStep.value.id,
|
||
items: (selectedStep.value.reagents || []).map((r: Record<string, any>) => ({
|
||
reagentCode: r.reagentCode,
|
||
reagentName: r.reagentName,
|
||
spec: r.spec,
|
||
standardUsage: r.standardUsage,
|
||
unit: r.unit,
|
||
isKeyMaterial: toApiFlag(r.isKeyMaterial),
|
||
remark: r.remark || ''
|
||
}))
|
||
})
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
// --- 耗材 ---
|
||
function addConsumableRow() {
|
||
if (!selectedStep.value) return
|
||
if (!selectedStep.value.consumables) selectedStep.value.consumables = []
|
||
selectedStep.value.consumables.push({
|
||
consumableCode: '',
|
||
consumableName: '',
|
||
specModel: '',
|
||
standardUsage: '',
|
||
unit: '',
|
||
isRequired: false,
|
||
remark: ''
|
||
})
|
||
saveConsumables()
|
||
}
|
||
|
||
function removeConsumableRow(index: number) {
|
||
selectedStep.value?.consumables?.splice(index, 1)
|
||
saveConsumables()
|
||
}
|
||
|
||
async function saveConsumables() {
|
||
if (!selectedStep.value || configSuppressSave.value) return
|
||
try {
|
||
await fetchUpdateStepConsumables({
|
||
stepId: selectedStep.value.id,
|
||
items: (selectedStep.value.consumables || []).map((c: Record<string, any>) => ({
|
||
consumableCode: c.consumableCode,
|
||
consumableName: c.consumableName,
|
||
specModel: c.specModel,
|
||
standardUsage: c.standardUsage,
|
||
unit: c.unit,
|
||
isRequired: toApiFlag(c.isRequired),
|
||
remark: c.remark || ''
|
||
}))
|
||
})
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
// --- 产出物 ---
|
||
function addOutputRow() {
|
||
if (!selectedStep.value) return
|
||
if (!selectedStep.value.outputs) selectedStep.value.outputs = []
|
||
selectedStep.value.outputs.push({
|
||
outputTypeId: '',
|
||
outputName: '',
|
||
outputBarcode: '',
|
||
remark: ''
|
||
})
|
||
saveOutputs()
|
||
}
|
||
|
||
function removeOutputRow(index: number) {
|
||
selectedStep.value?.outputs?.splice(index, 1)
|
||
saveOutputs()
|
||
}
|
||
|
||
async function saveOutputs() {
|
||
if (!selectedStep.value || configSuppressSave.value) return
|
||
try {
|
||
await fetchUpdateStepOutputs({
|
||
stepId: selectedStep.value.id,
|
||
items: (selectedStep.value.outputs || []).map((o: Record<string, any>) => ({
|
||
outputTypeId: o.outputTypeId,
|
||
outputName: o.outputName,
|
||
outputBarcode: o.outputBarcode,
|
||
remark: o.remark || ''
|
||
}))
|
||
})
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
// --- 数据采集 ---
|
||
function addDataCollectionRow() {
|
||
if (!selectedStep.value) return
|
||
if (!selectedStep.value.dataCollections) selectedStep.value.dataCollections = []
|
||
selectedStep.value.dataCollections.push({
|
||
collectionName: '',
|
||
unit: '',
|
||
orderNum: 0,
|
||
remark: ''
|
||
})
|
||
saveDataCollections()
|
||
}
|
||
|
||
function removeDataCollectionRow(index: number) {
|
||
selectedStep.value?.dataCollections?.splice(index, 1)
|
||
saveDataCollections()
|
||
}
|
||
|
||
async function saveDataCollections() {
|
||
if (!selectedStep.value || configSuppressSave.value) return
|
||
try {
|
||
await fetchUpdateStepDataCollections({
|
||
stepId: selectedStep.value.id,
|
||
items: (selectedStep.value.dataCollections || []).map((d: Record<string, any>) => ({
|
||
collectionName: d.collectionName,
|
||
unit: d.unit,
|
||
orderNum: d.orderNum ?? 0,
|
||
remark: d.remark || ''
|
||
}))
|
||
})
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 初始化
|
||
// ---------------------------------------------------------------------------
|
||
onMounted(() => {
|
||
loadData()
|
||
loadProductTypes()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.route-dialog-form {
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
/* 路线图配置弹窗 */
|
||
.route-config-body {
|
||
display: flex;
|
||
gap: 16px;
|
||
height: calc(90vh - 120px);
|
||
min-height: 500px;
|
||
}
|
||
|
||
/* 左侧面板:工艺库 */
|
||
.route-config-left {
|
||
width: 280px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.route-config-left-header {
|
||
padding: 12px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
flex-shrink: 0;
|
||
|
||
h3 {
|
||
margin: 0 0 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
}
|
||
}
|
||
|
||
.route-config-search {
|
||
width: 100%;
|
||
}
|
||
|
||
.route-config-collapse {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
border: none;
|
||
|
||
:deep(.el-collapse-item__header) {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
background: #ecfdf5;
|
||
padding: 0 12px;
|
||
height: 36px;
|
||
line-height: 36px;
|
||
}
|
||
|
||
:deep(.el-collapse-item__wrap) {
|
||
border-bottom: none;
|
||
}
|
||
|
||
:deep(.el-collapse-item__content) {
|
||
padding: 4px 8px 8px;
|
||
}
|
||
}
|
||
|
||
.route-config-cat-bar {
|
||
display: inline-block;
|
||
width: 3px;
|
||
height: 14px;
|
||
background: #10b981;
|
||
border-radius: 2px;
|
||
margin-right: 8px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.route-config-process-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 8px 8px 10px;
|
||
margin-bottom: 4px;
|
||
background: #fff;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 6px;
|
||
cursor: grab;
|
||
transition: all 0.15s ease;
|
||
|
||
&:hover {
|
||
border-color: #10b981;
|
||
box-shadow: 0 2px 4px rgba(16, 185, 129, 0.12);
|
||
}
|
||
|
||
&:active {
|
||
cursor: grabbing;
|
||
}
|
||
}
|
||
|
||
.route-config-process-name {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #1f2937;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.route-config-process-seq {
|
||
color: #1f2937;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
margin-right: 2px;
|
||
}
|
||
|
||
.route-config-process-code {
|
||
font-size: 11px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.route-config-process-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.route-config-process-drag {
|
||
flex-shrink: 0;
|
||
color: #d1d5db;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 中间面板:步骤列表 */
|
||
.route-config-center {
|
||
width: 640px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.route-config-center-header {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-shrink: 0;
|
||
|
||
h3 {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
}
|
||
}
|
||
|
||
.route-config-step-count {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.route-config-center-dropzone {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 12px;
|
||
}
|
||
|
||
.route-config-center-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
color: #9ca3af;
|
||
|
||
p {
|
||
margin-top: 8px;
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
.route-config-step-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
margin-bottom: 6px;
|
||
background: #fff;
|
||
border: 2px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
|
||
&:hover {
|
||
border-color: #6ee7b7;
|
||
}
|
||
|
||
&.is-active {
|
||
border-color: #10b981;
|
||
background: #ecfdf5;
|
||
}
|
||
}
|
||
|
||
.route-config-step-handle {
|
||
cursor: grab;
|
||
color: #9ca3af;
|
||
flex-shrink: 0;
|
||
font-size: 16px;
|
||
|
||
&:active {
|
||
cursor: grabbing;
|
||
}
|
||
}
|
||
|
||
.route-config-step-order {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
background: #10b981;
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.route-config-step-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.route-config-step-name {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #1f2937;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.route-config-step-desc {
|
||
font-size: 11px;
|
||
color: #9ca3af;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 右侧面板:步骤详情 */
|
||
.route-config-right {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.route-config-right-header {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
flex-shrink: 0;
|
||
|
||
h3 {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
}
|
||
}
|
||
|
||
.route-config-right-body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
.route-config-right-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
color: #9ca3af;
|
||
|
||
p {
|
||
margin-top: 12px;
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
.route-config-step-form {
|
||
margin-bottom: 12px;
|
||
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 8px;
|
||
}
|
||
}
|
||
|
||
.route-config-tabs {
|
||
:deep(.el-tabs__header) {
|
||
margin-bottom: 8px;
|
||
}
|
||
}
|
||
|
||
.route-config-tab-toolbar {
|
||
margin-bottom: 8px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
/* 路线图配置弹窗内的对话框滚动区域 */
|
||
.route-config-dialog {
|
||
:deep(.el-dialog__body) {
|
||
max-height: calc(90vh - 60px);
|
||
overflow: hidden;
|
||
padding: 16px;
|
||
}
|
||
}
|
||
</style>
|