权限模块功能基本改写完成
This commit is contained in:
+315
-155
@@ -1,170 +1,330 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<el-aside width="300px" v-loading="menuloading">
|
||||
<el-container>
|
||||
<el-header>
|
||||
<el-input placeholder="输入关键字进行过滤" v-model="menuFilterText" clearable></el-input>
|
||||
</el-header>
|
||||
<el-main class="nopadding">
|
||||
<el-tree ref="menu" class="menu" node-key="id" :data="menuList" :props="menuProps" draggable highlight-current :expand-on-click-node="false" check-strictly show-checkbox :filter-node-method="menuFilterNode" @node-click="menuClick" @node-drop="nodeDrop">
|
||||
|
||||
<template #default="{node, data}">
|
||||
<span class="custom-tree-node el-tree-node__label">
|
||||
<span class="label">
|
||||
{{ node.label }}
|
||||
</span>
|
||||
<span class="do">
|
||||
<el-icon @click.stop="add(node, data)"><el-icon-plus /></el-icon>
|
||||
</span>
|
||||
</span>
|
||||
<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>
|
||||
|
||||
</el-tree>
|
||||
</el-main>
|
||||
<el-footer style="height:51px;">
|
||||
<el-button type="primary" icon="el-icon-plus" @click="add()"></el-button>
|
||||
<el-button type="danger" plain icon="el-icon-delete" @click="delMenu"></el-button>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-main class="nopadding" style="padding:20px;" ref="main">
|
||||
<save ref="save" :menu="menuList"></save>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</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>
|
||||
let newMenuIndex = 1;
|
||||
import save from './save'
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import saveForm from './save.vue'
|
||||
import authApi from '@/api/auth'
|
||||
|
||||
export default {
|
||||
name: "auth.permission",
|
||||
components: {
|
||||
save
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
menuloading: false,
|
||||
menuList: [],
|
||||
menuProps: {
|
||||
label: (data)=>{
|
||||
return data.title
|
||||
}
|
||||
},
|
||||
menuFilterText: ""
|
||||
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 || []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
menuFilterText(val){
|
||||
this.$refs.menu.filter(val);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getMenu();
|
||||
},
|
||||
methods: {
|
||||
//加载树数据
|
||||
async getMenu(){
|
||||
this.menuloading = true
|
||||
var res = await this.$API.auth.menu.list.get({is_tree: 1});
|
||||
if(res.code == 1){
|
||||
this.menuloading = false
|
||||
this.menuList = res.data;
|
||||
}
|
||||
},
|
||||
//树点击
|
||||
menuClick(data, node){
|
||||
var pid = node.level==1?undefined:node.parent.data.id;
|
||||
this.$refs.save.setData(data, pid)
|
||||
this.$refs.main.$el.scrollTop = 0
|
||||
},
|
||||
//树过滤
|
||||
menuFilterNode(value, data){
|
||||
if (!value) return true;
|
||||
var targetText = data.title;
|
||||
return targetText.indexOf(value) !== -1;
|
||||
},
|
||||
//树拖拽
|
||||
nodeDrop(draggingNode, dropNode, dropType){
|
||||
if(dropType == 'before'){
|
||||
} catch (error) {
|
||||
console.error('加载菜单树失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
}else if(dropType == 'after'){
|
||||
// 菜单搜索
|
||||
const handleMenuSearch = (e) => {
|
||||
const keyword = e.target?.value || ''
|
||||
menuFilterText.value = keyword
|
||||
if (!keyword) {
|
||||
filteredMenuTree.value = menuTree.value
|
||||
return
|
||||
}
|
||||
|
||||
}else if(dropType == 'inner'){
|
||||
// 递归过滤菜单树
|
||||
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) : []
|
||||
|
||||
}
|
||||
},
|
||||
//增加
|
||||
async add(node, data){
|
||||
var newMenuName = "新菜单" + newMenuIndex++;
|
||||
var newMenuData = {
|
||||
parent_id: data ? data.id : 0,
|
||||
name: newMenuName,
|
||||
path: "",
|
||||
component: "",
|
||||
title: newMenuName,
|
||||
type: "menu",
|
||||
sort: 0
|
||||
}
|
||||
this.menuloading = true
|
||||
var res = await this.$API.auth.menu.add.post(newMenuData)
|
||||
this.menuloading = false
|
||||
newMenuData.id = res.data.id
|
||||
|
||||
this.$refs.menu.append(newMenuData, node)
|
||||
this.$refs.menu.setCurrentKey(newMenuData.id)
|
||||
var pid = node ? node.data.id : ""
|
||||
this.$refs.save.setData(newMenuData, pid)
|
||||
},
|
||||
//删除菜单
|
||||
async delMenu(){
|
||||
var CheckedNodes = this.$refs.menu.getCheckedNodes()
|
||||
if(CheckedNodes.length == 0){
|
||||
this.$message.warning("请选择需要删除的项")
|
||||
return false;
|
||||
}
|
||||
|
||||
var confirm = await this.$confirm('确认删除已选择的菜单吗?','提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
confirmButtonClass: 'el-button--danger'
|
||||
}).catch(() => {})
|
||||
if(confirm != 'confirm'){
|
||||
return false
|
||||
}
|
||||
|
||||
this.menuloading = true
|
||||
var reqData = {
|
||||
ids: CheckedNodes.map(item => item.id)
|
||||
}
|
||||
var res = await this.$API.auth.menu.delete.post(reqData)
|
||||
this.menuloading = false
|
||||
|
||||
if(res.code == 1){
|
||||
CheckedNodes.forEach(item => {
|
||||
var node = this.$refs.menu.getNode(item)
|
||||
if(node.isCurrent){
|
||||
this.$refs.save.setData({})
|
||||
}
|
||||
this.$refs.menu.remove(item)
|
||||
if (isMatch || filteredChildren.length > 0) {
|
||||
acc.push({
|
||||
...node,
|
||||
children: filteredChildren.length > 0 ? filteredChildren : undefined
|
||||
})
|
||||
}else{
|
||||
this.$message.warning(res.msg)
|
||||
}
|
||||
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, node }) => {
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-tree-node {display: flex;flex: 1;align-items: center;justify-content: space-between;font-size: 14px;padding-right: 24px;height:100%;}
|
||||
.custom-tree-node .label {display: flex;align-items: center;;height: 100%;}
|
||||
.custom-tree-node .label .el-tag {margin-left: 5px;}
|
||||
.custom-tree-node .do {display: none;}
|
||||
.custom-tree-node .do i {margin-left:5px;color: #999;}
|
||||
.custom-tree-node .do i:hover {color: #333;}
|
||||
|
||||
.custom-tree-node:hover .do {display: inline-block;}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user