From 0473a68dc230c780c3adf61b7bfc17f8614254cf Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Fri, 20 Mar 2026 12:42:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E5=85=A8=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=8F=B0=E5=8A=9F=E8=83=BD=20-=20=E6=80=BB=E8=A7=88=E6=8C=87?= =?UTF-8?q?=E6=A0=87=E3=80=81=E6=90=9C=E7=B4=A2=E7=AD=9B=E9=80=89=E3=80=81?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E6=A8=A1=E5=BC=8F=E3=80=81=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E3=80=81=E5=85=A8=E5=B1=80=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 ++- openspec/specs/admin-data-layer/spec.md | 27 ++++ openspec/specs/admin-global-logs/spec.md | 43 +++++ openspec/specs/admin-list-crud/spec.md | 86 ++++++++++ openspec/specs/admin-overview/spec.md | 22 +++ openspec/specs/data-service-layer/spec.md | 46 +++++- src/constants/pages.js | 1 + src/data/adminData.js | 88 +++++++++++ src/pages/AdminPage.jsx | 67 ++++++-- src/pages/admin/AddDepartmentPage.jsx | 26 ++-- src/pages/admin/AddProjectPage.jsx | 26 ++-- src/pages/admin/AddUserPage.jsx | 35 ++--- src/pages/admin/AdminLogsPage.jsx | 182 ++++++++++++++++++++++ src/pages/admin/AdminProjectsPage.jsx | 76 ++++++--- src/pages/admin/DepartmentsPage.jsx | 76 ++++++--- src/pages/admin/OverviewPage.jsx | 89 +++++++++-- src/pages/admin/UsersPage.jsx | 89 ++++++++--- src/services/api.js | 44 ++++++ src/styles/pages/_admin.scss | 61 ++++++++ 19 files changed, 962 insertions(+), 143 deletions(-) create mode 100644 openspec/specs/admin-data-layer/spec.md create mode 100644 openspec/specs/admin-global-logs/spec.md create mode 100644 openspec/specs/admin-list-crud/spec.md create mode 100644 openspec/specs/admin-overview/spec.md create mode 100644 src/data/adminData.js create mode 100644 src/pages/admin/AdminLogsPage.jsx diff --git a/README.md b/README.md index 5bc711d..bb2aaf7 100644 --- a/README.md +++ b/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`:项目成员数据 ## 构建和部署 diff --git a/openspec/specs/admin-data-layer/spec.md b/openspec/specs/admin-data-layer/spec.md new file mode 100644 index 0000000..40b3437 --- /dev/null +++ b/openspec/specs/admin-data-layer/spec.md @@ -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** 数据包含不同用户、部门、类型、状态的日志记录,至少包含"成功"、"失败"、"警告"三种状态 diff --git a/openspec/specs/admin-global-logs/spec.md b/openspec/specs/admin-global-logs/spec.md new file mode 100644 index 0000000..0948336 --- /dev/null +++ b/openspec/specs/admin-global-logs/spec.md @@ -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** 显示空状态组件,提示"暂无匹配日志" diff --git a/openspec/specs/admin-list-crud/spec.md b/openspec/specs/admin-list-crud/spec.md new file mode 100644 index 0000000..8d260af --- /dev/null +++ b/openspec/specs/admin-list-crud/spec.md @@ -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** 弹框关闭,列表不变 diff --git a/openspec/specs/admin-overview/spec.md b/openspec/specs/admin-overview/spec.md new file mode 100644 index 0000000..e0527d2 --- /dev/null +++ b/openspec/specs/admin-overview/spec.md @@ -0,0 +1,22 @@ +## ADDED Requirements + +### Requirement: 运营指标展示 +管理台总览页 SHALL 展示平台核心运营指标数据,以卡片形式呈现。 + +#### Scenario: 指标卡片展示 +- **WHEN** 用户打开管理台总览页 +- **THEN** 页面顶部显示4个指标卡片:用户总数、部门数量、项目数量、今日调用次数,每个卡片包含数值和趋势变化值 + +### Requirement: 异常/待办事项提醒 +管理台总览页 SHALL 展示平台异常事件和待办事项列表。 + +#### Scenario: 异常事项展示 +- **WHEN** 用户打开管理台总览页 +- **THEN** 页面左侧区域显示异常/待办事项列表,每条包含警告图标和事项描述(如定时任务执行失败、用户账号被禁用、项目处于禁用状态等) + +### Requirement: 最近操作日志展示 +管理台总览页 SHALL 展示最近的操作日志精简列表。 + +#### Scenario: 日志列表展示 +- **WHEN** 用户打开管理台总览页 +- **THEN** 页面右侧区域显示最近5条操作日志,每条包含时间、用户、操作类型、状态标签 diff --git a/openspec/specs/data-service-layer/spec.md b/openspec/specs/data-service-layer/spec.md index ca6ac53..08f41f0 100644 --- a/openspec/specs/data-service-layer/spec.md +++ b/openspec/specs/data-service-layer/spec.md @@ -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** 返回全局日志列表或筛选后的日志列表 diff --git a/src/constants/pages.js b/src/constants/pages.js index 53708d9..4c130ea 100644 --- a/src/constants/pages.js +++ b/src/constants/pages.js @@ -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 }, diff --git a/src/data/adminData.js b/src/data/adminData.js new file mode 100644 index 0000000..7a0854d --- /dev/null +++ b/src/data/adminData.js @@ -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 } +]; diff --git a/src/pages/AdminPage.jsx b/src/pages/AdminPage.jsx index d98d07a..4e43637 100644 --- a/src/pages/AdminPage.jsx +++ b/src/pages/AdminPage.jsx @@ -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 ; case 'departments': - return setCurrentPage('addDepartment')} />; + return navigateTo('addDepartment')} + onEdit={(dept) => navigateTo('addDepartment', dept)} + />; case 'users': - return setCurrentPage('addUser')} />; + return navigateTo('addUser')} + onEdit={(user) => navigateTo('addUser', user)} + />; case 'projects': - return setCurrentPage('addProject')} />; + return navigateTo('addProject')} + onEdit={(project) => navigateTo('addProject', project)} + />; + case 'adminLogs': + return ; case 'addDepartment': - return setCurrentPage('departments')} />; + return navigateTo('departments')} + editData={editData} + />; case 'addUser': - return setCurrentPage('users')} />; + return navigateTo('users')} + editData={editData} + />; case 'addProject': - return setCurrentPage('projects')} />; + return navigateTo('projects')} + editData={editData} + />; default: return
Page not found
; } }; 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={} 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={} 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={} 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={} label="项目管理" active={currentPage === 'projects'} - onClick={() => setCurrentPage('projects')} + onClick={() => navigateTo('projects')} + itemClassName="admin-nav-item" + iconClassName="admin-nav-icon" + textClassName="admin-nav-text" + /> + } + 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; \ No newline at end of file +export default AdminPage; diff --git a/src/pages/admin/AddDepartmentPage.jsx b/src/pages/admin/AddDepartmentPage.jsx index 60ff687..ec82536 100644 --- a/src/pages/admin/AddDepartmentPage.jsx +++ b/src/pages/admin/AddDepartmentPage.jsx @@ -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 (
-
新增部门
+
{isEdit ? '编辑部门' : '新增部门'}
- + setName(e.target.value)} />
- +
diff --git a/src/pages/admin/AddProjectPage.jsx b/src/pages/admin/AddProjectPage.jsx index ba5b764..f314bf6 100644 --- a/src/pages/admin/AddProjectPage.jsx +++ b/src/pages/admin/AddProjectPage.jsx @@ -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 (
-
新增项目
+
{isEdit ? '编辑项目' : '新增项目'}
- + setName(e.target.value)} />
- +
diff --git a/src/pages/admin/AddUserPage.jsx b/src/pages/admin/AddUserPage.jsx index 2d4e65b..11f0109 100644 --- a/src/pages/admin/AddUserPage.jsx +++ b/src/pages/admin/AddUserPage.jsx @@ -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 (
-
新增用户
+
{isEdit ? '编辑用户' : '新增用户'}
- + setName(e.target.value)} />
@@ -48,7 +47,7 @@ function AddUserPage({ onBack }) {
- setRole(e.target.value)}> @@ -56,11 +55,11 @@ function AddUserPage({ onBack }) {
- + setEmail(e.target.value)} />
- + setPhone(e.target.value)} />
diff --git a/src/pages/admin/AdminLogsPage.jsx b/src/pages/admin/AdminLogsPage.jsx new file mode 100644 index 0000000..45fe9e4 --- /dev/null +++ b/src/pages/admin/AdminLogsPage.jsx @@ -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 ( + <> +
+
+
+
+ + handleFilterChange('keyword', e.target.value)} + /> +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + handleFilterChange('startDate', e.target.value)} + /> +
+
+ + handleFilterChange('endDate', e.target.value)} + /> +
+
+
+ + +
+
+
+
+
+
日志列表
+ +
+
+ {filteredLogs.length > 0 ? ( + <> +
+ + + + + + + + + + + + + + {filteredLogs.map((log, index) => ( + + + + + + + + + + ))} + +
时间用户部门类型操作状态详情
{log.time}{log.user}{log.department}{log.type}{log.action}{log.status}{log.detail}
+
+
+
+
1
+
2
+
3
+
+
+ + ) : ( + } + message="暂无匹配日志" + description="当前筛选条件下没有日志记录" + /> + )} +
+
+ + ); +} + +export default AdminLogsPage; diff --git a/src/pages/admin/AdminProjectsPage.jsx b/src/pages/admin/AdminProjectsPage.jsx index b6599e7..a97b876 100644 --- a/src/pages/admin/AdminProjectsPage.jsx +++ b/src/pages/admin/AdminProjectsPage.jsx @@ -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 {status}; } -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 ( <>
@@ -20,20 +41,30 @@ function AdminProjectsPage({ onAdd }) {
- + handleFilterChange('keyword', e.target.value)} + />
- handleFilterChange('status', e.target.value)} + > +
- - + +
@@ -57,7 +88,7 @@ function AdminProjectsPage({ onAdd }) { - {adminProjects.map(project => ( + {filteredList.map(project => ( {project.name} {project.description} @@ -68,8 +99,8 @@ function AdminProjectsPage({ onAdd }) {
- - + +
@@ -86,8 +117,17 @@ function AdminProjectsPage({ onAdd }) {
+ setDeleteTarget(null)} + confirmText="删除" + > + 确定要删除项目"{deleteTarget?.name}"吗?此操作不可撤销。 + ); } -export default AdminProjectsPage; \ No newline at end of file +export default AdminProjectsPage; diff --git a/src/pages/admin/DepartmentsPage.jsx b/src/pages/admin/DepartmentsPage.jsx index 454dc4f..e0bca3d 100644 --- a/src/pages/admin/DepartmentsPage.jsx +++ b/src/pages/admin/DepartmentsPage.jsx @@ -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 {status}; } -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 ( <>
@@ -20,20 +41,30 @@ function DepartmentsPage({ onAdd }) {
- + handleFilterChange('keyword', e.target.value)} + />
- handleFilterChange('status', e.target.value)} + > +
- - + +
@@ -57,7 +88,7 @@ function DepartmentsPage({ onAdd }) { - {departments.map(dept => ( + {filteredList.map(dept => ( {dept.name} {dept.description} @@ -68,8 +99,8 @@ function DepartmentsPage({ onAdd }) {
- - + +
@@ -86,8 +117,17 @@ function DepartmentsPage({ onAdd }) {
+ setDeleteTarget(null)} + confirmText="删除" + > + 确定要删除部门"{deleteTarget?.name}"吗?此操作不可撤销。 + ); } -export default DepartmentsPage; \ No newline at end of file +export default DepartmentsPage; diff --git a/src/pages/admin/OverviewPage.jsx b/src/pages/admin/OverviewPage.jsx index a7be047..06511ea 100644 --- a/src/pages/admin/OverviewPage.jsx +++ b/src/pages/admin/OverviewPage.jsx @@ -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 ( -
-
-
运营总览
-
-
-
-
-
运营总览页面
-
此处展示平台运营数据概览
+ <> +
+
+
用户总数
+
{data.userCount}
+ {data.userTrend &&
↑ {data.userTrend} 本月新增
} +
+
+
部门数量
+
{data.deptCount}
+ {data.deptTrend &&
↑ {data.deptTrend}
} +
+
+
项目数量
+
{data.projectCount}
+ {data.projectTrend &&
↑ {data.projectTrend} 本月新增
} +
+
+
今日调用
+
{data.todayCalls.toLocaleString()}
+ {data.todayTrend &&
↑ {data.todayTrend} 较昨日
}
-
+ +
+
+
+
异常 / 待办事项
+
+
+ {data.anomalies.map((item, index) => ( +
+ + {item.type === 'warning' ? : } + + {item.text} +
+ ))} +
+
+ +
+
+
最近操作日志
+
+
+ + + + + + + + + + + {data.recentLogs.map((log, index) => ( + + + + + + + ))} + +
时间用户操作状态
{log.time}{log.user}{log.action} + + {log.status} + +
+
+
+
+ ); } -export default OverviewPage; \ No newline at end of file +export default OverviewPage; diff --git a/src/pages/admin/UsersPage.jsx b/src/pages/admin/UsersPage.jsx index 0e6644c..e59e16c 100644 --- a/src/pages/admin/UsersPage.jsx +++ b/src/pages/admin/UsersPage.jsx @@ -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 {role}; } -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 ( <>
@@ -27,12 +49,22 @@ function UsersPage({ onAdd }) {
- + handleFilterChange('keyword', e.target.value)} + />
- handleFilterChange('department', e.target.value)} + > + @@ -42,16 +74,20 @@ function UsersPage({ onAdd }) {
- handleFilterChange('status', e.target.value)} + > +
- - + +
@@ -76,7 +112,7 @@ function UsersPage({ onAdd }) { - {adminUsers.map(user => ( + {filteredList.map(user => ( {user.name} {user.department} @@ -88,8 +124,8 @@ function UsersPage({ onAdd }) {
- - + +
@@ -106,8 +142,17 @@ function UsersPage({ onAdd }) {
+ setDeleteTarget(null)} + confirmText="删除" + > + 确定要删除用户"{deleteTarget?.name}"吗?此操作不可撤销。 + ); } -export default UsersPage; \ No newline at end of file +export default UsersPage; diff --git a/src/services/api.js b/src/services/api.js index 1bd44db..75fb280 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -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; diff --git a/src/styles/pages/_admin.scss b/src/styles/pages/_admin.scss index 722def5..906fde4 100644 --- a/src/styles/pages/_admin.scss +++ b/src/styles/pages/_admin.scss @@ -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; +}