362 lines
11 KiB
Vue
362 lines
11 KiB
Vue
/**
|
||
* @file index.vue 质控
|
||
* @author: w
|
||
* @since: 2025-12-29
|
||
*/
|
||
|
||
<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" />
|
||
<el-radio-group v-model="radiotype" @change="radioChange">
|
||
<el-radio value="1" size="large">仪器组</el-radio>
|
||
<el-radio value="2" size="large">所有仪器</el-radio>
|
||
</el-radio-group>
|
||
<div class="tree-container">
|
||
<el-tree :data="deptOptions" :props="treeProps" expand-on-click-node :filter-node-method="filterNode"
|
||
ref="deptTreeRef" node-key="id" highlight-current :expanded-keys="expandedKeys"
|
||
@node-click="handleNodeClick">
|
||
<!-- 自定义树节点模板:新增newflag徽章 -->
|
||
<template #default="{ node, data }">
|
||
<span class="tree-node-content">
|
||
{{ node.label }}
|
||
<!-- 新节点徽章:newflag=1时显示黄色"新"标 -->
|
||
<el-badge v-if="data.newflag === '1'" value="new" class="new-node-badge" />
|
||
</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-tabs v-model="activeName" type="card" class="tabs_box">
|
||
<el-tab-pane v-for="tab in tabList" :key="tab.key" :label="tab.label" :name="tab.key" />
|
||
</el-tabs>
|
||
<!-- 动态组件渲染 -->
|
||
<component :is="currentComponent" :key="activeName" v-model="queryParams.yq" @updateTree="getDeptTree" />
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, watch, nextTick, computed } from 'vue'
|
||
import { ElMessage, ElTree, TreeNode, ElMessageBox, ElBadge } from 'element-plus'
|
||
import { paramList, paramTree, getdef, updateParam } from '@/api/liswork/xtwh/ComOpt'
|
||
import { getFirstLetter } from '@/utils/pinyin'
|
||
import { formatDict, getDictData, dictData } from '@/hooks';
|
||
import { useCommonStore } from '@/store/modules/commonStore';
|
||
import SetRule from './components/setRule/index.vue';
|
||
import SetProject from './components/setProject/index.vue';
|
||
import SetParams from './components/setParams/index.vue';
|
||
import SetBatch from './components/setBatch/index.vue';
|
||
import SetSample from './components/setSample/index.vue';
|
||
|
||
const mbStore = useCommonStore();
|
||
// ========== 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
|
||
}
|
||
|
||
|
||
const radiotype = ref<string>("1")
|
||
// ========== 响应式变量 ==========
|
||
// 树相关
|
||
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: Record<string, any>) => !data.children || data.children.length === 0
|
||
})
|
||
|
||
// 查询参数
|
||
const queryParams = ref<QueryParams>({
|
||
yq: undefined,
|
||
xxdh: undefined,
|
||
xxqz: undefined,
|
||
groupId: undefined
|
||
})
|
||
|
||
const tabList = [
|
||
{ key: 'SetRule', label: '质控规则设定', component: SetRule },
|
||
{ key: 'SetProject', label: '质控项目设定', component: SetProject },
|
||
{ key: 'SetParams', label: '质控品参数设置', component: SetParams },
|
||
{ key: 'SetSample', label: '质控品样本号对应', component: SetSample },
|
||
{ key: 'SetBatch', label: '质控品批号管理', component: SetBatch },
|
||
]
|
||
const activeName = ref('SetBatch')
|
||
|
||
// 计算当前要渲染的组件
|
||
const currentComponent = computed(() => {
|
||
return tabList.find(tab => tab.key === activeName.value)?.component || 'SetRule'
|
||
})
|
||
|
||
const result: any = ref([])
|
||
|
||
const treeData = ref<InstrTreeNode[]>([])
|
||
const radioChange = (val: string) => {
|
||
if (val == "1") {
|
||
deptOptions.value = treeData.value
|
||
} else {
|
||
deptOptions.value = result.value
|
||
}
|
||
}
|
||
|
||
// ========== 树操作核心方法 ==========
|
||
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
|
||
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
|
||
}
|
||
}
|
||
// ========== 检索过滤逻辑 ==========
|
||
const filterNode = (value: string, data: any, _node: TreeNode): 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: any) => 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 TreeNode) && 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
|
||
result.value = []
|
||
if (response.code === "0" && response.data) {
|
||
for (let i = 0; i < response.data.length; i++) {
|
||
const group = response.data[i];
|
||
if (group.children && Array.isArray(group.children)) {
|
||
for (let j = 0; j < group.children.length; j++) {
|
||
result.value.push(group.children[j]);
|
||
}
|
||
}
|
||
}
|
||
treeData.value = response.data
|
||
radioChange('1')
|
||
if (!expandedKeys.value.length) {
|
||
// 默认展开第一个一级节点的子级
|
||
nextTick(() => {
|
||
if (mbStore.defaultConfig.defaultinstr) {
|
||
queryParams.value.yq = mbStore.defaultConfig.defaultinstr
|
||
yqmc.value = result.value.find((item: any) => item.id === queryParams.value.yq)?.label
|
||
deptTreeRef.value?.setCurrentKey(queryParams.value.yq)
|
||
} else {
|
||
if (deptOptions.value.length > 0) {
|
||
const firstLevelNode = deptOptions.value[0]
|
||
if (firstLevelNode && firstLevelNode.children && firstLevelNode.children.length > 0) {
|
||
expandedKeys.value = [firstLevelNode.id]
|
||
const firstChildNode = firstLevelNode.children[0]
|
||
queryParams.value.yq = firstChildNode.id
|
||
yqmc.value = firstChildNode.label
|
||
deptTreeRef.value?.setCurrentKey(queryParams.value.yq)
|
||
}
|
||
}
|
||
}
|
||
|
||
})
|
||
}
|
||
} else {
|
||
ElMessage.warning(response.msg || "获取树数据失败")
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error(`获取树数据异常:${(error as Error).message}`)
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
onMounted(() => {
|
||
getDictData('MD', 'LOT')
|
||
})
|
||
// ========== 初始化 ==========
|
||
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-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%;
|
||
|
||
// 仪器标题行样式
|
||
.yq-title-row {
|
||
margin-bottom: 8px;
|
||
|
||
.yq-title {
|
||
font-family: SourceHanSansCN, SourceHanSansCN;
|
||
font-size: 16px; // 缩小h1字体(默认24px→16px)
|
||
color: #409eff; // 蓝色(Element Plus主题色)
|
||
font-weight: 600;
|
||
margin: 0; // 清除默认margin
|
||
line-height: 1.2;
|
||
}
|
||
}
|
||
}
|
||
</style> |