web-模块、权限、成就

This commit is contained in:
2025-10-25 17:45:47 +08:00
parent f7057a0cc9
commit 5d211faee1
32 changed files with 4024 additions and 876 deletions

View File

@@ -0,0 +1,618 @@
<template>
<div class="achievement-management">
<div class="header">
<h2>成就管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增成就
</el-button>
</div>
<!-- 筛选条件 -->
<div class="filter-bar">
<div class="filter-item">
<span class="filter-label">成就类型</span>
<el-select v-model="filter.type" placeholder="全部" clearable style="width: 150px">
<el-option
v-for="option in achievementTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<div class="filter-item">
<span class="filter-label">成就等级</span>
<el-input-number
v-model="filter.level"
:min="0"
:max="10"
placeholder="全部"
clearable
style="width: 120px"
/>
</div>
<div class="filter-item">
<span class="filter-label">条件类型</span>
<el-select v-model="filter.conditionType" placeholder="全部" clearable style="width: 180px">
<el-option
v-for="option in conditionTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<div class="filter-actions">
<el-button type="primary" @click="loadAchievementList">查询</el-button>
<el-button @click="resetFilter">重置</el-button>
</div>
</div>
<!-- 成就列表 -->
<el-table
:data="achievementList"
style="width: 100%"
v-loading="loading"
border
stripe
row-key="achievementID"
>
<el-table-column prop="icon" label="图标" width="80" align="center">
<template #default="{ row }">
<el-image
:src="getIconUrl(row.icon)"
style="width: 48px; height: 48px"
fit="contain"
:preview-src-list="[getIconUrl(row.icon)]"
>
<template #error>
<div class="image-slot">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
</el-table-column>
<el-table-column prop="name" label="成就名称" min-width="150" />
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="type" label="类型" width="100">
<template #default="{ row }">
<el-tag :type="row.type === 1 ? 'success' : 'primary'">
{{ getAchievementTypeLabel(row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="level" label="等级" width="80" align="center" />
<el-table-column prop="conditionType" label="条件类型" width="150">
<template #default="{ row }">
{{ getConditionTypeLabel(row.conditionType) }}
</template>
</el-table-column>
<el-table-column prop="conditionValue" label="条件值" width="100" align="center" />
<el-table-column prop="points" label="积分奖励" width="100" align="center" />
<el-table-column prop="orderNum" label="排序号" width="80" align="center" />
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleEdit(row)"
link
>
编辑
</el-button>
<el-button
type="info"
size="small"
@click="handleViewUsers(row)"
link
>
查看获得者
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
link
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增/编辑成就对话框 -->
<el-dialog
v-model="achievementDialogVisible"
:title="isEdit ? '编辑成就' : '新增成就'"
width="700px"
@close="resetForm"
>
<el-form
ref="achievementFormRef"
:model="currentAchievement"
:rules="achievementFormRules"
label-width="120px"
>
<el-form-item label="成就名称" prop="name">
<el-input v-model="currentAchievement.name" placeholder="请输入成就名称" />
</el-form-item>
<el-form-item label="成就描述" prop="description">
<el-input
v-model="currentAchievement.description"
type="textarea"
:rows="3"
placeholder="请输入成就描述"
/>
</el-form-item>
<el-form-item label="成就图标" prop="icon">
<el-input
v-model="currentAchievement.icon"
placeholder="请输入图标路径,如:/img/achievement/v1-icon.svg 或完整URL"
>
<template #append>
<el-button @click="showIconUpload = true">上传</el-button>
</template>
</el-input>
<div v-if="currentAchievement.icon" style="margin-top: 10px">
<div style="display: flex; align-items: center; gap: 10px;">
<el-image
:src="getIconUrl(currentAchievement.icon)"
style="width: 80px; height: 80px"
fit="contain"
>
<template #error>
<div class="image-slot">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
<div style="font-size: 12px; color: #909399;">
<div>路径: {{ currentAchievement.icon }}</div>
<div>完整URL: {{ getIconUrl(currentAchievement.icon) }}</div>
</div>
</div>
</div>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="成就类型" prop="type">
<el-select v-model="currentAchievement.type" placeholder="请选择成就类型" style="width: 100%">
<el-option
v-for="option in achievementTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="成就等级" prop="level">
<el-input-number
v-model="currentAchievement.level"
:min="1"
:max="10"
placeholder="请输入等级"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="条件类型" prop="conditionType">
<el-select
v-model="currentAchievement.conditionType"
placeholder="请选择条件类型"
style="width: 100%"
>
<el-option
v-for="option in conditionTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="条件值" prop="conditionValue">
<el-input-number
v-model="currentAchievement.conditionValue"
:min="1"
placeholder="请输入条件值"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="条件预览" v-if="currentAchievement.conditionType && currentAchievement.conditionValue">
<el-alert
:title="formatConditionValue(currentAchievement.conditionType, currentAchievement.conditionValue)"
type="info"
:closable="false"
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="积分奖励" prop="points">
<el-input-number
v-model="currentAchievement.points"
:min="0"
placeholder="请输入积分奖励"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序号" prop="orderNum">
<el-input-number
v-model="currentAchievement.orderNum"
:min="0"
placeholder="请输入排序号"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="achievementDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveAchievement" :loading="submitting">
{{ isEdit ? '更新' : '创建' }}
</el-button>
</template>
</el-dialog>
<!-- 查看获得者对话框 -->
<el-dialog
v-model="usersDialogVisible"
title="成就获得者列表"
width="900px"
>
<div class="achievement-info-bar">
<el-alert
:title="`成就:${currentAchievement.name} - ${currentAchievement.description}`"
type="info"
:closable="false"
style="margin-bottom: 20px"
/>
</div>
<el-table
:data="achieverList"
v-loading="achieversLoading"
border
stripe
>
<el-table-column prop="userID" label="用户ID" width="200" />
<el-table-column prop="username" label="用户名" min-width="120" />
<el-table-column prop="realName" label="真实姓名" min-width="100" />
<el-table-column prop="obtainTime" label="获得时间" width="180" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button
type="danger"
size="small"
@click="handleRevokeAchievement(row)"
>
撤销成就
</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus, Picture } from '@element-plus/icons-vue';
import { achievementApi } from '@/apis/achievement';
import type { Achievement, UserAchievement } from '@/types';
import { AchievementEnumHelper } from '@/types/enums/achievement-enums';
import { PUBLIC_IMG_PATH } from '@/config';
// 响应式数据
const loading = ref(false);
const submitting = ref(false);
const achieversLoading = ref(false);
const achievementList = ref<Achievement[]>([]);
const achieverList = ref<UserAchievement[]>([]);
// 对话框控制
const achievementDialogVisible = ref(false);
const usersDialogVisible = ref(false);
const showIconUpload = ref(false);
const isEdit = ref(false);
// 筛选条件
const filter = ref<Partial<Achievement>>({});
// 当前操作的成就
const currentAchievement = ref<Achievement>({});
// 枚举选项
const achievementTypeOptions = AchievementEnumHelper.getAllAchievementTypeOptions();
const conditionTypeOptions = AchievementEnumHelper.getAllConditionTypeOptions();
// 表单引用
const achievementFormRef = ref();
// 表单验证规则
const achievementFormRules = {
name: [
{ required: true, message: '请输入成就名称', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入成就描述', trigger: 'blur' }
],
icon: [
{ required: true, message: '请输入图标URL', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择成就类型', trigger: 'change' }
],
level: [
{ required: true, message: '请输入成就等级', trigger: 'blur' }
],
conditionType: [
{ required: true, message: '请选择条件类型', trigger: 'change' }
],
conditionValue: [
{ required: true, message: '请输入条件值', trigger: 'blur' }
],
points: [
{ required: true, message: '请输入积分奖励', trigger: 'blur' }
],
orderNum: [
{ required: true, message: '请输入排序号', trigger: 'blur' }
]
};
// 获取成就类型标签
function getAchievementTypeLabel(type?: number): string {
if (type === undefined) return '未知';
return AchievementEnumHelper.getAchievementTypeDescription(type);
}
// 获取条件类型标签
function getConditionTypeLabel(type?: number): string {
if (type === undefined) return '未知';
return AchievementEnumHelper.getConditionTypeDescription(type);
}
// 格式化条件值显示
function formatConditionValue(conditionType?: number, conditionValue?: number): string {
if (conditionType === undefined || conditionValue === undefined) return '';
return AchievementEnumHelper.formatConditionValue(conditionType, conditionValue);
}
// 获取图标完整路径
function getIconUrl(icon?: string): string {
if (!icon) return '';
// 如果是http或https开头直接返回
if (icon.startsWith('http://') || icon.startsWith('https://')) {
return icon;
}
// 否则拼接默认成就图标路径
const path = `${PUBLIC_IMG_PATH}/achievement`;
return icon.startsWith('/') ? `${path}${icon}` : `${path}/${icon}`;
}
// 加载成就列表
async function loadAchievementList() {
try {
loading.value = true;
const result = await achievementApi.getAllAchievements(filter.value);
achievementList.value = result.dataList || [];
} catch (error) {
console.error('加载成就列表失败:', error);
ElMessage.error('加载成就列表失败');
} finally {
loading.value = false;
}
}
// 重置筛选条件
function resetFilter() {
filter.value = {};
loadAchievementList();
}
// 新增成就
function handleAdd() {
isEdit.value = false;
currentAchievement.value = {
type: 1,
level: 1,
points: 0,
orderNum: 0
};
achievementDialogVisible.value = true;
}
// 编辑成就
function handleEdit(row: Achievement) {
isEdit.value = true;
currentAchievement.value = { ...row };
achievementDialogVisible.value = true;
}
// 删除成就
async function handleDelete(row: Achievement) {
try {
await ElMessageBox.confirm(
`确定要删除成就 "${row.name}" 吗?删除后无法恢复。`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
await achievementApi.deleteAchievement(row);
ElMessage.success('删除成功');
await loadAchievementList();
} catch (error) {
if (error !== 'cancel') {
console.error('删除成就失败:', error);
ElMessage.error('删除成就失败');
}
}
}
// 保存成就
async function saveAchievement() {
try {
await achievementFormRef.value?.validate();
submitting.value = true;
if (isEdit.value) {
await achievementApi.updateAchievement(currentAchievement.value);
ElMessage.success('更新成功');
} else {
await achievementApi.createAchievement(currentAchievement.value);
ElMessage.success('创建成功');
}
achievementDialogVisible.value = false;
await loadAchievementList();
} catch (error) {
console.error('保存成就失败:', error);
if (error !== false) {
ElMessage.error('保存成就失败');
}
} finally {
submitting.value = false;
}
}
// 查看获得者
async function handleViewUsers(row: Achievement) {
currentAchievement.value = { ...row };
usersDialogVisible.value = true;
try {
achieversLoading.value = true;
const result = await achievementApi.getRecentAchievers(
{ page: 1, size: 100 },
{ achievementID: row.achievementID }
);
achieverList.value = result.dataList || [];
} catch (error) {
console.error('加载获得者列表失败:', error);
ElMessage.error('加载获得者列表失败');
} finally {
achieversLoading.value = false;
}
}
// 撤销成就
async function handleRevokeAchievement(row: UserAchievement) {
try {
await ElMessageBox.confirm(
'确定要撤销该用户的成就吗?',
'确认撤销',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
await achievementApi.revokeAchievement(row.userID || '', row.achievementID || '');
ElMessage.success('撤销成功');
// 刷新获得者列表
await handleViewUsers(currentAchievement.value);
} catch (error) {
if (error !== 'cancel') {
console.error('撤销成就失败:', error);
ElMessage.error('撤销成就失败');
}
}
}
// 重置表单
function resetForm() {
currentAchievement.value = {};
achievementFormRef.value?.clearValidate();
}
// 组件挂载时加载数据
onMounted(() => {
loadAchievementList();
});
</script>
<style scoped lang="scss">
.achievement-management {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #141F38;
}
}
.filter-bar {
display: flex;
align-items: center;
gap: 16px;
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
.filter-item {
display: flex;
align-items: center;
gap: 8px;
.filter-label {
font-size: 14px;
color: #606266;
white-space: nowrap;
}
}
.filter-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
}
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: #f5f5f5;
color: #ccc;
font-size: 24px;
}
.achievement-info-bar {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,2 @@
export { default as AchievementManagementView } from './AchievementManagementView.vue';

File diff suppressed because it is too large Load Diff

View File

@@ -1,721 +0,0 @@
<template>
<div class="permission-manage">
<div class="header">
<h2>权限管理</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增权限
</el-button>
</div>
<el-table
:data="permissionList"
style="width: 100%"
v-loading="loading"
border
stripe
>
<el-table-column prop="name" label="权限名称" min-width="150" />
<el-table-column prop="code" label="权限编码" min-width="200" />
<el-table-column prop="description" label="权限描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="creatorName" label="创建人" width="120" />
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleEdit(row)"
link
>
编辑
</el-button>
<el-button
type="primary"
size="small"
@click="handleBindMenu(row)"
link
>
绑定菜单
</el-button>
<el-button
type="primary"
size="small"
@click="handleBindRole(row)"
link
>
绑定角色
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
link
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑权限' : '新增权限'"
width="500px"
@close="resetForm"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="权限名称" prop="name">
<el-input
v-model="formData.name"
placeholder="请输入权限名称"
clearable
/>
</el-form-item>
<el-form-item label="权限编码" prop="code">
<el-input
v-model="formData.code"
placeholder="请输入权限编码"
clearable
/>
</el-form-item>
<el-form-item label="权限描述" prop="description">
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入权限描述"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="handleSubmit"
:loading="submitting"
>
确定
</el-button>
</template>
</el-dialog>
<!-- 绑定菜单对话框 -->
<el-dialog v-model="bindMenuDialogVisible" title="绑定菜单" width="800px" @close="resetBindList">
<div class="menu-binding-container">
<!-- 权限信息显示 -->
<div class="permission-info" v-if="currentPermission">
<h4>权限信息{{ currentPermission.name }}</h4>
<p>权限编码{{ currentPermission.code }}</p>
</div>
<!-- 菜单绑定状态表格 -->
<el-table :data="menuList" style="width: 100%" border stripe>
<el-table-column width="80" label="绑定状态">
<template #default="{ row }">
<el-tag
:type="isMenuSelected(row.menuID) ? 'success' : 'info'"
size="small"
>
{{ isMenuSelected(row.menuID) ? '已绑定' : '未绑定' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="name" label="菜单名称" min-width="150" />
<el-table-column prop="menuID" label="菜单ID" min-width="120" />
<el-table-column prop="url" label="菜单路径" min-width="200" show-overflow-tooltip />
<el-table-column prop="component" label="菜单组件" min-width="180" show-overflow-tooltip />
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button
:type="isMenuSelected(row.menuID) ? 'danger' : 'primary'"
size="small"
@click="toggleMenuSelection(row)"
>
{{ isMenuSelected(row.menuID) ? '解绑' : '绑定' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 统计信息 -->
<div class="binding-stats">
<el-alert
:title="`已绑定 ${selectedMenus.length} 个菜单,未绑定 ${menuList.length - selectedMenus.length} 个菜单`"
type="info"
:closable="false"
show-icon
/>
</div>
</div>
<template #footer>
<el-button @click="bindMenuDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveMenuBinding" :loading="submitting">
保存
</el-button>
</template>
</el-dialog>
<!-- 绑定角色对话框 -->
<el-dialog v-model="bindRoleDialogVisible" title="绑定角色" width="800px" @close="resetBindList">
<div class="role-binding-container">
<!-- 权限信息显示 -->
<div class="permission-info" v-if="currentPermission">
<h4>权限信息{{ currentPermission.name }}</h4>
<p>权限编码{{ currentPermission.code }}</p>
</div>
<!-- 角色绑定状态表格 -->
<el-table :data="roleList" style="width: 100%" border stripe>
<el-table-column width="80" label="绑定状态">
<template #default="{ row }">
<el-tag
:type="isRoleSelected(row.id) ? 'success' : 'info'"
size="small"
>
{{ isRoleSelected(row.id) ? '已绑定' : '未绑定' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="name" label="角色名称" min-width="150" />
<el-table-column prop="id" label="角色ID" min-width="120" />
<el-table-column prop="description" label="角色描述" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button
:type="isRoleSelected(row.id) ? 'danger' : 'primary'"
size="small"
@click="toggleRoleSelection(row)"
>
{{ isRoleSelected(row.id) ? '解绑' : '绑定' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 统计信息 -->
<div class="binding-stats">
<el-alert
:title="`已绑定 ${selectedRoles.length} 个角色,未绑定 ${roleList.length - selectedRoles.length} 个角色`"
type="info"
:closable="false"
show-icon
/>
</div>
</div>
<template #footer>
<el-button @click="bindRoleDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveRoleBinding" :loading="submitting">
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { permissionApi } from '@/apis/system/permission';
import { roleApi } from '@/apis/system/role';
import { menuApi } from '@/apis/system/menu';
import { SysPermission, SysRole, SysMenu } from '@/types';
import { ref, onMounted, reactive } from 'vue';
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
// 数据状态
const permissionList = ref<SysPermission[]>([]);
const loading = ref(false);
const submitting = ref(false);
const bindList = ref<SysPermission>({
menus: [],
roles: []
});
const roleList = ref<SysRole[]>([]);
const menuList = ref<SysMenu[]>([]);
const selectedMenus = ref<string[]>([]);
const selectedRoles = ref<string[]>([]);
const currentPermission = ref<SysPermission | null>(null);
// 对话框状态
const dialogVisible = ref(false);
const isEdit = ref(false);
const formRef = ref<FormInstance>();
const bindMenuDialogVisible = ref(false);
const bindRoleDialogVisible = ref(false);
const queryFilter = ref<SysPermission>({
name: '',
code: '',
description: ''
});
// 表单数据
const formData = reactive<SysPermission>({
name: '',
code: '',
description: ''
});
// 表单验证规则
const formRules: FormRules = {
name: [
{ required: true, message: '请输入权限名称', trigger: 'blur' },
{ min: 2, max: 50, message: '权限名称长度在 2 到 50 个字符', trigger: 'blur' }
],
code: [
{ required: true, message: '请输入权限编码', trigger: 'blur' },
{
pattern: /^[a-zA-Z][a-zA-Z0-9:_-]*$/,
message: '权限编码只能包含字母、数字、冒号、下划线和横线,且必须以字母开头',
trigger: 'blur'
},
{ min: 2, max: 100, message: '权限编码长度在 2 到 100 个字符', trigger: 'blur' }
],
description: [
{ max: 200, message: '权限描述不能超过 200 个字符', trigger: 'blur' }
]
};
// 加载权限列表
async function loadPermissionList() {
try {
loading.value = true;
const result = await permissionApi.getPermissionList(queryFilter.value);
permissionList.value = result.dataList || [];
} catch (error) {
console.error('加载权限列表失败:', error);
ElMessage.error('加载权限列表失败');
} finally {
loading.value = false;
}
}
// 新增权限
function handleAdd() {
isEdit.value = false;
dialogVisible.value = true;
}
// 编辑权限
function handleEdit(row: SysPermission) {
isEdit.value = true;
Object.assign(formData, row);
dialogVisible.value = true;
}
// 查看绑定菜单
async function handleBindMenu(row: SysPermission) {
currentPermission.value = row;
row.bindType = "menu";
try {
// 获取已绑定的菜单
const bindingResult = await permissionApi.getPermissionBindingList(row);
bindList.value.menus = bindingResult.data?.menus || [];
// 获取所有菜单
const menuResult = await menuApi.getAllMenuList();
menuList.value = menuResult.dataList || [];
// 设置已选中的菜单
selectedMenus.value = bindList.value.menus.map(menu => menu.menuID).filter((id): id is string => !!id);
console.log('已绑定的菜单:', bindList.value.menus);
console.log('所有菜单:', menuList.value);
bindMenuDialogVisible.value = true;
} catch (error) {
console.error('获取菜单绑定信息失败:', error);
ElMessage.error('获取菜单绑定信息失败');
}
}
// 查看绑定角色
async function handleBindRole(row: SysPermission) {
currentPermission.value = row;
row.bindType = "role";
try {
// 获取已绑定的角色
const bindingResult = await permissionApi.getPermissionBindingList(row);
bindList.value.roles = bindingResult.data?.roles || [];
// 获取所有角色
const roleResult = await roleApi.getAllRoles();
roleList.value = roleResult.dataList || [];
// 设置已选中的角色
selectedRoles.value = bindList.value.roles.map(role => role.id).filter((id): id is string => !!id);
console.log('已绑定的角色:', bindList.value.roles);
console.log('所有角色:', roleList.value);
bindRoleDialogVisible.value = true;
} catch (error) {
console.error('获取角色绑定信息失败:', error);
ElMessage.error('获取角色绑定信息失败');
}
}
// 删除权限
async function handleDelete(row: SysPermission) {
try {
await ElMessageBox.confirm(
`确定要删除权限 "${row.name}" 吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
await permissionApi.deletePermission(row);
ElMessage.success('删除成功');
await loadPermissionList();
} catch (error) {
if (error !== 'cancel') {
console.error('删除权限失败:', error);
ElMessage.error('删除权限失败');
}
}
}
// 重置绑定列表
function resetBindList() {
bindList.value = {
menus: [],
roles: []
};
selectedMenus.value = [];
selectedRoles.value = [];
currentPermission.value = null;
}
// 检查菜单是否已选中
function isMenuSelected(menuID: string | undefined): boolean {
return menuID ? selectedMenus.value.includes(menuID) : false;
}
// 切换菜单选择状态
function toggleMenuSelection(menu: SysMenu) {
if (!menu.menuID) return;
const index = selectedMenus.value.indexOf(menu.menuID);
if (index > -1) {
selectedMenus.value.splice(index, 1);
} else {
selectedMenus.value.push(menu.menuID);
}
}
// 保存菜单绑定
async function saveMenuBinding() {
if (!currentPermission.value || !currentPermission.value.permissionID) {
ElMessage.error('权限信息不完整');
return;
}
try {
submitting.value = true;
// 获取当前已绑定的菜单ID
const currentBoundMenus = (bindList.value.menus || []).map(menu => menu.menuID).filter((id): id is string => !!id);
// 找出需要绑定的菜单(新增的)
const menusToBind = selectedMenus.value.filter(menuID => !currentBoundMenus.includes(menuID));
// 找出需要解绑的菜单(移除的)
const menusToUnbind = currentBoundMenus.filter(menuID => !selectedMenus.value.includes(menuID));
// 构建需要绑定的菜单对象数组
if (menusToBind.length > 0) {
const menusToBindObjects = menusToBind.map(menuID => {
const menu = menuList.value.find(m => m.menuID === menuID);
return menu || { menuID: menuID };
});
const bindPermission = {
...currentPermission.value,
menus: menusToBindObjects,
permissions: [{permissionID: currentPermission.value.permissionID}]
};
await permissionApi.bindMenu(bindPermission);
}
// 构建需要解绑的菜单对象数组
if (menusToUnbind.length > 0) {
const menusToUnbindObjects = menusToUnbind.map(menuID => {
const menu = menuList.value.find(m => m.menuID === menuID);
return menu || { menuID: menuID };
});
const unbindPermission = {
...currentPermission.value,
menus: menusToUnbindObjects,
permissions: [{permissionID: currentPermission.value.permissionID}]
};
await permissionApi.unbindMenu(unbindPermission);
}
ElMessage.success('菜单绑定保存成功');
bindMenuDialogVisible.value = false;
// 刷新权限列表
await loadPermissionList();
} catch (error) {
console.error('保存菜单绑定失败:', error);
ElMessage.error('保存菜单绑定失败');
} finally {
submitting.value = false;
}
}
// 检查角色是否已选中
function isRoleSelected(roleID: string | undefined): boolean {
return roleID ? selectedRoles.value.includes(roleID) : false;
}
// 切换角色选择状态
function toggleRoleSelection(role: SysRole) {
if (!role.id) return;
const index = selectedRoles.value.indexOf(role.id);
if (index > -1) {
selectedRoles.value.splice(index, 1);
} else {
selectedRoles.value.push(role.id);
}
}
// 保存角色绑定
async function saveRoleBinding() {
if (!currentPermission.value || !currentPermission.value.permissionID) {
ElMessage.error('权限信息不完整');
return;
}
try {
submitting.value = true;
// 获取当前已绑定的角色ID
const currentBoundRoles = (bindList.value.roles || []).map(role => role.id).filter((id): id is string => !!id);
// 找出需要绑定的角色(新增的)
const rolesToBind = selectedRoles.value.filter(roleID => !currentBoundRoles.includes(roleID));
// 找出需要解绑的角色(移除的)
const rolesToUnbind = currentBoundRoles.filter(roleID => !selectedRoles.value.includes(roleID));
// 构建需要绑定的角色对象数组
if (rolesToBind.length > 0) {
const rolesToBindObjects = rolesToBind.map(roleID => {
const role = roleList.value.find(r => r.id === roleID);
return role || { id: roleID };
});
const bindPermission = {
...currentPermission.value,
roles: rolesToBindObjects,
permissions: [{permissionID: currentPermission.value.permissionID}]
};
await permissionApi.bindRole(bindPermission);
}
// 构建需要解绑的角色对象数组
if (rolesToUnbind.length > 0) {
const rolesToUnbindObjects = rolesToUnbind.map(roleID => {
const role = roleList.value.find(r => r.id === roleID);
return role || { id: roleID };
});
const unbindPermission = {
...currentPermission.value,
roles: rolesToUnbindObjects,
permissions: [{permissionID: currentPermission.value.permissionID}]
};
await permissionApi.unbindRole(unbindPermission);
}
ElMessage.success('角色绑定保存成功');
bindRoleDialogVisible.value = false;
// 刷新权限列表
await loadPermissionList();
} catch (error) {
console.error('保存角色绑定失败:', error);
ElMessage.error('保存角色绑定失败');
} finally {
submitting.value = false;
}
}
// 提交表单
async function handleSubmit() {
if (!formRef.value) return;
try {
await formRef.value.validate();
submitting.value = true;
if (isEdit.value) {
await permissionApi.updatePermission(formData);
ElMessage.success('更新成功');
} else {
await permissionApi.addPermission(formData);
ElMessage.success('新增成功');
}
dialogVisible.value = false;
await loadPermissionList();
} catch (error) {
if (error !== false) { // 表单验证失败时error为false
console.error('提交失败:', error);
ElMessage.error(isEdit.value ? '更新失败' : '新增失败');
}
} finally {
submitting.value = false;
}
}
// 重置表单
function resetForm() {
if (formRef.value) {
formRef.value.resetFields();
}
Object.assign(formData, {
name: '',
code: '',
description: ''
});
}
// 页面加载时获取权限列表
onMounted(() => {
loadPermissionList();
});
</script>
<style scoped lang="scss">
.permission-manage {
padding: 20px;
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
color: #303133;
font-size: 20px;
font-weight: 600;
}
}
.el-table {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
}
// 对话框样式优化
:deep(.el-dialog) {
.el-dialog__header {
padding: 20px 20px 10px;
border-bottom: 1px solid #ebeef5;
.el-dialog__title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.el-dialog__body {
padding: 20px;
}
.el-dialog__footer {
padding: 10px 20px 20px;
border-top: 1px solid #ebeef5;
}
}
// 表单样式优化
:deep(.el-form) {
.el-form-item__label {
font-weight: 500;
color: #606266;
}
.el-input__wrapper {
box-shadow: 0 0 0 1px #dcdfe6 inset;
&:hover {
box-shadow: 0 0 0 1px #c0c4cc inset;
}
&.is-focus {
box-shadow: 0 0 0 1px #409eff inset;
}
}
.el-textarea__inner {
box-shadow: 0 0 0 1px #dcdfe6 inset;
&:hover {
box-shadow: 0 0 0 1px #c0c4cc inset;
}
&:focus {
box-shadow: 0 0 0 1px #409eff inset;
}
}
}
// 按钮样式优化
:deep(.el-button) {
&.is-link {
padding: 0;
margin-right: 12px;
&:last-child {
margin-right: 0;
}
}
}
.menu-binding-container,
.role-binding-container {
.permission-info {
background: #f5f7fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
h4 {
margin: 0 0 8px 0;
color: #303133;
font-size: 16px;
}
p {
margin: 0;
color: #606266;
font-size: 14px;
}
}
.binding-stats {
margin-top: 20px;
}
}
</style>