Files
laravel_swoole/resources/admin/src/pages/auth/permissions/index.vue
2026-02-11 15:49:19 +08:00

484 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="pages-sidebar-layout permission-page">
<div class="left-box">
<div class="header">
<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-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-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">
<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, nextTick } from 'vue'
import { message, Modal } from 'ant-design-vue'
import {
SearchOutlined,
ReloadOutlined,
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'
defineOptions({
name: 'authPermission'
})
// 菜单树数据
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
}
}
// 刷新
const handleRefresh = () => {
loadMenuTree()
if (selectedMenu.value) {
// 重新获取当前选中的权限详情
const menuNode = findMenuNode(menuTree.value, selectedMenu.value.id)
if (menuNode) {
selectedMenu.value = menuNode
}
}
}
// 搜索权限
const handleMenuSearch = (e) => {
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 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) {
acc.push({
...node,
children: filteredChildren.length > 0 ? filteredChildren : undefined
})
}
return acc
}, [])
}
filteredMenuTree.value = filterTree(menuTree.value)
// 搜索时展开所有匹配节点
expandAllKeys(filteredMenuTree.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)
}
// 获取所有节点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'
}
// 批量删除权限
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.permissions.batchDelete.post({ ids: checkedMenuKeys.value })
if (res.code === 200) {
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
}
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>