Files
vueadmin/src/pages/auth/permission/index.vue
T
2026-01-24 10:12:31 +08:00

333 lines
7.5 KiB
Vue

<template>
<div class="pages permission-page">
<div class="left-box">
<div class="header">
<a-input v-model:value="menuFilterText" placeholder="搜索菜单..." allow-clear @change="handleMenuSearch">
<template #prefix>
<search-outlined style="color: rgba(0, 0, 0, 0.45)" />
</template>
</a-input>
</div>
<div class="body">
<a-tree v-model:selectedKeys="selectedMenuKeys" v-model:checkedKeys="checkedMenuKeys"
:tree-data="filteredMenuTree" :field-names="{ title: 'title', key: 'id', children: 'children' }"
showLine checkable :check-strictly="true" :expand-on-click-node="false" @select="onMenuSelect"
@check="onMenuCheck">
<template #icon="{ dataRef }">
<folder-outlined v-if="dataRef.children" />
<file-outlined v-else />
</template>
<template #title="{ dataRef }">
<span class="tree-node-title">{{ dataRef.title }}</span>
<plus-outlined class="tree-node-add" @click.stop="handleAdd(dataRef)" />
</template>
</a-tree>
</div>
<div class="footer">
<a-space>
<a-button type="primary" @click="handleAdd(null)">
<template #icon><plus-outlined /></template>
新增
</a-button>
<a-button danger @click="handleDeleteBatch">
<template #icon><delete-outlined /></template>
删除
</a-button>
</a-space>
</div>
</div>
<div class="right-box">
<div class="header">
<div class="title">{{ selectedMenu?.title || '请选择菜单' }}</div>
</div>
<div class="body">
<save-form v-if="selectedMenu" :menu="menuTree" :menu-id="selectedMenu.id" :parent-id="parentId"
@success="handleSaveSuccess" />
<a-empty v-else description="请选择左侧菜单后操作" :image-size="100" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue'
import saveForm from './save.vue'
import authApi from '@/api/auth'
defineOptions({
name: 'authPermission'
})
// 菜单树数据
const menuTree = ref([])
const filteredMenuTree = ref([])
const selectedMenuKeys = ref([])
const checkedMenuKeys = ref([])
const menuFilterText = ref('')
// 当前选中的菜单
const selectedMenu = ref(null)
const parentId = ref(null)
// 加载菜单树
const loadMenuTree = async () => {
try {
const res = await authApi.menu.list.get({ is_tree: 1 })
if (res.code === 1) {
menuTree.value = res.data || []
filteredMenuTree.value = res.data || []
}
} catch (error) {
console.error('加载菜单树失败:', error)
}
}
// 菜单搜索
const handleMenuSearch = (e) => {
const keyword = e.target?.value || ''
menuFilterText.value = keyword
if (!keyword) {
filteredMenuTree.value = menuTree.value
return
}
// 递归过滤菜单树
const filterTree = (nodes) => {
return nodes.reduce((acc, node) => {
const isMatch = node.title && node.title.toLowerCase().includes(keyword.toLowerCase())
const filteredChildren = node.children ? filterTree(node.children) : []
if (isMatch || filteredChildren.length > 0) {
acc.push({
...node,
children: filteredChildren.length > 0 ? filteredChildren : undefined
})
}
return acc
}, [])
}
filteredMenuTree.value = filterTree(menuTree.value)
}
// 查找菜单节点
const findMenuNode = (tree, id) => {
for (const node of tree) {
if (node.id === id) {
return node
}
if (node.children && node.children.length > 0) {
const found = findMenuNode(node.children, id)
if (found) return found
}
}
return null
}
// 查找父节点ID
const findParentId = (tree, id) => {
for (const node of tree) {
if (node.children && node.children.length > 0) {
const child = node.children.find(child => child.id === id)
if (child) {
return node.id
}
const found = findParentId(node.children, id)
if (found !== null) return found
}
}
return null
}
// 菜单选择事件
const onMenuSelect = (selectedKeys, { selected }) => {
if (selected) {
const menuId = selectedKeys[0]
const menuNode = findMenuNode(menuTree.value, menuId)
selectedMenu.value = menuNode
parentId.value = findParentId(menuTree.value, menuId)
} else {
selectedMenu.value = null
parentId.value = null
}
}
// 菜单勾选事件
const onMenuCheck = (checkedKeys, info) => {
console.log('checkedKeys:', checkedKeys, 'info:', info)
}
// 新增菜单
const handleAdd = async (parentNode) => {
try {
let newMenuData = {
parent_id: parentNode ? parentNode.id : 0,
name: '新菜单',
path: '',
component: '',
title: '新菜单',
type: 'menu',
sort: 0
}
const res = await authApi.menu.add.post(newMenuData)
if (res.code === 1) {
newMenuData.id = res.data.id
message.success('添加成功')
await loadMenuTree()
// 选中新增的菜单
selectedMenuKeys.value = [newMenuData.id]
const menuNode = findMenuNode(menuTree.value, newMenuData.id)
selectedMenu.value = menuNode
parentId.value = parentNode ? parentNode.id : null
} else {
message.error(res.message || '添加失败')
}
} catch (error) {
console.error('添加菜单失败:', error)
message.error('添加失败')
}
}
// 批量删除菜单
const handleDeleteBatch = async () => {
if (checkedMenuKeys.value.length === 0) {
message.warning('请选择需要删除的菜单')
return
}
Modal.confirm({
title: '确认删除',
content: `确定删除已选择的 ${checkedMenuKeys.value.length} 个菜单吗?`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
const res = await authApi.menu.delete.post({ ids: checkedMenuKeys.value })
if (res.code === 1) {
message.success('删除成功')
// 如果当前选中的菜单被删除了,清空选择
if (selectedMenu.value && checkedMenuKeys.value.includes(selectedMenu.value.id)) {
selectedMenu.value = null
selectedMenuKeys.value = []
}
checkedMenuKeys.value = []
await loadMenuTree()
} else {
message.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除菜单失败:', error)
message.error('删除失败')
}
}
})
}
// 保存成功回调
const handleSaveSuccess = async () => {
await loadMenuTree()
// 重新设置当前选中的菜单
if (selectedMenu.value) {
const menuNode = findMenuNode(menuTree.value, selectedMenu.value.id)
selectedMenu.value = menuNode
}
}
// 初始化
onMounted(() => {
loadMenuTree()
})
</script>
<style scoped lang="scss">
.permission-page {
display: flex;
flex-direction: row;
height: 100%;
padding: 0;
.left-box {
width: 300px;
border-right: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
background: #fff;
.header {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
height: 56px;
}
.body {
flex: 1;
overflow-y: auto;
padding: 12px;
.tree-node-title {
flex: 1;
}
.tree-node-add {
margin-left: 8px;
color: #999;
display: none;
&:hover {
color: #1890ff;
}
}
:deep(.ant-tree-node-content-wrapper) {
width: 100%;
&:hover {
.tree-node-add {
display: inline-block;
}
}
}
}
.footer {
padding: 12px 16px;
border-top: 1px solid #f0f0f0;
background: #fafafa;
}
}
.right-box {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.header {
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
height: 56px;
.title {
font-size: 18px;
font-weight: 500;
color: #333;
}
}
.body {
flex: 1;
overflow-y: auto;
background: #f5f5f5;
}
}
}
</style>