refactor(dict): 优化字典数据查询逻辑和接口

- 移除不必要的 ISysDictTypeService 依赖,简化 SysDictDataController
- 新增 selectDictDataByTypeRealtime 方法,支持实时查询字典数据,避免缓存问题
- 更新 SysDictDataController 中的字典数据查询逻辑,使用新方法
- 在 SysDictTypeController 中添加按字典类型编码精确查询的接口
- 更新前端组件以支持新的字典查询接口,优化字典选择器的加载逻辑
This commit is contained in:
王文昊
2026-04-28 19:12:50 +08:00
parent dde947516d
commit 5a56094e4f
11 changed files with 541 additions and 100 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}

View File

@@ -11,7 +11,6 @@ import com.klp.common.core.page.TableDataInfo;
import com.klp.common.enums.BusinessType; import com.klp.common.enums.BusinessType;
import com.klp.common.utils.poi.ExcelUtil; import com.klp.common.utils.poi.ExcelUtil;
import com.klp.system.service.ISysDictDataService; import com.klp.system.service.ISysDictDataService;
import com.klp.system.service.ISysDictTypeService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -32,7 +31,6 @@ import java.util.List;
public class SysDictDataController extends BaseController { public class SysDictDataController extends BaseController {
private final ISysDictDataService dictDataService; private final ISysDictDataService dictDataService;
private final ISysDictTypeService dictTypeService;
/** /**
* 查询字典数据列表 * 查询字典数据列表
@@ -72,7 +70,7 @@ public class SysDictDataController extends BaseController {
*/ */
@GetMapping(value = "/type/{dictType}") @GetMapping(value = "/type/{dictType}")
public R<List<SysDictData>> dictType(@PathVariable("dictType") String dictType) { public R<List<SysDictData>> dictType(@PathVariable("dictType") String dictType) {
List<SysDictData> data = dictTypeService.selectDictDataByType(dictType); List<SysDictData> data = dictDataService.selectDictDataByTypeRealtime(dictType);
if (ObjectUtil.isNull(data)) { if (ObjectUtil.isNull(data)) {
data = new ArrayList<>(); data = new ArrayList<>();
} }

View File

@@ -50,6 +50,14 @@ public class SysDictTypeController extends BaseController {
ExcelUtil.exportExcel(list, "字典类型", SysDictType.class, response); ExcelUtil.exportExcel(list, "字典类型", SysDictType.class, response);
} }
/**
* 按字典类型编码精确查询(避免 /list 条件对 dict_type 使用 LIKE 时下划线通配问题;供页内齿轮等使用)
*/
@GetMapping("/byType/{dictType}")
public R<SysDictType> getByDictType(@PathVariable String dictType) {
return R.ok(dictTypeService.selectDictTypeByType(dictType));
}
/** /**
* 查询字典类型详细 * 查询字典类型详细
* *

View File

@@ -41,6 +41,11 @@ public interface ISysDictDataService {
*/ */
SysDictData selectDictDataById(Long dictCode); SysDictData selectDictDataById(Long dictCode);
/**
* 按字典类型查询字典数据(直查库)。用于 HTTP `/dict/data/type/{type}`,避免 Spring Cache 在无界面操作灌数后仍返回空。
*/
List<SysDictData> selectDictDataByTypeRealtime(String dictType);
/** /**
* 批量删除字典数据信息 * 批量删除字典数据信息
* *

View File

@@ -15,6 +15,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
@@ -81,6 +82,18 @@ public class SysDictDataServiceImpl implements ISysDictDataService {
return baseMapper.selectById(dictCode); return baseMapper.selectById(dictCode);
} }
/**
* 按类型查字典数据(直查数据库,不走 {@link com.klp.system.service.impl.SysDictTypeServiceImpl#selectDictDataByType} 的 Spring Cache
* SQL 脚本直插或外部改表后仍需立即在「按类型」HTTP 接口中可见,避免长期使用空缓存。
*/
@Override
public List<SysDictData> selectDictDataByTypeRealtime(String dictType) {
if (StringUtils.isBlank(dictType)) {
return Collections.emptyList();
}
return baseMapper.selectDictDataByType(dictType);
}
/** /**
* 批量删除字典数据信息 * 批量删除字典数据信息
* *

View File

@@ -119,7 +119,7 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
@Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType") @Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
@Override @Override
public SysDictType selectDictTypeByType(String dictType) { public SysDictType selectDictTypeByType(String dictType) {
return baseMapper.selectById(new LambdaQueryWrapper<SysDictType>().eq(SysDictType::getDictType, dictType)); return baseMapper.selectOne(new LambdaQueryWrapper<SysDictType>().eq(SysDictType::getDictType, dictType));
} }
/** /**

View File

@@ -58,3 +58,11 @@ export function optionselect() {
method: 'get' method: 'get'
}) })
} }
/** 按字典类型编码精确查询 sys_dict_type路径需编码 */
export function getDictTypeByCode(dictType) {
return request({
url: '/system/dict/type/byType/' + encodeURIComponent(dictType),
method: 'get'
})
}

View File

@@ -2,6 +2,7 @@
<div style="display: flex; align-items: center;" v-loading="loading"> <div style="display: flex; align-items: center;" v-loading="loading">
<!-- 下拉选择器绑定计算属性做双向绑定保留原有样式+清空功能 --> <!-- 下拉选择器绑定计算属性做双向绑定保留原有样式+清空功能 -->
<el-select <el-select
v-if="!toolbarOnly"
v-model="innerValue" v-model="innerValue"
:placeholder="placeholder" :placeholder="placeholder"
clearable filterable clearable filterable
@@ -20,13 +21,13 @@
<div <div
v-if="editable" v-if="editable"
@click="openDictDialog" @click="openDictDialog"
style="cursor: pointer; min-height: 24px; min-width: 24px; margin-top: 4px; display: flex; align-items: center; justify-content: center; border: 1px solid #828991; margin-left: 8px; border-radius: 2px;" :style="toolbarOnly ? 'cursor: pointer; min-height: 24px; min-width: 24px; margin-top: 4px; display: flex; align-items: center; justify-content: center; border: 1px solid #828991; border-radius: 2px;' : 'cursor: pointer; min-height: 24px; min-width: 24px; margin-top: 4px; display: flex; align-items: center; justify-content: center; border: 1px solid #828991; margin-left: 8px; border-radius: 2px;'"
> >
<i class="el-icon-setting"></i> <i class="el-icon-setting"></i>
</div> </div>
<div <div
v-if="refresh" v-if="refresh && !toolbarOnly"
@click="handleRefresh" @click="handleRefresh"
style="cursor: pointer; min-height: 24px; min-width: 24px; margin-top: 4px; display: flex; align-items: center; justify-content: center; border: 1px solid #828991; margin-left: 8px; border-radius: 2px;" style="cursor: pointer; min-height: 24px; min-width: 24px; margin-top: 4px; display: flex; align-items: center; justify-content: center; border: 1px solid #828991; margin-left: 8px; border-radius: 2px;"
> >
@@ -37,7 +38,7 @@
<el-dialog <el-dialog
v-if="editable" v-if="editable"
:visible.sync="open" :visible.sync="open"
title="字典数据配置" :title="panelTitle || '字典数据配置'"
width="600px" width="600px"
append-to-body append-to-body
> >
@@ -145,14 +146,17 @@
</template> </template>
<script> <script>
import { addData, updateData, delData, listData } from '@/api/system/dict/data' import { addData, updateData, delData, getDicts } from '@/api/system/dict/data'
import { listType } from '@/api/system/dict/type'
export default { export default {
name: 'DictSelectEdit', name: 'DictSelectEdit',
props: { props: {
dictType: { type: String, default: '' }, dictType: { type: String, default: '' },
editable: { type: Boolean, default: true }, editable: { type: Boolean, default: true },
/** 仅展示字典配置入口(齿轮)与弹窗,不渲染下拉框 — 用于列表页 Tab 旁内联维护字典 */
toolbarOnly: { type: Boolean, default: false },
/** 字典配置弹窗标题 */
panelTitle: { type: String, default: '' },
kisv: { type: Boolean, default: true }, kisv: { type: Boolean, default: true },
value: { type: String, default: '' }, value: { type: String, default: '' },
placeholder: { type: String, default: '请选择' }, placeholder: { type: String, default: '请选择' },
@@ -163,18 +167,17 @@ export default {
data() { data() {
return { return {
dictOptions: [], dictOptions: [],
dictId: '',
loading: false, loading: false,
btnLoading: false, btnLoading: false,
delBtnLoading: '', delBtnLoading: '',
open: false, open: false,
editRowId: '', // 控制当前编辑行,为空则所有单元格都是文本状态 editRowId: '', // 控制当前编辑行,为空则所有单元格都是文本状态
form: { form: {
dictId: '',
dictLabel: '', dictLabel: '',
dictValue: '', dictValue: '',
dictType: '', dictType: '',
sort: 0 dictSort: 0,
status: '0'
}, },
dictRules: { dictRules: {
dictLabel: [{ required: true, message: '请输入字典标签', trigger: 'blur' }], dictLabel: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
@@ -214,16 +217,16 @@ export default {
watch: { watch: {
dictType: { dictType: {
async handler(newVal) { async handler(newVal) {
if (newVal) { if (!newVal) return
this.loading = true // toolbarOnly仅打开弹窗时加载避免无谓请求下拉模式按 dict_type 加载选项
try { if (this.toolbarOnly) return
const dictId = await this.getDictId(newVal) this.loading = true
await this.getDictOptions(dictId) try {
} catch (err) { await this.loadDictRows()
console.error('加载字典失败:', err) } catch (err) {
} finally { console.error('加载字典失败:', err)
this.loading = false } finally {
} this.loading = false
} }
}, },
immediate: true immediate: true
@@ -236,14 +239,11 @@ export default {
} }
}, },
methods: { methods: {
async getDictId(type) { /** 读字典数据行:优先 getDicts/dict/data/type/{type}),无权限点与系统字典页 list 不一致问题 */
const res = await listType({ dictType: type }) async loadDictRows() {
if (res.rows?.length !== 1) { const res = await getDicts(this.dictType)
this.$message.error('字典类型异常,未查询到对应配置') this.dictOptions = res.data || []
return Promise.reject('字典类型异常') return this.dictOptions
}
this.dictId = res.rows[0].dictId
return this.dictId
}, },
disabledFormat(item) { disabledFormat(item) {
if (this.disables) { if (this.disables) {
@@ -256,27 +256,36 @@ export default {
async handleRefresh() { async handleRefresh() {
this.loading = true this.loading = true
try { try {
await this.getDictOptions(this.dictId) await this.loadDictRows()
} catch (err) { } catch (err) {
console.error('刷新字典失败:', err) console.error('刷新字典失败:', err)
} finally { } finally {
this.loading = false this.loading = false
} }
}, },
async getDictOptions(dictId) { async openDictDialog() {
const res = await listData({ dictType: this.dictType, pageSize: 1000 })
this.dictOptions = res.rows || []
return this.dictOptions
},
openDictDialog() {
this.open = true this.open = true
this.resetDictForm() this.editRowId = ''
this.form.dictId = this.dictId this.loading = true
this.form.dictType = this.dictType try {
this.editRowId = '' // 打开弹窗重置编辑状态 await this.loadDictRows()
this.resetDictForm()
} catch (err) {
this.dictOptions = []
console.error('打开字典配置失败:', err)
this.$message.error('字典数据加载失败,请检查字典类型是否正确或稍后重试')
} finally {
this.loading = false
}
}, },
resetDictForm() { resetDictForm() {
this.form = { dictId: this.dictId, dictLabel: '', dictValue: '', dictType: this.dictType, sort: 0 } this.form = {
dictLabel: '',
dictValue: '',
dictType: this.dictType,
dictSort: 0,
status: '0'
}
this.$refs.dictFormRef && this.$refs.dictFormRef.clearValidate() this.$refs.dictFormRef && this.$refs.dictFormRef.clearValidate()
}, },
handleKisvTableSync(row) { handleKisvTableSync(row) {
@@ -290,7 +299,8 @@ export default {
try { try {
await addData(this.form) await addData(this.form)
this.$message.success('字典项新增成功!') this.$message.success('字典项新增成功!')
await this.getDictOptions(this.dictId) await this.loadDictRows()
this.$emit('dict-updated')
this.resetDictForm() this.resetDictForm()
} catch (err) { } catch (err) {
this.$message.error('新增失败,请稍后重试') this.$message.error('新增失败,请稍后重试')
@@ -309,18 +319,19 @@ export default {
async handleSaveRow(row) { async handleSaveRow(row) {
if (!row.dictLabel || !row.dictValue) { if (!row.dictLabel || !row.dictValue) {
this.$message.warning('字典标签和字典值不能为空!') this.$message.warning('字典标签和字典值不能为空!')
await this.getDictOptions(this.dictId) await this.loadDictRows()
this.editRowId = '' // 校验失败,也必须还原单元格 this.editRowId = '' // 校验失败,也必须还原单元格
return return
} }
row.sort = 0 if (row.dictSort == null) row.dictSort = 0
this.loading = true this.loading = true
try { try {
await updateData(row) await updateData(row)
this.$message.success('字典项修改成功!') this.$message.success('字典项修改成功!')
this.$emit('dict-updated')
} catch (err) { } catch (err) {
this.$message.error('修改失败,请稍后重试') this.$message.error('修改失败,请稍后重试')
await this.getDictOptions(this.dictId) await this.loadDictRows()
console.error(err) console.error(err)
} finally { } finally {
this.loading = false this.loading = false
@@ -339,7 +350,8 @@ export default {
try { try {
await delData(row.dictCode) await delData(row.dictCode)
this.$message.success('删除成功!') this.$message.success('删除成功!')
await this.getDictOptions(this.dictId) await this.loadDictRows()
this.$emit('dict-updated')
} catch (err) { } catch (err) {
this.$message.error('删除失败,请稍后重试') this.$message.error('删除失败,请稍后重试')
console.error(err) console.error(err)

View File

@@ -1,27 +1,49 @@
<template> <template>
<div class="spec-page"> <div class="spec-page">
<!-- 规程类型 tabs --> <!-- 规程类型 -->
<div class="type-tab-bar"> <div class="dict-toolbar-row">
<span <div class="type-tab-bar">
v-for="t in specTypeTab" <span
:key="t.value" v-for="t in specTypeTab"
:class="['type-tab', { active: activeSpecType === t.value }]" :key="'stype-' + (t.value === '' ? 'all' : t.value)"
@click="switchSpecType(t.value)" :class="['type-tab', { active: activeSpecType === t.value }]"
>{{ t.label }}</span> @click="switchSpecType(t.value)"
>{{ t.label }}</span>
</div>
<dict-select
toolbar-only
:kisv="false"
:editable="true"
:refresh="false"
:dict-type="DICT_SPEC_TYPE"
panel-title="规程工艺类型字典"
@dict-updated="loadSpecTypeDict"
/>
</div> </div>
<!-- 产线 tabs --> <!-- 产线 -->
<div class="line-tab-bar"> <div class="dict-toolbar-row line-row">
<span <div class="line-tab-bar">
:class="['line-tab', { active: activeLineId === '' }]" <span
@click="switchLine('')" :class="['line-tab', { active: activeLineId === '' }]"
>全部</span> @click="switchLine('')"
<span >全部</span>
v-for="line in lineOptions" <span
:key="line.lineId" v-for="line in lineOptions"
:class="['line-tab', { active: activeLineId === line.lineId }]" :key="'ln-' + line.lineId"
@click="switchLine(line.lineId)" :class="['line-tab', { active: lineTabActive(line) }]"
>{{ line.lineName }}</span> @click="switchLine(line.lineId)"
>{{ line.lineName }}</span>
</div>
<dict-select
toolbar-only
:kisv="false"
:editable="true"
:refresh="false"
:dict-type="DICT_LINE"
panel-title="规程产线筛选项字典值为产线 line_id"
@dict-updated="loadLineOptions"
/>
</div> </div>
<!-- 工具栏 --> <!-- 工具栏 -->
@@ -95,15 +117,20 @@
</el-form-item> </el-form-item>
<el-form-item label="规程类型" prop="specType"> <el-form-item label="规程类型" prop="specType">
<el-select v-model="form.specType" style="width:100%"> <el-select v-model="form.specType" style="width:100%">
<el-option v-for="t in specTypeOptions" :key="t.value" :label="t.label" :value="t.value" /> <el-option
v-for="t in specTypeOptionsForForm"
:key="t.dictValue"
:label="t.dictLabel"
:value="t.dictValue"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="产线" prop="lineId"> <el-form-item label="产线" prop="lineId">
<el-select v-model="form.lineId" filterable placeholder="请选择" style="width:100%"> <el-select v-model="form.lineId" filterable placeholder="请选择" style="width:100%">
<el-option <el-option
v-for="line in lineOptions" v-for="line in lineOptionsForForm"
:key="line.lineId" :key="line.lineId"
:label="line.lineCode ? line.lineName + '' + line.lineCode + '' : line.lineName" :label="lineOptLabel(line)"
:value="line.lineId" :value="line.lineId"
/> />
</el-select> </el-select>
@@ -127,18 +154,24 @@
</template> </template>
<script> <script>
import { getDicts } from '@/api/system/dict/data'
import { listProcessSpec, getProcessSpec, delProcessSpec, updateProcessSpec, addProcessSpec } from '@/api/wms/processSpec' import { listProcessSpec, getProcessSpec, delProcessSpec, updateProcessSpec, addProcessSpec } from '@/api/wms/processSpec'
import { listProductionLine } from '@/api/wms/productionLine' import { listProductionLine } from '@/api/wms/productionLine'
const SPEC_TYPES = [ const DICT_SPEC_TYPE = 'wms_process_spec_type'
{ label: '工艺规程', value: 'PROCESS' }, const DICT_LINE = 'wms_process_spec_line'
{ label: '标准', value: 'STANDARD' }
const DEFAULT_SPEC_TYPES = [
{ dictLabel: '工艺规程', dictValue: 'PROCESS', dictSort: 10 },
{ dictLabel: '标准', dictValue: 'STANDARD', dictSort: 20 }
] ]
export default { export default {
name: 'ProcessSpec', name: 'ProcessSpec',
data() { data() {
return { return {
DICT_SPEC_TYPE,
DICT_LINE,
loading: false, loading: false,
btnLoading: false, btnLoading: false,
total: 0, total: 0,
@@ -148,9 +181,8 @@ export default {
multiple: true, multiple: true,
open: false, open: false,
dialogTitle: '', dialogTitle: '',
specTypeRows: [],
lineOptions: [], lineOptions: [],
specTypeTab: [{ label: '全部', value: '' }, ...SPEC_TYPES],
specTypeOptions: SPEC_TYPES,
activeSpecType: '', activeSpecType: '',
activeLineId: '', activeLineId: '',
queryParams: { queryParams: {
@@ -169,13 +201,159 @@ export default {
} }
} }
}, },
computed: {
specTypeTab() {
const rows = this.mergeSpecTypeRowsWithDefaults()
const sorted = [...rows].sort((a, b) => (Number(a.dictSort) || 0) - (Number(b.dictSort) || 0))
return [{ label: '全部', value: '' }, ...sorted.map(r => ({ label: r.dictLabel, value: r.dictValue }))]
},
specTypeOptionsForForm() {
const rows = this.mergeSpecTypeRowsWithDefaults()
return [...rows].sort((a, b) => (Number(a.dictSort) || 0) - (Number(b.dictSort) || 0))
},
lineOptionsForForm() {
return this.lineOptions
}
},
created() { created() {
listProductionLine({ pageNum: 1, pageSize: 500 }).then(res => { Promise.all([this.loadSpecTypeDict(), this.loadLineOptions()]).finally(() => {
this.lineOptions = res.rows || [] this.getList()
}) })
this.getList()
}, },
methods: { methods: {
/**
* 工艺类型:默认 PROCESS/STANDARD 与字典合并。若仅有字典中存在的新项,仍可保留两行基础兜底;同 dict_value 以字典为准(可改名、调 sort
*/
mergeSpecTypeRowsWithDefaults() {
const byVal = new Map()
DEFAULT_SPEC_TYPES.forEach(row => {
byVal.set(row.dictValue, { ...row })
})
for (const row of this.specTypeRows || []) {
if (!row || row.dictValue === undefined || row.dictValue === null || row.dictValue === '') continue
const v = String(row.dictValue)
const prev = byVal.get(v)
const sort = row.dictSort != null && row.dictSort !== ''
? Number(row.dictSort)
: (prev && prev.dictSort != null ? prev.dictSort : 999)
const label = (row.dictLabel != null && String(row.dictLabel).trim() !== '')
? row.dictLabel
: (prev && prev.dictLabel)
byVal.set(v, {
dictLabel: label,
dictValue: v,
dictSort: sort
})
}
return Array.from(byVal.values())
},
/**
* 产线 ID 与字典值:用数字字符串,避免超过 Number.MAX_SAFE_INTEGER 时精度丢失(雪花 id
*/
normalizeLineIdString(value) {
if (value === undefined || value === null || value === '') return ''
const s = String(value).trim()
return /^\d+$/.test(s) ? s : ''
},
/** Tab 高亮:雪花 id 一律按字符串比较 */
lineTabActive(line) {
if (this.activeLineId === '' || this.activeLineId === undefined) return false
return String(this.activeLineId) === String(line.lineId)
},
parseDictRows(res) {
const rows = (res.data || []).filter(d => d.status === '0' || d.status === undefined)
rows.sort((a, b) => (Number(a.dictSort) || 0) - (Number(b.dictSort) || 0))
return rows
},
async loadSpecTypeDict() {
try {
const res = await getDicts(DICT_SPEC_TYPE)
this.specTypeRows = this.parseDictRows(res)
} catch (err) {
console.error('规程工艺类型字典加载失败', err)
this.specTypeRows = []
}
},
/**
* 产线 Tab产线主表 字典中合法数字 line_id字典独有也会显示 Tab
* line_id 全程用数字字符串,避免超过 Number.MAX_SAFE_INTEGER 时精度丢失;字典值须为数字 line_id。
*/
async loadLineOptions() {
const previousOptions = Array.isArray(this.lineOptions) && this.lineOptions.length
? this.lineOptions.map(o => ({ ...o }))
: []
let dictRows = []
try {
const res = await getDicts(DICT_LINE)
dictRows = this.parseDictRows(res)
} catch (err) {
console.error('规程产线字典加载失败', err)
}
const dictMeta = new Map()
for (const d of dictRows) {
const idStr = this.normalizeLineIdString(d.dictValue)
if (!idStr) continue
const sort = Number(d.dictSort) || 0
const label = (d.dictLabel != null && String(d.dictLabel).trim() !== '')
? String(d.dictLabel).trim()
: idStr
dictMeta.set(idStr, { label, sort })
}
let tableLines = []
try {
const res = await listProductionLine({ pageNum: 1, pageSize: 500 })
tableLines = (res.rows || []).map(p => {
const idStr = this.normalizeLineIdString(p.lineId) || (p.lineId != null ? String(p.lineId).trim() : '')
return {
lineId: idStr,
lineName: p.lineName,
lineCode: p.lineCode
}
}).filter(p => p.lineId)
} catch (e2) {
console.error('产线列表加载失败', e2)
}
const tableIdSet = new Set(tableLines.map(l => String(l.lineId)))
const next = []
for (const line of tableLines) {
const idStr = String(line.lineId)
const dm = dictMeta.get(idStr)
next.push({
lineId: idStr,
lineName: dm ? dm.label : line.lineName,
lineCode: line.lineCode
})
}
const dictOnly = []
for (const [idStr, meta] of dictMeta) {
if (!tableIdSet.has(idStr)) {
dictOnly.push({ lineId: idStr, lineName: meta.label, lineCode: undefined, _sort: meta.sort })
}
}
dictOnly.sort((a, b) => a._sort - b._sort)
dictOnly.forEach(d => {
const { _sort, ...rest } = d
next.push(rest)
})
if (next.length === 0 && previousOptions.length > 0) {
console.warn('[规程产线] 本次未解析出有效筛选项(可能字典值非数字或产线接口异常),保留上一版 Tab')
return
}
this.lineOptions = next
},
lineOptLabel(line) {
if (line.lineCode) {
return `${line.lineName}${line.lineCode}`
}
return line.lineName
},
getList() { getList() {
this.loading = true this.loading = true
listProcessSpec(this.queryParams).then(res => { listProcessSpec(this.queryParams).then(res => {
@@ -190,8 +368,13 @@ export default {
this.getList() this.getList()
}, },
switchLine(lineId) { switchLine(lineId) {
this.activeLineId = lineId if (lineId === '' || lineId === undefined || lineId === null) {
this.queryParams.lineId = lineId || undefined this.activeLineId = ''
} else {
const s = this.normalizeLineIdString(lineId)
this.activeLineId = s || String(lineId).trim()
}
this.queryParams.lineId = this.activeLineId === '' ? undefined : this.activeLineId
this.queryParams.pageNum = 1 this.queryParams.pageNum = 1
this.getList() this.getList()
}, },
@@ -208,8 +391,21 @@ export default {
this.single = sel.length !== 1 this.single = sel.length !== 1
this.multiple = !sel.length this.multiple = !sel.length
}, },
defaultSpecType() {
const first = this.specTypeOptionsForForm[0]
return first ? first.dictValue : 'PROCESS'
},
reset() { reset() {
this.form = { specId: undefined, specCode: undefined, specName: undefined, specType: 'PROCESS', lineId: undefined, productType: undefined, isEnabled: 1, remark: undefined } this.form = {
specId: undefined,
specCode: undefined,
specName: undefined,
specType: this.defaultSpecType(),
lineId: undefined,
productType: undefined,
isEnabled: 1,
remark: undefined
}
this.$refs.form && this.$refs.form.clearValidate() this.$refs.form && this.$refs.form.clearValidate()
}, },
handleAdd() { handleAdd() {
@@ -222,6 +418,10 @@ export default {
const specId = row ? row.specId : this.ids[0] const specId = row ? row.specId : this.ids[0]
getProcessSpec(specId).then(res => { getProcessSpec(specId).then(res => {
this.form = res.data || {} this.form = res.data || {}
if (this.form.lineId != null && this.form.lineId !== '') {
const sid = this.normalizeLineIdString(this.form.lineId)
this.form.lineId = sid || String(this.form.lineId).trim()
}
this.dialogTitle = '修改规程' this.dialogTitle = '修改规程'
this.open = true this.open = true
}) })
@@ -273,11 +473,23 @@ export default {
/* ── 双色主题:默认=白底灰边,激活/主操作=深藏青 #5F7BA0 ── */ /* ── 双色主题:默认=白底灰边,激活/主操作=深藏青 #5F7BA0 ── */
.dict-toolbar-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-bottom: 6px;
}
.dict-toolbar-row.line-row {
margin-bottom: 12px;
}
.type-tab-bar { .type-tab-bar {
display: flex; display: flex;
flex: 0 1 auto;
flex-wrap: wrap;
gap: 0; gap: 0;
margin-bottom: 10px;
width: fit-content;
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
border: 1px solid #dcdfe6; border: 1px solid #dcdfe6;
@@ -307,9 +519,11 @@ export default {
.line-tab-bar { .line-tab-bar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
flex: 0 1 auto;
align-items: center;
gap: 6px; gap: 6px;
margin-bottom: 12px;
padding: 10px 0; padding: 10px 0;
min-width: 0;
} }
.line-tab { .line-tab {
@@ -381,4 +595,9 @@ export default {
::v-deep .el-button--text.btn-danger { color: #f56c6c !important; } ::v-deep .el-button--text.btn-danger { color: #f56c6c !important; }
.btn-danger { color: #f56c6c; } .btn-danger { color: #f56c6c; }
/* 与 Tab 同一行时,将齿轮框配色贴近规程主题 */
::v-deep .dict-toolbar-row .el-icon-setting {
color: #5F7BA0;
}
</style> </style>

View File

@@ -23,14 +23,14 @@
<!-- 左侧段分组 --> <!-- 左侧段分组 -->
<div class="left-tree"> <div class="left-tree">
<div <div
:class="['tree-item', { active: activeSegment === '' }]" :class="['tree-item', { active: activeSegmentType === '' }]"
@click="activeSegment = ''" @click="selectSegmentType('')"
>全部</div> >全部</div>
<div <div
v-for="seg in segmentOptions" v-for="seg in segmentTypeTabOptions"
:key="seg.value" :key="String(seg.value)"
:class="['tree-item', { active: activeSegment === seg.value }]" :class="['tree-item', { active: String(activeSegmentType) === String(seg.value) }]"
@click="activeSegment = seg.value" @click="selectSegmentType(seg.value)"
> >
<span class="tree-arrow"></span> {{ seg.label }} <span class="tree-arrow"></span> {{ seg.label }}
</div> </div>
@@ -40,6 +40,24 @@
<div class="right-content"> <div class="right-content">
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="search-bar"> <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> <span class="search-label">点位名称</span>
<el-input <el-input
v-model="filterName" v-model="filterName"
@@ -116,7 +134,7 @@
<el-form ref="planFormRef" :model="planForm" :rules="planRules" label-width="90px" size="small"> <el-form ref="planFormRef" :model="planForm" :rules="planRules" label-width="90px" size="small">
<el-form-item label="段类型" prop="segmentType"> <el-form-item label="段类型" prop="segmentType">
<el-select v-model="planForm.segmentType" style="width:100%"> <el-select v-model="planForm.segmentType" style="width:100%">
<el-option v-for="s in segmentOptions" :key="s.value" :label="s.label" :value="s.value" /> <el-option v-for="s in segmentFormOptionsForDialog" :key="String(s.value)" :label="s.label" :value="s.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="段名称" prop="segmentName"> <el-form-item label="段名称" prop="segmentName">
@@ -193,7 +211,8 @@
import { listProcessPlan, addProcessPlan, updateProcessPlan, delProcessPlan } from '@/api/wms/processPlan' import { listProcessPlan, addProcessPlan, updateProcessPlan, delProcessPlan } from '@/api/wms/processPlan'
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam, delProcessPlanParam } from '@/api/wms/processPlanParam' import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam, delProcessPlanParam } from '@/api/wms/processPlanParam'
const SEGMENTS = [ /** 表单内可选段类型(新建/编辑仍支持全部枚举) */
const SEGMENT_FORM_OPTIONS = [
{ label: '入口段', value: 'INLET' }, { label: '入口段', value: 'INLET' },
{ label: '工艺段', value: 'PROCESS' }, { label: '工艺段', value: 'PROCESS' },
{ label: '出口段', value: 'OUTLET' } { label: '出口段', value: 'OUTLET' }
@@ -208,10 +227,12 @@ export default {
versionCode: '', versionCode: '',
specId: undefined, specId: undefined,
configMode: 'configurable', configMode: 'configurable',
activeSegment: '', /** 左侧:段类型;空=全部 */
activeSegmentType: '',
/** 工具栏下拉:当前段类型下的段名称;空字符串=该段类型下全部名称__EMPTY__=未命名 */
activeSegmentName: '',
filterName: '', filterName: '',
appliedFilterName: '', appliedFilterName: '',
segmentOptions: SEGMENTS,
planList: [], planList: [],
planLoading: false, planLoading: false,
selectedPlan: null, selectedPlan: null,
@@ -237,16 +258,125 @@ export default {
} }
}, },
computed: { 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() { filteredPlans() {
const type = this.activeSegmentType
const sub = this.activeSegmentName
return this.planList.filter(p => { return this.planList.filter(p => {
const segOk = !this.activeSegment || p.segmentType === this.activeSegment 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) const nameOk = !this.appliedFilterName || (p.pointName || '').includes(this.appliedFilterName)
return segOk && nameOk return typeOk && nameOkSeg && nameOk
}) })
} }
}, },
watch: { watch: {
$route: { immediate: true, handler() { this.syncFromRoute() } } $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: { methods: {
syncFromRoute() { syncFromRoute() {
@@ -257,9 +387,22 @@ export default {
if (this.versionId) this.loadPlans() if (this.versionId) this.loadPlans()
}, },
goBack() { this.$router.go(-1) }, 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) { segLabel(val) {
const hit = SEGMENTS.find(s => s.value === val) return this.segmentTypeDisplayLabel(val)
return hit ? hit.label : val || ''
}, },
loadPlans() { loadPlans() {
this.planLoading = true this.planLoading = true
@@ -281,7 +424,11 @@ export default {
this.loadParams(row.planId) this.loadParams(row.planId)
}, },
applyFilter() { this.appliedFilterName = this.filterName }, applyFilter() { this.appliedFilterName = this.filterName },
resetFilter() { this.filterName = ''; this.appliedFilterName = '' }, resetFilter() {
this.filterName = ''
this.appliedFilterName = ''
this.activeSegmentName = ''
},
openPlanDialog(row) { openPlanDialog(row) {
this.planForm = row this.planForm = row
? { ...row } ? { ...row }

View File

@@ -0,0 +1,28 @@
-- -------------------------------------------------------------------------------------
-- 规程管理页面字典:工艺类型(wms_process_spec_type)、产线筛选项(wms_process_spec_line)
-- 执行前提:已有 sys_dict_type / sys_dict_data 表。
--
-- 产线字典(dict.value):须为数字型的产线 ID与 wms_production_line.line_id / wms_process_spec.line_id 一致),不可用字母;
-- 字典标签为该产线在规程页 Tab/下拉的展示文案。前端以产线主表列表为全集,字典仅覆盖同 line_id 的显示名(见 processSpec/loadLineOptions
-- -------------------------------------------------------------------------------------
START TRANSACTION;
INSERT INTO sys_dict_type (dict_id, dict_name, dict_type, status, create_time, remark)
VALUES
(2036400000000000001, N'规程工艺类型', 'wms_process_spec_type', '0', NOW(), N'规程页筛选用dict_value = spec_type'),
(2036400000000000002, N'规程产线筛选项', 'wms_process_spec_line', '0', NOW(), N'规程页产线筛选dict_value = line_id')
ON DUPLICATE KEY UPDATE dict_name = VALUES(dict_name), remark = VALUES(remark), status = '0';
INSERT INTO sys_dict_data (dict_code, dict_sort, dict_label, dict_value, dict_type, status, create_time)
VALUES
(2036400000000100001, 10, N'工艺规程', 'PROCESS', 'wms_process_spec_type', '0', NOW()),
(2036400000000100002, 20, N'标准', 'STANDARD', 'wms_process_spec_type', '0', NOW())
ON DUPLICATE KEY UPDATE dict_label = VALUES(dict_label), dict_sort = VALUES(dict_sort), dict_value = VALUES(dict_value), status = '0';
-- 产线筛选项暂不批量插入(避免 dict_code 与库内既有数据冲突)。
-- 若需字典内可编辑的产线与页面一致在「规程管理」页产线行齿轮中新增或按产线主表自行插入dict_value = CAST(line_id AS CHAR)。
COMMIT;
-- 执行完成后建议在「系统管理 - 字典管理」中刷新缓存,或使用接口 /system/dict/type/refreshCache