20 KiB
20 KiB
RBAC权限管理前端实施计划
For Claude: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan.
Goal: 实现权限管理前端界面,包括权限管理、角色管理、用户角色分配、审计日志和项目成员管理
Architecture: Vue 3 + TypeScript + Ant Design Vue,在现有 ether-admin 项目基础上扩展。系统管理员功能(权限/角色/用户)与业务功能(项目成员管理)分离
Tech Stack: Vue 3, TypeScript, Ant Design Vue, Pinia, Vite
Phase 1: API层
Task 1: 创建权限相关API
Files:
- Modify:
ether-admin/src/api/permission.ts
Step 1: 添加权限API
// ether-admin/src/api/permission.ts
import request from '@/utils/request';
export interface Permission {
id: string;
code: string;
name: string;
type: 'MENU' | 'BUTTON' | 'API';
resource?: string;
method?: string;
parentCode?: string;
sortOrder: number;
}
export const getPermissions = () => {
return request.get<Permission[]>('/api/permissions');
};
export const getPermission = (id: string) => {
return request.get<Permission>(`/api/permissions/${id}`);
};
export const createPermission = (data: Partial<Permission>) => {
return request.post<Permission>('/api/permissions', data);
};
export const updatePermission = (id: string, data: Partial<Permission>) => {
return request.put<Permission>(`/api/permissions/${id}`, data);
};
export const deletePermission = (id: string) => {
return request.delete(`/api/permissions/${id}`);
};
Task 2: 创建角色相关API
Files:
- Modify:
ether-admin/src/api/role.ts
Step 1: 扩展角色API
// 添加到现有role.ts
export interface Role {
id: string;
code: string;
name: string;
type: 'SYSTEM' | 'PROJECT' | 'DEPARTMENT';
dataScope: 'ALL' | 'PROJECT' | 'SELF';
permissions: Permission[];
status: 'ENABLED' | 'DISABLED';
}
export const getRoles = () => {
return request.get<Role[]>('/api/roles');
};
export const getRole = (id: string) => {
return request.get<Role>(`/api/roles/${id}`);
};
export const createRole = (data: Partial<Role>) => {
return request.post<Role>('/api/roles', data);
};
export const updateRole = (id: string, data: Partial<Role>) => {
return request.put<Role>(`/api/roles/${id}`, data);
};
export const deleteRole = (id: string) => {
return request.delete(`/api/roles/${id}`);
};
export const assignRolePermissions = (roleId: string, permissionIds: string[]) => {
return request.post(`/api/roles/${roleId}/permissions`, { permissionIds });
};
Task 3: 创建用户-项目相关API
Files:
- Modify:
ether-admin/src/api/user.ts
Step 1: 扩展用户API
// 添加到现有user.ts
export interface UserProject {
id: string;
userId: string;
projectId: string;
roleInProject: 'leader' | 'member' | 'viewer';
joinedAt: string;
}
export const getUserProjects = (userId: string) => {
return request.get<UserProject[]>(`/api/users/${userId}/projects`);
};
export const addUserToProject = (userId: string, projectId: string, roleInProject: string) => {
return request.post(`/api/users/${userId}/projects`, { projectId, roleInProject });
};
export const removeUserFromProject = (userId: string, projectId: string) => {
return request.delete(`/api/users/${userId}/projects/${projectId}`);
};
Phase 2: 页面组件
Task 4: 创建权限管理页面
Files:
- Modify:
ether-admin/src/views/system/Permissions.vue - Create:
ether-admin/src/components/PermissionTree/index.vue
Step 1: 创建权限管理页面
<!-- ether-admin/src/views/system/Permissions.vue -->
<template>
<div class="permissions-page">
<a-card>
<template #title>
<span>权限管理</span>
<a-button type="primary" @click="openCreateModal">新建权限</a-button>
</template>
<a-table :columns="columns" :data-source="permissions" :loading="loading" row-key="id">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<a-tag :color="getTypeColor(record.type)">{{ record.type }}</a-tag>
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="openEditModal(record)">编辑</a-button>
<a-popconfirm title="确定删除?" @confirm="handleDelete(record.id)">
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
<a-modal v-model:open="modalVisible" :title="isEdit ? '编辑权限' : '新建权限'" @ok="handleSubmit">
<a-form :model="formData" :label-col="{ span: 6 }">
<a-form-item label="权限代码" name="code">
<a-input v-model:value="formData.code" placeholder="如: user:create" />
</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="type">
<a-select v-model:value="formData.type">
<a-select-option value="MENU">菜单</a-select-option>
<a-select-option value="BUTTON">按钮</a-select-option>
<a-select-option value="API">API</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="资源路径" name="resource">
<a-input v-model:value="formData.resource" placeholder="/api/users" />
</a-form-item>
<a-form-item label="请求方法" name="method">
<a-select v-model:value="formData.method" allow-clear>
<a-select-option value="GET">GET</a-select-option>
<a-select-option value="POST">POST</a-select-option>
<a-select-option value="PUT">PUT</a-select-option>
<a-select-option value="DELETE">DELETE</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { getPermissions, createPermission, updatePermission, deletePermission, type Permission } from '@/api/permission';
const columns = [
{ title: '权限代码', dataIndex: 'code', key: 'code' },
{ title: '权限名称', dataIndex: 'name', key: 'name' },
{ title: '类型', dataIndex: 'type', key: 'type' },
{ title: '资源', dataIndex: 'resource', key: 'resource' },
{ title: '方法', dataIndex: 'method', key: 'method' },
{ title: '操作', key: 'actions', width: 150 },
];
const permissions = ref<Permission[]>([]);
const loading = ref(false);
const modalVisible = ref(false);
const isEdit = ref(false);
const formData = ref<Partial<Permission>>({ type: 'BUTTON' });
const loadPermissions = async () => {
loading.value = true;
try {
const res = await getPermissions();
permissions.value = res.data;
} finally {
loading.value = false;
}
};
const openCreateModal = () => {
isEdit.value = false;
formData.value = { type: 'BUTTON' };
modalVisible.value = true;
};
const openEditModal = (record: Permission) => {
isEdit.value = true;
formData.value = { ...record };
modalVisible.value = true;
};
const handleSubmit = async () => {
try {
if (isEdit.value) {
await updatePermission(formData.value.id!, formData.value);
message.success('更新成功');
} else {
await createPermission(formData.value);
message.success('创建成功');
}
modalVisible.value = false;
loadPermissions();
} catch {
message.error('操作失败');
}
};
const handleDelete = async (id: string) => {
try {
await deletePermission(id);
message.success('删除成功');
loadPermissions();
} catch {
message.error('删除失败');
}
};
const getTypeColor = (type: string) => {
const colors: Record<string, string> = { MENU: 'blue', BUTTON: 'green', API: 'orange' };
return colors[type] || 'default';
};
onMounted(loadPermissions);
</script>
Task 5: 创建角色管理页面
Files:
- Modify:
ether-admin/src/views/system/Roles.vue
Step 1: 更新角色管理页面
<!-- ether-admin/src/views/system/Roles.vue -->
<template>
<div class="roles-page">
<a-card>
<template #title>
<span>角色管理</span>
<a-button type="primary" @click="openCreateModal">新建角色</a-button>
</template>
<a-table :columns="columns" :data-source="roles" :loading="loading" row-key="id">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<a-tag>{{ record.type }}</a-tag>
</template>
<template v-if="column.key === 'dataScope'">
<a-tag :color="getScopeColor(record.dataScope)">{{ record.dataScope }}</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-switch :checked="record.status === 'ENABLED'" @change="toggleStatus(record)" />
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="openEditModal(record)">编辑</a-button>
<a-popconfirm title="确定删除?" @confirm="handleDelete(record.id)">
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
<a-modal v-model:open="modalVisible" :title="isEdit ? '编辑角色' : '新建角色'" width="600px" @ok="handleSubmit">
<a-form :model="formData" :label-col="{ span: 6 }">
<a-form-item label="角色代码" name="code">
<a-input v-model:value="formData.code" :disabled="isEdit" />
</a-form-item>
<a-form-item label="角色名称" name="name">
<a-input v-model:value="formData.name" />
</a-form-item>
<a-form-item label="角色类型" name="type">
<a-select v-model:value="formData.type">
<a-select-option value="SYSTEM">系统级</a-select-option>
<a-select-option value="PROJECT">项目级</a-select-option>
<a-select-option value="DEPARTMENT">部门级</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="数据权限" name="dataScope">
<a-select v-model:value="formData.dataScope">
<a-select-option value="ALL">全部数据</a-select-option>
<a-select-option value="PROJECT">本项目数据</a-select-option>
<a-select-option value="SELF">本人数据</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="权限分配" name="permissions">
<PermissionTree v-model:selected="formData.permissionIds" :permissions="allPermissions" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { getRoles, createRole, updateRole, deleteRole, type Role } from '@/api/role';
import { getPermissions } from '@/api/permission';
import PermissionTree from '@/components/PermissionTree/index.vue';
const columns = [
{ title: '角色代码', dataIndex: 'code', key: 'code' },
{ title: '角色名称', dataIndex: 'name', key: 'name' },
{ title: '类型', dataIndex: 'type', key: 'type' },
{ title: '数据权限', dataIndex: 'dataScope', key: 'dataScope' },
{ title: '状态', key: 'status' },
{ title: '操作', key: 'actions', width: 150 },
];
const roles = ref<Role[]>([]);
const allPermissions = ref<any[]>([]);
const loading = ref(false);
const modalVisible = ref(false);
const isEdit = ref(false);
const formData = ref<any>({ type: 'SYSTEM', dataScope: 'SELF', permissionIds: [] });
const loadData = async () => {
loading.value = true;
try {
const [rolesRes, permsRes] = await Promise.all([getRoles(), getPermissions()]);
roles.value = rolesRes.data;
allPermissions.value = permsRes.data;
} finally {
loading.value = false;
}
};
const openCreateModal = () => {
isEdit.value = false;
formData.value = { type: 'SYSTEM', dataScope: 'SELF', permissionIds: [] };
modalVisible.value = true;
};
const openEditModal = (record: Role) => {
isEdit.value = true;
formData.value = { ...record, permissionIds: record.permissions?.map(p => p.id) || [] };
modalVisible.value = true;
};
const handleSubmit = async () => {
try {
if (isEdit.value) {
await updateRole(formData.value.id, formData.value);
} else {
await createRole(formData.value);
}
message.success('操作成功');
modalVisible.value = false;
loadData();
} catch {
message.error('操作失败');
}
};
const handleDelete = async (id: string) => {
try {
await deleteRole(id);
message.success('删除成功');
loadData();
} catch {
message.error('删除失败');
}
};
const toggleStatus = async (record: Role) => {
const newStatus = record.status === 'ENABLED' ? 'DISABLED' : 'ENABLED';
try {
await updateRole(record.id, { ...record, status: newStatus });
loadData();
} catch {
message.error('操作失败');
}
};
const getScopeColor = (scope: string) => {
const colors: Record<string, string> = { ALL: 'red', PROJECT: 'blue', SELF: 'green' };
return colors[scope] || 'default';
};
onMounted(loadData);
</script>
Task 6: 创建PermissionTree组件
Files:
- Create:
ether-admin/src/components/PermissionTree/index.vue
Step 1: 创建权限树组件
<template>
<a-tree
v-model:selectedKeys="selectedKeys"
v-model:checkedKeys="checkedKeys"
checkable
:tree-data="treeData"
:field-names="{ children: 'children', title: 'name', key: 'id' }"
/>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
const props = defineProps<{
permissions: any[];
selected?: string[];
}>();
const emit = defineEmits<{
(e: 'update:selected', value: string[]): void;
}>();
const selectedKeys = ref<string[]>(props.selected || []);
const checkedKeys = ref<string[]>(props.selected || []);
const treeData = computed(() => {
const menuPerms = props.permissions.filter(p => p.type === 'MENU');
const buttonPerms = props.permissions.filter(p => p.type === 'BUTTON' || p.type === 'API');
const buildTree = (items: any[]) => {
const map: Record<string, any> = {};
const roots: any[] = [];
items.forEach(item => {
map[item.id] = { ...item, children: [] };
});
items.forEach(item => {
if (item.parentCode) {
const parent = items.find(p => p.code === item.parentCode);
if (parent) {
map[parent.id]?.children?.push(map[item.id]);
} else {
roots.push(map[item.id]);
}
} else {
roots.push(map[item.id]);
}
});
return roots;
};
return [
{
id: 'menu',
name: '菜单权限',
children: buildTree(menuPerms)
},
{
id: 'button',
name: '按钮/API权限',
children: buildTree(buttonPerms)
},
];
});
watch(selectedKeys, (val) => {
emit('update:selected', val);
});
</script>
Task 7: 创建用户详情页面(角色分配)
Files:
- Modify:
ether-admin/src/views/system/Users.vue
Step 1: 添加用户角色分配功能
在用户列表中添加操作列,点击跳转用户详情页,包含角色分配和项目参与标签页。
<!-- 用户详情页片段 -->
<template>
<a-tabs>
<a-tab-pane key="roles" tab="角色分配">
<a-space direction="vertical" style="width: 100%">
<a-space>
<span>当前角色:</span>
<a-tag v-for="role in userRoles" :key="role.id" closable @close="removeRole(role.id)">
{{ role.name }}
</a-tag>
</a-space>
<a-button @click="showRoleModal = true">分配角色</a-button>
</a-space>
</a-tab-pane>
<a-tab-pane key="projects" tab="项目参与">
<a-table :columns="projectColumns" :data-source="userProjects" row-key="id" size="small">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'roleInProject'">
<a-select v-model:value="record.roleInProject" style="width: 100px" @change="updateProjectRole(record)">
<a-select-option value="leader">项目负责人</a-select-option>
<a-select-option value="member">成员</a-select-option>
<a-select-option value="viewer">访客</a-select-option>
</a-select>
</template>
<template v-else-if="column.key === 'actions'">
<a-popconfirm title="确定移除?" @confirm="removeFromProject(record.projectId)">
<a-button type="link" danger size="small">移除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<a-button type="link" @click="showProjectModal = true">+ 添加到项目</a-button>
</a-tab-pane>
</a-tabs>
</template>
Task 8: 创建审计日志页面
Files:
- Create:
ether-admin/src/views/system/Audit.vue
Step 1: 创建审计日志页面
<template>
<div class="audit-page">
<a-card>
<template #title>操作审计日志</template>
<a-space style="margin-bottom: 16px">
<a-select v-model="filters.type" placeholder="操作类型" allow-clear style="width: 120px">
<a-select-option value="PERMISSION">权限变更</a-select-option>
<a-select-option value="ROLE">角色分配</a-select-option>
<a-select-option value="PROJECT">项目参与</a-select-option>
</a-select>
<a-range-picker v-model:value="filters.dateRange" />
<a-button @click="loadLogs">查询</a-button>
</a-space>
<a-table :columns="columns" :data-source="logs" :loading="loading" row-key="id" />
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const columns = [
{ title: '时间', dataIndex: 'createdAt', key: 'createdAt' },
{ title: '操作用户', dataIndex: 'operator', key: 'operator' },
{ title: '操作类型', dataIndex: 'type', key: 'type' },
{ title: '操作内容', dataIndex: 'content', key: 'content' },
{ title: '目标对象', dataIndex: 'target', key: 'target' },
];
const logs = ref([]);
const loading = ref(false);
const filters = ref({ type: undefined, dateRange: [] });
const loadLogs = async () => {
loading.value = true;
// TODO: 调用审计日志API
loading.value = false;
};
onMounted(loadLogs);
</script>
Phase 3: 路由配置
Task 9: 添加路由
Files:
- Modify:
ether-admin/src/router/index.ts
Step 1: 添加权限管理路由
{
path: '/system',
component: Layout,
children: [
// ... 现有路由
{
path: 'permissions',
component: () => import('@/views/system/Permissions.vue'),
meta: { title: '权限管理' }
},
{
path: 'roles',
component: () => import('@/views/system/Roles.vue'),
meta: { title: '角色管理' }
},
{
path: 'audit',
component: () => import('@/views/system/Audit.vue'),
meta: { title: '审计日志' }
},
]
}
Phase 4: 测试验证
Task 10: 功能测试
Step 1: 测试权限CRUD
# 启动前端
cd ether-admin && npm run dev
# 测试流程:
# 1. 登录管理端
# 2. 进入权限管理页面,创建/编辑/删除权限
# 3. 进入角色管理页面,创建角色并分配权限
# 4. 进入用户管理,给用户分配角色
# 5. 测试项目成员管理(添加到项目)
Step 2: 验证预期
- 权限创建成功后在列表显示
- 角色创建时可选择权限
- 用户可被分配多个角色
- 项目成员变更后自动更新权限
Verification Checklist
- 权限管理页面可正常打开
- 权限CRUD功能正常
- 角色管理页面可正常打开
- 角色创建时可选择权限
- 用户角色分配功能正常
- 用户项目关联功能正常
- 审计日志页面可正常打开
- 路由导航正常