更新功能
This commit is contained in:
@@ -1,465 +1,333 @@
|
||||
<template>
|
||||
<div class="permission-management">
|
||||
<!-- 搜索表单 -->
|
||||
<a-card class="search-card" :bordered="false">
|
||||
<a-form :model="searchParams" layout="inline">
|
||||
<a-form-item label="关键词">
|
||||
<a-input v-model:value="searchParams.keyword" placeholder="权限名称/编码" allow-clear style="width: 200px" />
|
||||
</a-form-item>
|
||||
<a-form-item label="类型">
|
||||
<a-select v-model:value="searchParams.type" allow-clear placeholder="请选择类型" style="width: 150px">
|
||||
<a-select-option value="menu">菜单</a-select-option>
|
||||
<a-select-option value="api">接口</a-select-option>
|
||||
<a-select-option value="button">按钮</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select v-model:value="searchParams.status" allow-clear placeholder="请选择状态" style="width: 120px">
|
||||
<a-select-option :value="1">启用</a-select-option>
|
||||
<a-select-option :value="0">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><RedoOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<a-card class="table-card" :bordered="false">
|
||||
<template #title>
|
||||
<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>
|
||||
<SearchOutlined 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 }">
|
||||
<FolderOutlined 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>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<a-button type="primary" @click="handleAdd(null)">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增
|
||||
</a-button>
|
||||
<a-button v-if="selectedRowKeys.length > 0" @click="handleBatchDelete" danger>
|
||||
<a-button danger @click="handleDeleteBatch">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
批量删除
|
||||
删除
|
||||
</a-button>
|
||||
<a-button v-if="selectedRowKeys.length > 0" @click="handleBatchStatus">
|
||||
<template #icon><CheckOutlined /></template>
|
||||
批量启用
|
||||
</a-button>
|
||||
<a-button v-if="selectedRowKeys.length > 0" @click="handleBatchStatus(false)" danger>
|
||||
<template #icon><CloseOutlined /></template>
|
||||
批量禁用
|
||||
</a-button>
|
||||
<a-switch
|
||||
v-model:checked="viewMode"
|
||||
checked-children="树形"
|
||||
un-checked-children="列表"
|
||||
@change="handleViewModeChange"
|
||||
/>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 树形表格 -->
|
||||
<a-table
|
||||
v-if="viewMode"
|
||||
:columns="treeColumns"
|
||||
:data-source="treeDataSource"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:row-selection="treeRowSelection"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<a-space>
|
||||
<a-icon v-if="record.icon" :type="record.icon" />
|
||||
<span>{{ record.name }}</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="getTypeColor(record.type)">
|
||||
{{ getTypeLabel(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'success' : 'error'">
|
||||
{{ record.status === 1 ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleAddChild(record)">添加子权限</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-popconfirm title="确定要删除该权限吗?" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 列表表格 -->
|
||||
<sc-table
|
||||
v-else
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-selection="rowSelection"
|
||||
@page-change="handlePageChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<a-space>
|
||||
<a-icon v-if="record.icon" :type="record.icon" />
|
||||
<span>{{ record.name }}</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="getTypeColor(record.type)">
|
||||
{{ getTypeLabel(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'success' : 'error'">
|
||||
{{ record.status === 1 ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleAddChild(record)">添加子权限</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-popconfirm title="确定要删除该权限吗?" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</sc-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:width="700"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="formRules" :label-col="{ span: 6 }">
|
||||
<a-form-item label="上级权限" name="parent_id">
|
||||
<a-tree-select
|
||||
v-model:value="formData.parent_id"
|
||||
:tree-data="permissionTree"
|
||||
allow-clear
|
||||
placeholder="请选择上级权限"
|
||||
:field-names="{ label: 'name', value: 'id', children: 'children' }"
|
||||
tree-default-expand-all
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="权限名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入权限名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="权限编码" name="code">
|
||||
<a-input v-model:value="formData.code" placeholder="请输入权限编码,如:system.user.list" />
|
||||
</a-form-item>
|
||||
<a-form-item label="权限类型" name="type">
|
||||
<a-select v-model:value="formData.type" placeholder="请选择权限类型">
|
||||
<a-select-option value="menu">菜单</a-select-option>
|
||||
<a-select-option value="api">接口</a-select-option>
|
||||
<a-select-option value="button">按钮</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="路由" name="route">
|
||||
<a-input v-model:value="formData.route" placeholder="请输入路由,如:/admin/users" />
|
||||
</a-form-item>
|
||||
<a-form-item label="组件" name="component">
|
||||
<a-input v-model:value="formData.component" placeholder="请输入组件路径" />
|
||||
</a-form-item>
|
||||
<a-form-item label="图标" name="icon">
|
||||
<a-input v-model:value="formData.icon" placeholder="请输入图标名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sort">
|
||||
<a-input-number v-model:value="formData.sort" :min="0" :max="9999" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="1">启用</a-radio>
|
||||
<a-radio :value="0">禁用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="元数据" name="meta">
|
||||
<a-textarea v-model:value="formData.meta" placeholder="JSON格式的元数据" :rows="3" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</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, computed, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
RedoOutlined,
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { SearchOutlined, FolderOutlined, FileOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import saveForm from './save.vue'
|
||||
import authApi from '@/api/auth'
|
||||
import scTable from '@/components/scTable/index.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'PermissionManagement',
|
||||
name: 'authPermission'
|
||||
})
|
||||
|
||||
const searchParams = ref({
|
||||
keyword: '',
|
||||
type: undefined,
|
||||
status: undefined,
|
||||
})
|
||||
// 菜单树数据
|
||||
const menuTree = ref([])
|
||||
const filteredMenuTree = ref([])
|
||||
const selectedMenuKeys = ref([])
|
||||
const checkedMenuKeys = ref([])
|
||||
const menuFilterText = ref('')
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const treeDataSource = ref([])
|
||||
const pagination = ref({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
// 当前选中的菜单
|
||||
const selectedMenu = ref(null)
|
||||
const parentId = ref(null)
|
||||
|
||||
const viewMode = ref(true)
|
||||
const selectedRowKeys = ref([])
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys
|
||||
},
|
||||
}))
|
||||
|
||||
const treeRowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys
|
||||
},
|
||||
}))
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||||
{ title: '权限名称', key: 'name', width: 200 },
|
||||
{ title: '权限编码', dataIndex: 'code', key: 'code', width: 200 },
|
||||
{ title: '类型', key: 'type', width: 100, align: 'center' },
|
||||
{ title: '路由', dataIndex: 'route', key: 'route', width: 200 },
|
||||
{ title: '组件', dataIndex: 'component', key: 'component', width: 200 },
|
||||
{ title: '图标', dataIndex: 'icon', key: 'icon', width: 100 },
|
||||
{ title: '排序', dataIndex: 'sort', key: 'sort', width: 80, align: 'center' },
|
||||
{ title: '状态', key: 'status', width: 80, align: 'center' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180 },
|
||||
{ title: '操作', key: 'action', width: 220, fixed: 'right' },
|
||||
]
|
||||
|
||||
const treeColumns = [
|
||||
{ title: '权限名称', key: 'name', width: 300 },
|
||||
{ title: '权限编码', dataIndex: 'code', key: 'code', width: 200 },
|
||||
{ title: '类型', key: 'type', width: 100, align: 'center' },
|
||||
{ title: '路由', dataIndex: 'route', key: 'route', width: 200 },
|
||||
{ title: '排序', dataIndex: 'sort', key: 'sort', width: 80, align: 'center' },
|
||||
{ title: '状态', key: 'status', width: 80, align: 'center' },
|
||||
{ title: '操作', key: 'action', width: 220, fixed: 'right' },
|
||||
]
|
||||
|
||||
const permissionTree = ref([])
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('新增权限')
|
||||
const formData = ref({})
|
||||
const formRef = ref(null)
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入权限名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入权限编码', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择权限类型', trigger: 'change' }],
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
if (viewMode.value) {
|
||||
const res = await authApi.permissions.tree.get()
|
||||
treeDataSource.value = res.data
|
||||
} else {
|
||||
const res = await authApi.permissions.list.get({
|
||||
page: pagination.value.current,
|
||||
page_size: pagination.value.pageSize,
|
||||
...searchParams.value,
|
||||
})
|
||||
dataSource.value = res.data.list
|
||||
pagination.value.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取权限列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPermissionTree = async () => {
|
||||
// 加载权限树
|
||||
const loadMenuTree = async () => {
|
||||
try {
|
||||
const res = await authApi.permissions.tree.get()
|
||||
permissionTree.value = [{ id: 0, name: '顶级权限', children: res.data }]
|
||||
if (res.code === 200) {
|
||||
menuTree.value = res.data || []
|
||||
filteredMenuTree.value = res.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取权限树失败')
|
||||
console.error('加载权限树失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.value.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.value = {
|
||||
keyword: '',
|
||||
type: undefined,
|
||||
status: undefined,
|
||||
// 权限搜索
|
||||
const handleMenuSearch = (e) => {
|
||||
const keyword = e.target?.value || ''
|
||||
menuFilterText.value = keyword
|
||||
if (!keyword) {
|
||||
filteredMenuTree.value = menuTree.value
|
||||
return
|
||||
}
|
||||
pagination.value.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleViewModeChange = () => {
|
||||
selectedRowKeys.value = []
|
||||
fetchData()
|
||||
}
|
||||
// 递归过滤权限树
|
||||
const filterTree = (nodes) => {
|
||||
return nodes.reduce((acc, node) => {
|
||||
const isMatch = node.name && node.name.toLowerCase().includes(keyword.toLowerCase())
|
||||
const filteredChildren = node.children ? filterTree(node.children) : []
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
pagination.value.current = page
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增权限'
|
||||
formData.value = {
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
if (isMatch || filteredChildren.length > 0) {
|
||||
acc.push({
|
||||
...node,
|
||||
children: filteredChildren.length > 0 ? filteredChildren : undefined
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
modalVisible.value = true
|
||||
|
||||
filteredMenuTree.value = filterTree(menuTree.value)
|
||||
}
|
||||
|
||||
const handleAddChild = (record) => {
|
||||
modalTitle.value = '新增子权限'
|
||||
formData.value = {
|
||||
parent_id: record.id,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
// 查找权限节点
|
||||
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
|
||||
}
|
||||
}
|
||||
modalVisible.value = true
|
||||
return null
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
modalTitle.value = '编辑权限'
|
||||
formData.value = { ...record }
|
||||
modalVisible.value = true
|
||||
// 查找父节点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 handleSubmit = async () => {
|
||||
// 权限选择事件
|
||||
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 {
|
||||
await formRef.value.validate()
|
||||
if (formData.value.id) {
|
||||
await authApi.permissions.edit.put(formData.value.id, formData.value)
|
||||
message.success('更新成功')
|
||||
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 {
|
||||
await authApi.permissions.add.post(formData.value)
|
||||
message.success('创建成功')
|
||||
message.error(res.message || '添加失败')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
if (error.errorFields) {
|
||||
return
|
||||
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.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('删除失败')
|
||||
}
|
||||
}
|
||||
message.error(error.message || '操作失败')
|
||||
})
|
||||
}
|
||||
|
||||
// 保存成功回调
|
||||
const handleSaveSuccess = async () => {
|
||||
await loadMenuTree()
|
||||
// 重新设置当前选中的权限
|
||||
if (selectedMenu.value) {
|
||||
const menuNode = findMenuNode(menuTree.value, selectedMenu.value.id)
|
||||
selectedMenu.value = menuNode
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
formData.value = {}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
await authApi.permissions.delete.delete(id)
|
||||
message.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
try {
|
||||
await authApi.permissions.batchDelete.post({ ids: selectedRowKeys.value })
|
||||
message.success('批量删除成功')
|
||||
selectedRowKeys.value = []
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
message.error('批量删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchStatus = async (status = 1) => {
|
||||
try {
|
||||
await authApi.permissions.batchStatus.post({ ids: selectedRowKeys.value, status })
|
||||
message.success(status ? '批量启用成功' : '批量禁用成功')
|
||||
selectedRowKeys.value = []
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeColor = (type) => {
|
||||
const colors = {
|
||||
menu: 'blue',
|
||||
api: 'green',
|
||||
button: 'orange',
|
||||
}
|
||||
return colors[type] || 'default'
|
||||
}
|
||||
|
||||
const getTypeLabel = (type) => {
|
||||
const labels = {
|
||||
menu: '菜单',
|
||||
api: '接口',
|
||||
button: '按钮',
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
fetchPermissionTree()
|
||||
loadMenuTree()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.permission-management {
|
||||
padding: 16px;
|
||||
.permission-page {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.table-card {
|
||||
:deep(.ant-card-body) {
|
||||
padding: 16px;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
379
resources/admin/src/pages/auth/permissions/save.vue
Normal file
379
resources/admin/src/pages/auth/permissions/save.vue
Normal file
@@ -0,0 +1,379 @@
|
||||
<template>
|
||||
<div class="save-form">
|
||||
<a-form :model="form" :rules="rules" ref="dialogForm" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
|
||||
<!-- 第一行:权限名称和类型 -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="权限名称" name="title">
|
||||
<a-input v-model:value="form.title" placeholder="权限名称" allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="类型" name="type" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-radio-group v-model:value="form.type" button-style="solid">
|
||||
<a-radio-button value="menu">菜单</a-radio-button>
|
||||
<a-radio-button value="api">接口</a-radio-button>
|
||||
<a-radio-button value="button">按钮</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 第二行:上级菜单和权限编码 -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="上级权限" name="parent_id">
|
||||
<a-tree-select v-model:value="form.parent_id" :tree-data="menuOptions"
|
||||
:field-names="menuFieldNames" :tree-default-expand-all="false" show-icon placeholder="顶级权限"
|
||||
allow-clear tree-node-filter-prop="title" :disabled="!!menuId" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="权限编码" name="name">
|
||||
<a-input v-model:value="form.name" placeholder="如: system.user.list" allow-clear />
|
||||
<div class="form-item-msg">格式:模块.功能.操作,系统唯一标识</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 第三行:路由地址和组件路径 -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="路由地址" name="path">
|
||||
<a-input v-model:value="form.path" placeholder="/system/users" allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="组件路径" name="component">
|
||||
<a-input v-model:value="form.component" placeholder="system/users/index" allow-clear>
|
||||
<template #addonBefore>pages/</template>
|
||||
</a-input>
|
||||
<div class="form-item-msg">顶级菜单或有子菜单的父节点不需要填写</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 第四行:菜单图标和排序 -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="菜单图标" name="icon">
|
||||
<sc-icon-picker v-model:value="form.icon" placeholder="请选择图标" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="排序" name="sort">
|
||||
<a-input-number v-model:value="form.sort" :min="0" :max="10000" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<a-divider />
|
||||
|
||||
<!-- 选项设置区域 -->
|
||||
<div class="options-section">
|
||||
<h4>选项设置</h4>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="是否隐藏" name="hidden" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
|
||||
<a-checkbox v-model:checked="form.hidden">隐藏菜单</a-checkbox>
|
||||
<a-checkbox v-model:checked="form.hiddenBreadcrumb">隐藏面包屑</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="是否缓存" name="keepAlive" :label-col="{ span: 8 }" :wrapper-col="{ span: 14 }">
|
||||
<a-switch v-model:checked="form.keepAlive" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="是否固定" name="affix" :label-col="{ span: 8 }" :wrapper-col="{ span: 14 }">
|
||||
<a-switch v-model:checked="form.affix" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 状态 -->
|
||||
<a-divider />
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" name="status" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-switch v-model:checked="statusChecked" checked-children="启用" un-checked-children="禁用" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<a-form-item :wrapper-col="{ span: 18, offset: 4 }" style="margin-top: 32px">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSave" :loading="loading" size="large">保存</a-button>
|
||||
<a-button @click="$emit('cancel')" size="large">取消</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import authApi from '@/api/auth'
|
||||
import ScIconPicker from '@/components/scIconPicker/index.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'PermissionSave'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
menu: { type: [Object, Array], default: () => [] },
|
||||
menuId: { type: [Number, String], default: null },
|
||||
parentId: { type: [Number, String], default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['success', 'cancel'])
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
id: '',
|
||||
parent_id: 0,
|
||||
name: '',
|
||||
title: '',
|
||||
path: '',
|
||||
component: '',
|
||||
icon: '',
|
||||
sort: 0,
|
||||
type: 'menu',
|
||||
status: 1,
|
||||
// meta 字段内容
|
||||
hidden: false,
|
||||
hiddenBreadcrumb: false,
|
||||
keepAlive: false,
|
||||
affix: false
|
||||
})
|
||||
|
||||
// 状态开关计算属性
|
||||
const statusChecked = computed({
|
||||
get: () => form.status === 1,
|
||||
set: (val) => {
|
||||
form.status = val ? 1 : 0
|
||||
}
|
||||
})
|
||||
|
||||
// 表单引用
|
||||
const dialogForm = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
// 验证规则
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入权限名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入权限编码', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 菜单选项
|
||||
const menuOptions = ref([])
|
||||
const menuFieldNames = {
|
||||
value: 'id',
|
||||
label: 'title',
|
||||
children: 'children'
|
||||
}
|
||||
|
||||
// 简单化菜单树,排除自己和子节点
|
||||
const treeToMap = (tree, excludeId = null) => {
|
||||
const map = []
|
||||
tree.forEach(item => {
|
||||
if (item.id === excludeId) return // 排除自己
|
||||
const obj = {
|
||||
id: item.id,
|
||||
parent_id: item.parent_id,
|
||||
title: item.title,
|
||||
children: item.children && item.children.length > 0 ? treeToMap(item.children, excludeId) : null
|
||||
}
|
||||
map.push(obj)
|
||||
})
|
||||
return map
|
||||
}
|
||||
|
||||
// 查找权限节点
|
||||
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
|
||||
}
|
||||
|
||||
// 监听菜单树变化
|
||||
watch(
|
||||
() => props.menu,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
// 排除当前编辑的节点,避免选择自己作为父节点
|
||||
menuOptions.value = treeToMap(newVal, props.menuId)
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
// 监听 menuId 变化,从菜单树中查找并赋值
|
||||
watch(
|
||||
() => props.menuId,
|
||||
(newVal) => {
|
||||
if (newVal && props.menu && props.menu.length > 0) {
|
||||
const menuNode = findMenuNode(props.menu, newVal)
|
||||
if (menuNode) {
|
||||
setData(menuNode, props.parentId)
|
||||
}
|
||||
} else if (!newVal) {
|
||||
// 清空表单
|
||||
setData({
|
||||
id: '',
|
||||
parent_id: props.parentId || 0,
|
||||
name: '',
|
||||
title: '',
|
||||
route: '',
|
||||
component: '',
|
||||
icon: '',
|
||||
sort: 0,
|
||||
type: 'menu',
|
||||
status: 1,
|
||||
hidden: false,
|
||||
hiddenBreadcrumb: false,
|
||||
keepAlive: false,
|
||||
affix: false
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 加载权限详情
|
||||
const loadMenuDetail = async (id) => {
|
||||
try {
|
||||
const res = await authApi.permissions.detail.get(id)
|
||||
if (res.code === 200 && res.data) {
|
||||
setData(res.data, props.parentId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载权限详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await dialogForm.value.validate()
|
||||
loading.value = true
|
||||
|
||||
// 构建提交数据
|
||||
const submitData = {
|
||||
id: form.id || undefined,
|
||||
parent_id: form.parent_id || 0,
|
||||
name: form.name,
|
||||
title: form.title,
|
||||
path: form.path,
|
||||
component: form.component,
|
||||
icon: form.icon,
|
||||
sort: form.sort,
|
||||
type: form.type,
|
||||
status: form.status,
|
||||
meta: {
|
||||
hidden: form.hidden,
|
||||
hiddenBreadcrumb: form.hiddenBreadcrumb,
|
||||
keepAlive: form.keepAlive,
|
||||
affix: form.affix
|
||||
}
|
||||
}
|
||||
|
||||
let res = {}
|
||||
if (form.id) {
|
||||
res = await authApi.permissions.edit.put(form.id, submitData)
|
||||
} else {
|
||||
res = await authApi.permissions.add.post(submitData)
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
if (res.code === 200) {
|
||||
message.success('保存成功')
|
||||
emit('success')
|
||||
} else {
|
||||
message.error(res.message || '保存失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('表单验证失败', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 表单注入数据
|
||||
const setData = (data, pid) => {
|
||||
form.id = data.id || ''
|
||||
form.parent_id = data.parent_id !== undefined ? data.parent_id : (pid || 0)
|
||||
form.name = data.name || ''
|
||||
form.title = data.title || ''
|
||||
form.path = data.path || ''
|
||||
form.component = data.component || ''
|
||||
form.icon = data.icon || ''
|
||||
form.sort = data.sort || 0
|
||||
form.type = data.type || 'menu'
|
||||
form.status = data.status !== undefined ? data.status : 1
|
||||
|
||||
// 解析 meta 字段
|
||||
const meta = data.meta && typeof data.meta === 'string' ? JSON.parse(data.meta) : (data.meta || {})
|
||||
form.hidden = meta.hidden || false
|
||||
form.hiddenBreadcrumb = meta.hiddenBreadcrumb || false
|
||||
form.keepAlive = meta.keepAlive || false
|
||||
form.affix = meta.affix || false
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
if (props.menuId) {
|
||||
loadMenuDetail(props.menuId)
|
||||
} else if (props.parentId) {
|
||||
form.parent_id = props.parentId
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
setData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.save-form {
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
|
||||
.form-item-msg {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-top: 4px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.options-section {
|
||||
margin-top: 16px;
|
||||
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-divider) {
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user