一、环境准备
Vue3 + ant-design-vue
二、代码用例
<template>
<div>
<span class="tree-node-actions">
<a-button type="text" size="small" @click="handleAddChild">
<plus-outlined />
</a-button>
<a-button type="text" size="small" @click="handleEdit">
<edit-outlined />
</a-button>
<a-button type="text" size="small" danger @click="handleDelete">
<delete-outlined />
</a-button>
</span>
<a-tree
:key="treeKey"
:tree-data="treeData"
:expanded-keys="expandedKeys"
v-model:selectedKeys="selectedKeys"
@select="onSelect"
@expand="onExpand"
>
<template #title="{ key, title }">
<div style="display: flex; align-items: center">
<span v-if="editingKey !== key">{{ title }}</span>
<a-input
v-else
ref="inputRef"
v-model:value="editingTitle"
size="small"
style="width: 120px"
@blur="handleSave(key)"
@press-enter="handleSave(key)"
/>
</div>
</template>
</a-tree>
</div>
</template>
<script setup>
import { ref, nextTick, onMounted } from 'vue';
import { EditOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
const expandedKeys = ref([]); // 存储已展开的节点keys
// 树数据
const treeData = ref([
{
key: '0',
title: '根节点',
children: [
{
key: '1',
title: '节点1',
children: [
{ key: '1-1', title: '节点1-1' },
{ key: '1-2', title: '节点1-2' },
],
},
{
key: '2',
title: '节点2',
},
],
},
]);
// 选中的节点key
const selectedKeys = ref([]);
const selectedKey = ref(null);
const selectTitle = ref('');
const treeKey = ref(0); // 用于强制刷新的key
// 编辑相关状态
const editingKey = ref('');
const editingTitle = ref('');
const inputRef = ref(null);
// 选择节点
const onSelect = (keys, { node }) => {
selectedKey.value = keys[0] || null;
selectTitle.value = node.title;
};
// 处理展开事件
const onExpand = (keys) => {
expandedKeys.value = keys;
};
// 添加子节点
const handleAddChild = async () => {
// 1. 确保父节点在expandedKeys中
if (!expandedKeys.value.includes(selectedKey.value)) {
expandedKeys.value = [...expandedKeys.value, selectedKey.value];
}
await nextTick();
editingKey.value = `new-${Date.now()}`;
editingTitle.value = '';
const addNode = (nodes) => {
for (let node of nodes) {
if (node.key === selectedKey.value) {
if (!node.children) {
node.children = [];
}
// 先添加一个空节点
node.children.push({
key: editingKey.value,
title: '',
});
return true;
}
if (node.children && node.children.length > 0) {
if (addNode(node.children)) return true;
}
}
return false;
};
if (addNode(treeData.value)) {
await nextTick();
inputRef.value?.focus();
}
};
// 编辑节点
const handleEdit = () => {
editingKey.value = selectedKey.value;
editingTitle.value = selectTitle.value;
nextTick(() => {
inputRef.value?.focus();
});
};
// 保存编辑
const handleSave = (key) => {
if (!editingTitle.value.trim()) {
// 如果内容为空,取消编辑或删除空节点
if (key.startsWith('new-')) {
removeEmptyNode(key);
}
editingKey.value = '';
return;
}
const updateNode = (nodes) => {
for (let node of nodes) {
if (node.key === key) {
node.title = editingTitle.value;
// 如果是新节点,更新key
if (key.startsWith('new-')) {
node.key = `node-${Date.now()}`;
}
return true;
}
if (node.children && node.children.length > 0) {
if (updateNode(node.children)) return true;
}
}
return false;
};
if (updateNode(treeData.value)) {
treeKey.value++; // 修改key值强制重新渲染
message.success('保存成功');
}
editingKey.value = '';
};
// 删除空节点
const removeEmptyNode = (key) => {
const deleteNode = (nodes) => {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].key === key) {
nodes.splice(i, 1);
return true;
}
if (nodes[i].children && nodes[i].children.length > 0) {
if (deleteNode(nodes[i].children)) return true;
}
}
return false;
};
deleteNode(treeData.value);
};
// 删除节点
const handleDelete = () => {
if (selectedKey.value == '0') {
message.warning('不能删除根节点');
return;
}
const deleteNode = (nodes) => {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].key === selectedKey.value) {
nodes.splice(i, 1);
return true;
}
if (nodes[i].children && nodes[i].children.length > 0) {
if (deleteNode(nodes[i].children)) return true;
}
}
return false;
};
if (deleteNode(treeData.value)) {
message.success('删除成功');
}
};
// 第一次渲染展开前两级节点
const getFirstTwoLevelKeys = (data) => {
const keys = [];
data.forEach((item) => {
keys.push(item.key); // 第一级
if (item.children) {
item.children.forEach((child) => {
keys.push(child.key); // 第二级
});
}
});
return keys;
};
onMounted(() => {
expandedKeys.value = getFirstTwoLevelKeys(treeData.value);
});
</script>
<style scoped>
.tree-node-edit {
display: flex;
align-items: center;
gap: 8px;
}
.tree-node-view {
display: flex;
align-items: center;
gap: 4px;
}
.tree-node-view:hover {
background-color: #f5f5f5;
}
</style>