feat: 补全管理台功能 - 总览指标、搜索筛选、编辑模式、删除确认、全局日志查询

This commit is contained in:
2026-03-20 12:42:25 +08:00
parent 181cf09ad2
commit 0473a68dc2
19 changed files with 962 additions and 143 deletions

View File

@@ -60,7 +60,8 @@ grandclaw-archtype/
│ │ ├── logs.js # 日志数据
│ │ ├── members.js # 成员数据
│ │ ├── skills.js # 技能数据
│ │ ── tasks.js # 定时任务数据
│ │ ── tasks.js # 定时任务数据
│ │ └── adminData.js # 管理台数据(部门/用户/项目/总览/日志)
│ ├── pages/ # 页面组件
│ │ ├── HomePage.jsx # 首页
│ │ ├── LoginPage.jsx # 登录页面
@@ -81,11 +82,12 @@ grandclaw-archtype/
│ │ ├── admin/ # 管理台子页面
│ │ │ ├── OverviewPage.jsx # 运营总览
│ │ │ ├── DepartmentsPage.jsx # 部门管理
│ │ │ ├── AddDepartmentPage.jsx # 新增部门
│ │ │ ├── AddDepartmentPage.jsx # 新增/编辑部门
│ │ │ ├── UsersPage.jsx # 用户管理
│ │ │ ├── AddUserPage.jsx # 新增用户
│ │ │ ├── AddUserPage.jsx # 新增/编辑用户
│ │ │ ├── AdminProjectsPage.jsx # 项目管理
│ │ │ ── AddProjectPage.jsx # 新增项目
│ │ │ ── AddProjectPage.jsx # 新增/编辑项目
│ │ │ └── AdminLogsPage.jsx # 全局日志查询
│ │ └── developer/ # 开发台子页面
│ │ ├── MySkillsPage.jsx # 我的技能
│ │ ├── UploadSkillPage.jsx # 创建技能
@@ -156,10 +158,11 @@ pnpm build
- **账号管理**:个人信息和密码修改
### 4. 管理台Admin
- **运营总览**:平台运营数据概览
- **部门管理**:部门列表,支持新增、编辑、启用/禁用、删除
- **用户管理**:用户列表,支持新增、编辑、启用/禁用、删除,角色区分(管理员/开发者/成员)
- **项目管理**:项目列表,支持新增、编辑、启用/禁用、删除
- **运营总览**:平台运营指标卡片(用户总数、部门数量、项目数量、今日调用)、异常/待办事项提醒、最近操作日志
- **部门管理**:部门列表,支持搜索筛选、新增、编辑、启用/禁用、删除确认
- **用户管理**:用户列表,支持搜索筛选(关键词/部门/状态)、新增、编辑、启用/禁用、删除确认,角色区分(管理员/开发者/成员)
- **项目管理**:项目列表,支持搜索筛选、新增、编辑、启用/禁用、删除确认
- **日志查询**:全局系统日志查询,支持多维度筛选(关键词、用户、部门、类型、状态、时间范围)
### 5. 开发台Developer
- **我的技能**:已开发的技能列表
@@ -535,6 +538,7 @@ const members = api.members.list();
- `api.developer` - 开发台数据(技能、分类、模型、文档)
- `api.members` - 项目成员
- `api.tasks` - 定时任务
- `api.admin` - 管理台(总览、部门、用户、项目、全局日志)
## 数据模拟
@@ -546,6 +550,7 @@ const members = api.members.list();
- `developerData.js`:开发台数据,包含我的技能、技能分类、开发文档
- `logs.js`:操作日志数据(成功/失败/警告状态)
- `tasks.js`:定时任务数据(包含任务配置和执行日志)
- `adminData.js`:管理台数据(部门列表、用户列表、项目列表、总览指标、全局日志、可选项数据)
- `members.js`:项目成员数据
## 构建和部署

View File

@@ -0,0 +1,27 @@
## ADDED Requirements
### Requirement: 管理台静态数据文件
管理台 SHALL 有独立的数据文件,提供部门、用户、项目、总览指标、全局日志的模拟数据。
#### Scenario: 数据文件结构
- **WHEN** 项目加载
- **THEN** `src/data/adminData.js` 导出 adminDepartments部门列表、adminUsers用户列表、adminProjects项目列表、adminOverview总览指标和异常数据、adminLogs全局日志数据
### Requirement: 示例数据展示多种状态
管理台数据 SHALL 包含不同状态的示例记录,以展示页面的各种展示状态。
#### Scenario: 部门数据状态
- **WHEN** 加载部门数据
- **THEN** 数据包含"正常"和"禁用"两种状态的部门记录
#### Scenario: 用户数据状态
- **WHEN** 加载用户数据
- **THEN** 数据包含"管理员"、"开发者"、"成员"三种角色,以及"正常"和"禁用"两种状态的用户记录
#### Scenario: 项目数据状态
- **WHEN** 加载项目数据
- **THEN** 数据包含"正常"和"禁用"两种状态的项目记录
#### Scenario: 日志数据状态
- **WHEN** 加载全局日志数据
- **THEN** 数据包含不同用户、部门、类型、状态的日志记录,至少包含"成功"、"失败"、"警告"三种状态

View File

@@ -0,0 +1,43 @@
## ADDED Requirements
### Requirement: 全局日志列表展示
管理台日志查询页 SHALL 展示平台全局系统操作日志列表。
#### Scenario: 日志列表渲染
- **WHEN** 用户点击侧边栏"日志查询"导航项
- **THEN** 页面显示日志列表表格,列包含时间、用户、部门、类型、操作、状态、详情
### Requirement: 多维度日志筛选
管理台日志查询页 SHALL 支持按关键词、用户、部门、类型、状态、时间范围进行筛选。
#### Scenario: 关键词筛选
- **WHEN** 用户在关键词输入框输入文本并点击查询
- **THEN** 日志列表仅显示操作或详情中包含该关键词的记录
#### Scenario: 用户筛选
- **WHEN** 用户选择某个用户并点击查询
- **THEN** 日志列表仅显示该用户的操作记录
#### Scenario: 部门筛选
- **WHEN** 用户选择某个部门并点击查询
- **THEN** 日志列表仅显示该部门成员的操作记录
#### Scenario: 类型筛选
- **WHEN** 用户选择某种类型(登录、实例操作、技能、配置修改、文件上传)并点击查询
- **THEN** 日志列表仅显示该类型的记录
#### Scenario: 状态筛选
- **WHEN** 用户选择某种状态(成功、失败、警告)并点击查询
- **THEN** 日志列表仅显示该状态的记录
#### Scenario: 时间范围筛选
- **WHEN** 用户设置开始日期和结束日期并点击查询
- **THEN** 日志列表仅显示时间范围内的记录
#### Scenario: 筛选重置
- **WHEN** 用户点击重置按钮
- **THEN** 所有筛选条件清空,日志列表恢复显示全部记录
#### Scenario: 无匹配结果
- **WHEN** 用户筛选后无匹配日志
- **THEN** 显示空状态组件,提示"暂无匹配日志"

View File

@@ -0,0 +1,86 @@
## ADDED Requirements
### Requirement: 列表搜索筛选生效
部门管理、用户管理、项目管理列表页 SHALL 支持按关键词和其他条件筛选列表数据。
#### Scenario: 部门关键词搜索
- **WHEN** 用户在部门列表页关键词输入框输入文本并点击查询
- **THEN** 列表仅显示部门名称或描述中包含该关键词的记录
#### Scenario: 部门状态筛选
- **WHEN** 用户在部门列表页选择某个状态(正常/禁用)并点击查询
- **THEN** 列表仅显示该状态的部门记录
#### Scenario: 部门筛选重置
- **WHEN** 用户在部门列表页点击重置按钮
- **THEN** 筛选条件清空,列表恢复显示全部部门
#### Scenario: 用户关键词搜索
- **WHEN** 用户在用户列表页关键词输入框输入文本并点击查询
- **THEN** 列表仅显示姓名或邮箱中包含该关键词的记录
#### Scenario: 用户部门筛选
- **WHEN** 用户在用户列表页选择某个部门并点击查询
- **THEN** 列表仅显示该部门的用户记录
#### Scenario: 用户状态筛选
- **WHEN** 用户在用户列表页选择某个状态并点击查询
- **THEN** 列表仅显示该状态的用户记录
#### Scenario: 用户筛选重置
- **WHEN** 用户在用户列表页点击重置按钮
- **THEN** 筛选条件清空,列表恢复显示全部用户
#### Scenario: 项目关键词搜索
- **WHEN** 用户在项目列表页关键词输入框输入文本并点击查询
- **THEN** 列表仅显示项目名称或描述中包含该关键词的记录
#### Scenario: 项目状态筛选
- **WHEN** 用户在项目列表页选择某个状态并点击查询
- **THEN** 列表仅显示该状态的项目记录
#### Scenario: 项目筛选重置
- **WHEN** 用户在项目列表页点击重置按钮
- **THEN** 筛选条件清空,列表恢复显示全部项目
### Requirement: 编辑功能复用新增页
部门管理、用户管理、项目管理的编辑功能 SHALL 复用新增页面,通过编辑模式预填表单数据。
#### Scenario: 进入部门编辑模式
- **WHEN** 用户点击某个部门的"编辑"按钮
- **THEN** 页面切换到新增部门表单,表单标题显示"编辑部门",表单字段预填该部门的现有数据
#### Scenario: 进入用户编辑模式
- **WHEN** 用户点击某个用户的"编辑"按钮
- **THEN** 页面切换到新增用户表单,表单标题显示"编辑用户",表单字段预填该用户的现有数据
#### Scenario: 进入项目编辑模式
- **WHEN** 用户点击某个项目的"编辑"按钮
- **THEN** 页面切换到新增项目表单,表单标题显示"编辑项目",表单字段预填该项目的现有数据
#### Scenario: 编辑页返回
- **WHEN** 用户在编辑页面点击取消按钮
- **THEN** 返回对应的列表页
### Requirement: 删除操作确认
部门管理、用户管理、项目管理的删除操作 SHALL 弹出确认弹框。
#### Scenario: 部门删除确认
- **WHEN** 用户点击某个部门的"删除"按钮
- **THEN** 弹出确认弹框,显示"确定要删除"{部门名称}"吗?此操作不可撤销。"
#### Scenario: 用户删除确认
- **WHEN** 用户点击某个用户的"删除"按钮
- **THEN** 弹出确认弹框,显示"确定要删除用户"{用户姓名}"吗?此操作不可撤销。"
#### Scenario: 项目删除确认
- **WHEN** 用户点击某个项目的"删除"按钮
- **THEN** 弹出确认弹框,显示"确定要删除项目"{项目名称}"吗?此操作不可撤销。"
#### Scenario: 确认删除
- **WHEN** 用户在确认弹框中点击删除按钮
- **THEN** 弹框关闭,该记录从列表中移除
#### Scenario: 取消删除
- **WHEN** 用户在确认弹框中点击取消按钮
- **THEN** 弹框关闭,列表不变

View File

@@ -0,0 +1,22 @@
## ADDED Requirements
### Requirement: 运营指标展示
管理台总览页 SHALL 展示平台核心运营指标数据,以卡片形式呈现。
#### Scenario: 指标卡片展示
- **WHEN** 用户打开管理台总览页
- **THEN** 页面顶部显示4个指标卡片用户总数、部门数量、项目数量、今日调用次数每个卡片包含数值和趋势变化值
### Requirement: 异常/待办事项提醒
管理台总览页 SHALL 展示平台异常事件和待办事项列表。
#### Scenario: 异常事项展示
- **WHEN** 用户打开管理台总览页
- **THEN** 页面左侧区域显示异常/待办事项列表,每条包含警告图标和事项描述(如定时任务执行失败、用户账号被禁用、项目处于禁用状态等)
### Requirement: 最近操作日志展示
管理台总览页 SHALL 展示最近的操作日志精简列表。
#### Scenario: 日志列表展示
- **WHEN** 用户打开管理台总览页
- **THEN** 页面右侧区域显示最近5条操作日志每条包含时间、用户、操作类型、状态标签

View File

@@ -7,15 +7,16 @@
## Requirements
### Requirement: 统一数据访问接口
系统 SHALL 提供 api 服务对象,包含按功能模块划分的数据访问方法,作为所有数据访问的统一入口
数据访问层 SHALL 提供按功能模块划分的数据访问方法,所有数据获取通过 API 层进行。所有数据获取通过 API 层进行便于未来对接后端服务。API 层为纯函数,返回静态数据或对静态数据的引用,不涉及网络请求
#### Scenario: api.user 模块提供用户信息访问
- **WHEN** 调用 api.user.getInfo()
- **THEN** 系统返回用户信息对象(包含 name、avatar、role 等字段)
#### Scenario: api.skills 模块提供技能数据访问
- **WHEN** 调用 api.skills.list()
- **THEN** 系统返回所有技能列表数
#### Scenario: 技能数据访问
- **WHEN** 调用 `api.skills.list()`
- **THEN** 返回技能列表数
- **AND** 返回类型为数组每个元素包含技能基本信息id, name, description, category, author, icon, usageCount, rating, subscribed
#### Scenario: api.skills 支持按 ID 查询单个技能
- **WHEN** 调用 api.skills.getById(skillId)
@@ -29,9 +30,17 @@
- **WHEN** 调用 api.conversations.getScene(sceneName)
- **THEN** 系统返回对应场景的 HTML 内容字符串
#### Scenario: api.logs 模块支持筛选查询
- **WHEN** 调用 api.logs.list(filters)
- **THEN** 系统根据 filters 参数(用户、类型、状态)筛选并返回日志列表
#### Scenario: 日志数据访问
- **WHEN** 调用 `api.logs.list()``api.logs.filter({ user, type, status })`
- **THEN** 返回日志列表或筛选后的日志列表
#### Scenario: 定时任务数据访问
- **WHEN** 调用 `api.tasks.list()``api.tasks.getById(id)`
- **THEN** 返回任务列表或单个任务详情
#### Scenario: 项目成员数据访问
- **WHEN** 调用 `api.members.list()``api.members.getById(id)`
- **THEN** 返回成员列表或单个成员信息
### Requirement: 数据层与静态文件分离
系统 SHALL 将数据访问逻辑与静态数据文件分离,便于后续对接真实 API。
@@ -47,3 +56,26 @@
#### Scenario: API 层提供一致的返回格式
- **WHEN** 调用 API 层方法
- **THEN** 系统返回符合约定格式的数据(如对象、数组),无论底层存储格式如何
### Requirement: 管理台数据 API
数据访问层 SHALL 提供管理台相关的数据访问方法,覆盖总览、部门、用户、项目、全局日志。
#### Scenario: 总览数据访问
- **WHEN** 调用 `api.admin.getOverview()`
- **THEN** 返回总览数据对象,包含用户总数、部门数量、项目数量、今日调用次数、异常事项列表、最近操作日志
#### Scenario: 部门数据访问
- **WHEN** 调用 `api.admin.departments.list()``api.admin.departments.getById(id)`
- **THEN** 返回部门列表或单个部门信息
#### Scenario: 用户数据访问
- **WHEN** 调用 `api.admin.users.list()``api.admin.users.getById(id)`
- **THEN** 返回用户列表或单个用户信息
#### Scenario: 项目数据访问
- **WHEN** 调用 `api.admin.projects.list()``api.admin.projects.getById(id)`
- **THEN** 返回项目列表或单个项目信息
#### Scenario: 全局日志数据访问
- **WHEN** 调用 `api.admin.logs.list()``api.admin.logs.filter(filters)`
- **THEN** 返回全局日志列表或筛选后的日志列表

View File

@@ -24,6 +24,7 @@ export const ADMIN_PAGES = {
departments: { title: '部门管理', icon: 'FiBarChart2' },
users: { title: '用户管理', icon: 'FiUsers' },
projects: { title: '项目管理', icon: 'FiList' },
adminLogs: { title: '日志查询', icon: 'FiActivity' },
addDepartment: { title: '新增部门', icon: null },
addUser: { title: '新增用户', icon: null },
addProject: { title: '新增项目', icon: null },

88
src/data/adminData.js Normal file
View File

@@ -0,0 +1,88 @@
export const adminDepartments = [
{ id: 1, name: 'AI 产品部', description: '负责AI产品规划与设计', head: '张三', members: 8, status: '正常', createTime: '2025-06-01' },
{ id: 2, name: '技术研发部', description: '负责核心技术研发与实现', head: '李四', members: 15, status: '正常', createTime: '2025-06-01' },
{ id: 3, name: '数据分析部', description: '负责数据分析与挖掘', head: '王五', members: 10, status: '正常', createTime: '2025-06-01' },
{ id: 4, name: '运营部', description: '负责产品运营与推广', head: '钱七', members: 6, status: '正常', createTime: '2025-08-15' },
{ id: 5, name: '测试部', description: '负责产品质量保障', head: '周九', members: 5, status: '正常', createTime: '2025-07-01' },
{ id: 6, name: '客户服务部', description: '负责客户支持与服务', head: '吴十', members: 12, status: '禁用', createTime: '2025-09-10' }
];
export const adminUsers = [
{ id: 1, name: '张三', department: 'AI 产品部', role: '管理员', email: 'zhangsan@example.com', phone: '138****8888', status: '正常', lastLogin: '2026-03-19 10:30' },
{ id: 2, name: '李四', department: '技术研发部', role: '开发者', email: 'lisi@example.com', phone: '139****1234', status: '正常', lastLogin: '2026-03-19 09:15' },
{ id: 3, name: '王五', department: '数据分析部', role: '成员', email: 'wangwu@example.com', phone: '136****5678', status: '禁用', lastLogin: '2026-03-15 18:20' },
{ id: 4, name: '赵六', department: 'AI 产品部', role: '管理员', email: 'zhaoliu@example.com', phone: '135****9012', status: '正常', lastLogin: '2026-03-19 11:45' },
{ id: 5, name: '钱七', department: '技术研发部', role: '开发者', email: 'qianqi@example.com', phone: '137****3456', status: '正常', lastLogin: '2026-03-18 16:30' },
{ id: 6, name: '孙八', department: '技术研发部', role: '成员', email: 'sunba@example.com', phone: '133****7890', status: '正常', lastLogin: '2026-03-19 08:00' },
{ id: 7, name: '周九', department: '测试部', role: '成员', email: 'zhoujiu@example.com', phone: '158****2345', status: '正常', lastLogin: '2026-03-17 14:20' },
{ id: 8, name: '吴十', department: '技术研发部', role: '开发者', email: 'wushi@example.com', phone: '159****6789', status: '正常', lastLogin: '2026-03-19 12:10' }
];
export const adminProjects = [
{ id: 1, name: '企业 AI 智算平台', description: '企业级AI智能助手平台', owner: '张三', members: 8, status: '正常', createTime: '2026-01-15', lastActive: '2026-03-19 10:30' },
{ id: 2, name: '知识库管理系统', description: '智能知识库检索与管理', owner: '李四', members: 5, status: '正常', createTime: '2026-02-10', lastActive: '2026-03-18 16:20' },
{ id: 3, name: '数据分析平台', description: '大数据分析与可视化平台', owner: '王五', members: 12, status: '正常', createTime: '2026-01-20', lastActive: '2026-03-19 09:45' },
{ id: 4, name: '智能客服系统', description: 'AI驱动的客户服务系统', owner: '赵六', members: 6, status: '禁用', createTime: '2026-02-28', lastActive: '2026-03-10 14:15' },
{ id: 5, name: '文档自动化平台', description: '智能文档生成与处理', owner: '钱七', members: 4, status: '正常', createTime: '2026-03-01', lastActive: '2026-03-19 11:20' },
{ id: 6, name: '代码审查助手', description: '自动化代码审查与优化建议', owner: '孙八', members: 7, status: '正常', createTime: '2026-02-15', lastActive: '2026-03-17 15:30' }
];
export const adminOverview = {
userCount: 128,
userTrend: '+12',
deptCount: 6,
deptTrend: '',
projectCount: 42,
projectTrend: '+3',
todayCalls: 1234,
todayTrend: '+8.5%',
updateTime: '2026-03-19 16:42',
anomalies: [
{ type: 'warning', text: '2 个定时任务执行失败' },
{ type: 'warning', text: '1 个用户账号被禁用' },
{ type: 'info', text: '1 个项目处于禁用状态' }
],
recentLogs: [
{ time: '2026-03-19 16:42', user: '张三', action: '启动实例', status: '成功' },
{ time: '2026-03-19 15:28', user: '李四', action: '上传文件', status: '成功' },
{ time: '2026-03-19 14:55', user: '王五', action: '调用技能', status: '失败' },
{ time: '2026-03-19 12:30', user: '张三', action: '配置修改', status: '成功' },
{ time: '2026-03-19 11:20', user: '赵六', action: '订阅技能', status: '成功' }
]
};
export const adminLogs = [
{ time: '2026-03-19 16:42:33', user: '张三', department: 'AI 产品部', type: '实例操作', action: '启动实例', status: '成功', detail: '实例 teleclaw-zhangsan 启动成功' },
{ time: '2026-03-19 15:28:17', user: '张三', department: 'AI 产品部', type: '技能', action: '调用 代码生成助手', status: '成功', detail: 'Token 消耗: 1,234' },
{ time: '2026-03-19 14:55:02', user: '李四', department: '技术研发部', type: '文件上传', action: '上传数据文件', status: '成功', detail: '文件 sales_2026_q1.xlsx 上传完成' },
{ time: '2026-03-19 12:30:45', user: '王五', department: '数据分析部', type: '实例操作', action: '启动实例', status: '失败', detail: '资源配额不足,请联系管理员' },
{ time: '2026-03-19 11:20:33', user: '张三', department: 'AI 产品部', type: '配置修改', action: '更新模型偏好', status: '成功', detail: 'Doubao-lite-128k → Doubao-pro-32k' },
{ time: '2026-03-19 10:45:18', user: '李四', department: '技术研发部', type: '技能', action: '订阅 文档智能撰写', status: '成功', detail: 'Skill v1.2.0 已挂载' },
{ time: '2026-03-19 09:15:07', user: '张三', department: 'AI 产品部', type: '实例操作', action: '停止实例', status: '成功', detail: '实例运行时长: 6小时23分' },
{ time: '2026-03-19 09:10:22', user: '李四', department: '技术研发部', type: '登录', action: '用户登录', status: '成功', detail: 'IP: 192.168.1.105' },
{ time: '2026-03-18 18:32:11', user: '王五', department: '数据分析部', type: '实例操作', action: '重启实例', status: '警告', detail: '实例异常重启,请检查运行状态' },
{ time: '2026-03-18 16:55:40', user: '张三', department: 'AI 产品部', type: '文件上传', action: '上传数据文件', status: '失败', detail: '文件大小超过限制 (最大 50MB)' },
{ time: '2026-03-18 14:20:05', user: '张三', department: 'AI 产品部', type: '配置修改', action: '更新 API Key', status: '成功', detail: 'API Key 已更新' },
{ time: '2026-03-18 11:08:55', user: '李四', department: '技术研发部', type: '技能', action: '调用 CRM客户查询', status: '成功', detail: '查询结果: 23条记录' },
{ time: '2026-03-17 22:15:30', user: '赵六', department: 'AI 产品部', type: '登录', action: '用户登录', status: '失败', detail: '密码错误连续3次' },
{ time: '2026-03-17 16:40:12', user: '钱七', department: '技术研发部', type: '技能', action: '发布技能 代码审查助手', status: '成功', detail: '版本 v1.0.0 已上线' },
{ time: '2026-03-17 10:22:08', user: '孙八', department: '技术研发部', type: '配置修改', action: '修改项目配额', status: '成功', detail: 'Token 配额: 100,000 → 200,000' },
{ time: '2026-03-16 15:33:41', user: '周九', department: '测试部', type: '实例操作', action: '启动实例', status: '成功', detail: '实例 teleclaw-zhoujiu 启动成功' }
];
export const availableLeaders = [
{ id: 1, name: '张三', department: 'AI 产品部', phone: '138****8888', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', department: '技术研发部', phone: '139****1234', email: 'lisi@example.com' },
{ id: 3, name: '王五', department: '数据分析部', phone: '136****5678', email: 'wangwu@example.com' },
{ id: 4, name: '赵六', department: 'AI 产品部', phone: '135****9012', email: 'zhaoliu@example.com' },
{ id: 5, name: '钱七', department: '运营部', phone: '137****3456', email: 'qianqi@example.com' }
];
export const availableDepartments = [
{ id: 1, name: 'AI 产品部', description: '负责AI产品规划与设计', head: '张三', memberCount: 8 },
{ id: 2, name: '技术研发部', description: '负责核心技术研发与实现', head: '李四', memberCount: 15 },
{ id: 3, name: '数据分析部', description: '负责数据分析与挖掘', head: '王五', memberCount: 10 },
{ id: 4, name: '运营部', description: '负责产品运营与推广', head: '钱七', memberCount: 6 },
{ id: 5, name: '测试部', description: '负责产品质量保障', head: '周九', memberCount: 5 },
{ id: 6, name: '客户服务部', description: '负责客户支持与服务', head: '吴十', memberCount: 12 }
];

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { FiHome, FiBarChart2, FiUsers, FiList } from 'react-icons/fi';
import { FiHome, FiBarChart2, FiUsers, FiList, FiActivity } from 'react-icons/fi';
import Layout from '../components/Layout.jsx';
import SidebarBrand from '../components/layout/SidebarBrand.jsx';
import SidebarUser from '../components/layout/SidebarUser.jsx';
@@ -15,40 +15,72 @@ import AdminProjectsPage from './admin/AdminProjectsPage.jsx';
import AddDepartmentPage from './admin/AddDepartmentPage.jsx';
import AddUserPage from './admin/AddUserPage.jsx';
import AddProjectPage from './admin/AddProjectPage.jsx';
import AdminLogsPage from './admin/AdminLogsPage.jsx';
function AdminPage() {
const location = useLocation();
const navigate = useNavigate();
// 使用 usePageState 管理页面状态
const { currentPage, setCurrentPage } = usePageState({
storageKey: ADMIN_KEYS.CURRENT_PAGE,
defaultPage: 'overview',
pageTitles: ADMIN_PAGES,
});
const [editData, setEditData] = useState(null);
const navigateTo = (page, data) => {
setEditData(data || null);
setCurrentPage(page);
};
const renderPage = () => {
switch (currentPage) {
case 'overview':
return <OverviewPage />;
case 'departments':
return <DepartmentsPage onAdd={() => setCurrentPage('addDepartment')} />;
return <DepartmentsPage
onAdd={() => navigateTo('addDepartment')}
onEdit={(dept) => navigateTo('addDepartment', dept)}
/>;
case 'users':
return <UsersPage onAdd={() => setCurrentPage('addUser')} />;
return <UsersPage
onAdd={() => navigateTo('addUser')}
onEdit={(user) => navigateTo('addUser', user)}
/>;
case 'projects':
return <AdminProjectsPage onAdd={() => setCurrentPage('addProject')} />;
return <AdminProjectsPage
onAdd={() => navigateTo('addProject')}
onEdit={(project) => navigateTo('addProject', project)}
/>;
case 'adminLogs':
return <AdminLogsPage />;
case 'addDepartment':
return <AddDepartmentPage onBack={() => setCurrentPage('departments')} />;
return <AddDepartmentPage
onBack={() => navigateTo('departments')}
editData={editData}
/>;
case 'addUser':
return <AddUserPage onBack={() => setCurrentPage('users')} />;
return <AddUserPage
onBack={() => navigateTo('users')}
editData={editData}
/>;
case 'addProject':
return <AddProjectPage onBack={() => setCurrentPage('projects')} />;
return <AddProjectPage
onBack={() => navigateTo('projects')}
editData={editData}
/>;
default:
return <div>Page not found</div>;
}
};
const getPageTitle = () => {
if (editData && (currentPage === 'addDepartment' || currentPage === 'addUser' || currentPage === 'addProject')) {
const prefix = '编辑';
const nameMap = { addDepartment: '部门', addUser: '用户', addProject: '项目' };
return prefix + nameMap[currentPage];
}
return ADMIN_PAGES[currentPage]?.title || '';
};
@@ -62,7 +94,7 @@ function AdminPage() {
icon={<FiHome />}
label="总览"
active={currentPage === 'overview'}
onClick={() => setCurrentPage('overview')}
onClick={() => navigateTo('overview')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
@@ -71,7 +103,7 @@ function AdminPage() {
icon={<FiBarChart2 />}
label="部门管理"
active={currentPage === 'departments'}
onClick={() => setCurrentPage('departments')}
onClick={() => navigateTo('departments')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
@@ -80,7 +112,7 @@ function AdminPage() {
icon={<FiUsers />}
label="用户管理"
active={currentPage === 'users'}
onClick={() => setCurrentPage('users')}
onClick={() => navigateTo('users')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
@@ -89,7 +121,16 @@ function AdminPage() {
icon={<FiList />}
label="项目管理"
active={currentPage === 'projects'}
onClick={() => setCurrentPage('projects')}
onClick={() => navigateTo('projects')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiActivity />}
label="日志查询"
active={currentPage === 'adminLogs'}
onClick={() => navigateTo('adminLogs')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
@@ -116,4 +157,4 @@ function AdminPage() {
);
}
export default AdminPage;
export default AdminPage;

View File

@@ -1,16 +1,14 @@
import { useState } from 'react';
import ListSelector from '../../components/ListSelector.jsx';
import { availableLeaders } from '../../data/adminData.js';
const availableLeaders = [
{ id: 1, name: '张三', department: 'AI 产品部', phone: '138****8888', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', department: '技术研发部', phone: '139****1234', email: 'lisi@example.com' },
{ id: 3, name: '王五', department: '数据分析部', phone: '136****5678', email: 'wangwu@example.com' },
{ id: 4, name: '赵六', department: 'AI 产品部', phone: '135****9012', email: 'zhaoliu@example.com' },
{ id: 5, name: '钱七', department: '运营部', phone: '137****3456', email: 'qianqi@example.com' }
];
function AddDepartmentPage({ onBack }) {
const [selectedLeader, setSelectedLeader] = useState(null);
function AddDepartmentPage({ onBack, editData }) {
const isEdit = !!editData;
const [name, setName] = useState(editData?.name || '');
const [description, setDescription] = useState(editData?.description || '');
const [selectedLeader, setSelectedLeader] = useState(
editData ? availableLeaders.find(l => l.name === editData.head)?.id || null : null
);
const leaderColumns = [
{ key: 'name', label: '姓名' },
@@ -18,23 +16,23 @@ function AddDepartmentPage({ onBack }) {
{ key: 'email', label: '邮箱' }
];
const selectedLabel = selectedLeader
const selectedLabel = selectedLeader
? `${availableLeaders.find(l => l.id === selectedLeader)?.name} - ${availableLeaders.find(l => l.id === selectedLeader)?.department}`
: null;
return (
<div className="card">
<div className="card-header">
<div className="card-title">新增部门</div>
<div className="card-title">{isEdit ? '编辑部门' : '新增部门'}</div>
</div>
<div className="card-body">
<div className="form-group">
<label className="form-label required">部门名称</label>
<input type="text" className="form-control" placeholder="请输入部门名称" />
<input type="text" className="form-control" placeholder="请输入部门名称" value={name} onChange={e => setName(e.target.value)} />
</div>
<div className="form-group">
<label className="form-label">部门描述</label>
<textarea className="form-control" rows="3" placeholder="请输入部门描述"></textarea>
<textarea className="form-control" rows="3" placeholder="请输入部门描述" value={description} onChange={e => setDescription(e.target.value)}></textarea>
</div>
<div className="form-group">
<label className="form-label required">负责人</label>

View File

@@ -1,16 +1,14 @@
import { useState } from 'react';
import ListSelector from '../../components/ListSelector.jsx';
import { availableLeaders } from '../../data/adminData.js';
const availableLeaders = [
{ id: 1, name: '张三', department: 'AI 产品部', phone: '138****8888', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', department: '技术研发部', phone: '139****1234', email: 'lisi@example.com' },
{ id: 3, name: '王五', department: '数据分析部', phone: '136****5678', email: 'wangwu@example.com' },
{ id: 4, name: '赵六', department: 'AI 产品部', phone: '135****9012', email: 'zhaoliu@example.com' },
{ id: 5, name: '钱七', department: '运营部', phone: '137****3456', email: 'qianqi@example.com' }
];
function AddProjectPage({ onBack }) {
const [selectedLeader, setSelectedLeader] = useState(null);
function AddProjectPage({ onBack, editData }) {
const isEdit = !!editData;
const [name, setName] = useState(editData?.name || '');
const [description, setDescription] = useState(editData?.description || '');
const [selectedLeader, setSelectedLeader] = useState(
editData ? availableLeaders.find(l => l.name === editData.owner)?.id || null : null
);
const leaderColumns = [
{ key: 'name', label: '姓名' },
@@ -18,23 +16,23 @@ function AddProjectPage({ onBack }) {
{ key: 'phone', label: '联系电话' }
];
const selectedLabel = selectedLeader
const selectedLabel = selectedLeader
? `${availableLeaders.find(l => l.id === selectedLeader)?.name} - ${availableLeaders.find(l => l.id === selectedLeader)?.department}`
: null;
return (
<div className="card">
<div className="card-header">
<div className="card-title">新增项目</div>
<div className="card-title">{isEdit ? '编辑项目' : '新增项目'}</div>
</div>
<div className="card-body">
<div className="form-group">
<label className="form-label required">项目名称</label>
<input type="text" className="form-control" placeholder="请输入项目名称" />
<input type="text" className="form-control" placeholder="请输入项目名称" value={name} onChange={e => setName(e.target.value)} />
</div>
<div className="form-group">
<label className="form-label">项目描述</label>
<textarea className="form-control" rows="3" placeholder="请输入项目描述"></textarea>
<textarea className="form-control" rows="3" placeholder="请输入项目描述" value={description} onChange={e => setDescription(e.target.value)}></textarea>
</div>
<div className="form-group">
<label className="form-label required">负责人</label>

View File

@@ -1,17 +1,16 @@
import { useState } from 'react';
import ListSelector from '../../components/ListSelector.jsx';
import { availableDepartments } from '../../data/adminData.js';
const availableDepartments = [
{ id: 1, name: 'AI 产品部', description: '负责AI产品规划与设计', head: '张三', memberCount: 8 },
{ id: 2, name: '技术研发部', description: '负责核心技术研发与实现', head: '李四', memberCount: 15 },
{ id: 3, name: '数据分析部', description: '负责数据分析与挖掘', head: '王五', memberCount: 10 },
{ id: 4, name: '运营部', description: '负责产品运营与推广', head: '钱七', memberCount: 6 },
{ id: 5, name: '测试部', description: '负责产品质量保障', head: '周九', memberCount: 5 },
{ id: 6, name: '客户服务部', description: '负责客户支持与服务', head: '吴十', memberCount: 12 }
];
function AddUserPage({ onBack }) {
const [selectedDepartment, setSelectedDepartment] = useState(null);
function AddUserPage({ onBack, editData }) {
const isEdit = !!editData;
const [name, setName] = useState(editData?.name || '');
const [role, setRole] = useState(editData?.role || '成员');
const [email, setEmail] = useState(editData?.email || '');
const [phone, setPhone] = useState(editData?.phone || '');
const [selectedDepartment, setSelectedDepartment] = useState(
editData ? availableDepartments.find(d => d.name === editData.department)?.id || null : null
);
const departmentColumns = [
{ key: 'name', label: '部门名称' },
@@ -19,19 +18,19 @@ function AddUserPage({ onBack }) {
{ key: 'head', label: '负责人' }
];
const selectedLabel = selectedDepartment
? availableDepartments.find(d => d.id === selectedDepartment)?.name
const selectedLabel = selectedDepartment
? availableDepartments.find(d => d.id === selectedDepartment)?.name
: null;
return (
<div className="card">
<div className="card-header">
<div className="card-title">新增用户</div>
<div className="card-title">{isEdit ? '编辑用户' : '新增用户'}</div>
</div>
<div className="card-body">
<div className="form-group">
<label className="form-label required">姓名</label>
<input type="text" className="form-control" placeholder="请输入用户姓名" />
<input type="text" className="form-control" placeholder="请输入用户姓名" value={name} onChange={e => setName(e.target.value)} />
</div>
<div className="form-group">
<label className="form-label required">部门</label>
@@ -48,7 +47,7 @@ function AddUserPage({ onBack }) {
</div>
<div className="form-group">
<label className="form-label required">角色</label>
<select className="form-control">
<select className="form-control" value={role} onChange={e => setRole(e.target.value)}>
<option>成员</option>
<option>管理员</option>
<option>开发者</option>
@@ -56,11 +55,11 @@ function AddUserPage({ onBack }) {
</div>
<div className="form-group">
<label className="form-label required">邮箱</label>
<input type="email" className="form-control" placeholder="请输入邮箱" />
<input type="email" className="form-control" placeholder="请输入邮箱" value={email} onChange={e => setEmail(e.target.value)} />
</div>
<div className="form-group">
<label className="form-label">手机号</label>
<input type="tel" className="form-control" placeholder="请输入手机号" />
<input type="tel" className="form-control" placeholder="请输入手机号" value={phone} onChange={e => setPhone(e.target.value)} />
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
<button className="btn" onClick={onBack}>取消</button>

View File

@@ -0,0 +1,182 @@
import { useState } from 'react';
import { FiInbox } from 'react-icons/fi';
import { api } from '../../services/api.js';
import EmptyState from '../../components/common/EmptyState.jsx';
function AdminLogsPage() {
const [filters, setFilters] = useState({
keyword: '',
user: '',
department: '',
type: '',
status: '',
startDate: '',
endDate: '',
});
const handleFilterChange = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
};
const handleReset = () => {
setFilters({ keyword: '', user: '', department: '', type: '', status: '', startDate: '', endDate: '' });
};
const filteredLogs = api.admin.logs.filter(filters);
return (
<>
<div className="card">
<div className="card-body">
<div className="search-bar">
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
<label>关键词</label>
<input
type="text"
className="form-control"
placeholder="搜索操作、详情..."
value={filters.keyword}
onChange={e => handleFilterChange('keyword', e.target.value)}
/>
</div>
<div className="search-item">
<label>用户</label>
<select
className="form-control"
value={filters.user}
onChange={e => handleFilterChange('user', e.target.value)}
>
<option value="">全部用户</option>
<option>张三</option>
<option>李四</option>
<option>王五</option>
<option>赵六</option>
<option>钱七</option>
<option>孙八</option>
<option>周九</option>
</select>
</div>
<div className="search-item">
<label>部门</label>
<select
className="form-control"
value={filters.department}
onChange={e => handleFilterChange('department', e.target.value)}
>
<option value="">全部部门</option>
<option>AI 产品部</option>
<option>技术研发部</option>
<option>数据分析部</option>
</select>
</div>
<div className="search-item">
<label>类型</label>
<select
className="form-control"
value={filters.type}
onChange={e => handleFilterChange('type', e.target.value)}
>
<option value="">全部类型</option>
<option>登录</option>
<option>实例操作</option>
<option>技能</option>
<option>配置修改</option>
<option>文件上传</option>
</select>
</div>
<div className="search-item">
<label>状态</label>
<select
className="form-control"
value={filters.status}
onChange={e => handleFilterChange('status', e.target.value)}
>
<option value="">全部</option>
<option>成功</option>
<option>失败</option>
<option>警告</option>
</select>
</div>
<div className="search-item">
<label>开始日期</label>
<input
type="date"
className="form-control"
value={filters.startDate}
onChange={e => handleFilterChange('startDate', e.target.value)}
/>
</div>
<div className="search-item">
<label>结束日期</label>
<input
type="date"
className="form-control"
value={filters.endDate}
onChange={e => handleFilterChange('endDate', e.target.value)}
/>
</div>
</div>
<div className="search-actions">
<button className="btn btn-primary" onClick={() => {}}>查询</button>
<button className="btn" onClick={handleReset}>重置</button>
</div>
</div>
</div>
<div className="card">
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="card-title">日志列表</div>
<button className="btn btn-primary btn-sm">导出日志</button>
</div>
<div className="card-body">
{filteredLogs.length > 0 ? (
<>
<div className="table-wrapper">
<table className="table">
<thead>
<tr>
<th>时间</th>
<th>用户</th>
<th>部门</th>
<th>类型</th>
<th>操作</th>
<th>状态</th>
<th>详情</th>
</tr>
</thead>
<tbody>
{filteredLogs.map((log, index) => (
<tr key={index}>
<td>{log.time}</td>
<td>{log.user}</td>
<td>{log.department}</td>
<td>{log.type}</td>
<td>{log.action}</td>
<td><span className={`status ${log.status === '成功' ? 'status-running' : log.status === '失败' ? 'status-error' : 'status-warning'}`}>{log.status}</span></td>
<td>{log.detail}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="pagination">
<div className="pagination-item"></div>
<div className="pagination-item active">1</div>
<div className="pagination-item">2</div>
<div className="pagination-item">3</div>
<div className="pagination-item"></div>
</div>
</>
) : (
<EmptyState
icon={<FiInbox size={48} />}
message="暂无匹配日志"
description="当前筛选条件下没有日志记录"
/>
)}
</div>
</div>
</>
);
}
export default AdminLogsPage;

View File

@@ -1,18 +1,39 @@
const adminProjects = [
{ id: 1, name: '企业 AI 智算平台', description: '企业级AI智能助手平台', owner: '张三', members: 8, status: '正常', createTime: '2026-01-15', lastActive: '2026-03-19 10:30' },
{ id: 2, name: '知识库管理系统', description: '智能知识库检索与管理', owner: '李四', members: 5, status: '正常', createTime: '2026-02-10', lastActive: '2026-03-18 16:20' },
{ id: 3, name: '数据分析平台', description: '大数据分析与可视化平台', owner: '王五', members: 12, status: '正常', createTime: '2026-01-20', lastActive: '2026-03-19 09:45' },
{ id: 4, name: '智能客服系统', description: 'AI驱动的客户服务系统', owner: '赵六', members: 6, status: '禁用', createTime: '2026-02-28', lastActive: '2026-03-10 14:15' },
{ id: 5, name: '文档自动化平台', description: '智能文档生成与处理', owner: '钱七', members: 4, status: '正常', createTime: '2026-03-01', lastActive: '2026-03-19 11:20' },
{ id: 6, name: '代码审查助手', description: '自动化代码审查与优化建议', owner: '孙八', members: 7, status: '正常', createTime: '2026-02-15', lastActive: '2026-03-17 15:30' }
];
import { useState } from 'react';
import { api } from '../../services/api.js';
import Modal from '../../components/common/Modal.jsx';
function StatusTag({ status }) {
const statusClass = status === '正常' ? 'status-running' : status === '禁用' ? 'status-error' : '';
return <span className={`status ${statusClass}`}>{status}</span>;
}
function AdminProjectsPage({ onAdd }) {
function AdminProjectsPage({ onAdd, onEdit }) {
const sourceData = api.admin.projects.list();
const [filters, setFilters] = useState({ keyword: '', status: '' });
const [deleteTarget, setDeleteTarget] = useState(null);
const handleFilterChange = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
};
const handleReset = () => {
setFilters({ keyword: '', status: '' });
};
const filteredList = sourceData.filter(project => {
if (filters.keyword && !project.name.includes(filters.keyword) && !project.description.includes(filters.keyword)) {
return false;
}
if (filters.status && project.status !== filters.status) {
return false;
}
return true;
});
const confirmDelete = () => {
setDeleteTarget(null);
};
return (
<>
<div className="card">
@@ -20,20 +41,30 @@ function AdminProjectsPage({ onAdd }) {
<div className="search-bar">
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
<label>关键词</label>
<input type="text" className="form-control" placeholder="搜索项目名称、描述..." />
<input
type="text"
className="form-control"
placeholder="搜索项目名称、描述..."
value={filters.keyword}
onChange={e => handleFilterChange('keyword', e.target.value)}
/>
</div>
<div className="search-item">
<label>状态</label>
<select className="form-control">
<option>全部</option>
<select
className="form-control"
value={filters.status}
onChange={e => handleFilterChange('status', e.target.value)}
>
<option value="">全部</option>
<option>正常</option>
<option>禁用</option>
</select>
</div>
</div>
<div className="search-actions">
<button className="btn btn-primary">查询</button>
<button className="btn">重置</button>
<button className="btn btn-primary" onClick={() => {}}>查询</button>
<button className="btn" onClick={handleReset}>重置</button>
</div>
</div>
</div>
@@ -57,7 +88,7 @@ function AdminProjectsPage({ onAdd }) {
</tr>
</thead>
<tbody>
{adminProjects.map(project => (
{filteredList.map(project => (
<tr key={project.id}>
<td><strong>{project.name}</strong></td>
<td>{project.description}</td>
@@ -68,8 +99,8 @@ function AdminProjectsPage({ onAdd }) {
<td style={{ width: '200px' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<button className={`text-btn ${project.status === '正常' ? 'text-btn-danger' : 'text-btn-primary'}`}>{project.status === '正常' ? '禁用' : '启用'}</button>
<button className="text-btn text-btn-primary">编辑</button>
<button className="text-btn text-btn-danger">删除</button>
<button className="text-btn text-btn-primary" onClick={() => onEdit(project)}>编辑</button>
<button className="text-btn text-btn-danger" onClick={() => setDeleteTarget(project)}>删除</button>
</div>
</td>
</tr>
@@ -86,8 +117,17 @@ function AdminProjectsPage({ onAdd }) {
</div>
</div>
</div>
<Modal
visible={!!deleteTarget}
title="确认删除"
onConfirm={confirmDelete}
onCancel={() => setDeleteTarget(null)}
confirmText="删除"
>
确定要删除项目"{deleteTarget?.name}"此操作不可撤销
</Modal>
</>
);
}
export default AdminProjectsPage;
export default AdminProjectsPage;

View File

@@ -1,18 +1,39 @@
const departments = [
{ id: 1, name: 'AI 产品部', description: '负责AI产品规划与设计', head: '张三', members: 8, status: '正常', createTime: '2025-06-01' },
{ id: 2, name: '技术研发部', description: '负责核心技术研发与实现', head: '李四', members: 15, status: '正常', createTime: '2025-06-01' },
{ id: 3, name: '数据分析部', description: '负责数据分析与挖掘', head: '王五', members: 10, status: '正常', createTime: '2025-06-01' },
{ id: 4, name: '运营部', description: '负责产品运营与推广', head: '钱七', members: 6, status: '正常', createTime: '2025-08-15' },
{ id: 5, name: '测试部', description: '负责产品质量保障', head: '周九', members: 5, status: '正常', createTime: '2025-07-01' },
{ id: 6, name: '客户服务部', description: '负责客户支持与服务', head: '吴十', members: 12, status: '禁用', createTime: '2025-09-10' }
];
import { useState } from 'react';
import { api } from '../../services/api.js';
import Modal from '../../components/common/Modal.jsx';
function StatusTag({ status }) {
const statusClass = status === '正常' ? 'status-running' : status === '禁用' ? 'status-error' : '';
return <span className={`status ${statusClass}`}>{status}</span>;
}
function DepartmentsPage({ onAdd }) {
function DepartmentsPage({ onAdd, onEdit }) {
const sourceData = api.admin.departments.list();
const [filters, setFilters] = useState({ keyword: '', status: '' });
const [deleteTarget, setDeleteTarget] = useState(null);
const handleFilterChange = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
};
const handleReset = () => {
setFilters({ keyword: '', status: '' });
};
const filteredList = sourceData.filter(dept => {
if (filters.keyword && !dept.name.includes(filters.keyword) && !dept.description.includes(filters.keyword)) {
return false;
}
if (filters.status && dept.status !== filters.status) {
return false;
}
return true;
});
const confirmDelete = () => {
setDeleteTarget(null);
};
return (
<>
<div className="card">
@@ -20,20 +41,30 @@ function DepartmentsPage({ onAdd }) {
<div className="search-bar">
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
<label>关键词</label>
<input type="text" className="form-control" placeholder="搜索部门名称、描述..." />
<input
type="text"
className="form-control"
placeholder="搜索部门名称、描述..."
value={filters.keyword}
onChange={e => handleFilterChange('keyword', e.target.value)}
/>
</div>
<div className="search-item">
<label>状态</label>
<select className="form-control">
<option>全部</option>
<select
className="form-control"
value={filters.status}
onChange={e => handleFilterChange('status', e.target.value)}
>
<option value="">全部</option>
<option>正常</option>
<option>禁用</option>
</select>
</div>
</div>
<div className="search-actions">
<button className="btn btn-primary">查询</button>
<button className="btn">重置</button>
<button className="btn btn-primary" onClick={() => {}}>查询</button>
<button className="btn" onClick={handleReset}>重置</button>
</div>
</div>
</div>
@@ -57,7 +88,7 @@ function DepartmentsPage({ onAdd }) {
</tr>
</thead>
<tbody>
{departments.map(dept => (
{filteredList.map(dept => (
<tr key={dept.id}>
<td><strong>{dept.name}</strong></td>
<td>{dept.description}</td>
@@ -68,8 +99,8 @@ function DepartmentsPage({ onAdd }) {
<td style={{ width: '200px' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<button className={`text-btn ${dept.status === '正常' ? 'text-btn-danger' : 'text-btn-primary'}`}>{dept.status === '正常' ? '禁用' : '启用'}</button>
<button className="text-btn text-btn-primary">编辑</button>
<button className="text-btn text-btn-danger">删除</button>
<button className="text-btn text-btn-primary" onClick={() => onEdit(dept)}>编辑</button>
<button className="text-btn text-btn-danger" onClick={() => setDeleteTarget(dept)}>删除</button>
</div>
</td>
</tr>
@@ -86,8 +117,17 @@ function DepartmentsPage({ onAdd }) {
</div>
</div>
</div>
<Modal
visible={!!deleteTarget}
title="确认删除"
onConfirm={confirmDelete}
onCancel={() => setDeleteTarget(null)}
confirmText="删除"
>
确定要删除部门"{deleteTarget?.name}"此操作不可撤销
</Modal>
</>
);
}
export default DepartmentsPage;
export default DepartmentsPage;

View File

@@ -1,18 +1,85 @@
import { FiUsers, FiGrid, FiFolder, FiZap, FiAlertTriangle, FiInfo } from 'react-icons/fi';
import { api } from '../../services/api.js';
function OverviewPage() {
const data = api.admin.getOverview();
return (
<div className="card">
<div className="card-header">
<div className="card-title">运营总览</div>
</div>
<div className="card-body">
<div style={{ textAlign: 'center', padding: '60px 20px', color: 'var(--color-text-3)' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}></div>
<div style={{ fontSize: '18px', fontWeight: 600, marginBottom: '8px' }}>运营总览页面</div>
<div>此处展示平台运营数据概览</div>
<>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-title">用户总数</div>
<div className="stat-value">{data.userCount}</div>
{data.userTrend && <div className="stat-trend up"> {data.userTrend} 本月新增</div>}
</div>
<div className="stat-card">
<div className="stat-title">部门数量</div>
<div className="stat-value">{data.deptCount}</div>
{data.deptTrend && <div className="stat-trend up"> {data.deptTrend}</div>}
</div>
<div className="stat-card">
<div className="stat-title">项目数量</div>
<div className="stat-value">{data.projectCount}</div>
{data.projectTrend && <div className="stat-trend up"> {data.projectTrend} 本月新增</div>}
</div>
<div className="stat-card">
<div className="stat-title">今日调用</div>
<div className="stat-value">{data.todayCalls.toLocaleString()}</div>
{data.todayTrend && <div className="stat-trend up"> {data.todayTrend} 较昨日</div>}
</div>
</div>
</div>
<div className="overview-bottom-row">
<div className="card overview-anomalies">
<div className="card-header">
<div className="card-title">异常 / 待办事项</div>
</div>
<div className="card-body">
{data.anomalies.map((item, index) => (
<div key={index} className={`anomaly-item anomaly-${item.type}`}>
<span className="anomaly-icon">
{item.type === 'warning' ? <FiAlertTriangle /> : <FiInfo />}
</span>
<span className="anomaly-text">{item.text}</span>
</div>
))}
</div>
</div>
<div className="card overview-recent-logs">
<div className="card-header">
<div className="card-title">最近操作日志</div>
</div>
<div className="card-body">
<table className="table">
<thead>
<tr>
<th>时间</th>
<th>用户</th>
<th>操作</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{data.recentLogs.map((log, index) => (
<tr key={index}>
<td>{log.time}</td>
<td>{log.user}</td>
<td>{log.action}</td>
<td>
<span className={`status ${log.status === '成功' ? 'status-running' : log.status === '失败' ? 'status-error' : 'status-warning'}`}>
{log.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</>
);
}
export default OverviewPage;
export default OverviewPage;

View File

@@ -1,13 +1,6 @@
const adminUsers = [
{ id: 1, name: '张三', department: 'AI 产品部', role: '管理员', email: 'zhangsan@example.com', phone: '138****8888', status: '正常', lastLogin: '2026-03-19 10:30' },
{ id: 2, name: '李四', department: '技术研发部', role: '开发者', email: 'lisi@example.com', phone: '139****1234', status: '正常', lastLogin: '2026-03-19 09:15' },
{ id: 3, name: '王五', department: '数据分析部', role: '成员', email: 'wangwu@example.com', phone: '136****5678', status: '禁用', lastLogin: '2026-03-15 18:20' },
{ id: 4, name: '赵六', department: 'AI 产品部', role: '管理员', email: 'zhaoliu@example.com', phone: '135****9012', status: '正常', lastLogin: '2026-03-19 11:45' },
{ id: 5, name: '钱七', department: '技术研发部', role: '开发者', email: 'qianqi@example.com', phone: '137****3456', status: '正常', lastLogin: '2026-03-18 16:30' },
{ id: 6, name: '孙八', department: '技术研发部', role: '成员', email: 'sunba@example.com', phone: '133****7890', status: '正常', lastLogin: '2026-03-19 08:00' },
{ id: 7, name: '周九', department: '测试部', role: '成员', email: 'zhoujiu@example.com', phone: '158****2345', status: '正常', lastLogin: '2026-03-17 14:20' },
{ id: 8, name: '吴十', department: '技术研发部', role: '开发者', email: 'wushi@example.com', phone: '159****6789', status: '正常', lastLogin: '2026-03-19 12:10' }
];
import { useState } from 'react';
import { api } from '../../services/api.js';
import Modal from '../../components/common/Modal.jsx';
function StatusTag({ status }) {
const statusClass = status === '正常' ? 'status-running' : status === '禁用' ? 'status-error' : '';
@@ -19,7 +12,36 @@ function RoleTag({ role }) {
return <span className={`status ${roleClass}`}>{role}</span>;
}
function UsersPage({ onAdd }) {
function UsersPage({ onAdd, onEdit }) {
const sourceData = api.admin.users.list();
const [filters, setFilters] = useState({ keyword: '', department: '', status: '' });
const [deleteTarget, setDeleteTarget] = useState(null);
const handleFilterChange = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
};
const handleReset = () => {
setFilters({ keyword: '', department: '', status: '' });
};
const filteredList = sourceData.filter(user => {
if (filters.keyword && !user.name.includes(filters.keyword) && !user.email.includes(filters.keyword)) {
return false;
}
if (filters.department && user.department !== filters.department) {
return false;
}
if (filters.status && user.status !== filters.status) {
return false;
}
return true;
});
const confirmDelete = () => {
setDeleteTarget(null);
};
return (
<>
<div className="card">
@@ -27,12 +49,22 @@ function UsersPage({ onAdd }) {
<div className="search-bar">
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
<label>关键词</label>
<input type="text" className="form-control" placeholder="搜索用户姓名、邮箱..." />
<input
type="text"
className="form-control"
placeholder="搜索用户姓名、邮箱..."
value={filters.keyword}
onChange={e => handleFilterChange('keyword', e.target.value)}
/>
</div>
<div className="search-item">
<label>部门</label>
<select className="form-control">
<option>全部部门</option>
<select
className="form-control"
value={filters.department}
onChange={e => handleFilterChange('department', e.target.value)}
>
<option value="">全部部门</option>
<option>AI 产品部</option>
<option>技术研发部</option>
<option>数据分析部</option>
@@ -42,16 +74,20 @@ function UsersPage({ onAdd }) {
</div>
<div className="search-item">
<label>状态</label>
<select className="form-control">
<option>全部</option>
<select
className="form-control"
value={filters.status}
onChange={e => handleFilterChange('status', e.target.value)}
>
<option value="">全部</option>
<option>正常</option>
<option>禁用</option>
</select>
</div>
</div>
<div className="search-actions">
<button className="btn btn-primary">查询</button>
<button className="btn">重置</button>
<button className="btn btn-primary" onClick={() => {}}>查询</button>
<button className="btn" onClick={handleReset}>重置</button>
</div>
</div>
</div>
@@ -76,7 +112,7 @@ function UsersPage({ onAdd }) {
</tr>
</thead>
<tbody>
{adminUsers.map(user => (
{filteredList.map(user => (
<tr key={user.id}>
<td><strong>{user.name}</strong></td>
<td>{user.department}</td>
@@ -88,8 +124,8 @@ function UsersPage({ onAdd }) {
<td style={{ width: '200px' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<button className={`text-btn ${user.status === '正常' ? 'text-btn-danger' : 'text-btn-primary'}`}>{user.status === '正常' ? '禁用' : '启用'}</button>
<button className="text-btn text-btn-primary">编辑</button>
<button className="text-btn text-btn-danger">删除</button>
<button className="text-btn text-btn-primary" onClick={() => onEdit(user)}>编辑</button>
<button className="text-btn text-btn-danger" onClick={() => setDeleteTarget(user)}>删除</button>
</div>
</td>
</tr>
@@ -106,8 +142,17 @@ function UsersPage({ onAdd }) {
</div>
</div>
</div>
<Modal
visible={!!deleteTarget}
title="确认删除"
onConfirm={confirmDelete}
onCancel={() => setDeleteTarget(null)}
confirmText="删除"
>
确定要删除用户"{deleteTarget?.name}"此操作不可撤销
</Modal>
</>
);
}
export default UsersPage;
export default UsersPage;

View File

@@ -10,6 +10,7 @@ import { logs } from '../data/logs.js';
import { mySkills, skillCategories, supportedModels, devDocs } from '../data/developerData.js';
import { projectMembers } from '../data/members.js';
import { scheduledTasks } from '../data/tasks.js';
import { adminDepartments, adminUsers, adminProjects, adminOverview, adminLogs } from '../data/adminData.js';
/**
* 用户相关 API
@@ -184,6 +185,48 @@ export const tasksApi = {
getById: (id) => scheduledTasks.find(task => task.id === id),
};
/**
* 管理台相关 API
*/
export const adminApi = {
/**
* 获取总览数据
* @returns {Object} 总览数据
*/
getOverview: () => adminOverview,
departments: {
list: () => adminDepartments,
getById: (id) => adminDepartments.find(d => d.id === id),
},
users: {
list: () => adminUsers,
getById: (id) => adminUsers.find(u => u.id === id),
},
projects: {
list: () => adminProjects,
getById: (id) => adminProjects.find(p => p.id === id),
},
logs: {
list: () => adminLogs,
filter: ({ keyword, user, department, type, status, startDate, endDate } = {}) => {
return adminLogs.filter(log => {
if (keyword && !log.action.includes(keyword) && !log.detail.includes(keyword)) return false;
if (user && log.user !== user) return false;
if (department && log.department !== department) return false;
if (type && log.type !== type) return false;
if (status && log.status !== status) return false;
if (startDate && log.time < startDate) return false;
if (endDate && log.time > endDate + ' 23:59:59') return false;
return true;
});
},
},
};
/**
* 统一 API 导出对象
*/
@@ -195,6 +238,7 @@ export const api = {
developer: developerApi,
members: membersApi,
tasks: tasksApi,
admin: adminApi,
};
export default api;

View File

@@ -140,3 +140,64 @@
font-weight: 600;
flex-shrink: 0;
}
/* 总览页底部两栏布局 */
.overview-bottom-row {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 20px;
margin-bottom: 20px;
}
.overview-anomalies {
margin-bottom: 0;
}
.overview-recent-logs {
margin-bottom: 0;
}
/* 异常/待办事项列表 */
.anomaly-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-radius: var(--radius-md);
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
}
.anomaly-item:last-child {
margin-bottom: 0;
}
.anomaly-warning {
background: var(--color-warning-light);
color: #92400E;
}
.anomaly-warning .anomaly-icon {
color: var(--color-warning);
}
.anomaly-info {
background: var(--color-primary-light);
color: #1E40AF;
}
.anomaly-info .anomaly-icon {
color: var(--color-primary);
}
.anomaly-icon {
display: flex;
align-items: center;
flex-shrink: 0;
font-size: 16px;
}
.anomaly-text {
flex: 1;
}