Files
klp-oa/klp-ui/src/views/wms/processSpec/planSpec.vue
王文昊 5a56094e4f refactor(dict): 优化字典数据查询逻辑和接口
- 移除不必要的 ISysDictTypeService 依赖,简化 SysDictDataController
- 新增 selectDictDataByTypeRealtime 方法,支持实时查询字典数据,避免缓存问题
- 更新 SysDictDataController 中的字典数据查询逻辑,使用新方法
- 在 SysDictTypeController 中添加按字典类型编码精确查询的接口
- 更新前端组件以支持新的字典查询接口,优化字典选择器的加载逻辑
2026-04-28 19:12:50 +08:00

681 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="plan-spec-page" v-loading="pageLoading">
<!-- 头部 -->
<div class="page-header">
<el-button type="text" icon="el-icon-arrow-left" @click="goBack">返回</el-button>
<span class="page-title">方案详情</span>
<span v-if="versionCode" class="version-badge">版本 {{ versionCode }}</span>
</div>
<!-- 可配置 / 不可配置 切换 -->
<div class="config-tabs">
<span
:class="['config-tab', { active: configMode === 'configurable' }]"
@click="configMode = 'configurable'"
>可配置</span>
<span
:class="['config-tab', { active: configMode === 'readonly' }]"
@click="configMode = 'readonly'"
>不可配置</span>
</div>
<div class="main-layout">
<!-- 左侧段分组 -->
<div class="left-tree">
<div
:class="['tree-item', { active: activeSegmentType === '' }]"
@click="selectSegmentType('')"
>全部</div>
<div
v-for="seg in segmentTypeTabOptions"
:key="String(seg.value)"
:class="['tree-item', { active: String(activeSegmentType) === String(seg.value) }]"
@click="selectSegmentType(seg.value)"
>
<span class="tree-arrow"></span> {{ seg.label }}
</div>
</div>
<!-- 右侧内容 -->
<div class="right-content">
<!-- 搜索栏 -->
<div class="search-bar">
<template v-if="activeSegmentType !== '' && activeSegmentType !== undefined && activeSegmentType !== null">
<span class="search-label">段名称</span>
<el-select
v-model="activeSegmentName"
placeholder="请选择"
size="small"
clearable
style="width:180px"
@change="onSegmentNameFilterChange"
>
<el-option
v-for="opt in segmentNameFilterOptions"
:key="String(opt.value) + '-' + opt.label"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</template>
<span class="search-label">点位名称</span>
<el-input
v-model="filterName"
placeholder="请输入"
size="small"
style="width:200px"
clearable
@keyup.enter.native="applyFilter"
/>
<el-button size="mini" type="primary" @click="applyFilter">查询</el-button>
<el-button size="mini" @click="resetFilter">重置</el-button>
<el-button
size="mini"
type="primary"
icon="el-icon-plus"
style="margin-left:auto"
@click="openPlanDialog()"
>新建方案点位</el-button>
</div>
<!-- 方案点位表 -->
<el-table
v-loading="planLoading"
:data="filteredPlans"
size="small"
highlight-current-row
@current-change="onPlanSelect"
>
<el-table-column label="序号" type="index" align="center" />
<el-table-column label="父级名称" show-overflow-tooltip>
<template slot-scope="{ row }">{{ segLabel(row.segmentType) }} {{ row.segmentName || '—' }}</template>
</el-table-column>
<el-table-column label="点位名称" prop="pointName" show-overflow-tooltip />
<el-table-column label="实际值ID" prop="actualValueId" show-overflow-tooltip />
<el-table-column label="L1设定值ID" prop="l1SetValueId" show-overflow-tooltip />
<el-table-column label="设定值" prop="targetValue" align="center" />
<el-table-column label="下限" prop="lowerLimit" align="center" />
<el-table-column label="上限" prop="upperLimit" align="center" />
<el-table-column label="操作" align="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click.stop="openPlanDialog(row)">编辑</el-button>
<el-button type="text" size="mini" @click.stop="openParamDialog(row)">参数</el-button>
<el-button type="text" size="mini" class="btn-danger" @click.stop="removePlan(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 方案参数面板 -->
<template v-if="selectedPlan">
<div class="param-header">
<span>{{ selectedPlan.pointName || selectedPlan.pointCode }} 参数</span>
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openParamDialog()">新建参数</el-button>
</div>
<el-table v-loading="paramLoading" :data="paramList" size="small">
<el-table-column label="参数编码" prop="paramCode" show-overflow-tooltip />
<el-table-column label="参数名称" prop="paramName" show-overflow-tooltip />
<el-table-column label="设定值" prop="targetValue" align="center" />
<el-table-column label="下限" prop="lowerLimit" align="center" />
<el-table-column label="上限" prop="upperLimit" align="center" />
<el-table-column label="单位" prop="unit" align="center" />
<el-table-column label="操作" align="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click="openParamDialog(null, row)">编辑</el-button>
<el-button type="text" size="mini" class="btn-danger" @click="removeParam(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
</div>
</div>
<!-- 方案点位 dialog -->
<el-dialog :title="planTitle" :visible.sync="planOpen" width="520px" append-to-body @close="planForm = {}">
<el-form ref="planFormRef" :model="planForm" :rules="planRules" label-width="90px" size="small">
<el-form-item label="段类型" prop="segmentType">
<el-select v-model="planForm.segmentType" style="width:100%">
<el-option v-for="s in segmentFormOptionsForDialog" :key="String(s.value)" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="段名称" prop="segmentName">
<el-input v-model="planForm.segmentName" maxlength="100" />
</el-form-item>
<el-form-item label="点位名称" prop="pointName">
<el-input v-model="planForm.pointName" maxlength="200" />
</el-form-item>
<el-form-item label="点位编码" prop="pointCode">
<el-input v-model="planForm.pointCode" maxlength="64" />
</el-form-item>
<el-form-item label="实际值ID" prop="actualValueId">
<el-input v-model="planForm.actualValueId" maxlength="64" />
</el-form-item>
<el-form-item label="L1设定值ID" prop="l1SetValueId">
<el-input v-model="planForm.l1SetValueId" maxlength="64" />
</el-form-item>
<el-form-item label="设定值" prop="targetValue">
<el-input v-model="planForm.targetValue" />
</el-form-item>
<el-form-item label="下限" prop="lowerLimit">
<el-input v-model="planForm.lowerLimit" />
</el-form-item>
<el-form-item label="上限" prop="upperLimit">
<el-input v-model="planForm.upperLimit" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="planForm.sortOrder" :min="0" controls-position="right" style="width:100%" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="planForm.remark" type="textarea" rows="2" maxlength="500" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button size="small" @click="planOpen = false">取消</el-button>
<el-button size="small" type="primary" :loading="planSubmitLoading" @click="submitPlan">确定</el-button>
</div>
</el-dialog>
<!-- 方案参数 dialog -->
<el-dialog :title="paramTitle" :visible.sync="paramOpen" width="480px" append-to-body @close="paramForm = {}">
<el-form ref="paramFormRef" :model="paramForm" :rules="paramRules" label-width="90px" size="small">
<el-form-item label="参数编码" prop="paramCode">
<el-input v-model="paramForm.paramCode" maxlength="64" />
</el-form-item>
<el-form-item label="参数名称" prop="paramName">
<el-input v-model="paramForm.paramName" maxlength="200" />
</el-form-item>
<el-form-item label="设定值">
<el-input v-model="paramForm.targetValue" />
</el-form-item>
<el-form-item label="下限">
<el-input v-model="paramForm.lowerLimit" />
</el-form-item>
<el-form-item label="上限">
<el-input v-model="paramForm.upperLimit" />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="paramForm.unit" maxlength="32" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="paramForm.remark" type="textarea" rows="2" maxlength="500" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button size="small" @click="paramOpen = false">取消</el-button>
<el-button size="small" type="primary" :loading="paramSubmitLoading" @click="submitParam">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listProcessPlan, addProcessPlan, updateProcessPlan, delProcessPlan } from '@/api/wms/processPlan'
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam, delProcessPlanParam } from '@/api/wms/processPlanParam'
/** 表单内可选段类型(新建/编辑仍支持全部枚举) */
const SEGMENT_FORM_OPTIONS = [
{ label: '入口段', value: 'INLET' },
{ label: '工艺段', value: 'PROCESS' },
{ label: '出口段', value: 'OUTLET' }
]
export default {
name: 'ProcessSpecPlanSpec',
data() {
return {
pageLoading: false,
versionId: undefined,
versionCode: '',
specId: undefined,
configMode: 'configurable',
/** 左侧:段类型;空=全部 */
activeSegmentType: '',
/** 工具栏下拉:当前段类型下的段名称;空字符串=该段类型下全部名称__EMPTY__=未命名 */
activeSegmentName: '',
filterName: '',
appliedFilterName: '',
planList: [],
planLoading: false,
selectedPlan: null,
paramList: [],
paramLoading: false,
planOpen: false,
planTitle: '',
planSubmitLoading: false,
planForm: {},
planRules: {
segmentType: [{ required: true, message: '请选择段类型', trigger: 'change' }],
pointName: [{ required: true, message: '点位名称不能为空', trigger: 'blur' }],
pointCode: [{ required: true, message: '点位编码不能为空', trigger: 'blur' }]
},
paramOpen: false,
paramTitle: '',
paramSubmitLoading: false,
paramForm: {},
paramRules: {
paramCode: [{ required: true, message: '参数编码不能为空', trigger: 'blur' }],
paramName: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }]
}
}
},
computed: {
/**
* 左侧 Tab仅从点位数据汇总「段类型」展示用语义名称入口段/工艺段/出口段)
*/
segmentTypeTabOptions() {
const orderIdx = {}
SEGMENT_FORM_OPTIONS.forEach((s, i) => { orderIdx[String(s.value)] = i })
const seen = new Set()
const list = []
for (const plan of this.planList) {
const t = plan.segmentType
if (t === undefined || t === null || String(t).trim() === '') continue
const key = String(t)
if (seen.has(key)) continue
seen.add(key)
list.push({
value: plan.segmentType,
label: this.segmentTypeDisplayLabel(t)
})
}
list.sort((a, b) => {
const ia = orderIdx[String(a.value)]
const ib = orderIdx[String(b.value)]
const aKnown = ia !== undefined
const bKnown = ib !== undefined
if (aKnown && bKnown) return ia - ib
if (aKnown) return -1
if (bKnown) return 1
return String(a.label).localeCompare(String(b.label), 'zh-CN')
})
return list
},
/** 当前选中段类型下,出现于数据中的段名称下拉项 */
segmentNameFilterOptions() {
const t = this.activeSegmentType
if (t === '' || t === undefined || t === null) return [{ value: '', label: '全部' }]
const names = new Set()
let hasEmpty = false
for (const p of this.planList) {
if (String(p.segmentType) !== String(t)) continue
const sn = p.segmentName != null ? String(p.segmentName).trim() : ''
if (!sn) hasEmpty = true
else names.add(sn)
}
const sorted = [...names].sort((a, b) => a.localeCompare(b, 'zh-CN'))
const opts = [{ value: '', label: '全部' }]
if (hasEmpty) opts.push({ value: '__EMPTY__', label: '(未命名)' })
sorted.forEach(n => opts.push({ value: n, label: n }))
return opts
},
/** 新建/编辑下拉:标准三项 + 当前方案中已出现的其它段类型 */
segmentFormOptionsForDialog() {
const seen = new Set(SEGMENT_FORM_OPTIONS.map(s => String(s.value)))
const extra = []
for (const plan of this.planList) {
const v = plan.segmentType
if (v === undefined || v === null || String(v).trim() === '') continue
const key = String(v)
if (seen.has(key)) continue
seen.add(key)
const name = plan.segmentName != null ? String(plan.segmentName).trim() : ''
extra.push({
value: plan.segmentType,
label: name || this.segmentTypeDisplayLabel(v)
})
}
extra.sort((a, b) => String(a.label).localeCompare(String(b.label), 'zh-CN'))
return [...SEGMENT_FORM_OPTIONS, ...extra]
},
filteredPlans() {
const type = this.activeSegmentType
const sub = this.activeSegmentName
return this.planList.filter(p => {
const typeOk =
type === '' || type === undefined || type === null
? true
: String(p.segmentType) === String(type)
let nameOkSeg = true
if (typeOk && type !== '' && type !== undefined && type !== null) {
if (sub === '' || sub === undefined || sub === null) {
nameOkSeg = true
} else if (sub === '__EMPTY__') {
const sn = p.segmentName != null ? String(p.segmentName).trim() : ''
nameOkSeg = !sn
} else {
const sn = p.segmentName != null ? String(p.segmentName).trim() : ''
nameOkSeg = sn === String(sub).trim()
}
}
const nameOk = !this.appliedFilterName || (p.pointName || '').includes(this.appliedFilterName)
return typeOk && nameOkSeg && nameOk
})
}
},
watch: {
$route: { immediate: true, handler() { this.syncFromRoute() } },
segmentTypeTabOptions: {
handler() {
const a = this.activeSegmentType
if (a === '' || a === undefined || a === null) return
const still = this.segmentTypeTabOptions.some(s => String(s.value) === String(a))
if (!still) {
this.activeSegmentType = ''
this.activeSegmentName = ''
}
}
},
planList: {
handler() {
this.$nextTick(() => {
if (this.activeSegmentType === '' || this.activeSegmentType === undefined || this.activeSegmentType === null) return
const allowed = this.segmentNameFilterOptions.map(o => o.value)
const cur = this.activeSegmentName
if (cur !== '' && cur !== undefined && cur !== null && !allowed.includes(cur)) {
this.activeSegmentName = ''
}
})
},
deep: true
}
},
methods: {
syncFromRoute() {
const q = this.$route.query
this.versionId = q.versionId || undefined
this.versionCode = q.versionCode || ''
this.specId = q.specId || undefined
if (this.versionId) this.loadPlans()
},
goBack() { this.$router.go(-1) },
selectSegmentType(val) {
this.activeSegmentType = val === undefined || val === null ? '' : val
this.activeSegmentName = ''
},
onSegmentNameFilterChange() {
const allowed = this.segmentNameFilterOptions.map(o => o.value)
if (!allowed.includes(this.activeSegmentName)) {
this.activeSegmentName = ''
}
},
segmentTypeDisplayLabel(segmentType) {
const hit = SEGMENT_FORM_OPTIONS.find(s => String(s.value) === String(segmentType))
return hit ? hit.label : (segmentType != null ? String(segmentType) : '')
},
segLabel(val) {
return this.segmentTypeDisplayLabel(val)
},
loadPlans() {
this.planLoading = true
this.selectedPlan = null
this.paramList = []
listProcessPlan({ versionId: this.versionId, pageNum: 1, pageSize: 500 }).then(res => {
this.planList = res.rows || []
}).catch(e => console.error(e)).finally(() => { this.planLoading = false })
},
loadParams(planId) {
this.paramLoading = true
listProcessPlanParam({ planId, pageNum: 1, pageSize: 500 }).then(res => {
this.paramList = res.rows || []
}).catch(e => console.error(e)).finally(() => { this.paramLoading = false })
},
onPlanSelect(row) {
if (!row) return
this.selectedPlan = row
this.loadParams(row.planId)
},
applyFilter() { this.appliedFilterName = this.filterName },
resetFilter() {
this.filterName = ''
this.appliedFilterName = ''
this.activeSegmentName = ''
},
openPlanDialog(row) {
this.planForm = row
? { ...row }
: {
versionId: this.versionId,
segmentType: 'PROCESS',
segmentName: undefined,
pointName: undefined,
pointCode: undefined,
actualValueId: undefined,
l1SetValueId: undefined,
targetValue: undefined,
lowerLimit: undefined,
upperLimit: undefined,
sortOrder: 0,
remark: undefined
}
this.planTitle = row ? '编辑方案点位' : '新建方案点位'
this.planOpen = true
this.$nextTick(() => this.$refs.planFormRef && this.$refs.planFormRef.clearValidate())
},
submitPlan() {
this.$refs.planFormRef.validate(ok => {
if (!ok) return
this.planSubmitLoading = true
const req = this.planForm.planId ? updateProcessPlan(this.planForm) : addProcessPlan(this.planForm)
req.then(() => {
this.$modal.msgSuccess('保存成功')
this.planOpen = false
this.loadPlans()
}).catch(e => console.error(e)).finally(() => { this.planSubmitLoading = false })
})
},
removePlan(row) {
this.$modal.confirm('确认删除该方案点位?').then(() => {
return delProcessPlan(row.planId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
if (this.selectedPlan && this.selectedPlan.planId === row.planId) {
this.selectedPlan = null
this.paramList = []
}
this.loadPlans()
}).catch(() => {})
},
openParamDialog(planRow, paramRow) {
const targetPlan = planRow || this.selectedPlan
if (!targetPlan) { this.$message.warning('请先选择一个方案点位'); return }
if (!this.selectedPlan || this.selectedPlan.planId !== targetPlan.planId) {
this.selectedPlan = targetPlan
this.loadParams(targetPlan.planId)
}
this.paramForm = paramRow
? { ...paramRow }
: {
planId: targetPlan.planId,
paramCode: undefined,
paramName: undefined,
targetValue: undefined,
lowerLimit: undefined,
upperLimit: undefined,
unit: undefined,
remark: undefined
}
this.paramTitle = paramRow ? '编辑方案参数' : '新建方案参数'
this.paramOpen = true
this.$nextTick(() => this.$refs.paramFormRef && this.$refs.paramFormRef.clearValidate())
},
submitParam() {
this.$refs.paramFormRef.validate(ok => {
if (!ok) return
this.paramSubmitLoading = true
const req = this.paramForm.paramId ? updateProcessPlanParam(this.paramForm) : addProcessPlanParam(this.paramForm)
req.then(() => {
this.$modal.msgSuccess('保存成功')
this.paramOpen = false
this.loadParams(this.selectedPlan.planId)
}).catch(e => console.error(e)).finally(() => { this.paramSubmitLoading = false })
})
},
removeParam(row) {
this.$modal.confirm('确认删除该参数?').then(() => {
return delProcessPlanParam(row.paramId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
this.loadParams(this.selectedPlan.planId)
}).catch(() => {})
}
}
}
</script>
<style scoped>
.plan-spec-page {
padding: 16px 20px;
min-height: 100%;
}
.page-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.page-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.version-badge {
font-size: 12px;
color: #909399;
background: #f0f2f5;
padding: 2px 8px;
border-radius: 10px;
}
.config-tabs {
display: flex;
gap: 0;
margin-bottom: 14px;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
width: fit-content;
}
.config-tab {
padding: 5px 20px;
font-size: 12px;
cursor: pointer;
color: #606266;
background: #fff;
border: none;
border-right: 1px solid #dcdfe6;
transition: color .15s, background .15s;
user-select: none;
line-height: 1;
}
.config-tab:last-child { border-right: none; }
.config-tab:hover { color: #5F7BA0; }
.config-tab.active {
color: #fff;
background: #5F7BA0;
}
.main-layout {
display: flex;
gap: 0;
background: #fff;
border-radius: 4px;
border: 1px solid #ebeef5;
overflow: hidden;
}
.left-tree {
width: 120px;
flex-shrink: 0;
border-right: 1px solid #ebeef5;
padding: 8px 0;
}
.tree-item {
padding: 9px 16px;
font-size: 13px;
cursor: pointer;
color: #606266;
transition: all .15s;
user-select: none;
}
.tree-item:hover { background: #f5f7fa; }
.tree-item.active {
background: #5F7BA0;
color: #fff;
font-weight: 500;
}
.tree-item.active .tree-arrow { color: rgba(255,255,255,.6); }
.tree-arrow {
margin-right: 4px;
color: #c0c4cc;
}
.right-content {
flex: 1;
min-width: 0;
padding: 12px;
}
.search-bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.search-label {
font-size: 13px;
color: #606266;
white-space: nowrap;
}
.param-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0 8px;
margin-top: 12px;
border-top: 1px solid #ebeef5;
font-size: 13px;
font-weight: 600;
color: #606266;
}
.el-table { border-radius: 0; }
::v-deep .el-button--primary {
color: #fff !important;
background: #5F7BA0 !important;
border-color: #5F7BA0 !important;
}
::v-deep .el-button--primary:hover,
::v-deep .el-button--primary:focus { background: #4d6a8e !important; border-color: #4d6a8e !important; }
::v-deep .el-button--primary:active { background: #4a6585 !important; border-color: #4a6585 !important; }
::v-deep .el-button--primary.is-disabled { opacity: .5; }
::v-deep .el-button:not(.el-button--primary):not(.el-button--text):not(.el-button--danger) {
color: #606266 !important;
background: #fff !important;
border-color: #dcdfe6 !important;
}
::v-deep .el-button:not(.el-button--primary):not(.el-button--text):not(.el-button--danger):hover {
color: #5F7BA0 !important;
border-color: #5F7BA0 !important;
}
::v-deep .el-button--text { background: transparent !important; border-color: transparent !important; }
::v-deep .el-button--text.btn-danger { color: #f56c6c !important; }
.btn-danger { color: #f56c6c; }
</style>