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