653 lines
19 KiB
Vue
653 lines
19 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<el-row :gutter="20" class="main-row">
|
||
<!-- 左侧树区域 -->
|
||
<el-col :span="4" :xs="24" class="tree-col">
|
||
<el-input
|
||
v-model="deptName"
|
||
placeholder="请输入仪器名称"
|
||
clearable
|
||
prefix-icon="Search"
|
||
class="tree-search-input"
|
||
/>
|
||
<div class="tree-container">
|
||
<el-tree
|
||
:data="deptOptions"
|
||
:props="treeProps"
|
||
:expand-on-click-node="false"
|
||
:filter-node-method="filterNode"
|
||
ref="deptTreeRef"
|
||
node-key="id"
|
||
highlight-current
|
||
:expanded-keys="expandedKeys"
|
||
@node-click="handleNodeClick"
|
||
@expand-change="handleForceExpand"
|
||
:default-expanded-keys="[]"
|
||
>
|
||
<!-- 自定义树节点模板:新增newflag徽章 -->
|
||
<template #default="{ node, data }">
|
||
<span class="tree-node-content">
|
||
{{ node.label }}
|
||
<!-- 新节点徽章:newflag=1时显示黄色"新"标 -->
|
||
<el-badge
|
||
v-if="data.newflag === '1'"
|
||
value="新"
|
||
class="new-node-badge"
|
||
type="warning"
|
||
/>
|
||
|
||
</span>
|
||
</template>
|
||
</el-tree>
|
||
</div>
|
||
</el-col>
|
||
|
||
<!-- 右侧列表区域(三列表格) -->
|
||
<el-col :span="20" :xs="24" class="table-col">
|
||
<el-row :gutter="10" class="yq-title-row">
|
||
<h1 class="yq-title">仪器:{{ yqmc }}</h1>
|
||
</el-row>
|
||
<el-row :gutter="10" class="table-toolbar">
|
||
<el-col :span="1.5">
|
||
<el-button type="primary" plain icon="Plus" @click="handleSave">保存</el-button>
|
||
</el-col>
|
||
<el-col :span="1.5">
|
||
<el-button type="success" plain icon="Edit" @click="handleCancelEdit">取消修改</el-button>
|
||
</el-col>
|
||
<el-col :span="1.5">
|
||
<el-button type="danger" plain icon="Download" @click="handleRestoreDefault">还原默认</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 三列表格容器 -->
|
||
<div class="triple-table-wrapper">
|
||
<!-- 第一列表格 -->
|
||
<div class="table-panel table-1">
|
||
<el-table
|
||
v-loading="loading"
|
||
:data="tableData1"
|
||
@selection-change="handleSelectionChange"
|
||
class="config-table"
|
||
fit
|
||
border
|
||
height="100%"
|
||
:cell-spacing="0"
|
||
>
|
||
<el-table-column label="序号" align="center" prop="configSort" width="50" />
|
||
<el-table-column :label="`${menuname}编码`" align="center" prop="configCode" v-if="false" show-overflow-tooltip min-width="120" />
|
||
<el-table-column :label="`${menuname}名称`" align="left" prop="configName" show-overflow-tooltip min-width="150" />
|
||
<el-table-column label="参数值" align="center" prop="configDefvalue" show-overflow-tooltip min-width="100">
|
||
<template #default="scope">
|
||
<DynamicInput
|
||
:type="scope.row.configOptiontype"
|
||
v-model="scope.row.configDefvalue"
|
||
:dict-data="scope.row.remark"
|
||
:placeholder="`请输入${scope.row.configName}值`"
|
||
class="edit-input"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="值码" align="center" prop="configDefvalue" v-if="false" show-overflow-tooltip min-width="100" />
|
||
</el-table>
|
||
</div>
|
||
|
||
<!-- 第二列表格 -->
|
||
<div class="table-panel table-2">
|
||
<el-table
|
||
v-loading="loading"
|
||
:data="tableData2"
|
||
@selection-change="handleSelectionChange"
|
||
class="config-table"
|
||
fit
|
||
border
|
||
height="100%"
|
||
:cell-spacing="0"
|
||
>
|
||
<el-table-column label="序号" align="center" prop="configSort" width="50" />
|
||
<el-table-column :label="`${menuname}编码`" align="center" prop="configCode" v-if="false" show-overflow-tooltip min-width="120" />
|
||
<el-table-column :label="`${menuname}名称`" align="left" prop="configName" show-overflow-tooltip min-width="150" />
|
||
<el-table-column label="参数值" align="center" prop="configDefvalue" show-overflow-tooltip min-width="100">
|
||
<template #default="scope">
|
||
<DynamicInput
|
||
:type="scope.row.configOptiontype"
|
||
v-model="scope.row.configDefvalue"
|
||
:dict-data="scope.row.remark"
|
||
:placeholder="`请输入${scope.row.configName}值`"
|
||
class="edit-input"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="值码" align="center" prop="configDefvalue"v-if="false" show-overflow-tooltip min-width="100" />
|
||
</el-table>
|
||
</div>
|
||
|
||
<!-- 第三列表格 -->
|
||
<div class="table-panel table-3">
|
||
<el-table
|
||
v-loading="loading"
|
||
:data="tableData3"
|
||
@selection-change="handleSelectionChange"
|
||
class="config-table"
|
||
fit
|
||
border
|
||
height="100%"
|
||
:cell-spacing="0"
|
||
>
|
||
<el-table-column label="序号" align="center" prop="configSort" width="50" />
|
||
<el-table-column :label="`${menuname}编码`" align="center" prop="configCode" v-if="false" show-overflow-tooltip min-width="120" />
|
||
<el-table-column :label="`${menuname}名称`" align="left" prop="configName" show-overflow-tooltip min-width="150" />
|
||
<el-table-column label="参数值" align="center" prop="configDefvalue" show-overflow-tooltip min-width="100">
|
||
<template #default="scope">
|
||
<DynamicInput
|
||
:type="scope.row.configOptiontype"
|
||
v-model="scope.row.configDefvalue"
|
||
:dict-data="scope.row.remark"
|
||
:placeholder="`请输入${scope.row.configName}值`"
|
||
class="edit-input"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="值码" align="center" prop="configDefvalue" v-if="false" show-overflow-tooltip min-width="100" />
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup name="ComOpt" lang="ts">
|
||
import { ref, watch, nextTick, computed } from 'vue'
|
||
import { ElMessage, ElTree, ElTreeNode, ElMessageBox, ElBadge } from 'element-plus'
|
||
import { paramList, paramTree, getdef, updateParam } from '@/api/liswork/xtwh/ComOpt'
|
||
import { getFirstLetter } from '@/utils/pinyin.js'
|
||
import DynamicInput from "@/views/liswork/xtwh/localconfig/components/DynamicInput.vue"
|
||
|
||
// ========== TypeScript 类型定义 ==========
|
||
interface InstrTreeNode {
|
||
id: string
|
||
label: string
|
||
lx: string
|
||
newflag:string // 新增newflag字段
|
||
children?: InstrTreeNode[]
|
||
}
|
||
|
||
interface ParamTreeResponse {
|
||
code: string
|
||
msg: string
|
||
data: InstrTreeNode[]
|
||
url: null | string
|
||
method: null | string
|
||
params: null | any
|
||
problemId: null | string
|
||
}
|
||
|
||
interface QueryParams {
|
||
yq?: string | undefined
|
||
xxdh?: string | undefined
|
||
xxqz?: string | undefined
|
||
groupId?: string | undefined
|
||
}
|
||
|
||
interface RegDictItem {
|
||
configId: string | number
|
||
configSort: number
|
||
configCode: string
|
||
configName: string
|
||
configDefvalue: string | number
|
||
configOptiontype: string
|
||
remark: string
|
||
}
|
||
|
||
// ========== 响应式变量 ==========
|
||
// 树相关
|
||
const deptTreeRef = ref<InstanceType<typeof ElTree>>()
|
||
const deptOptions = ref<InstrTreeNode[]>([])
|
||
const deptName = ref<string>("")
|
||
const yqmc = ref<string>("")
|
||
const expandedKeys = ref<string[]>([])
|
||
const isFiltering = ref<boolean>(false)
|
||
const treeProps = ref({
|
||
label: 'label',
|
||
children: 'children',
|
||
isLeaf: (data: InstrTreeNode) => !data.children || data.children.length === 0
|
||
})
|
||
|
||
// 查询参数
|
||
const queryParams = ref<QueryParams>({
|
||
yq: undefined,
|
||
xxdh: undefined,
|
||
xxqz: undefined,
|
||
groupId: undefined
|
||
})
|
||
|
||
// 列表相关
|
||
const menuname = ref<string>("仪器全局")
|
||
const regdictList = ref<RegDictItem[]>([])
|
||
const originRegdictList = ref<RegDictItem[]>([])
|
||
const loading = ref<boolean>(false)
|
||
const total = ref<number>(0)
|
||
|
||
// 选择相关
|
||
const ids = ref<(string | number)[]>([])
|
||
const single = ref<boolean>(true)
|
||
const multiple = ref<boolean>(true)
|
||
|
||
// ========== 核心:拆分三列表格数据 ==========
|
||
const tableData1 = computed<RegDictItem[]>(() => {
|
||
const splitSize = Math.ceil(total.value / 3) // 每列基础行数
|
||
return regdictList.value.slice(0, splitSize)
|
||
})
|
||
|
||
const tableData2 = computed<RegDictItem[]>(() => {
|
||
const splitSize = Math.ceil(total.value / 3)
|
||
return regdictList.value.slice(splitSize, splitSize * 2)
|
||
})
|
||
|
||
const tableData3 = computed<RegDictItem[]>(() => {
|
||
const splitSize = Math.ceil(total.value / 3)
|
||
return regdictList.value.slice(splitSize * 2)
|
||
})
|
||
|
||
// ========== 树操作核心方法 ==========
|
||
const forceExpandSingleNode = (nodeId: string): void => {
|
||
expandedKeys.value = []
|
||
nextTick(() => {
|
||
expandedKeys.value = [nodeId]
|
||
deptTreeRef.value?.expand(nodeId, false)
|
||
})
|
||
}
|
||
|
||
const handleNodeClick = (data: InstrTreeNode): void => {
|
||
if (isFiltering.value) {
|
||
queryParams.value.groupId = data.id === "-1" ? undefined : data.id
|
||
handleQuery()
|
||
return
|
||
}
|
||
|
||
if (data.children && data.children.length > 0) {
|
||
const isExpanded = expandedKeys.value.includes(data.id)
|
||
expandedKeys.value = isExpanded ? [] : [data.id]
|
||
!isExpanded && forceExpandSingleNode(data.id)
|
||
} else {
|
||
queryParams.value.yq = data.id
|
||
yqmc.value=data.label
|
||
handleQuery()
|
||
}
|
||
}
|
||
|
||
const handleForceExpand = (_: string[], node: ElTreeNode): void => {
|
||
if (isFiltering.value) return
|
||
|
||
const nodeData = node.data as InstrTreeNode
|
||
if (nodeData.children && nodeData.children.length > 0) {
|
||
if (node.expanded) {
|
||
forceExpandSingleNode(node.key as string)
|
||
} else {
|
||
expandedKeys.value = []
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========== 检索过滤逻辑 ==========
|
||
const filterNode = (value: string, data: InstrTreeNode, _node: ElTreeNode): boolean => {
|
||
if (!value) return true
|
||
|
||
const keyword = value.trim().toUpperCase()
|
||
const nodeLabel = data.label?.toLowerCase() || ''
|
||
const nodePinyinFirst = data.label ? getFirstLetter(data.label).toUpperCase() : ''
|
||
|
||
const currentMatch = nodeLabel.includes(keyword.toLowerCase()) || nodePinyinFirst.includes(keyword)
|
||
const childrenMatch = (data.children || []).some(child => filterNode(value, child, _node))
|
||
return currentMatch || childrenMatch
|
||
}
|
||
|
||
const debounceFilter = (() => {
|
||
let timer: number | undefined
|
||
return (val: string): void => {
|
||
clearTimeout(timer)
|
||
timer = setTimeout(() => {
|
||
isFiltering.value = true
|
||
deptTreeRef.value?.filter(val)
|
||
|
||
if (val) {
|
||
expandedKeys.value = []
|
||
const expandMatched = (nodes: InstrTreeNode[]) => {
|
||
nodes.forEach(node => {
|
||
if (filterNode(val, node, {} as ElTreeNode) && node.children?.length) {
|
||
expandedKeys.value.push(node.id)
|
||
}
|
||
node.children && expandMatched(node.children)
|
||
})
|
||
}
|
||
expandMatched(deptOptions.value)
|
||
} else {
|
||
expandedKeys.value = []
|
||
}
|
||
isFiltering.value = false
|
||
}, 300)
|
||
}
|
||
})()
|
||
|
||
watch(deptName, debounceFilter, { immediate: false })
|
||
|
||
// ========== 业务逻辑 ==========
|
||
const getDeptTree = async (): Promise<void> => {
|
||
try {
|
||
const response = await paramTree() as ParamTreeResponse
|
||
if (response.code === "0" && response.data) {
|
||
deptOptions.value = response.data
|
||
} else {
|
||
ElMessage.warning(response.msg || "获取树数据失败")
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error(`获取树数据异常:${(error as Error).message}`)
|
||
}
|
||
}
|
||
|
||
const handleQuery = (): void => {
|
||
getList()
|
||
}
|
||
|
||
const getList = async (): Promise<void> => {
|
||
loading.value = true
|
||
try {
|
||
const response = await paramList({ yq: queryParams.value.yq })
|
||
regdictList.value = JSON.parse(JSON.stringify(response.data))
|
||
originRegdictList.value = JSON.parse(JSON.stringify(response.data))
|
||
total.value = response.data.length
|
||
} catch (error) {
|
||
ElMessage.error(`获取列表异常:${(error as Error).message}`)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const handleSelectionChange = (val: RegDictItem[]): void => {
|
||
ids.value = val.map(item => item.configId)
|
||
single.value = val.length === 1
|
||
multiple.value = val.length > 1
|
||
}
|
||
|
||
const handleSave = async (): Promise<void> => {
|
||
try {
|
||
loading.value = true
|
||
// 合并三列表格数据提交
|
||
const allData = [...tableData1.value, ...tableData2.value, ...tableData3.value]
|
||
const res = await updateParam(allData)
|
||
if (res.code === 200) {
|
||
ElMessage.success("批量保存成功!")
|
||
originRegdictList.value = JSON.parse(JSON.stringify(allData))
|
||
} else {
|
||
ElMessage.error(res.msg || "保存失败,请重试")
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error(`保存失败:${(error as Error).message}`)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const handleCancelEdit = (): void => {
|
||
ElMessageBox.confirm(
|
||
"是否确认取消所有修改,恢复原始数据?",
|
||
"提示",
|
||
{
|
||
confirmButtonText: "确定",
|
||
cancelButtonText: "取消",
|
||
type: "warning"
|
||
}
|
||
).then(() => {
|
||
regdictList.value = JSON.parse(JSON.stringify(originRegdictList.value))
|
||
ElMessage.success("已恢复原始数据!")
|
||
})
|
||
}
|
||
|
||
const handleRestoreDefault = async (): Promise<void> => {
|
||
ElMessageBox.confirm(
|
||
"是否确认还原所有默认值?此操作会覆盖当前修改!",
|
||
"警告",
|
||
{
|
||
confirmButtonText: "确定",
|
||
cancelButtonText: "取消",
|
||
type: "danger"
|
||
}
|
||
).then(async () => {
|
||
loading.value = true
|
||
try {
|
||
const res = await getdef({ yq: queryParams.value.yq })
|
||
regdictList.value = JSON.parse(JSON.stringify(res.data))
|
||
originRegdictList.value = JSON.parse(JSON.stringify(res.data))
|
||
total.value = res.data.length
|
||
ElMessage.success("还原默认值成功!")
|
||
} catch (error) {
|
||
ElMessage.error(`还原失败:${(error as Error).message}`)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
// ========== 初始化 ==========
|
||
getDeptTree()
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.app-container {
|
||
width: 100%;
|
||
height: 90vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding: 8px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.main-row {
|
||
display: flex;
|
||
flex: 1;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tree-col {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
|
||
.tree-search-input {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.tree-container {
|
||
flex: 1;
|
||
height: 100%;
|
||
overflow: auto;
|
||
background: #fff;
|
||
border: 1px solid #e6e6e6;
|
||
border-radius: 4px;
|
||
padding: 8px;
|
||
|
||
// 核心:树选中节点样式
|
||
:deep(.el-tree) {
|
||
--el-tree-node-transition-duration: 0.2s;
|
||
|
||
.el-tree-node__children {
|
||
transition: all var(--el-tree-node-transition-duration) ease-in-out;
|
||
}
|
||
|
||
.el-tree-node__expand-icon.is-leaf {
|
||
display: none;
|
||
}
|
||
|
||
// 选中节点的背景色(蓝色)+ 文字白色
|
||
.el-tree-node.is-current > .el-tree-node__content {
|
||
background-color: #409eff !important;
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
// 选中节点hover样式(加深蓝色)
|
||
.el-tree-node.is-current > .el-tree-node__content:hover {
|
||
background-color: #1e88e5 !important;
|
||
}
|
||
|
||
// 树节点内容容器样式
|
||
.tree-node-content {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px; // 徽章和文字间距
|
||
height: 100%;
|
||
}
|
||
|
||
// 新节点徽章样式
|
||
.new-node-badge {
|
||
--el-badge-font-size: 10px !important;
|
||
--el-badge-height: 16px !important;
|
||
--el-badge-padding: 0 4px !important;
|
||
--el-badge-warning-color: #FFEC24 !important; // 黄色徽章
|
||
--el-badge-warning-bg-color: #fff7e6 !important; // 浅黄色背景
|
||
--el-badge-border-radius: 2px !important; // 圆角优化
|
||
|
||
// 徽章文字样式
|
||
.el-badge__content {
|
||
font-size: 10px !important;
|
||
line-height: 16px !important;
|
||
padding: 0 4px !important;
|
||
height: 16px !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.table-col {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
|
||
// 仪器标题行样式
|
||
.yq-title-row {
|
||
margin-bottom: 8px;
|
||
|
||
.yq-title {
|
||
font-size: 16px; // 缩小h1字体(默认24px→16px)
|
||
color: #409eff; // 蓝色(Element Plus主题色)
|
||
font-weight: 600;
|
||
margin: 0; // 清除默认margin
|
||
line-height: 1.2;
|
||
}
|
||
}
|
||
|
||
.table-toolbar {
|
||
margin-bottom: 8px;
|
||
height: 36px;
|
||
line-height: 36px;
|
||
}
|
||
|
||
// 三列表格容器
|
||
.triple-table-wrapper {
|
||
flex: 1;
|
||
height: calc(100% - 36px - 24px); // 减去标题行+工具栏高度
|
||
display: flex;
|
||
gap: 0px; // 表格间间距
|
||
overflow: hidden;
|
||
|
||
// 单个表格面板
|
||
.table-panel {
|
||
flex: 1; // 三列均等分
|
||
height: 100%;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
border: 1px solid #e6e6e6;
|
||
border-radius: 4px;
|
||
|
||
// 细微间距优化
|
||
&.table-1 {
|
||
margin-right: 0px;
|
||
}
|
||
&.table-2 {
|
||
margin: 0 0px;
|
||
}
|
||
&.table-3 {
|
||
margin-left: 0px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 核心:清除表格内上下间隙
|
||
:deep(.config-table) {
|
||
// 清除表格整体的边框间距
|
||
border-collapse: collapse !important;
|
||
border-spacing: 0 !important;
|
||
|
||
// 清除表格头部间距
|
||
.el-table__header {
|
||
padding: 0 !important;
|
||
|
||
th {
|
||
padding: 0 !important;
|
||
border-bottom: 1px solid #e6e6e6 !important;
|
||
line-height: 25px !important;
|
||
height: 25px !important;
|
||
}
|
||
}
|
||
|
||
// 清除表格内容区间距
|
||
.el-table__body {
|
||
padding: 0 !important;
|
||
|
||
// 清除行间距
|
||
.el-table__row {
|
||
height: 25px !important;
|
||
line-height: 25px !important;
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
}
|
||
|
||
// 清除单元格内边距和间距
|
||
.el-table__cell {
|
||
padding: 0 4px !important; // 仅保留左右内边距,上下清零
|
||
height: 25px !important;
|
||
line-height: 25px !important;
|
||
vertical-align: middle !important;
|
||
font-size: 12px;
|
||
border-bottom: 1px solid #e6e6e6 !important;
|
||
border-right: 1px solid #e6e6e6 !important;
|
||
}
|
||
|
||
// 最后一列清除右侧边框
|
||
.el-table__cell:last-child {
|
||
border-right: none !important;
|
||
}
|
||
}
|
||
|
||
// 清除表格底部间距
|
||
.el-table__footer {
|
||
padding: 0 !important;
|
||
}
|
||
|
||
// 编辑输入框适配紧凑布局
|
||
.edit-input {
|
||
width: 95%;
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
|
||
:deep(.el-input__inner) {
|
||
padding: 0 5px;
|
||
height: 25px !important; // 缩小输入框高度适配紧凑行高
|
||
line-height: 25px !important;
|
||
font-size: 12px;
|
||
border: 1px solid #dcdfe6;
|
||
margin: 0 !important;
|
||
|
||
&:focus {
|
||
border-color: #409eff;
|
||
outline: none;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style> |