更新优化调整
This commit is contained in:
@@ -2,61 +2,117 @@
|
||||
<div class="pages-sidebar-layout permission-page">
|
||||
<div class="left-box">
|
||||
<div class="header">
|
||||
<a-input v-model:value="menuFilterText" placeholder="搜索权限..." allow-clear @change="handleMenuSearch">
|
||||
<template #prefix>
|
||||
<SearchOutlined style="color: rgba(0, 0, 0, 0.45)" />
|
||||
</template>
|
||||
</a-input>
|
||||
<div class="search-wrapper">
|
||||
<a-input v-model:value="menuFilterText" placeholder="搜索权限名称或编码..." allow-clear
|
||||
@change="handleMenuSearch">
|
||||
<template #prefix>
|
||||
<SearchOutlined style="color: rgba(0, 0, 0, 0.45)" />
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a-space size="small">
|
||||
<a-tooltip title="展开全部">
|
||||
<a-button type="text" size="small" @click="handleExpandAll">
|
||||
<template #icon><UnorderedListOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="折叠全部">
|
||||
<a-button type="text" size="small" @click="handleCollapseAll">
|
||||
<template #icon><OrderedListOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="添加根权限">
|
||||
<a-button type="text" size="small" @click="handleAdd(null)">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</div>
|
||||
</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' }"
|
||||
show-line
|
||||
checkable
|
||||
:check-strictly="false"
|
||||
:expand-on-click-node="false"
|
||||
@select="onMenuSelect"
|
||||
@check="onMenuCheck">
|
||||
<template #icon="{ dataRef }">
|
||||
<ApartmentOutlined v-if="dataRef.children" />
|
||||
<FileOutlined v-else />
|
||||
</template>
|
||||
<template #title="{ dataRef }">
|
||||
<span class="tree-node-title">{{ dataRef.title }}</span>
|
||||
<PlusOutlined class="tree-node-add" @click.stop="handleAdd(dataRef)" />
|
||||
</template>
|
||||
</a-tree>
|
||||
<a-spin :spinning="loading" :delay="200">
|
||||
<a-tree
|
||||
ref="treeRef"
|
||||
v-model:selectedKeys="selectedMenuKeys"
|
||||
v-model:checkedKeys="checkedMenuKeys"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
:tree-data="filteredMenuTree"
|
||||
:field-names="{ title: 'title', key: 'id', children: 'children' }"
|
||||
show-line
|
||||
checkable
|
||||
:check-strictly="false"
|
||||
:expand-on-click-node="false"
|
||||
block-node
|
||||
@select="onMenuSelect"
|
||||
@check="onMenuCheck">
|
||||
<template #icon="{ dataRef }">
|
||||
<FolderOutlined v-if="dataRef.type === 'menu' && dataRef.children?.length" />
|
||||
<FolderOpenOutlined v-else-if="dataRef.type === 'menu'" />
|
||||
<ApiOutlined v-else-if="dataRef.type === 'api'" />
|
||||
<ControlOutlined v-else />
|
||||
</template>
|
||||
<template #title="{ dataRef }">
|
||||
<span class="tree-node-content">
|
||||
<span class="tree-node-title">{{ dataRef.title }}</span>
|
||||
<a-tag v-if="dataRef.name" class="tree-node-code" size="small">{{ dataRef.name }}</a-tag>
|
||||
<a-tag v-if="dataRef.type !== 'menu'" :color="getTypeColor(dataRef.type)" size="small">
|
||||
{{ getTypeLabel(dataRef.type) }}
|
||||
</a-tag>
|
||||
<span v-if="!dataRef.status" class="tree-node-disabled">
|
||||
<StopOutlined />
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-box">
|
||||
<div class="header">
|
||||
<div class="title">{{ selectedMenu?.title || '请选择权限节点' }}</div>
|
||||
<a-button type="link" size="small" @click="handleRefresh">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
<div class="title-wrapper">
|
||||
<span class="title">{{ selectedMenu?.title || '请选择权限节点' }}</span>
|
||||
<a-tag v-if="selectedMenu" :color="getTypeColor(selectedMenu.type)" size="small">
|
||||
{{ getTypeLabel(selectedMenu.type) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button v-if="checkedMenuKeys.length > 0" danger size="small" @click="handleDeleteBatch">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
批量删除 ({{ checkedMenuKeys.length }})
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleRefresh">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</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" />
|
||||
<a-spin :spinning="detailLoading" :delay="200">
|
||||
<save-form v-if="selectedMenu" :menu="menuTree" :menu-id="selectedMenu.id" :parent-id="parentId"
|
||||
@success="handleSaveSuccess" />
|
||||
<a-empty v-else description="请选择左侧权限节点后操作" :image-size="100" />
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
ApartmentOutlined,
|
||||
FileOutlined,
|
||||
PlusOutlined
|
||||
PlusOutlined,
|
||||
FolderOutlined,
|
||||
FolderOpenOutlined,
|
||||
ApiOutlined,
|
||||
ControlOutlined,
|
||||
UnorderedListOutlined,
|
||||
OrderedListOutlined,
|
||||
DeleteOutlined,
|
||||
StopOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import saveForm from './components/SaveForm.vue'
|
||||
import authApi from '@/api/auth'
|
||||
@@ -70,22 +126,38 @@ const menuTree = ref([])
|
||||
const filteredMenuTree = ref([])
|
||||
const selectedMenuKeys = ref([])
|
||||
const checkedMenuKeys = ref([])
|
||||
const expandedKeys = ref([])
|
||||
const menuFilterText = ref('')
|
||||
|
||||
// 当前选中的菜单
|
||||
const selectedMenu = ref(null)
|
||||
const parentId = ref(null)
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
|
||||
// 树引用
|
||||
const treeRef = ref()
|
||||
|
||||
// 加载权限树
|
||||
const loadMenuTree = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await authApi.permissions.tree.get()
|
||||
if (res.code === 200) {
|
||||
menuTree.value = res.data || []
|
||||
filteredMenuTree.value = res.data || []
|
||||
// 默认展开第一层
|
||||
expandAllKeys(menuTree.value, 1)
|
||||
} else {
|
||||
message.error(res.message || '加载权限树失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载权限树失败:', error)
|
||||
message.error('加载权限树失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,17 +175,19 @@ const handleRefresh = () => {
|
||||
|
||||
// 搜索权限
|
||||
const handleMenuSearch = (e) => {
|
||||
const keyword = e.target?.value || ''
|
||||
const keyword = (e.target?.value || '').trim()
|
||||
menuFilterText.value = keyword
|
||||
if (!keyword) {
|
||||
filteredMenuTree.value = menuTree.value
|
||||
return
|
||||
}
|
||||
|
||||
// 递归过滤权限树
|
||||
// 递归过滤权限树(支持搜索名称和编码)
|
||||
const filterTree = (nodes) => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const isMatch = node.name && node.name.toLowerCase().includes(keyword.toLowerCase())
|
||||
const titleMatch = node.title && node.title.toLowerCase().includes(keyword.toLowerCase())
|
||||
const nameMatch = node.name && node.name.toLowerCase().includes(keyword.toLowerCase())
|
||||
const isMatch = titleMatch || nameMatch
|
||||
const filteredChildren = node.children ? filterTree(node.children) : []
|
||||
|
||||
if (isMatch || filteredChildren.length > 0) {
|
||||
@@ -127,6 +201,8 @@ const handleMenuSearch = (e) => {
|
||||
}
|
||||
|
||||
filteredMenuTree.value = filterTree(menuTree.value)
|
||||
// 搜索时展开所有匹配节点
|
||||
expandAllKeys(filteredMenuTree.value)
|
||||
}
|
||||
|
||||
// 查找权限节点
|
||||
@@ -176,38 +252,66 @@ const onMenuCheck = (checkedKeys, info) => {
|
||||
console.log('checkedKeys:', checkedKeys, 'info:', info)
|
||||
}
|
||||
|
||||
// 新增权限
|
||||
const handleAdd = async (parentNode) => {
|
||||
try {
|
||||
let newMenuData = {
|
||||
parent_id: parentNode ? parentNode.id : 0,
|
||||
name: '新权限',
|
||||
code: '',
|
||||
route: '',
|
||||
component: '',
|
||||
type: 'menu',
|
||||
sort: 0,
|
||||
status: 1
|
||||
}
|
||||
|
||||
const res = await authApi.permissions.add.post(newMenuData)
|
||||
if (res.code === 200) {
|
||||
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('添加失败')
|
||||
// 获取所有节点ID(用于展开/折叠)
|
||||
const getAllKeys = (nodes) => {
|
||||
const keys = []
|
||||
const traverse = (items) => {
|
||||
items.forEach(item => {
|
||||
keys.push(item.id)
|
||||
if (item.children?.length) {
|
||||
traverse(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
traverse(nodes)
|
||||
return keys
|
||||
}
|
||||
|
||||
// 展开全部
|
||||
const handleExpandAll = () => {
|
||||
expandedKeys.value = getAllKeys(filteredMenuTree.value)
|
||||
}
|
||||
|
||||
// 折叠全部
|
||||
const handleCollapseAll = () => {
|
||||
expandedKeys.value = []
|
||||
}
|
||||
|
||||
// 自动展开指定层级的节点
|
||||
const expandAllKeys = (nodes, maxLevel = 3) => {
|
||||
const keys = []
|
||||
const traverse = (items, level = 1) => {
|
||||
items.forEach(item => {
|
||||
if (level < maxLevel && item.children?.length) {
|
||||
keys.push(item.id)
|
||||
traverse(item.children, level + 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
traverse(nodes)
|
||||
expandedKeys.value = keys
|
||||
}
|
||||
|
||||
// 获取权限类型标签
|
||||
const getTypeLabel = (type) => {
|
||||
const typeMap = {
|
||||
menu: '菜单',
|
||||
api: '接口',
|
||||
button: '按钮',
|
||||
url: '链接'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 获取权限类型颜色
|
||||
const getTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
menu: 'blue',
|
||||
api: 'green',
|
||||
button: 'orange',
|
||||
url: 'purple'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
}
|
||||
|
||||
// 批量删除权限
|
||||
@@ -256,10 +360,124 @@ const handleSaveSuccess = async () => {
|
||||
const menuNode = findMenuNode(menuTree.value, selectedMenu.value.id)
|
||||
selectedMenu.value = menuNode
|
||||
}
|
||||
message.success('保存成功')
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadMenuTree()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
loadMenuTree,
|
||||
handleExpandAll,
|
||||
handleCollapseAll
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.permission-page {
|
||||
.left-box {
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.search-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 8px;
|
||||
|
||||
:deep(.ant-tree) {
|
||||
background: transparent;
|
||||
|
||||
.ant-tree-node-content-wrapper {
|
||||
padding: 4px 0;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tree-switcher {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.ant-tree-iconEle {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.tree-node-title {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.tree-node-code {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 11px;
|
||||
background: #f0f0f0;
|
||||
border: none;
|
||||
padding: 1px 6px;
|
||||
border-radius: 2px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.tree-node-disabled {
|
||||
color: #ff4d4f;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tree-treenode-selected) {
|
||||
> .ant-tree-node-content-wrapper {
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-box {
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user