基于AntD树形组织结构支持删除、重命名等
效果图

Code
<template>
<div class="file-side-bar">
<div class="header">
<span class="title">文档分组</span>
<img alt="add" class="cursor-pointer" src="@/assets/plus.svg" @click="onModalVisibleChange(true)" />
</div>
<a-input
v-model:value="searchValue"
placeholder="请输入分类名称"
style="margin-bottom: 8px"
@change="throttledLoadTreeData"
@pressEnter="throttledLoadTreeData"
>
<template #suffix>
<img alt="" class="search-icon" src="@/assets/images/search-grey.svg" @click="throttledLoadTreeData" />
</template>
</a-input>
<div class="tree-box">
<a-spin :spinning="spinning">
<a-tree
v-model:expandedKeys="expandedKeys"
v-model:selectedKeys="selectedKeys"
:tree-data="treeData"
style="background: transparent; overflow: hidden;"
@select="onSelect"
>
<template #title="{ title, value, key }">
<div class="title-box tree-title">
<div v-if="editingNodeId !== key" class="single-text">{{ title }}</div>
<div
v-else
>
<a-input
v-model:value="editingTitle"
size="small"
class="rename-input"
/>
<CheckOutlined style="color: lightgreen;" class="rename-icon confirm-icon" @click="confirmRename(key)" />
<CloseOutlined class="rename-icon cancel-icon" title="取消" @click="cancelRename" />
</div>
<a-dropdown>
<img alt class="cursor-pointer" src="@/assets/opt.svg" @click.stop />
<template #overlay>
<a-menu @click="({ key }) => onMenuClick(key, title, value )">
<a-menu-item key="rename">重命名</a-menu-item>
<a-menu-item key="move">移动至</a-menu-item>
<a-menu-item key="delete">删除</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</template>
</a-tree>
</a-spin>
</div>
</div>
<HzModal
:is-show-footer="true"
:loading="modalLoading"
:title="modalTitle"
:visible="isModalVisible"
:width="520"
@cancel="onModalVisibleChange(false)"
@ok="handleOk"
>
<a-form ref="formRef" :label-col="{span: 4}" :model="modalState" :rules="rules">
<a-form-item label="所属分类" name="parentId">
<a-tree-select
v-model:value="modalState.parentId"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:tree-data="treeData"
allow-clear
class="api-ant-select full-width"
placeholder="请选择上级分类"
show-search
tree-default-expand-all
>
</a-tree-select>
</a-form-item>
<a-form-item label="分类名称" name="name">
<a-input v-model:value="modalState.name" class="modal-input" placeholder="请输入分类名称" />
</a-form-item>
</a-form>
</HzModal>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { findParentKey, isResponseSuccess } from '@/utils/bean'
import { isEmpty, throttle } from 'lodash'
import { useStore } from 'vuex'
import mitt from '@/utils/mitt'
import { addCatalogue, deleteCatalogue, getCatalogueTree, updateCatalogue } from '@/api/biz/common'
import { useHzModal } from '@/views/pages/archive/components/ModalComs/AddUpdateComs/extends/useHzModal'
import HzModal from '@/components/common/HzModal.vue'
import { message } from 'ant-design-vue'
import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue'
const store = useStore()
const searchValue = ref('')
const expandedKeys = ref([])
const selectedKeys = ref([])
const treeData = ref([])
const isExpanded = ref(true)
const formRef = ref()
const resetForm = () => {
formRef.value?.resetFields()
}
const rules = {
// parentId: [{ required: true, message: '请选择所属分类', trigger: 'change' }],
name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }]
}
const modalState = ref({
parentId: null,
name: ''
})
const modalTitle = ref('新增分类')
const submitForm = () => {
formRef.value?.validate().then(async () => {
console.log('✅ 表单验证通过,提交数据:', modalState.value)
modalLoading.value = true
let payload = {
...modalState.value
}
let execFun;
if(modalTitle.value.includes("更新分类")) {
execFun = updateCatalogue
payload.id = curUpdateId.value
}else {
execFun = addCatalogue
}
const res = await execFun(payload)
if (res.status === 200) {
modalLoading.value = false
message.success('操作成功')
isModalVisible.value = false
resetForm()
await loadTreeData()
}
// 提交逻辑
}).catch(err => {
console.log('❌ 表单验证失败:', err)
})
}
const onCancel = () => {
handleCancel()
}
const {
isModalVisible,
onModalVisibleChange,
handleOk,
handleCancel,
modalLoading,
onModalLoadingChange
} = useHzModal(submitForm, onCancel)
const onSelect = (value) => {
store.dispatch('param/setFileDirectorySelectedIdAction', value[0]).then(() => {
mitt.emit('load-list-file')
})
}
// 提取第一层
const getExpandedKeys = (tree) => {
const keys = []
for (const node of tree) {
keys.push(node.id)
}
return keys
}
const spinning = ref(false)
const loadTreeData = async () => {
try {
spinning.value = true
const res = await getCatalogueTree({ name: searchValue.value })
if (isResponseSuccess(res) && Array.isArray(res?.data?.data)) {
spinning.value = false
const rawTree = res.data.data
if (isEmpty(rawTree)) return
treeData.value = recursive(rawTree)
expandedKeys.value = getExpandedKeys(rawTree)
isExpanded.value = true
} else {
console.warn('🚨 deptTree response invalid:', res)
}
} catch (err) {
console.error('❌ Error loading tree data:', err)
}
}
const throttledLoadTreeData = throttle(loadTreeData, 600)
const recursive = (arr) => {
if (isEmpty(arr)) return []
return arr.map(item => ({
key: item.id,
title: item.name,
value: item.id,
children: recursive(item.children || [])
}))
}
const doDelete = async (id) => {
const res = await deleteCatalogue({ id })
if (res.status === 200) {
message.success('删除成功')
await loadTreeData()
}
}
const curUpdateId = ref(null)
const handleMove = (id, title) => {
modalTitle.value = '更新分类'
let parentId = findParentKey(treeData.value, id)
console.log('output-> parentId::: ❤️🔥❤️🔥', parentId)
modalState.value.name = title
modalState.value.parentId = parentId
curUpdateId.value = id
isModalVisible.value = true
}
const editingNodeId = ref(null)
const editingTitle = ref('')
const startRename = (key, title) => {
editingNodeId.value = key
editingTitle.value = title
}
const cancelRename = () => {
editingNodeId.value = null
editingTitle.value = ''
}
const confirmRename = async (id) => {
if (!editingTitle.value.trim()) {
message.warning('分类名称不能为空')
return
}
const res = await updateCatalogue({ id, name: editingTitle.value })
if (res.status === 200) {
message.success('重命名成功')
cancelRename()
await loadTreeData() // 确保你定义了 loadTreeData 用于刷新树数据
}
}
function onMenuClick(key, title, value) {
console.log('output-> {title,key,value}', title, key, value)
switch (key) {
case 'rename':
startRename(value, title)
console.log('执行重命名逻辑')
break
case 'move':
handleMove(value, title)
console.log('执行移动逻辑')
break
case 'delete':
doDelete(value)
console.log('执行删除逻辑')
break
}
}
onMounted(async () => {
await loadTreeData()
})
onBeforeUnmount(() => {
mitt.off('reset-selectedKeys')
})
</script>
<style lang="scss" scoped>
.file-side-bar {
width: 100%;
height: 100%;
background: rgba(0, 76, 198, 0.3);
padding: 8px 12px;
position: relative;
.header {
width: 100%;
display: flex;
justify-content: space-between;
margin-bottom: 12px;
.title {
font-family: PingFang SC;
font-weight: 500;
font-size: 16px;
line-height: 22px;
vertical-align: middle;
color: rgba(255, 255, 255, 1);
}
}
.search-icon {
cursor: pointer;
}
.toggle-icon {
width: 50px;
height: 50px;
}
.expand-toggle {
position: absolute;
right: -23px;
top: 30%;
transform: translateY(-30%);
z-index: 999;
padding: 4px;
border-radius: 4px;
cursor: pointer;
}
.tree-box {
position: relative;
margin-top: 8px;
max-height: 610px;
overflow-y: scroll;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
//// 隐藏滚动条(兼容 Chrome、Edge)
//&::-webkit-scrollbar {
// width: 0;
// height: 0;
//}
//
//// Firefox
//scrollbar-width: none;
//
//// IE/Edge 旧版本
//-ms-overflow-style: none;
}
span {
border: 2px solid var(--light-mono-a500, rgba(15, 34, 67, 0.2));
}
}
.title-box {
display: flex;
justify-content: space-between;
span {
font-family: Microsoft YaHei;
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-transform: capitalize;
color: rgba(255, 255, 255, 1);
}
}
:deep(.ant-tree .ant-tree-node-selected) {
background: transparent !important;
}
:deep(.ant-tree-treenode-selected) {
background: linear-gradient(90deg, rgba(61, 133, 255, 0.6) 0%, rgba(61, 133, 255, 0.382973) 36.17%, rgba(61, 133, 255, 0.144) 100%);
:deep(.ant-tree-treenode-selected::after) {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 28px;
background: rgba(1, 150, 255, 1);
border-radius: 2px;
z-index: 1;
}
}
:deep(.ant-tree.ant-tree-directory .ant-tree-treenode-selected::before) {
background: linear-gradient(90deg, #146BFD 0%, rgba(20, 107, 253, 0.638288) 36.17%, rgba(20, 107, 253, 0.2) 100%) !important;
}
:deep(.ant-tree-list-holder),
:deep(.ant-tree) {
background-color: transparent !important;
color: #fff;
}
:deep(.ant-input-affix-wrapper),
:deep(.ant-input) {
border: 1px solid rgba(204, 219, 255, 0.24);
background-color: #103b6c;
color: rgba(255, 255, 255, 0.48) !important;
}
:deep(.ant-input::placeholder) {
color: rgba(255, 255, 255, 0.48) !important;
}
:deep(.ant-input-affix-wrapper) {
border: 1px solid rgba(0, 117, 255, 1) !important;
}
:deep(.anticon) {
color: rgba(71, 179, 255, 1);
}
.modal-input {
background: #FFFFFF;
color: #000 !important;
}
:deep(.ant-input::placeholder) {
color: grey !important;
}
.single-text {
width: 140px;
overflow: hidden; //超出的文本隐藏
text-overflow: ellipsis; //用省略号显示
white-space: nowrap; //不换行(文字不允许换行,单行文本)
}
.tree-title {
min-width: 210px;
display: flex;
align-items: center;
}
.rename-input {
width: 120px;
margin-right: 4px;
}
.rename-icon {
cursor: pointer;
margin-left: 8px;
}
.confirm-icon {
color: lightgreen;
}
.cancel-icon {
color: #fff;
}
</style>
学而不思则罔,思而不学则殆!

浙公网安备 33010602011771号