fix(权限): 修复权限校验位置错误并优化用户信息更新逻辑

修复多处权限校验位置错误,将v-hasPerm从按钮移至父级dropdown组件
优化系统管理模块提交后的用户信息更新逻辑,自动刷新权限状态
修复菜单管理表单校验和默认值问题
重构资源管理界面布局和交互体验
This commit is contained in:
zhangtao
2025-10-10 00:33:03 +08:00
parent 1ee6278107
commit cbbcdfde4b
13 changed files with 176 additions and 288 deletions
@@ -65,6 +65,7 @@
</div>
</div>
<el-dropdown
v-hasPerm="['application:myapp:update']"
trigger="click"
@command="(command) => handleAppAction(command, app)"
>
@@ -76,8 +77,8 @@
/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-hasPerm="['application:myapp:update']" command="edit" icon="Edit">编辑</el-dropdown-item>
<el-dropdown-item v-hasPerm="['application:myapp:delete']" command="delete" icon="Delete" divided>删除</el-dropdown-item>
<el-dropdown-item command="edit" icon="Edit">编辑</el-dropdown-item>
<el-dropdown-item command="delete" icon="Delete" divided>删除</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -64,8 +64,8 @@
<div class="data-table__toolbar--actions">
<el-button v-hasPerm="['workflow:operator:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
<el-button v-hasPerm="['workflow:operator:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
<el-dropdown trigger="click">
<el-button v-hasPerm="['workflow:operator:status']" type="default" :disabled="selectIds.length === 0" icon="ArrowDown">更多</el-button>
<el-dropdown v-hasPerm="['application:myapp:update']" trigger="click">
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
@@ -114,8 +114,8 @@
</el-col>
<el-col :span="1.5">
<el-tooltip content="列表筛选">
<el-dropdown trigger="click">
<el-button v-hasPerm="['demo:example:filter']" type="default" icon="operation" circle/>
<el-dropdown v-hasPerm="['demo:example:filter']" trigger="click">
<el-button type="default" icon="operation" circle/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="column in tableColumns" :key="column.prop" :command="column">
+2 -2
View File
@@ -97,8 +97,8 @@
</el-col>
<el-col :span="1.5">
<el-tooltip content="列表筛选">
<el-dropdown trigger="click">
<el-button v-hasPerm="['generator:demo:filter']" type="default" icon="operation" circle/>
<el-dropdown v-hasPerm="['generator:demo:filter']" trigger="click">
<el-button type="default" icon="operation" circle/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="column in tableColumns" :key="column.prop" :command="column">
+57 -156
View File
@@ -18,11 +18,26 @@
<template #header>
<div class="card-header">
<span>
<el-tooltip content="资源文件管理系统">
<el-tooltip content="资源文件管理系统: 点击路径可以快速返回上级目录">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
文件列表
文件列表(当前路径):
</span>
<!-- 资源路径 -->
<div class="breadcrumb-container">
<span class="breadcrumb-label"></span>
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="(item, index) in breadcrumbList"
:key="index"
:class="{ 'is-link': index < breadcrumbList.length - 1 }"
@click="handleBreadcrumbClick(item)"
>
{{ item.name }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
</template>
@@ -75,28 +90,6 @@
</div>
</div>
<!-- 资源路径 -->
<div class="breadcrumb-section">
<div class="breadcrumb-container">
<div class="breadcrumb-header">
<el-tooltip content="点击路径可以快速返回上级目录">
<el-icon class="breadcrumb-icon"><QuestionFilled /></el-icon>
</el-tooltip>
<span class="breadcrumb-label">当前路径:</span>
</div>
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="(item, index) in breadcrumbList"
:key="index"
:class="{ 'is-link': index < breadcrumbList.length - 1 }"
@click="handleBreadcrumbClick(item)"
>
{{ item.name }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
<!-- 表格区域 -->
<el-table
v-if="viewMode === 'list'"
@@ -105,7 +98,7 @@
:data="fileList"
row-key="file_url"
class="data-table__content"
height="540"
height="450"
border
stripe
@selection-change="handleSelectionChange"
@@ -137,11 +130,6 @@
<span v-if="!row.is_dir">{{ formatFileSize(row.size) }}</span>
</template>
</el-table-column>
<el-table-column label="类型" prop="file_type" min-width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.file_type" size="small">{{ row.file_type }}</el-tag>
</template>
</el-table-column>
<el-table-column label="修改时间" prop="modified_time" min-width="180" sortable />
<el-table-column fixed="right" label="操作" align="center" min-width="200">
<template #default="{ row }">
@@ -314,14 +302,14 @@ const currentPath = ref('/') // 当前路径的响应式变量
// 分页查询参数
const pagination = reactive({
page_no: 1,
page_size: 20
page_size: 10
})
// 搜索表单数据
const queryFormData = reactive<ResourcePageQuery>({
name: undefined,
page_no: 1,
page_size: 20
page_size: 10
})
// 对话框状态
@@ -377,10 +365,7 @@ async function loadFileList() {
const pageResult = response.data?.data
if (pageResult && Array.isArray(pageResult.items)) {
fileList.value = pageResult.items.map(item => ({
...item,
is_hidden: item.name.startsWith('.')
}))
fileList.value = pageResult.items
total.value = pageResult.total
if (pageResult.page_no !== undefined) {
pagination.page_no = pageResult.page_no
@@ -393,7 +378,6 @@ async function loadFileList() {
total.value = 0
}
} catch (error) {
ElMessage.error('加载文件列表失败')
console.error('Load file list error:', error)
fileList.value = []
total.value = 0
@@ -515,11 +499,9 @@ async function handleUploadConfirm() {
formData.append('target_path', targetPath)
await ResourceAPI.uploadFile(formData)
ElMessage.success('上传成功')
uploadDialogVisible.value = false
loadFileList()
} catch (error) {
ElMessage.error('上传失败')
console.error('Upload error:', error)
} finally {
uploading.value = false
@@ -552,11 +534,9 @@ async function handleCreateDirConfirm() {
parent_path: parentPath,
dir_name: createDirForm.dir_name.trim()
})
ElMessage.success('创建成功')
createDirDialogVisible.value = false
loadFileList()
} catch (error) {
ElMessage.error('创建失败')
console.error('Create directory error:', error)
}
}
@@ -600,7 +580,6 @@ async function handleDownload(item: ResourceItem) {
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
} catch (error) {
ElMessage.error('下载失败')
console.error('Download error:', error)
}
}
@@ -625,11 +604,9 @@ async function handleRenameConfirm() {
old_path: renameForm.old_path,
new_name: renameForm.new_name.trim()
})
ElMessage.success('重命名成功')
renameDialogVisible.value = false
loadFileList()
} catch (error) {
ElMessage.error('重命名失败')
console.error('Rename error:', error)
}
}
@@ -649,11 +626,9 @@ async function handleDelete(item: ResourceItem) {
// 使用file_url字段
await ResourceAPI.deleteResource([item.file_url])
ElMessage.success('删除成功')
loadFileList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
console.error('Delete error:', error)
}
}
@@ -688,11 +663,9 @@ async function handleBatchDelete() {
const paths = selectedItems.value.map(item => item.file_url)
await ResourceAPI.deleteResource(paths)
ElMessage.success('删除成功')
loadFileList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
console.error('Batch delete error:', error)
}
}
@@ -720,122 +693,50 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
.app-container {
.search-container {
margin-bottom: 16px;
padding: 20px;
border-radius: 8px;
// 使用系统主题颜色
.card-header {
display: flex;
align-items: center;
}
.search-buttons {
margin-left: 16px;
}
}
.data-table {
.card-header {
display: flex;
align-items: center;
}
.breadcrumb-section {
margin: 16px 0;
padding: 12px 0;
// 使用系统主题颜色
.breadcrumb-container {
display: flex;
align-items: center;
gap: 12px;
.breadcrumb-header {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
.breadcrumb-icon {
font-size: 14px;
}
.breadcrumb-label {
font-size: 14px;
font-weight: 500;
}
}
}
}
.data-table__toolbar {
.data-table__content {
.file-name {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
gap: 8px;
.data-table__toolbar--actions {
display: flex;
gap: 8px;
}
.data-table__toolbar--tools {
display: flex;
align-items: center;
gap: 16px;
}
}
.data-table__content {
.file-name {
display: flex;
align-items: center;
gap: 8px;
.file-name-clickable {
cursor: pointer;
color: var(--el-color-primary);
&:hover {
color: var(--el-color-primary-light-3);
text-decoration: underline;
}
}
}
}
.grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
padding: 20px;
.grid-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
// 使用系统主题颜色
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
.item-icon {
margin-bottom: 10px;
}
.item-name {
font-size: 14px;
text-align: center;
word-break: break-all;
margin-bottom: 5px;
}
.item-size {
font-size: 12px;
}
.file-name-clickable {
cursor: pointer;
color: var(--el-color-primary);
&:hover {
color: var(--el-color-primary-light-3);
text-decoration: underline;
}
}
}
}
.grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
.grid-item {
display: flex;
flex-direction: column;
align-items: center;
// 使用系统主题颜色
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
.item-name {
font-size: 14px;
text-align: center;
word-break: break-all;
margin-bottom: 5px;
}
}
}
:deep(.el-breadcrumb__item) {
+14 -19
View File
@@ -202,6 +202,7 @@ defineOptions({
});
import DeptAPI, { DeptTable, DeptForm, DeptPageQuery } from "@/api/system/dept";
import { useUserStore } from "@/store";
import { formatTree } from "@/utils/common";
const queryFormRef = ref();
@@ -386,28 +387,22 @@ async function handleSubmit() {
loading.value = true;
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
const id = formData.id;
if (id) {
try {
try {
if (id) {
await DeptAPI.updateDept(id, { id, ...formData })
dialogVisible.visible = false;
resetForm();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
} else {
try {
} else {
await DeptAPI.createDept(formData)
dialogVisible.visible = false;
resetForm();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
dialogVisible.visible = false;
resetForm();
handleResetQuery();
// 更新全局用户状态,刷新部门信息
const userStore = useUserStore();
await userStore.getUserInfo();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}
});
+31 -30
View File
@@ -271,7 +271,7 @@
<el-input v-model="formData.route_path" placeholder="请输入外链完整路径" />
</el-form-item>
<el-form-item prop="routeName">
<el-form-item prop="route_name">
<template #label>
<div class="flex-y-center">
路由名称
@@ -288,7 +288,7 @@
<el-input v-model="formData.route_name" placeholder="请输入路由名称" />
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.CATALOG || formData.type == MenuTypeEnum.MENU" prop="routePath">
<el-form-item v-if="formData.type == MenuTypeEnum.CATALOG || formData.type == MenuTypeEnum.MENU" prop="route_path">
<template #label>
<div class="flex-y-center">
路由路径
@@ -303,8 +303,7 @@
</el-tooltip>
</div>
</template>
<el-input v-if="formData.type == MenuTypeEnum.CATALOG" v-model="formData.route_path" placeholder="system" />
<el-input v-else v-model="formData.route_path" placeholder="请输入路由路径" />
<el-input v-model="formData.route_path" placeholder="请输入路由路径,如:/system" />
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="component">
@@ -516,14 +515,14 @@ const queryFormData = reactive<MenuPageQuery>({
const formData = reactive<MenuForm>({
id: undefined,
name: undefined,
type: MenuTypeEnum.MENU,
type: MenuTypeEnum.CATALOG,
icon: undefined,
order: 1,
order: 999,
permission: '',
route_name: '',
route_path: '',
component_path: '',
redirect: '',
component_path: undefined,
redirect: undefined,
parent_id: undefined,
keep_alive: false,
hidden: false,
@@ -555,7 +554,19 @@ const rules = reactive({
order: [{ required: true, message: "请输入排序", trigger: "blur" }],
permission: [{ required: true, message: "请输入权限标识", trigger: "blur" }],
route_name: [{ required: true, message: "请输入路由名称", trigger: "blur" }],
route_path: [{ required: true, message: "请输入路由路径", trigger: "blur" }],
route_path: [
{ required: true, message: "请输入路由路径", trigger: "blur" },
{
validator: (rule: any, value: string, callback: any) => {
if (value && !value.startsWith('/')) {
callback(new Error('目录和菜单路由必须以/开头'));
} else {
callback();
}
},
trigger: 'blur'
}
],
component_path: [{ required: true, message: "请输入组件路径", trigger: "blur" }],
title: [{ required: true, message: "请输入菜单标题", trigger: "blur" }],
keep_alive: [{ required: true, message: "请选择是否缓存", trigger: "change" }],
@@ -724,30 +735,20 @@ async function handleSubmit() {
loading.value = true;
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
const id = formData.id;
if (id) {
try {
try {
if (id) {
await MenuAPI.updateMenu(id, formData)
await userStore.getUserInfo();
dialogVisible.visible = false;
resetForm();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
} else {
try {
} else {
await MenuAPI.createMenu(formData)
await userStore.getUserInfo();
dialogVisible.visible = false;
resetForm();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
await userStore.getUserInfo();
dialogVisible.visible = false;
resetForm();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}
});
+14 -19
View File
@@ -212,6 +212,7 @@ defineOptions({
});
import PositionAPI, { PositionTable, PositionForm, PositionPageQuery } from "@/api/system/position";
import { useUserStore } from "@/store";
const queryFormRef = ref();
const dataFormRef = ref();
@@ -383,28 +384,22 @@ async function handleSubmit() {
loading.value = true;
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
const id = formData.id;
if (id) {
try {
try {
if (id) {
await PositionAPI.updatePosition(id, { id, ...formData })
dialogVisible.visible = false;
resetForm();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
} else {
try {
} else {
await PositionAPI.createPosition(formData)
dialogVisible.visible = false;
resetForm();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
dialogVisible.visible = false;
resetForm();
handleResetQuery();
// 更新全局用户状态,刷新岗位信息
const userStore = useUserStore();
await userStore.getUserInfo();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}
});
@@ -1,4 +1,4 @@
<!-- 角色授权 -->
<!-- 角色授权 -->
<template>
<!-- 分配权限弹窗 -->
<el-drawer v-model="drawerVisible" :title="'【' + props.roleName + '】权限分配'" :size="drawerSize">
@@ -152,6 +152,7 @@ import MenuAPI from "@/api/system/menu";
import type { TreeInstance } from 'element-plus'
import { useAppStore } from "@/store/modules/app.store";
import { DeviceEnum } from "@/enums/settings/device.enum";
import { useUserStore } from "@/store";
const appStore = useAppStore();
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "800px" : "90%"));
@@ -233,7 +234,7 @@ function handleCancel() {
drawerVisible.value = false;
}
// 保存权限设置
// 保存权限分配
async function handleDrawerSave () {
try {
loading.value = true;
@@ -247,8 +248,14 @@ async function handleDrawerSave () {
};
await RoleAPI.setPermission(submitData)
drawerVisible.value = false;
ElMessage.success('权限分配成功');
// 更新全局用户状态,刷新权限信息
const userStore = useUserStore();
await userStore.getUserInfo();
drawerVisible.value = false;
} catch (error: any) {
console.error(error);
} finally {
+18 -23
View File
@@ -97,10 +97,10 @@
</el-col>
<el-col :span="1.5">
<el-tooltip content="列表筛选">
<el-dropdown trigger="click">
<el-button type="default" icon="operation" circle />
<template #dropdown>
<el-dropdown-menu v-hasPerm="['system:role:filter']">
<el-dropdown v-hasPerm="['system:role:filter']" trigger="click">
<el-button type="default" icon="operation" circle />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="column in tableColumns" :key="column.prop" :command="column">
<el-checkbox v-model="column.show">{{ column.label }}</el-checkbox>
</el-dropdown-item>
@@ -276,6 +276,7 @@ defineOptions({
import { ElMessage, ElMessageBox } from "element-plus";
import RoleAPI, { RoleTable, RoleForm, TablePageQuery } from "@/api/system/role";
import { useUserStore } from "@/store";
const queryFormRef = ref();
const dataFormRef = ref();
@@ -454,28 +455,22 @@ async function handleSubmit() {
loading.value = true;
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
const id = formData.id;
if (id) {
try {
try {
if (id) {
await RoleAPI.updateRole(id, { id, ...formData })
dialogVisible.visible = false;
resetForm();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
} else {
try {
} else {
await RoleAPI.createRole(formData)
dialogVisible.visible = false;
resetForm();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
dialogVisible.visible = false;
resetForm();
handleResetQuery();
// 更新全局用户状态,刷新权限信息
const userStore = useUserStore();
await userStore.getUserInfo();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}
});
+17 -25
View File
@@ -362,6 +362,7 @@ import DeptAPI from "@/api/system/dept";
import RoleAPI from "@/api/system/role";
import DeptTree from "./components/DeptTree.vue";
import { useUserStore } from "@/store";
const appStore = useAppStore();
@@ -617,33 +618,24 @@ async function handleSubmit() {
loading.value = true;
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
const id = formData.id;
if (id) {
try {
// 编辑用户时,不传递密码字段
const updateData = { id, ...formData };
delete updateData.password;
await UserAPI.updateUser(id, updateData)
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
} else {
try {
try {
if (id) {
await UserAPI.updateUser(id, { id, ...formData })
} else {
await UserAPI.createUser(formData)
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} catch (error: any) {
console.log(error.message);
} finally {
loading.value = false;
}
dialogVisible.visible = false;
resetForm();
handleResetQuery();
// 如果当前编辑的是登录用户,更新全局用户状态
const userStore = useUserStore();
if (id === userStore.basicInfo.id) {
await userStore.getUserInfo();
}
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}
});