ether-docs/plans/rbac-permission-frontend-pl...

707 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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**
```typescript
// 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**
```typescript
// 添加到现有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**
```typescript
// 添加到现有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: 创建权限管理页面**
```vue
<!-- 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: 更新角色管理页面**
```vue
<!-- 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: 创建权限树组件**
```vue
<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: 添加用户角色分配功能**
在用户列表中添加操作列,点击跳转用户详情页,包含角色分配和项目参与标签页。
```vue
<!-- 用户详情页片段 -->
<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: 创建审计日志页面**
```vue
<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: 添加权限管理路由**
```typescript
{
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**
```bash
# 启动前端
cd ether-admin && npm run dev
# 测试流程:
# 1. 登录管理端
# 2. 进入权限管理页面,创建/编辑/删除权限
# 3. 进入角色管理页面,创建角色并分配权限
# 4. 进入用户管理,给用户分配角色
# 5. 测试项目成员管理(添加到项目)
```
**Step 2: 验证预期**
- 权限创建成功后在列表显示
- 角色创建时可选择权限
- 用户可被分配多个角色
- 项目成员变更后自动更新权限
---
## Verification Checklist
- [ ] 权限管理页面可正常打开
- [ ] 权限CRUD功能正常
- [ ] 角色管理页面可正常打开
- [ ] 角色创建时可选择权限
- [ ] 用户角色分配功能正常
- [ ] 用户项目关联功能正常
- [ ] 审计日志页面可正常打开
- [ ] 路由导航正常