2025-12-09 18:07:12 +08:00

653 lines
19 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="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>