行业树选择器组件:基于Vue3与Element Plus的高性能树形选择组件优化与重构
组件概述与背景
行业树选择器是一个基于Element Plus的ElTreeSelect封装的业务组件,主要应用于能源管理系统中,用于展示和选择国标行业分类体系的四级层级结构。该组件位于项目的/src/views/energy-saving-management/key-energy-using/
目录中,由多个文件协同工作实现。
功能定位与使用场景
该组件主要用于:
- 重点用能单位表单中的行业分类选择
- 能源管理档案中的行业数据展示与编辑
- 企业分类统计与筛选
开发背景
在能源管理系统中,行业分类是重要的基础数据。国标行业分类采用四级结构(如A-农林牧渔业->A01-农业->A011-谷物种植->A0111-稻谷种植),系统需要一个能清晰展示此层级结构并支持各级选择的组件。
现有实现的不足
- 行业树展开问题 - 选择三级行业代码时不能自动展开到正确的层级结构
- 代码冗余 - 现有代码中存在大量重复逻辑和条件判断
- 错误处理不足 - 在数据加载失败或节点查找失败时缺乏有效的降级策略
技术定位
该组件属于业务组件,基于Element Plus的ElTreeSelect进行封装,并与系统中的行业数据API集成。
设计目标与原则
功能目标
- 支持四级行业分类树的展示与选择
- 实现行业代码输入与节点自动匹配
- 优化展开逻辑,确保选择任一级别的行业代码时都能正确展开其层级结构
性能目标
- 减少API调用次数,避免重复加载数据
- 优化树节点查找算法
- 使用缓存机制提高响应速度
可维护性目标
- 减少代码冗余,提高代码质量
- 增强错误处理能力
- 优化函数结构,明确职责分工
组件设计与API
在表单中的使用
组件在add-modal.vue
中的使用方式:
<el-tree-select
v-else-if="item.type === 'tree-select' && item.prop === 'industry'"
:placeholder="item.rules[0].message"
v-model="formData[item.prop]"
:data="industryTreeData"
:props="industryTreeProps"
node-key="code"
check-strictly
value-key="code"
filterable
clearable
:default-expanded-keys="expandedKeys"
:render-after-expand="false"
@visible-change="handleTreeVisibleChange"
@change="handleIndustryChange"
@node-click="handleIndustryNodeClick"
/>
核心数据结构
// 行业树节点数据结构
interface IndustryTreeNode {
name: string; // 行业名称
code: string; // 行业代码
value: string; // 选择值,通常与code相同
isLeaf: boolean; // 是否叶节点
children?: IndustryTreeNode[]; // 子节点
}
// 树属性定义
const industryTreeProps = {
label: 'name',
value: 'code',
children: 'children',
isLeaf: 'isLeaf'
};
组件内部状态
// 行业树数据
const industryTreeData = ref<any[]>([]);
// 当前选中节点
const industrySelectedNode = ref<any>(null);
// 展开的节点键值
const expandedKeys = ref<string[]>([]);
// 树数据缓存
const treeDataCache = ref<any[]>([]);
实现细节与优化对比
1. 行业树初始化
优化前:
// 初始化行业树
async function initIndustryTree() {
try {
const selectedIndustryCode = formData.value.industry;
// 如果已有缓存数据,直接使用
if (treeDataCache.value.length > 0) {
industryTreeData.value = treeDataCache.value;
if (selectedIndustryCode && typeof selectedIndustryCode === 'string') {
// 更新展开路径和选中节点
updateTreeSelection(selectedIndustryCode);
}
return;
}
// 无缓存则加载数据
if (selectedIndustryCode && typeof selectedIndustryCode === 'string') {
await loadTreeForCode(selectedIndustryCode);
return;
}
const treeData = await loadFullIndustryTree();
industryTreeData.value = treeData;
treeDataCache.value = treeData; // 缓存数据
} catch (error) {
console.error('初始化行业树失败', error);
industryTreeData.value = [];
}
}
优化思路:
此函数主要逻辑是处理行业树的初始化和缓存,不需要进行大幅度修改,但可以抽取重复的数据加载和缓存逻辑到单独的函数中,提高代码可读性。
优化后:
// 加载并缓存树数据
const loadAndCacheTree = async () => {
const treeData = await loadFullIndustryTree();
industryTreeData.value = treeData;
treeDataCache.value = treeData;
};
// 初始化行业树
const initIndustryTree = async () => {
try {
const selectedCode = formData.value.industry;
// 有缓存数据时直接使用
if (treeDataCache.value.length > 0) {
industryTreeData.value = treeDataCache.value;
selectedCode && typeof selectedCode === 'string' &&
updateTreeSelection(selectedCode);
return;
}
// 无缓存时加载数据
selectedCode && typeof selectedCode === 'string'
? await initLoadTree(selectedCode)
: loadAndCacheTree();
} catch (error) {
console.error('初始化行业树失败', error);
industryTreeData.value = [];
}
};
2. 更新树选择状态
优化前:
// 更新树的选中状态和展开路径
function updateTreeSelection(industryCode: string) {
if (!industryCode || !treeDataCache.value.length) return;
// 获取展开路径
const nodePath = getNodePath(treeDataCache.value, industryCode);
expandedKeys.value = nodePath;
// 更新选中节点
updateSelectedIndustryNode(treeDataCache.value, industryCode);
}
优化思路:
该函数逻辑简洁明了,主要关注点应该是getNodePath
函数的实现。可以通过添加日志输出增强代码可调试性。
优化后:
// 更新树的选中状态和展开路径
function updateTreeSelection(industryCode: string) {
if (!industryCode || !treeDataCache.value.length) return;
console.log('更新树选择状态,行业代码:', industryCode);
// 获取展开路径
const nodePath = getNodePath(treeDataCache.value, industryCode);
console.log('获取展开路径结果:', nodePath);
if (nodePath && nodePath.length > 0) {
// 确保展开路径中的代码都是字符串
expandedKeys.value = nodePath.map(code => String(code));
console.log('设置展开键值:', expandedKeys.value);
} else {
console.warn('未能获取展开路径,可能无法正确展示树形结构');
}
// 更新选中节点
updateSelectedIndustryNode(treeDataCache.value, industryCode);
}
3. 加载特定行业代码的树
优化前:
// 加载指定行业代码的节点并设置展开路径
async function loadTreeForCode(industryCode: string) {
if (!industryCode) return;
try {
// 加载完整树数据
const treeData = await loadFullIndustryTree();
industryTreeData.value = treeData;
treeDataCache.value = treeData; // 缓存数据
// 更新展开路径和选中节点
updateTreeSelection(industryCode);
} catch (error) {
console.error('加载行业树数据失败:', error);
formData.value.industry = industryCode;
}
}
优化思路:
此函数可以重用刚才定义的loadAndCacheTree
函数,减少代码重复。
优化后:
// 加载指定行业代码的树
const initLoadTree = async (industryCode: string) => {
if (!industryCode) return;
try {
console.log('加载行业树数据,代码:', industryCode);
await loadAndCacheTree();
updateTreeSelection(industryCode);
} catch (error) {
console.error('加载行业树数据失败:', error);
formData.value.industry = industryCode;
}
};
4. 节点路径查找算法
优化前:
// 获取节点的路径,返回节点code数组
export const getNodePath = (tree: any[], code: string): string[] => {
if (!code || !tree || !Array.isArray(tree) || tree.length === 0) return [];
// 进行前处理,确定行业代码的层级
let targetLevel = 0;
// 分析行业代码的层级结构
if (code.length === 1) {
// 一级行业代码,如 'A','B'
targetLevel = 1;
} else if (code.length === 3) {
// 二级行业代码,如 'A01', 'B06'
targetLevel = 2;
} else if (code.length === 4) {
// 三级行业代码,如 'A011', 'C171'
targetLevel = 3;
} else if (code.length === 5) {
// 四级行业代码,如 'C1711'
targetLevel = 4;
}
console.log('目标代码层级:', targetLevel, '代码:', code);
// 用于存储路径的数组
const path: string[] = [];
// 如果是多级代码,先尝试找到父级节点
if (targetLevel > 1) {
// 获取一级行业代码
const firstLevelCode = code.charAt(0);
// 查找一级节点
const firstLevelNode = tree.find((node) => node.code === firstLevelCode);
if (firstLevelNode) {
// 添加一级节点到路径
path.push(firstLevelCode);
if (targetLevel > 2 && firstLevelNode.children) { // 这里有问题
// 获取二级行业代码
const secondLevelCode = code.substring(0, 3);
// 查找二级节点
const secondLevelNode = firstLevelNode.children.find(
(node) => node.code === secondLevelCode
);
if (secondLevelNode) {
// 添加二级节点到路径
path.push(secondLevelCode);
if (targetLevel > 3 && secondLevelNode.children) {
// 获取三级行业代码
const thirdLevelCode = code.substring(0, 4);
// 查找三级节点
const thirdLevelNode = secondLevelNode.children.find(
(node) => node.code === thirdLevelCode
);
if (thirdLevelNode) {
// 添加三级节点到路径
path.push(thirdLevelCode);
// 如果是四级代码,则添加完整代码
if (targetLevel === 4) {
path.push(code);
}
}
}
}
}
}
}
// 如果通过层级分析没有找到路径,再尝试递归查找
if (path.length === 0) {
// 尝试精确匹配查找路径
const resultPath: string[] = [];
const foundExact = findPathRecursive(tree, code, [], resultPath, true);
if (foundExact) {
console.log('通过递归找到精确匹配的节点路径:', resultPath);
return resultPath;
}
// 如果精确匹配失败,尝试模糊匹配
const fuzzyResultPath: string[] = [];
const foundFuzzy = findPathRecursive(tree, code, [], fuzzyResultPath, false);
if (foundFuzzy) {
console.log('通过递归找到模糊匹配的节点路径:', fuzzyResultPath);
return fuzzyResultPath;
}
} else {
console.log('通过层级分析找到节点路径:', path);
}
// 如果仍然没有找到路径,尝试从目标代码本身构建路径
if (path.length === 0 && targetLevel > 1) {
console.log('尝试从目标代码本身构建路径');
// 一级代码
const level1Code = code.charAt(0);
path.push(level1Code);
// 如果是二级以上代码
if (targetLevel >= 2) {
const level2Code = code.substring(0, 3);
path.push(level2Code);
}
// 如果是三级以上代码
if (targetLevel >= 3) {
const level3Code = code.substring(0, 4);
path.push(level3Code);
}
// 如果是四级代码
if (targetLevel === 4) {
path.push(code);
}
console.log('从代码本身构建的路径:', path);
}
return path;
};
优化思路:
这个函数是整个组件的核心,包含了多种查找策略。注意到这里存在一个逻辑错误:对于二级行业代码,条件判断 targetLevel > 2
导致无法进入处理二级代码的分支。我们需要修改条件判断,并将函数重构为更小、更专注的部分。
优化后:
// 获取节点的路径,返回节点code数组
export const getNodePath = (tree: any[], code: string): string[] => {
if (!code || !tree?.length) return [];
// 确定行业代码的层级
const targetLevel = getCodeLevel(code);
console.log('目标代码层级:', targetLevel, '代码:', code);
// 通过层级分析构建路径
const path = buildPathByLevel(tree, code, targetLevel);
// 如果层级分析成功找到路径
if (path.length > 0) {
console.log('通过层级分析找到节点路径:', path);
return path;
}
// 尝试精确匹配查找路径
const resultPath: string[] = [];
if (findPathRecursive(tree, code, [], resultPath, true)) {
console.log('通过递归找到精确匹配的节点路径:', resultPath);
return resultPath;
}
// 如果精确匹配失败,尝试模糊匹配
const fuzzyResultPath: string[] = [];
if (findPathRecursive(tree, code, [], fuzzyResultPath, false)) {
console.log('通过递归找到模糊匹配的节点路径:', fuzzyResultPath);
return fuzzyResultPath;
}
// 所有方法都失败,从代码本身构建路径
if (targetLevel > 1) {
const fallbackPath = buildFallbackPath(code, targetLevel);
console.log('从代码本身构建的路径:', fallbackPath);
return fallbackPath;
}
return [];
};
// 根据代码长度确定层级
function getCodeLevel(code: string): number {
const codeLength = code.length;
if (codeLength === 1) return 1; // 一级行业代码,如 'A','B'
if (codeLength === 3) return 2; // 二级行业代码,如 'A01', 'B06'
if (codeLength === 4) return 3; // 三级行业代码,如 'A011', 'C171'
if (codeLength === 5) return 4; // 四级行业代码,如 'C1711'
return 0;
}
// 根据代码层级构建路径
function buildPathByLevel(tree: any[], code: string, targetLevel: number): string[] {
const path: string[] = [];
if (targetLevel <= 1) return path;
// 一级代码
const firstLevelCode = code.charAt(0);
const firstLevelNode = tree.find(node => node.code === firstLevelCode);
if (!firstLevelNode) return path;
path.push(firstLevelCode);
// 二级代码 - 关键修改
if (targetLevel > 1 && firstLevelNode.children?.length) { // 修改条件判断
const secondLevelCode = code.substring(0, 3);
const secondLevelNode = firstLevelNode.children.find(
node => node.code === secondLevelCode
);
if (!secondLevelNode) return path;
path.push(secondLevelCode);
// 三级代码
if (targetLevel > 2 && secondLevelNode.children?.length) {
const thirdLevelCode = code.substring(0, 4);
const thirdLevelNode = secondLevelNode.children.find(
node => node.code === thirdLevelCode
);
if (!thirdLevelNode) return path;
path.push(thirdLevelCode);
// 四级代码
if (targetLevel > 3) {
path.push(code);
}
}
}
return path;
}
// 构建后备路径(当树中找不到节点时)
function buildFallbackPath(code: string, targetLevel: number): string[] {
const path: string[] = [];
// 一级代码
path.push(code.charAt(0));
// 二级及以上代码
if (targetLevel >= 2) path.push(code.substring(0, 3));
// 三级及以上代码
if (targetLevel >= 3) path.push(code.substring(0, 4));
// 四级代码
if (targetLevel === 4) path.push(code);
return path;
}
5. 递归查找路径
优化前:
// 递归查找节点路径的辅助函数
const findPathRecursive = (
nodes: any[],
targetCode: string,
currentPath: string[],
resultPath: string[],
exactMatch: boolean
): boolean => {
if (!nodes || !Array.isArray(nodes)) return false;
for (const node of nodes) {
// 复制当前路径,添加当前节点
const tempPath = [...currentPath, node.code];
// 检查是否找到目标节点
let isTarget = false;
if (exactMatch) {
// 精确匹配
isTarget = node.code === targetCode;
} else {
// 模糊匹配
isTarget =
(node.code && targetCode.includes(node.code)) ||
(node.code && node.code.includes(targetCode));
}
// 如果找到目标节点,保存路径并返回成功
if (isTarget) {
resultPath.push(...tempPath);
return true;
}
// 如果有子节点,递归查找
if (node.children && Array.isArray(node.children) && node.children.length > 0) {
if (findPathRecursive(node.children, targetCode, tempPath, resultPath, exactMatch)) {
return true;
}
}
}
return false;
};
优化思路:
简化条件判断,使用更简洁的代码风格。
优化后:
// 递归查找节点路径的辅助函数
const findPathRecursive = (
nodes: any[],
targetCode: string,
currentPath: string[],
resultPath: string[],
exactMatch: boolean
): boolean => {
if (!nodes?.length) return false;
for (const node of nodes) {
// 添加当前节点到路径
const tempPath = [...currentPath, node.code];
// 检查是否找到目标节点
const isTarget = exactMatch
? node.code === targetCode // 精确匹配
: (node.code && targetCode.includes(node.code)) || (node.code && node.code.includes(targetCode)); // 模糊匹配
// 找到目标节点,保存路径并返回成功
if (isTarget) {
resultPath.push(...tempPath);
return true;
}
// 递归查找子节点
if (node.children?.length && findPathRecursive(node.children, targetCode, tempPath, resultPath, exactMatch)) {
return true;
}
}
return false;
};
6. 处理树选择器可见性变化
优化前:
// 处理树选择器可见性变化
function handleTreeVisibleChange(visible: boolean) {
if (visible && industryTreeData.value.length === 0) {
// 仅在首次显示且无数据时加载
initIndustryTree();
}
}
优化思路:
使用短路求值简化代码。
优化后:
// 处理树选择器可见性变化
const handleTreeVisibleChange = (visible: boolean) => {
visible && industryTreeData.value.length === 0 && initIndustryTree();
};
7. 处理行业选择变更
优化前:
// 处理行业树选择变更
function handleIndustryChange(val: any) {
console.log('行业选择变更:', val);
if (val && typeof val === 'object') {
formData.value.industry = val.code || '';
industrySelectedNode.value = val;
} else if (typeof val === 'string' && val) {
formData.value.industry = val;
updateIndustrySelectedNodeByCode(val);
} else {
formData.value.industry = '';
industrySelectedNode.value = null;
console.log('清空行业选择');
}
}
优化思路:
添加自动更新树选择状态的逻辑,确保选择后能正确展开节点。
优化后:
// 处理行业树选择变更
const handleIndustryChange = (val: any) => {
console.log('行业选择变更:', val);
if (val && typeof val === 'object') {
formData.value.industry = val.code || '';
industrySelectedNode.value = val;
// 如果选择了节点,确保展开到该节点
if (val.code) {
updateTreeSelection(val.code);
}
} else if (typeof val === 'string' && val) {
formData.value.industry = val;
updateIndustrySelectedNodeByCode(val);
// 确保展开到该节点
updateTreeSelection(val);
} else {
formData.value.industry = '';
industrySelectedNode.value = null;
expandedKeys.value = [];
}
};
8. 根据行业代码更新选中节点
优化前:
// 根据行业代码更新选中节点
function updateIndustrySelectedNodeByCode(industryCode: string) {
if (industryTreeData.value && industryTreeData.value.length > 0) {
const node = findNodeByCode(industryTreeData.value, industryCode);
if (node) {
industrySelectedNode.value = node;
console.log('根据代码找到并选中节点:', node);
} else {
industrySelectedNode.value = { code: industryCode };
console.log('未找到节点,保存代码:', industryCode);
}
} else {
industrySelectedNode.value = { code: industryCode };
}
}
优化思路:
简化逻辑,减少条件判断和重复代码。
优化后:
// 根据行业代码更新选中节点
const updateIndustrySelectedNodeByCode = (industryCode: string) => {
if (!industryTreeData.value?.length) {
industrySelectedNode.value = { code: industryCode };
return;
}
const node = findNodeByCode(industryTreeData.value, industryCode);
industrySelectedNode.value = node || { code: industryCode };
};
9. 处理行业节点点击事件
优化前:
// 处理行业节点点击事件
function handleIndustryNodeClick(data: any, node: any) {
console.log('行业节点点击:', data, node);
if (data && data.code) {
industrySelectedNode.value = data;
}
}
优化思路:
使用可选链和短路求值简化代码。
优化后:
// 处理行业节点点击事件
const handleIndustryNodeClick = (data: any) => {
data?.code && (industrySelectedNode.value = data);
};
核心问题修复
整个优化过程中,解决的最关键问题是修正了getNodePath
函数中的条件判断错误:
优化前:
if (targetLevel > 2 && firstLevelNode.children) {
// 获取二级行业代码
// ...
}
优化后:
if (targetLevel > 1 && firstLevelNode.children?.length) {
// 获取二级行业代码
// ...
}
这个看似微小的修改解决了选择三级行业代码不能自动展开到正确层级结构的问题。原来的条件targetLevel > 2
意味着只有当目标是三级或四级代码时才会处理二级代码的查找,这导致对于二级代码自身无法正确构建路径。
修改为targetLevel > 1
后,只要目标是二级及以上代码,就会尝试查找和构建二级路径,从而正确展开树形结构。
优化与性能
主要优化点
缓存机制:使用
treeDataCache
缓存树数据,避免重复加载// 如果已有缓存数据,直接使用 if (treeDataCache.value.length > 0) { industryTreeData.value = treeDataCache.value; // ... return; }
懒加载策略:只在实际需要时才加载行业树数据
// 仅在首次显示且无数据时加载 if (visible && industryTreeData.value.length === 0) { initIndustryTree(); }
多策略路径查找:采用层级分析、精确匹配、模糊匹配和后备构建等多种策略查找节点路径
展开路径优化:修复了选择三级行业代码不能自动展开的问题
问题解决
对于选择三级行业代码不能自动展开的问题,主要通过以下改进解决:
修正了路径查找逻辑中的条件判断:
// 原代码 if (targetLevel > 2 && firstLevelNode.children) { // ... } // 修改为 if (targetLevel > 1 && firstLevelNode.children) { // ... }
增强了路径查找失败时的后备策略,确保总能生成合理的展开路径
应用案例
在表单中的应用
在重点用能单位表单(add-modal.vue
)中,该组件用于选择企业所属行业:
<el-form-item :label="item.label" :prop="item.prop" :rules="item.rules">
<!-- 行业树形选择器 -->
<el-tree-select
v-else-if="item.type === 'tree-select' && item.prop === 'industry'"
:placeholder="item.rules[0].message"
v-model="formData[item.prop]"
:data="industryTreeData"
:props="industryTreeProps"
node-key="code"
check-strictly
value-key="code"
filterable
clearable
:default-expanded-keys="expandedKeys"
:render-after-expand="false"
@visible-change="handleTreeVisibleChange"
@change="handleIndustryChange"
@node-click="handleIndustryNodeClick"
/>
</el-form-item>
详情查看模式
在详情查看模式下,组件以只读方式展示所选行业:
// 加载行业数据(用于详情)
const loadIndustryDataForDetail = async () => {
if (
(props.type === viewType.EDIT || props.type === viewType.DETAIL) &&
formData.value.industry &&
typeof formData.value.industry === 'string'
) {
try {
await initLoadTree(formData.value.industry);
} catch (error) {
console.error('加载行业节点失败', error);
}
}
};
学习与收获
技术决策考量
- 多策略路径查找:在节点查找中采用多种策略,从精确到模糊,再到后备构建,确保总能生成合理的展开路径
- 缓存数据提升性能:通过缓存树数据,减少不必要的API调用,提升响应速度
- 逐层降级策略:在处理节点查找失败时,采用逐层降级策略,确保系统稳健运行
遇到的挑战与解决方法
节点路径查找复杂性:行业代码具有特定的层级结构,需要根据代码长度和内容进行不同的处理
- 解决方法:根据代码长度确定层级,采用多种查找策略并提供后备方案
条件判断繁杂:路径查找涉及多层嵌套条件
- 解决方法:优化条件判断逻辑,确保关键条件判断准确无误
设计模式应用
- 策略模式:在节点路径查找中采用多种策略(层级分析、精确匹配、模糊匹配、后备构建)
- 缓存模式:使用树数据缓存提升性能
- 降级策略:在高精度策略失效时逐步采用低精度策略,确保系统稳定性
未来规划
待解决的问题
- 代码冗余:路径查找算法仍然包含较多嵌套逻辑,可进一步优化
- 类型定义不足:部分函数参数和返回值缺乏精确的TypeScript类型定义
- 错误提示不足:在节点查找失败时缺乏对用户的友好提示
计划中的优化
- 重构路径查找算法:进一步简化路径查找算法,提取重复逻辑
- 增强类型定义:完善TypeScript类型定义,提高代码质量
- 用户体验优化:添加节点查找失败时的友好提示
总结
行业树选择器组件的优化重构展示了如何通过细致的代码分析找出潜在问题,并通过精确的修改和重构提高代码质量。这个案例也说明了在前端开发中,有时微小的条件判断错误可能导致功能异常,而细心的分析和优化是解决复杂问题的关键。
我们通过以下几个方面改进了组件:
- 功能修复 - 解决了选择三级行业代码不能自动展开的问题
- 代码优化 - 简化了条件判断、抽取公共逻辑、减少代码冗余
- 性能提升 - 通过缓存和懒加载策略减少API调用和不必要的计算
- 健壮性增强 - 增加了多种降级策略,确保在各种情况下都能正常工作
- 可读性提高 - 重构复杂函数,使代码更加清晰易读
这次优化不仅解决了具体的功能问题,也展示了如何在实际项目中运用代码重构技术来提高组件质量。通过这个案例,我们可以看到,即使是看似微小的修改,也可能对功能产生重大影响,因此在开发和重构过程中,需要深入理解代码逻辑,仔细分析潜在影响。
最终,经过优化的行业树选择器组件不仅功能更加完善,也更易于维护和扩展,为系统提供了更好的用户体验。