707 lines
20 KiB
Markdown
707 lines
20 KiB
Markdown
# 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功能正常
|
||
- [ ] 角色管理页面可正常打开
|
||
- [ ] 角色创建时可选择权限
|
||
- [ ] 用户角色分配功能正常
|
||
- [ ] 用户项目关联功能正常
|
||
- [ ] 审计日志页面可正常打开
|
||
- [ ] 路由导航正常
|