feat: 补全管理台功能 - 总览指标、搜索筛选、编辑模式、删除确认、全局日志查询
This commit is contained in:
21
README.md
21
README.md
@@ -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`:项目成员数据
|
||||
|
||||
## 构建和部署
|
||||
|
||||
27
openspec/specs/admin-data-layer/spec.md
Normal file
27
openspec/specs/admin-data-layer/spec.md
Normal 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** 数据包含不同用户、部门、类型、状态的日志记录,至少包含"成功"、"失败"、"警告"三种状态
|
||||
43
openspec/specs/admin-global-logs/spec.md
Normal file
43
openspec/specs/admin-global-logs/spec.md
Normal 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** 显示空状态组件,提示"暂无匹配日志"
|
||||
86
openspec/specs/admin-list-crud/spec.md
Normal file
86
openspec/specs/admin-list-crud/spec.md
Normal 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** 弹框关闭,列表不变
|
||||
22
openspec/specs/admin-overview/spec.md
Normal file
22
openspec/specs/admin-overview/spec.md
Normal file
@@ -0,0 +1,22 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 运营指标展示
|
||||
管理台总览页 SHALL 展示平台核心运营指标数据,以卡片形式呈现。
|
||||
|
||||
#### Scenario: 指标卡片展示
|
||||
- **WHEN** 用户打开管理台总览页
|
||||
- **THEN** 页面顶部显示4个指标卡片:用户总数、部门数量、项目数量、今日调用次数,每个卡片包含数值和趋势变化值
|
||||
|
||||
### Requirement: 异常/待办事项提醒
|
||||
管理台总览页 SHALL 展示平台异常事件和待办事项列表。
|
||||
|
||||
#### Scenario: 异常事项展示
|
||||
- **WHEN** 用户打开管理台总览页
|
||||
- **THEN** 页面左侧区域显示异常/待办事项列表,每条包含警告图标和事项描述(如定时任务执行失败、用户账号被禁用、项目处于禁用状态等)
|
||||
|
||||
### Requirement: 最近操作日志展示
|
||||
管理台总览页 SHALL 展示最近的操作日志精简列表。
|
||||
|
||||
#### Scenario: 日志列表展示
|
||||
- **WHEN** 用户打开管理台总览页
|
||||
- **THEN** 页面右侧区域显示最近5条操作日志,每条包含时间、用户、操作类型、状态标签
|
||||
@@ -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** 返回全局日志列表或筛选后的日志列表
|
||||
|
||||
@@ -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
88
src/data/adminData.js
Normal 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 }
|
||||
];
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
182
src/pages/admin/AdminLogsPage.jsx
Normal file
182
src/pages/admin/AdminLogsPage.jsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user