From 56c08a34fffd61d4e19cfeed864a8ad53f431a5e Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Fri, 20 Mar 2026 10:19:31 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BB=A3=E7=A0=81=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E9=87=8D=E6=9E=84=20-=20=E6=8F=90=E5=8F=96=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E3=80=81=E7=BB=9F=E4=B8=80=E7=8A=B6=E6=80=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=92=8C=E6=95=B0=E6=8D=AE=E8=AE=BF=E9=97=AE=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增布局组件(SidebarBrand、SidebarUser、SidebarNavItem) - 新增通用UI组件(EmptyState、StatusBadge、TagInput、SearchBar) - 新增全局状态管理(UserContext) - 新增自定义Hooks(usePageState、useNavigation) - 新增统一数据访问层(src/services/api.js) - 新增常量配置(constants/pages.js、constants/storageKeys.js) - 样式文件模块化,拆分页面特定样式 - 更新README文档,添加组件和使用说明 - 同步OpenSpec规范到主specs目录 --- README.md | 228 +++++++++++++++++- openspec/specs/data-service-layer/spec.md | 49 ++++ openspec/specs/modular-styles/spec.md | 52 ++++ openspec/specs/reusable-components/spec.md | 41 ++++ .../specs/unified-state-management/spec.md | 52 ++++ src/App.jsx | 21 +- src/components/common/EmptyState.jsx | 39 +++ src/components/common/SearchBar.jsx | 76 ++++++ src/components/common/StatusBadge.jsx | 34 +++ src/components/common/TagInput.jsx | 52 ++++ src/components/layout/SidebarBrand.jsx | 23 ++ src/components/layout/SidebarNavItem.jsx | 34 +++ src/components/layout/SidebarUser.jsx | 34 +++ src/constants/pages.js | 52 ++++ src/constants/storageKeys.js | 24 ++ src/contexts/UserContext.jsx | 49 ++++ src/hooks/useNavigation.js | 50 ++++ src/hooks/usePageState.js | 58 +++++ src/pages/AdminPage.jsx | 117 ++++----- src/pages/ConsolePage.jsx | 112 ++++----- src/pages/DeveloperPage.jsx | 81 +++---- src/services/api.js | 200 +++++++++++++++ src/styles/global.scss | 5 +- src/styles/pages/_admin.scss | 142 +++++++++++ src/styles/pages/_console.scss | 73 ++++++ src/styles/pages/_developer.scss | 91 +++++++ src/styles/pages/_home.scss | 222 +++++++++++++++++ 27 files changed, 1812 insertions(+), 199 deletions(-) create mode 100644 openspec/specs/data-service-layer/spec.md create mode 100644 openspec/specs/modular-styles/spec.md create mode 100644 openspec/specs/reusable-components/spec.md create mode 100644 openspec/specs/unified-state-management/spec.md create mode 100644 src/components/common/EmptyState.jsx create mode 100644 src/components/common/SearchBar.jsx create mode 100644 src/components/common/StatusBadge.jsx create mode 100644 src/components/common/TagInput.jsx create mode 100644 src/components/layout/SidebarBrand.jsx create mode 100644 src/components/layout/SidebarNavItem.jsx create mode 100644 src/components/layout/SidebarUser.jsx create mode 100644 src/constants/pages.js create mode 100644 src/constants/storageKeys.js create mode 100644 src/contexts/UserContext.jsx create mode 100644 src/hooks/useNavigation.js create mode 100644 src/hooks/usePageState.js create mode 100644 src/services/api.js create mode 100644 src/styles/pages/_admin.scss create mode 100644 src/styles/pages/_console.scss create mode 100644 src/styles/pages/_developer.scss create mode 100644 src/styles/pages/_home.scss diff --git a/README.md b/README.md index 97f7181..6b7621e 100644 --- a/README.md +++ b/README.md @@ -82,11 +82,22 @@ grandclaw-archtype/ ├── src/ # 源代码 │ ├── App.jsx # 主路由配置 │ ├── main.jsx # 应用入口 -│ ├── components/ # 通用组件 +│ ├── components/ # 组件 +│ │ ├── layout/ # 布局组件(SidebarBrand、SidebarUser、SidebarNavItem) +│ │ ├── common/ # 通用UI组件(EmptyState、StatusBadge、TagInput、SearchBar) │ │ ├── Layout.jsx # 通用布局组件(侧边栏+主内容) │ │ └── ListSelector.jsx # 列表选择器组件(支持单选/多选) +│ ├── contexts/ # 全局状态管理 +│ │ └── UserContext.jsx # 用户信息上下文 │ ├── hooks/ # 自定义Hook -│ │ └── useLocalStorage.js # localStorage状态管理Hook +│ │ ├── useLocalStorage.js # localStorage状态管理Hook +│ │ ├── usePageState.js # 页面状态持久化Hook +│ │ └── useNavigation.js # 导航逻辑Hook +│ ├── constants/ # 常量定义 +│ │ ├── pages.js # 页面配置(路由、标题、图标) +│ │ └── storageKeys.js # localStorage键名常量 +│ ├── services/ # 数据访问层 +│ │ └── api.js # 统一数据访问接口 │ ├── data/ # 模拟数据 │ │ ├── conversations.js # 聊天场景数据 │ │ ├── developerData.js # 开发台数据 @@ -232,7 +243,105 @@ import { HashRouter as Router, Routes, Route } from 'react-router-dom'; ## 通用组件 -### ListSelector 列表选择器 +### 布局组件 + +#### SidebarBrand 品牌区域 +侧边栏顶部的品牌展示组件。 + +```jsx +import { SidebarBrand } from '../components/layout/SidebarBrand.jsx'; + + +``` + +#### SidebarUser 用户信息 +侧边栏底部的用户信息展示组件,使用全局用户上下文。 + +```jsx +import { SidebarUser } from '../components/layout/SidebarUser.jsx'; + + navigate('/account')} + wrapperClassName="sidebar-user" + infoClassName="sidebar-user-info" + nameClassName="sidebar-user-name" + roleClassName="sidebar-user-role" +/> +``` + +#### SidebarNavItem 导航项 +侧边栏导航菜单项组件。 + +```jsx +import { SidebarNavItem } from '../components/layout/SidebarNavItem.jsx'; + +} + label="智能助手" + active={currentPage === 'chat'} + onClick={() => setCurrentPage('chat')} + itemClassName="sidebar-nav-item" + iconClassName="sidebar-nav-icon" + textClassName="sidebar-nav-text" +/> +``` + +### 通用UI组件 + +#### EmptyState 空状态 +用于展示空列表或无数据状态的组件。 + +```jsx +import EmptyState from '../components/common/EmptyState.jsx'; + +} + title="暂无数据" + description="当前没有可显示的内容" +/> +``` + +#### StatusBadge 状态标签 +用于显示状态(成功、失败、警告等)的标签组件。 + +```jsx +import StatusBadge from '../components/common/StatusBadge.jsx'; + + + + + +``` + +#### TagInput 标签输入 +支持输入标签的输入框组件。 + +```jsx +import TagInput from '../components/common/TagInput.jsx'; + + +``` + +#### SearchBar 搜索框 +通用的搜索输入框组件。 + +```jsx +import SearchBar from '../components/common/SearchBar.jsx'; + + +``` + +### 列表组件 + +#### ListSelector 列表选择器 通用的列表选择器组件,支持单选和多选模式。 ```jsx @@ -252,11 +361,69 @@ import ListSelector from '../components/ListSelector.jsx'; ## 状态管理 -### 1. 路由状态 -- **顶级路由**:由React Router的URL哈希管理 -- **子页面状态**:使用`localStorage`持久化 +### 全局状态 -### 2. 导航状态持久化策略 +#### UserContext 用户信息上下文 +全局用户信息状态,通过 `UserProvider` 提供给整个应用。 + +```jsx +import { UserProvider, useUserContext } from '../contexts/UserContext.jsx'; + +// 在 App.jsx 中包裹 + + + + +// 在组件中使用 +function Component() { + const { user } = useUserContext(); + return
{user.name}
; +} +``` + +### 自定义 Hooks + +#### usePageState 页面状态持久化 +处理页面切换状态的 Hook,支持 localStorage 持久化和主页跳转重置。 + +```javascript +import { usePageState } from '../hooks/usePageState.js'; +import { CONSOLE_PAGES } from '../constants/pages.js'; +import { CONSOLE_KEYS } from '../constants/storageKeys.js'; + +const { currentPage, setCurrentPage } = usePageState({ + storageKey: CONSOLE_KEYS.CURRENT_PAGE, + defaultPage: 'chat', + pageTitles: CONSOLE_PAGES, +}); +``` + +#### useNavigation 导航逻辑 +封装页面导航逻辑的 Hook,支持携带额外数据。 + +```javascript +import { useNavigation } from '../hooks/useNavigation.js'; + +const { navigateToPage, extraData } = useNavigation(setCurrentPage); + +// 导航到指定页面 +navigateToPage('skills', { skillId: '1' }); + +// 获取导航传递的数据 +const skillId = extraData.skillId; +``` + +#### useLocalStorage localStorage 状态管理 +同步组件状态到 localStorage 的 Hook。 + +```javascript +import { useLocalStorage } from '../hooks/useLocalStorage.js'; + +const [value, setValue] = useLocalStorage('myKey', 'defaultValue'); +setValue('newValue'); // 自动同步到 localStorage +``` + +### 导航状态持久化策略 每个主要页面(工作台、管理台、开发台)都有独立的`localStorage`键: ```javascript @@ -272,7 +439,7 @@ localStorage.setItem('developer_currentPage', 'mySkills'); localStorage.setItem('developer_currentSkillId', '1'); ``` -### 3. 主页跳转 vs 刷新浏览器 +### 主页跳转 vs 刷新浏览器 通过`location.state.fromHome`区分两种导航来源: ```javascript @@ -339,9 +506,46 @@ $radius-md: 8px; - `role-member` - 成员(灰色) - `role-developer` - 开发者(橙色) +## 数据访问层 + +项目使用统一的数据访问接口 `src/services/api.js`,所有数据获取都通过 API 层进行,便于未来对接后端服务。 + +### API 使用示例 + +```javascript +import { api } from '../services/api.js'; + +// 获取技能列表 +const skills = api.skills.list(); + +// 获取单个技能详情 +const skill = api.skills.getById('1'); + +// 获取日志列表 +const logs = api.logs.list(); + +// 按条件筛选日志 +const filteredLogs = api.logs.filter({ user, type, status }); + +// 获取开发者技能 +const mySkills = api.developer.getMySkills(); + +// 获取成员列表 +const members = api.members.list(); +``` + +### API 模块结构 +- `api.user` - 用户信息 +- `api.skills` - 技能市场(列表、详情、文件、版本、图标) +- `api.conversations` - 聊天场景和对话历史 +- `api.logs` - 操作日志(列表、筛选) +- `api.developer` - 开发台数据(技能、分类、模型、文档) +- `api.members` - 项目成员 +- `api.tasks` - 定时任务 + ## 数据模拟 -所有数据都存储在 `src/data/` 目录下的JavaScript文件中,作为静态模拟数据。 +所有数据都存储在 `src/data/` 目录下的JavaScript文件中,作为静态模拟数据。API 服务层统一从这些文件读取数据。 ### 数据文件说明 - `conversations.js`:聊天场景和对话历史 @@ -434,6 +638,12 @@ export default defineConfig({ ## 更新日志 ### 2026-03-20 +- 代码架构重构:提取布局组件(SidebarBrand、SidebarUser、SidebarNavItem) +- 代码架构重构:创建通用UI组件(EmptyState、StatusBadge、TagInput、SearchBar) +- 代码架构重构:新增全局状态管理(UserContext) +- 代码架构重构:新增自定义Hooks(usePageState、useNavigation、useLocalStorage) +- 代码架构重构:新增统一数据访问层(src/services/api.js) +- 代码架构重构:新增常量配置(constants/pages.js、constants/storageKeys.js) - 文档同步:更新开发台子页面列表,补充 NewVersionPage - 文档同步:补充开发台功能描述(上传新版本) diff --git a/openspec/specs/data-service-layer/spec.md b/openspec/specs/data-service-layer/spec.md new file mode 100644 index 0000000..ca6ac53 --- /dev/null +++ b/openspec/specs/data-service-layer/spec.md @@ -0,0 +1,49 @@ +# Data Service Layer Specification + +## Purpose + +提供统一的数据访问接口层,将数据访问逻辑与静态数据文件分离,便于后续对接真实 API。 + +## Requirements + +### Requirement: 统一数据访问接口 +系统 SHALL 提供 api 服务对象,包含按功能模块划分的数据访问方法,作为所有数据访问的统一入口。 + +#### Scenario: api.user 模块提供用户信息访问 +- **WHEN** 调用 api.user.getInfo() +- **THEN** 系统返回用户信息对象(包含 name、avatar、role 等字段) + +#### Scenario: api.skills 模块提供技能数据访问 +- **WHEN** 调用 api.skills.list() +- **THEN** 系统返回所有技能列表数组 + +#### Scenario: api.skills 支持按 ID 查询单个技能 +- **WHEN** 调用 api.skills.getById(skillId) +- **THEN** 系统返回对应 ID 的技能对象,若不存在则返回 undefined + +#### Scenario: api.conversations 模块提供会话数据访问 +- **WHEN** 调用 api.conversations.list() +- **THEN** 系统返回所有会话列表数组 + +#### Scenario: api.conversations 支持按场景获取聊天内容 +- **WHEN** 调用 api.conversations.getScene(sceneName) +- **THEN** 系统返回对应场景的 HTML 内容字符串 + +#### Scenario: api.logs 模块支持筛选查询 +- **WHEN** 调用 api.logs.list(filters) +- **THEN** 系统根据 filters 参数(用户、类型、状态)筛选并返回日志列表 + +### Requirement: 数据层与静态文件分离 +系统 SHALL 将数据访问逻辑与静态数据文件分离,便于后续对接真实 API。 + +#### Scenario: API 层内部调用静态数据文件 +- **WHEN** 调用 api 模块的任何方法 +- **THEN** 系统从对应的 data/*.js 文件读取并返回数据 + +#### Scenario: API 层支持数据转换 +- **WHEN** 静态数据结构与页面需求不一致 +- **THEN** 系统在 API 层进行数据转换,返回页面所需的格式 + +#### Scenario: API 层提供一致的返回格式 +- **WHEN** 调用 API 层方法 +- **THEN** 系统返回符合约定格式的数据(如对象、数组),无论底层存储格式如何 diff --git a/openspec/specs/modular-styles/spec.md b/openspec/specs/modular-styles/spec.md new file mode 100644 index 0000000..0648972 --- /dev/null +++ b/openspec/specs/modular-styles/spec.md @@ -0,0 +1,52 @@ +# Modular Styles Specification + +## Purpose + +提供模块化的样式文件组织结构,将页面特定样式从全局样式文件中拆分到独立的页面样式文件中,提高代码的可维护性。 + +## Requirements + +### Requirement: 样式文件按页面拆分 +系统 SHALL 将页面特定样式从 global.scss 中拆分到独立的页面样式文件中,按功能组织。 + +#### Scenario: 工作台样式独立文件 +- **WHEN** 系统包含 _console.scss 文件 +- **THEN** 该文件包含工作台相关的所有样式(聊天、技能市场、日志查询、定时任务、项目管理等) + +#### Scenario: 管理台样式独立文件 +- **WHEN** 系统包含 _admin.scss 文件 +- **THEN** 该文件包含管理台相关的所有样式(总览、部门管理、用户管理、项目管理等) + +#### Scenario: 开发台样式独立文件 +- **WHEN** 系统包含 _developer.scss 文件 +- **THEN** 该文件包含开发台相关的所有样式(我的技能、技能编辑、开发文档等) + +#### Scenario: 首页样式独立文件 +- **WHEN** 系统包含 _home.scss 文件 +- **THEN** 该文件包含首页相关的所有样式 + +### Requirement: global.scss 作为样式主入口 +系统 SHALL 保持 global.scss 作为样式主入口文件,导入所有样式模块。 + +#### Scenario: global.scss 导入设计系统模块 +- **WHEN** global.scss 文件被加载 +- **THEN** 系统按顺序导入 _variables.scss、_mixins.scss、_base.scss、_components.scss、_layout.scss + +#### Scenario: global.scss 导入页面样式模块 +- **WHEN** global.scss 文件被加载 +- **THEN** 系统导入 pages/_console.scss、pages/_admin.scss、pages/_developer.scss、pages/_home.scss + +#### Scenario: 通用样式保留在主文件中 +- **WHEN** 样式属于通用组件(按钮、表单、表格、状态标签等) +- **THEN** 该样式保留在 _components.scss 或 _layout.scss 中,不移动到页面样式文件 + +### Requirement: 页面样式文件组织结构 +系统 SHALL 在 src/styles/pages/ 目录下按页面组织样式文件,每个文件包含对应页面的所有特定样式。 + +#### Scenario: 页面样式文件命名规范 +- **WHEN** 创建页面样式文件 +- **THEN** 文件名使用 _.scss 格式(如 _console.scss、_admin.scss) + +#### Scenario: 页面样式文件内容结构 +- **WHEN** 查看页面样式文件 +- **THEN** 该文件包含页面特定的布局、组件、状态等样式,使用清晰的注释分节 diff --git a/openspec/specs/reusable-components/spec.md b/openspec/specs/reusable-components/spec.md new file mode 100644 index 0000000..1bece92 --- /dev/null +++ b/openspec/specs/reusable-components/spec.md @@ -0,0 +1,41 @@ +# Reusable Components Specification + +## Purpose + +提供可复用的布局组件和通用 UI 组件库,用于在不同页面中保持一致的视觉呈现和交互体验。 + +## Requirements + +### Requirement: 布局组件复用 +系统 SHALL 提供可复用的布局组件,包括品牌区域、用户信息区域、导航项组件,用于在不同页面中保持一致的视觉呈现。 + +#### Scenario: SidebarBrand 组件渲染 +- **WHEN** 页面使用 SidebarBrand 组件并传入 subtitle 属性 +- **THEN** 系统显示 GrandClaw 品牌 logo 和对应的副标题文本 + +#### Scenario: SidebarUser 组件显示用户信息 +- **WHEN** 页面使用 SidebarUser 组件 +- **THEN** 系统从 UserContext 获取用户信息并显示用户头像、姓名和角色 + +#### Scenario: SidebarNavItem 组件支持状态切换 +- **WHEN** 页面使用 SidebarNavItem 组件并传入 active 状态 +- **THEN** 系统根据 active 状态应用相应的激活样式 + +### Requirement: 通用 UI 组件库 +系统 SHALL 提供通用 UI 组件库,包括空状态组件、状态标签组件、标签输入组件、搜索栏组件,支持在多个页面中复用。 + +#### Scenario: EmptyState 组件显示空状态 +- **WHEN** 页面使用 EmptyState 组件并传入 icon、message、description 属性 +- **THEN** 系统显示居中的空状态提示,包含图标、标题和描述文本 + +#### Scenario: StatusBadge 组件显示不同状态 +- **WHEN** 页面使用 StatusBadge 组件并传入 status 属性(如 running、stopped、warning、error) +- **THEN** 系统根据 status 值应用对应的颜色样式和图标 + +#### Scenario: TagInput 组件支持标签增删 +- **WHEN** 用户在 TagInput 组件中输入文本并按回车 +- **THEN** 系统将输入内容添加为标签,并显示删除按钮 + +#### Scenario: SearchBar 组件提供筛选功能 +- **WHEN** 页面使用 SearchBar 组件并传入搜索条件配置 +- **THEN** 系统渲染对应的筛选输入框,并在用户输入时触发 onChange 回调 diff --git a/openspec/specs/unified-state-management/spec.md b/openspec/specs/unified-state-management/spec.md new file mode 100644 index 0000000..27911e7 --- /dev/null +++ b/openspec/specs/unified-state-management/spec.md @@ -0,0 +1,52 @@ +# Unified State Management Specification + +## Purpose + +提供统一的状态管理方案,包括全局用户信息上下文、页面状态持久化、导航逻辑管理,确保应用状态的一致性和可维护性。 + +## Requirements + +### Requirement: 用户信息全局上下文 +系统 SHALL 使用 React Context API 提供全局用户信息上下文,确保用户数据在整个应用中保持一致。 + +#### Scenario: UserContext 提供用户信息 +- **WHEN** 组件使用 useUserContext Hook +- **THEN** 系统返回包含用户 name、avatar、role 的用户信息对象 + +#### Scenario: 用户信息在多个页面同步显示 +- **WHEN** 用户信息在 UserContext 中更新 +- **THEN** 所有使用 UserContext 的组件自动更新显示 + +#### Scenario: UserContext 提供默认值 +- **WHEN** 应用启动且没有提供用户信息 +- **THEN** 系统使用默认用户信息(name: '张三', avatar: '张', role: 'AI 产品部') + +### Requirement: 页面状态持久化 Hook +系统 SHALL 提供 usePageState Hook,封装页面状态持久化逻辑,自动处理 localStorage 同步和主页跳转重置。 + +#### Scenario: usePageState 初始化从 localStorage 恢复状态 +- **WHEN** 页面使用 usePageState Hook 并传入 storageKey 和 defaultPage +- **THEN** 系统从 localStorage 读取之前保存的页面状态,若无则使用 defaultPage + +#### Scenario: usePageState 自动同步状态到 localStorage +- **WHEN** 调用 usePageState 返回的 setCurrentPage 函数 +- **THEN** 系统更新状态并自动保存到 localStorage + +#### Scenario: usePageState 处理主页跳转重置 +- **WHEN** 从主页跳转到页面(location.state.fromHome 为 true) +- **THEN** 系统重置页面状态为默认值,并清除路由 state + +#### Scenario: usePageState 提供 getPageTitle 函数 +- **WHEN** 调用 usePageState 返回的 getPageTitle 函数 +- **THEN** 系统根据当前页面 ID 从配置中查找并返回对应的页面标题 + +### Requirement: 导航逻辑 Hook +系统 SHALL 提供 useNavigation Hook,统一处理页面导航和路由状态管理。 + +#### Scenario: useNavigation 提供页面切换函数 +- **WHEN** 调用 useNavigation 返回的 navigateToPage 函数并传入目标页面 ID +- **THEN** 系统更新当前页面状态并执行相应导航逻辑 + +#### Scenario: useNavigation 处理带数据的页面切换 +- **WHEN** 调用 navigateToPage 并传入目标页面 ID 和附加数据(如 skillId) +- **THEN** 系统更新页面状态和附加数据状态 diff --git a/src/App.jsx b/src/App.jsx index f54f706..5784847 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,5 @@ import { HashRouter as Router, Routes, Route } from 'react-router-dom'; +import { UserProvider } from './contexts/UserContext.jsx'; import HomePage from './pages/HomePage.jsx'; import LoginPage from './pages/LoginPage.jsx'; import ConsolePage from './pages/ConsolePage.jsx'; @@ -7,15 +8,17 @@ import DeveloperPage from './pages/DeveloperPage.jsx'; function App() { return ( - - - } /> - } /> - } /> - } /> - } /> - - + + + + } /> + } /> + } /> + } /> + } /> + + + ); } diff --git a/src/components/common/EmptyState.jsx b/src/components/common/EmptyState.jsx new file mode 100644 index 0000000..eb6c7a6 --- /dev/null +++ b/src/components/common/EmptyState.jsx @@ -0,0 +1,39 @@ +/** + * EmptyState - 空状态组件 + * 用于显示空列表、无数据提示等场景 + * + * @param {Object} props - 组件属性 + * @param {React.ReactNode} props.icon - 图标(emoji 或图标组件) + * @param {string} props.message - 主标题文本 + * @param {string} [props.description] - 描述文本 + * @param {string} [props.actionText] - 操作按钮文本 + * @param {Function} [props.onAction] - 操作按钮点击回调 + */ +function EmptyState({ icon, message, description, actionText, onAction }) { + return ( +
+
{icon}
+
+
+ {message} +
+ {description && ( +
+ {description} +
+ )} + {actionText && onAction && ( + + )} +
+
+ ); +} + +export default EmptyState; diff --git a/src/components/common/SearchBar.jsx b/src/components/common/SearchBar.jsx new file mode 100644 index 0000000..8a2ae05 --- /dev/null +++ b/src/components/common/SearchBar.jsx @@ -0,0 +1,76 @@ +import { useState } from 'react'; +import { FiSearch, FiFilter } from 'react-icons/fi'; + +/** + * SearchBar - 搜索栏组件 + * 提供搜索和筛选功能 + * + * @param {Object} props - 组件属性 + * @param {Array} props.filters - 筛选条件配置 + * @param {Function} props.onSearch - 搜索回调 + * @param {Function} props.onFilterChange - 筛选变化回调 + * @param {boolean} [props.showFilter] - 是否显示筛选按钮 + */ +function SearchBar({ filters = [], onSearch, onFilterChange, showFilter = false }) { + const [filterValues, setFilterValues] = useState({}); + + const handleFilterChange = (key, value) => { + const newValues = { ...filterValues, [key]: value }; + setFilterValues(newValues); + if (onFilterChange) { + onFilterChange(newValues); + } + }; + + const handleSearch = (e) => { + if (onSearch) { + onSearch(e.target.value); + } + }; + + return ( +
+ {filters.map(filter => ( +
+ + {filter.type === 'select' ? ( + + ) : ( + handleFilterChange(filter.key, e.target.value)} + /> + )} +
+ ))} +
+ +
+ {showFilter && ( + + )} +
+ ); +} + +export default SearchBar; diff --git a/src/components/common/StatusBadge.jsx b/src/components/common/StatusBadge.jsx new file mode 100644 index 0000000..2856a99 --- /dev/null +++ b/src/components/common/StatusBadge.jsx @@ -0,0 +1,34 @@ +/** + * StatusBadge - 状态标签组件 + * 显示不同状态的标签(运行中、停止、警告、错误等) + * + * @param {Object} props - 组件属性 + * @param {string} props.status - 状态类型(running, stopped, starting, warning, error) + * @param {string} [props.text] - 自定义文本(可选,默认使用状态对应的文本) + * @param {boolean} [props.small] - 是否使用小尺寸 + */ +function StatusBadge({ status, text, small = false }) { + const statusConfig = { + running: { className: 'status-running', defaultText: '运行中' }, + stopped: { className: 'status-stopped', defaultText: '已停止' }, + starting: { className: 'status-starting', defaultText: '启动中' }, + warning: { className: 'status-warning', defaultText: '警告' }, + error: { className: 'status-error', defaultText: '失败' }, + // 附加状态 + success: { className: 'status-running', defaultText: '成功' }, + }; + + const config = statusConfig[status] || statusConfig.stopped; + const displayText = text || config.defaultText; + + return ( + + {displayText} + + ); +} + +export default StatusBadge; diff --git a/src/components/common/TagInput.jsx b/src/components/common/TagInput.jsx new file mode 100644 index 0000000..61c6d2f --- /dev/null +++ b/src/components/common/TagInput.jsx @@ -0,0 +1,52 @@ +/** + * TagInput - 标签输入组件 + * 支持添加和删除标签 + * + * @param {Object} props - 组件属性 + * @param {Array} props.tags - 当前标签列表 + * @param {Function} props.onChange - 标签变化回调 + * @param {string} [props.placeholder] - 输入框占位符 + */ +function TagInput({ tags = [], onChange, placeholder = '输入标签后按回车添加' }) { + const [inputValue, setInputValue] = useState(''); + + const handleKeyDown = (e) => { + if (e.key === 'Enter' && inputValue.trim()) { + e.preventDefault(); + if (!tags.includes(inputValue.trim())) { + onChange([...tags, inputValue.trim()]); + } + setInputValue(''); + } + }; + + const handleRemove = (tagToRemove) => { + onChange(tags.filter(tag => tag !== tagToRemove)); + }; + + return ( +
+ {tags.map(tag => ( + + {tag} + handleRemove(tag)} + > + × + + + ))} + setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={placeholder} + /> +
+ ); +} + +export default TagInput; diff --git a/src/components/layout/SidebarBrand.jsx b/src/components/layout/SidebarBrand.jsx new file mode 100644 index 0000000..7afba79 --- /dev/null +++ b/src/components/layout/SidebarBrand.jsx @@ -0,0 +1,23 @@ +/** + * SidebarBrand - 侧边栏品牌区域组件 + * 统一显示 GrandClaw 品牌标识和副标题 + * + * @param {Object} props - 组件属性 + * @param {string} [props.subtitle] - 副标题文本(如"企业级AI平台"、"运营管理台"、"技能开发台") + */ +function SidebarBrand({ subtitle = '企业级AI平台' }) { + return ( +
+
+ + +
+
+
GrandClaw
+
{subtitle}
+
+
+ ); +} + +export default SidebarBrand; diff --git a/src/components/layout/SidebarNavItem.jsx b/src/components/layout/SidebarNavItem.jsx new file mode 100644 index 0000000..e2149a7 --- /dev/null +++ b/src/components/layout/SidebarNavItem.jsx @@ -0,0 +1,34 @@ +/** + * SidebarNavItem - 侧边栏导航项组件 + * 统一的导航项样式和交互 + * + * @param {Object} props - 组件属性 + * @param {React.ReactNode} props.icon - 图标组件 + * @param {string} props.label - 导航项文本 + * @param {boolean} props.active - 是否激活状态 + * @param {Function} props.onClick - 点击回调函数 + * @param {string} [props.itemClassName] - 导航项类名(如"chat-nav-item"、"admin-nav-item") + * @param {string} [props.iconClassName] - 图标容器类名(如"chat-nav-icon"、"admin-nav-icon") + * @param {string} [props.textClassName] - 文本容器类名(如"chat-nav-text"、"admin-nav-text") + */ +function SidebarNavItem({ + icon, + label, + active = false, + onClick, + itemClassName = 'chat-nav-item', + iconClassName = 'chat-nav-icon', + textClassName = 'chat-nav-text', +}) { + return ( +
+ {icon} + {label} +
+ ); +} + +export default SidebarNavItem; diff --git a/src/components/layout/SidebarUser.jsx b/src/components/layout/SidebarUser.jsx new file mode 100644 index 0000000..0b4a95a --- /dev/null +++ b/src/components/layout/SidebarUser.jsx @@ -0,0 +1,34 @@ +import { useUserContext } from '../../contexts/UserContext.jsx'; + +/** + * SidebarUser - 侧边栏用户信息组件 + * 从 UserContext 获取用户信息并显示 + * + * @param {Object} props - 组件属性 + * @param {Function} [props.onClick] - 点击回调函数 + * @param {string} [props.wrapperClassName] - 包装器类名(如"chat-sidebar-user"、"admin-sidebar-user") + * @param {string} [props.infoClassName] - 信息容器类名(如"chat-sidebar-user-info"、"admin-sidebar-user-info") + * @param {string} [props.nameClassName] - 姓名容器类名(如"chat-sidebar-user-name"、"admin-sidebar-user-name") + * @param {string} [props.roleClassName] - 角色容器类名(如"chat-sidebar-user-role"、"admin-sidebar-user-role") + */ +function SidebarUser({ + onClick, + wrapperClassName = 'chat-sidebar-user', + infoClassName = 'chat-sidebar-user-info', + nameClassName = 'chat-sidebar-user-name', + roleClassName = 'chat-sidebar-user-role', +}) { + const { user } = useUserContext(); + + return ( +
+
{user.avatar}
+
+
{user.name}
+
{user.role}
+
+
+ ); +} + +export default SidebarUser; diff --git a/src/constants/pages.js b/src/constants/pages.js new file mode 100644 index 0000000..53708d9 --- /dev/null +++ b/src/constants/pages.js @@ -0,0 +1,52 @@ +// 页面配置常量 + +/** + * 工作台页面配置 + */ +export const CONSOLE_PAGES = { + chat: { title: '智能助手', icon: 'FiMessageSquare' }, + skills: { title: '技能市场', icon: 'FaPuzzlePiece' }, + skillDetail: { title: '技能详情', icon: null }, + logs: { title: '日志查询', icon: 'FiList' }, + scheduledTasks: { title: '定时任务', icon: 'FiClock' }, + taskDetail: { title: '任务详情', icon: null }, + account: { title: '账号管理', icon: 'FiUser' }, + projects: { title: '项目管理', icon: 'FiUsers' }, + memberConfig: { title: '成员配置', icon: null }, + addMember: { title: '增加成员', icon: null }, +}; + +/** + * 管理台页面配置 + */ +export const ADMIN_PAGES = { + overview: { title: '总览', icon: 'FiHome' }, + departments: { title: '部门管理', icon: 'FiBarChart2' }, + users: { title: '用户管理', icon: 'FiUsers' }, + projects: { title: '项目管理', icon: 'FiList' }, + addDepartment: { title: '新增部门', icon: null }, + addUser: { title: '新增用户', icon: null }, + addProject: { title: '新增项目', icon: null }, +}; + +/** + * 开发台页面配置 + */ +export const DEVELOPER_PAGES = { + mySkills: { title: '我的技能', icon: 'FaPuzzlePiece' }, + uploadSkill: { title: '创建技能', icon: 'FiPlus' }, + newVersion: { title: '上传新版本', icon: null }, + devDocs: { title: '开发文档', icon: 'FiTerminal' }, + devAccount: { title: '开发者设置', icon: 'FiSettings' }, + skillEditor: { title: '技能详情', icon: null }, +}; + +/** + * 获取页面标题 + * @param {string} pageId - 页面ID + * @param {Object} pagesConfig - 页面配置对象 + * @returns {string} 页面标题 + */ +export function getPageTitle(pageId, pagesConfig) { + return pagesConfig[pageId]?.title || ''; +} diff --git a/src/constants/storageKeys.js b/src/constants/storageKeys.js new file mode 100644 index 0000000..c73bd76 --- /dev/null +++ b/src/constants/storageKeys.js @@ -0,0 +1,24 @@ +// localStorage 键名常量 + +/** + * 工作台相关键名 + */ +export const CONSOLE_KEYS = { + CURRENT_PAGE: 'console_currentPage', + CURRENT_SCENE: 'console_currentScene', +}; + +/** + * 管理台相关键名 + */ +export const ADMIN_KEYS = { + CURRENT_PAGE: 'admin_currentPage', +}; + +/** + * 开发台相关键名 + */ +export const DEVELOPER_KEYS = { + CURRENT_PAGE: 'developer_currentPage', + CURRENT_SKILL_ID: 'developer_currentSkillId', +}; diff --git a/src/contexts/UserContext.jsx b/src/contexts/UserContext.jsx new file mode 100644 index 0000000..ea160c9 --- /dev/null +++ b/src/contexts/UserContext.jsx @@ -0,0 +1,49 @@ +import { createContext, useContext } from 'react'; + +/** + * 用户上下文 + * 提供全局用户信息管理 + */ + +// 默认用户信息 +const DEFAULT_USER = { + name: '张三', + avatar: '张', + role: 'AI 产品部', +}; + +// 创建用户上下文 +const UserContext = createContext({ + user: DEFAULT_USER, +}); + +/** + * 用户信息提供者组件 + * @param {Object} props - 组件属性 + * @param {Object} props.user - 用户信息对象 + * @param {React.ReactNode} props.children - 子组件 + */ +export function UserProvider({ user = DEFAULT_USER, children }) { + const value = { + user, + }; + + return {children}; +} + +/** + * 使用用户上下文的 Hook + * @returns {Object} 用户上下文值,包含 user 对象 + */ +export function useUserContext() { + const context = useContext(UserContext); + + if (!context) { + console.warn('useUserContext must be used within a UserProvider'); + return { user: DEFAULT_USER }; + } + + return context; +} + +export default UserContext; diff --git a/src/hooks/useNavigation.js b/src/hooks/useNavigation.js new file mode 100644 index 0000000..aa1da25 --- /dev/null +++ b/src/hooks/useNavigation.js @@ -0,0 +1,50 @@ +import { useState, useCallback } from 'react'; + +/** + * 导航逻辑 Hook + * 统一处理页面导航和附加数据状态管理 + * + * @param {Function} setPageCallback - 设置当前页面的回调函数 + * @returns {Object} 导航操作函数 + */ +function useNavigation(setPageCallback) { + const [extraData, setExtraData] = useState({}); + + /** + * 导航到指定页面 + * @param {string} pageId - 目标页面 ID + * @param {Object} data - 附加数据(如 skillId、taskId 等) + */ + const navigateToPage = useCallback((pageId, data = {}) => { + setPageCallback(pageId); + + // 如果有附加数据,更新 extraData + if (Object.keys(data).length > 0) { + setExtraData(data); + } + }, [setPageCallback]); + + /** + * 设置附加数据 + * @param {Object} data - 附加数据对象 + */ + const setExtraDataValue = useCallback((data) => { + setExtraData(prev => ({ ...prev, ...data })); + }, []); + + /** + * 清除附加数据 + */ + const clearExtraData = useCallback(() => { + setExtraData({}); + }, []); + + return { + extraData, + navigateToPage, + setExtraData: setExtraDataValue, + clearExtraData, + }; +} + +export default useNavigation; diff --git a/src/hooks/usePageState.js b/src/hooks/usePageState.js new file mode 100644 index 0000000..8294bc5 --- /dev/null +++ b/src/hooks/usePageState.js @@ -0,0 +1,58 @@ +import { useState, useEffect } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +/** + * 页面状态持久化 Hook + * 封装页面状态管理、localStorage 同步和主页跳转重置逻辑 + * + * @param {Object} options - 配置选项 + * @param {string} options.storageKey - localStorage 存储键名 + * @param {string} options.defaultPage - 默认页面 ID + * @param {Object} options.pageTitles - 页面标题映射对象 + * @param {Function} options.getPageTitle - 自定义获取页面标题函数(可选) + * @returns {Object} 状态和操作函数 + */ +function usePageState({ + storageKey, + defaultPage, + pageTitles, + getPageTitle: customGetPageTitle, +}) { + const location = useLocation(); + const navigate = useNavigate(); + + // 从 localStorage 恢复或使用默认值 + const [currentPage, setCurrentPage] = useState(() => { + const saved = localStorage.getItem(`${storageKey}_currentPage`); + return saved || defaultPage; + }); + + // 处理主页跳转重置 + useEffect(() => { + if (location.state?.fromHome) { + setCurrentPage(defaultPage); + navigate('.', { replace: true, state: {} }); + } + }, [location.state, navigate, defaultPage]); + + // 同步到 localStorage + useEffect(() => { + localStorage.setItem(`${storageKey}_currentPage`, currentPage); + }, [storageKey, currentPage]); + + // 获取页面标题 + const getPageTitle = (pageId = currentPage) => { + if (customGetPageTitle) { + return customGetPageTitle(pageId, currentPage); + } + return pageTitles[pageId] || ''; + }; + + return { + currentPage, + setCurrentPage, + getPageTitle, + }; +} + +export default usePageState; diff --git a/src/pages/AdminPage.jsx b/src/pages/AdminPage.jsx index 40c049c..d98d07a 100644 --- a/src/pages/AdminPage.jsx +++ b/src/pages/AdminPage.jsx @@ -2,6 +2,12 @@ import { useState, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { FiHome, FiBarChart2, FiUsers, FiList } from 'react-icons/fi'; import Layout from '../components/Layout.jsx'; +import SidebarBrand from '../components/layout/SidebarBrand.jsx'; +import SidebarUser from '../components/layout/SidebarUser.jsx'; +import SidebarNavItem from '../components/layout/SidebarNavItem.jsx'; +import usePageState from '../hooks/usePageState.js'; +import { ADMIN_PAGES } from '../constants/pages.js'; +import { ADMIN_KEYS } from '../constants/storageKeys.js'; import OverviewPage from './admin/OverviewPage.jsx'; import DepartmentsPage from './admin/DepartmentsPage.jsx'; import UsersPage from './admin/UsersPage.jsx'; @@ -13,21 +19,14 @@ import AddProjectPage from './admin/AddProjectPage.jsx'; function AdminPage() { const location = useLocation(); const navigate = useNavigate(); - const [currentPage, setCurrentPage] = useState(() => { - return localStorage.getItem('admin_currentPage') || 'overview'; + + // 使用 usePageState 管理页面状态 + const { currentPage, setCurrentPage } = usePageState({ + storageKey: ADMIN_KEYS.CURRENT_PAGE, + defaultPage: 'overview', + pageTitles: ADMIN_PAGES, }); - useEffect(() => { - if (location.state?.fromHome) { - setCurrentPage('overview'); - navigate('.', { replace: true, state: {} }); - } - }, [location.state, navigate, setCurrentPage]); - - useEffect(() => { - localStorage.setItem('admin_currentPage', currentPage); - }, [currentPage]); - const renderPage = () => { switch (currentPage) { case 'overview': @@ -50,69 +49,59 @@ function AdminPage() { }; const getPageTitle = () => { - const titles = { - overview: '总览', - departments: '部门管理', - users: '用户管理', - projects: '项目管理', - addDepartment: '新增部门', - addUser: '新增用户', - addProject: '新增项目' - }; - return titles[currentPage] || ''; + return ADMIN_PAGES[currentPage]?.title || ''; }; const sidebar = ( <>
-
-
- - -
-
-
GrandClaw
-
运营管理台
-
-
+
-
-
-
-
张三
-
系统管理员
-
-
+ {}} + wrapperClassName="admin-sidebar-user" + infoClassName="admin-sidebar-user-info" + nameClassName="admin-sidebar-user-name" + roleClassName="admin-sidebar-user-role" + /> ); diff --git a/src/pages/ConsolePage.jsx b/src/pages/ConsolePage.jsx index 8a6f79b..22e48aa 100644 --- a/src/pages/ConsolePage.jsx +++ b/src/pages/ConsolePage.jsx @@ -3,8 +3,13 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { FiPlus, FiClock, FiList, FiUsers } from 'react-icons/fi'; import { FaPuzzlePiece } from 'react-icons/fa'; import Layout from '../components/Layout.jsx'; -import { conversations, getChatScenes } from '../data/conversations.js'; -import { skills } from '../data/skills.js'; +import SidebarBrand from '../components/layout/SidebarBrand.jsx'; +import SidebarUser from '../components/layout/SidebarUser.jsx'; +import SidebarNavItem from '../components/layout/SidebarNavItem.jsx'; +import usePageState from '../hooks/usePageState.js'; +import { CONSOLE_PAGES } from '../constants/pages.js'; +import { CONSOLE_KEYS } from '../constants/storageKeys.js'; +import api from '../services/api.js'; import ChatPage from './console/ChatPage.jsx'; import SkillsPage from './console/SkillsPage.jsx'; import SkillDetailPage from './console/SkillDetailPage.jsx'; @@ -19,15 +24,22 @@ import AddMemberPage from './console/AddMemberPage.jsx'; function ConsolePage() { const location = useLocation(); const navigate = useNavigate(); - const [currentPage, setCurrentPage] = useState(() => { - return localStorage.getItem('console_currentPage') || 'chat'; + + // 使用 usePageState 管理 currentPage(不使用其返回的 getPageTitle,因为需要访问组件局部变量) + const { currentPage, setCurrentPage } = usePageState({ + storageKey: CONSOLE_KEYS.CURRENT_PAGE, + defaultPage: 'chat', + pageTitles: CONSOLE_PAGES, }); + + // 保留额外的状态(scene 和 skillId 等需要特殊处理) const [currentScene, setCurrentScene] = useState(() => { - return localStorage.getItem('console_currentScene') || 'welcome'; + return localStorage.getItem(CONSOLE_KEYS.CURRENT_SCENE) || 'welcome'; }); const [currentSkillId, setCurrentSkillId] = useState(null); const [currentTaskId, setCurrentTaskId] = useState(null); + // 处理主页跳转重置 useEffect(() => { if (location.state?.fromHome) { setCurrentPage('chat'); @@ -36,12 +48,9 @@ function ConsolePage() { } }, [location.state, navigate, setCurrentPage, setCurrentScene]); + // 同步 currentScene 到 localStorage useEffect(() => { - localStorage.setItem('console_currentPage', currentPage); - }, [currentPage]); - - useEffect(() => { - localStorage.setItem('console_currentScene', currentScene); + localStorage.setItem(CONSOLE_KEYS.CURRENT_SCENE, currentScene); }, [currentScene]); const switchPage = (pageId, data = {}) => { @@ -109,25 +118,13 @@ function ConsolePage() { }; const getPageTitle = () => { - const pageTitles = { - chat: '智能助手', - skills: '技能市场', - skillDetail: '技能详情', - logs: '日志查询', - scheduledTasks: '定时任务', - taskDetail: '任务详情', - account: '账号管理', - projects: '项目管理', - memberConfig: '成员配置', - addMember: '增加成员' - }; - let title = pageTitles[currentPage] || ''; + let title = CONSOLE_PAGES[currentPage]?.title || ''; if (currentPage === 'chat') { - const conv = conversations.find(c => c.scene === currentScene); + const conv = api.conversations.list().find(c => c.scene === currentScene); title = conv?.title || '智能助手'; } if (currentPage === 'skillDetail' && currentSkillId) { - const skill = skills.find(s => s.id === currentSkillId); + const skill = api.skills.getById(currentSkillId); title = skill?.name || '技能详情'; } return title; @@ -136,16 +133,7 @@ function ConsolePage() { const sidebar = ( <>
-
-
- - -
-
-
GrandClaw
-
企业级AI平台
-
-
+
- {conversations.map(conv => ( + {api.conversations.list().map(conv => (
-
} + label="技能市场" + active={currentPage === 'skills'} onClick={() => switchPage('skills')} - > - - 技能市场 -
-
+ } + label="定时任务" + active={currentPage === 'scheduledTasks'} onClick={() => switchPage('scheduledTasks')} - > - - 定时任务 -
-
+ } + label="日志查询" + active={currentPage === 'logs'} onClick={() => switchPage('logs')} - > - - 日志查询 -
-
+ } + label="项目管理" + active={currentPage === 'projects'} onClick={() => switchPage('projects')} - > - - 项目管理 -
-
-
switchPage('account')}> -
-
-
张三
-
AI 产品部
-
+ />
+ switchPage('account')} /> ); diff --git a/src/pages/DeveloperPage.jsx b/src/pages/DeveloperPage.jsx index aca4c38..4ed5b33 100644 --- a/src/pages/DeveloperPage.jsx +++ b/src/pages/DeveloperPage.jsx @@ -3,7 +3,13 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { FiPlus, FiTerminal } from 'react-icons/fi'; import { FaPuzzlePiece } from 'react-icons/fa'; import Layout from '../components/Layout.jsx'; -import { mySkills } from '../data/developerData.js'; +import SidebarBrand from '../components/layout/SidebarBrand.jsx'; +import SidebarUser from '../components/layout/SidebarUser.jsx'; +import SidebarNavItem from '../components/layout/SidebarNavItem.jsx'; +import usePageState from '../hooks/usePageState.js'; +import { DEVELOPER_PAGES } from '../constants/pages.js'; +import { DEVELOPER_KEYS } from '../constants/storageKeys.js'; +import api from '../services/api.js'; import MySkillsPage from './developer/MySkillsPage.jsx'; import UploadSkillPage from './developer/UploadSkillPage.jsx'; import NewVersionPage from './developer/NewVersionPage.jsx'; @@ -14,11 +20,17 @@ import SkillEditorPage from './developer/SkillEditorPage.jsx'; function DeveloperPage() { const location = useLocation(); const navigate = useNavigate(); - const [currentPage, setCurrentPage] = useState(() => { - return localStorage.getItem('developer_currentPage') || 'mySkills'; + + // 使用 usePageState 管理页面状态 + const { currentPage, setCurrentPage } = usePageState({ + storageKey: DEVELOPER_KEYS.CURRENT_PAGE, + defaultPage: 'mySkills', + pageTitles: DEVELOPER_PAGES, }); + + // 保留额外的状态(currentSkillId 需要持久化到 localStorage) const [currentSkillId, setCurrentSkillId] = useState(() => { - const saved = localStorage.getItem('developer_currentSkillId'); + const saved = localStorage.getItem(DEVELOPER_KEYS.CURRENT_SKILL_ID); return saved ? JSON.parse(saved) : null; }); const [newVersionSkillName, setNewVersionSkillName] = useState(''); @@ -32,12 +44,8 @@ function DeveloperPage() { }, [location.state, navigate, setCurrentPage, setCurrentSkillId]); useEffect(() => { - localStorage.setItem('developer_currentPage', currentPage); - }, [currentPage]); - - useEffect(() => { - localStorage.setItem('developer_currentSkillId', JSON.stringify(currentSkillId)); - }, [currentSkillId]); + localStorage.setItem(DEVELOPER_KEYS.CURRENT_SKILL_ID, JSON.stringify(currentSkillId)); + }, [DEVELOPER_KEYS.CURRENT_SKILL_ID, currentSkillId]); const switchPage = (pageId, data = {}) => { setCurrentPage(pageId); @@ -90,30 +98,13 @@ function DeveloperPage() { }; const getPageTitle = () => { - const titles = { - mySkills: '我的技能', - uploadSkill: '创建技能', - newVersion: '上传新版本', - devDocs: '开发文档', - devAccount: '开发者设置', - skillEditor: '技能详情' - }; - return titles[currentPage] || ''; + return DEVELOPER_PAGES[currentPage]?.title || ''; }; const sidebar = ( <>
-
-
- - -
-
-
GrandClaw
-
技能开发台
-
-
+
- {mySkills.map(skill => ( + {api.developer.getMySkills().map(skill => (
-
} + label="我的技能" + active={currentPage === 'mySkills'} onClick={() => switchPage('mySkills')} - > - - 我的技能 -
-
+ } + label="开发文档" + active={currentPage === 'devDocs'} onClick={() => switchPage('devDocs')} - > - - 开发文档 -
-
-
switchPage('devAccount')}> -
-
-
张三
-
开发者
-
+ />
+ switchPage('devAccount')} /> ); diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 0000000..1bd44db --- /dev/null +++ b/src/services/api.js @@ -0,0 +1,200 @@ +/** + * 统一数据访问接口 + * 提供按功能模块划分的数据访问方法 + */ + +// 导入静态数据 +import { conversations, getChatScenes } from '../data/conversations.js'; +import { skills, skillFiles, skillVersions, getSkillIcon } from '../data/skills.js'; +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'; + +/** + * 用户相关 API + */ +export const user = { + /** + * 获取用户信息 + * @returns {Object} 用户信息对象 + */ + getInfo: () => ({ + name: '张三', + avatar: '张', + role: 'AI 产品部', + }), +}; + +/** + * 技能相关 API + */ +export const skillsApi = { + /** + * 获取所有技能列表 + * @returns {Array} 技能列表 + */ + list: () => skills, + + /** + * 根据 ID 获取技能详情 + * @param {number} id - 技能 ID + * @returns {Object|undefined} 技能对象 + */ + getById: (id) => skills.find(skill => skill.id === id), + + /** + * 获取技能文件列表 + * @returns {Array} 文件列表 + */ + getFiles: () => skillFiles, + + /** + * 获取技能版本历史 + * @returns {Array} 版本列表 + */ + getVersions: () => skillVersions, + + /** + * 获取技能图标 + * @param {number} id - 技能 ID + * @returns {string} 图标 emoji + */ + getIcon: (id) => getSkillIcon(id), +}; + +/** + * 会话相关 API + */ +export const conversationsApi = { + /** + * 获取所有会话列表 + * @returns {Array} 会话列表 + */ + list: () => conversations, + + /** + * 获取聊天场景的 HTML 内容 + * @param {string} sceneName - 场景名称 + * @returns {string} HTML 内容 + */ + getScene: (sceneName) => { + const scenes = getChatScenes(); + return scenes[sceneName] || ''; + }, +}; + +/** + * 日志相关 API + */ +export const logsApi = { + /** + * 获取所有日志 + * @returns {Array} 日志列表 + */ + list: () => logs, + + /** + * 根据条件筛选日志 + * @param {Object} filters - 筛选条件 + * @param {string} [filters.user] - 用户名 + * @param {string} [filters.type] - 日志类型 + * @param {string} [filters.status] - 状态 + * @returns {Array} 筛选后的日志列表 + */ + filter: ({ user, type, status } = {}) => { + return logs.filter(log => { + if (user && log.user !== user) return false; + if (type && log.type !== type) return false; + if (status && log.status !== status) return false; + return true; + }); + }, +}; + +/** + * 开发台相关 API + */ +export const developerApi = { + /** + * 获取我的技能列表 + * @returns {Array} 技能列表 + */ + getMySkills: () => mySkills, + + /** + * 根据 ID 获取技能详情 + * @param {number} id - 技能 ID + * @returns {Object|undefined} 技能对象 + */ + getSkillById: (id) => mySkills.find(skill => skill.id === id), + + /** + * 获取技能分类 + * @returns {Array} 分类列表 + */ + getCategories: () => skillCategories, + + /** + * 获取支持的模型列表 + * @returns {Array} 模型列表 + */ + getSupportedModels: () => supportedModels, + + /** + * 获取开发文档列表 + * @returns {Array} 文档列表 + */ + getDocs: () => devDocs, +}; + +/** + * 项目成员相关 API + */ +export const membersApi = { + /** + * 获取所有成员 + * @returns {Array} 成员列表 + */ + list: () => projectMembers, + + /** + * 根据 ID 获取成员 + * @param {string} id - 成员 ID + * @returns {Object|undefined} 成员对象 + */ + getById: (id) => projectMembers.find(member => member.id === id), +}; + +/** + * 定时任务相关 API + */ +export const tasksApi = { + /** + * 获取所有任务 + * @returns {Array} 任务列表 + */ + list: () => scheduledTasks, + + /** + * 根据 ID 获取任务 + * @param {string} id - 任务 ID + * @returns {Object|undefined} 任务对象 + */ + getById: (id) => scheduledTasks.find(task => task.id === id), +}; + +/** + * 统一 API 导出对象 + */ +export const api = { + user, + skills: skillsApi, + conversations: conversationsApi, + logs: logsApi, + developer: developerApi, + members: membersApi, + tasks: tasksApi, +}; + +export default api; diff --git a/src/styles/global.scss b/src/styles/global.scss index 434f952..81a2039 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -6,7 +6,10 @@ @use 'base' as *; @use 'components' as *; @use 'layout' as *; -@use 'pages' as *; +@use 'pages/console' as *; +@use 'pages/admin' as *; +@use 'pages/developer' as *; +@use 'pages/home' as *; /* ============================================ 原始样式内容(待进一步拆分) diff --git a/src/styles/pages/_admin.scss b/src/styles/pages/_admin.scss new file mode 100644 index 0000000..722def5 --- /dev/null +++ b/src/styles/pages/_admin.scss @@ -0,0 +1,142 @@ +/* ============ 管理台页面样式 ============ */ +/* 本文件包含管理台(Admin)相关的所有页面样式 */ + +/* 管理台侧边栏 */ +.admin-sidebar { + width: 240px; + background: var(--color-bg-1); + border-right: 1px solid var(--color-border-2); + display: flex; + flex-direction: column; + flex-shrink: 0; + height: 100%; +} + +.admin-sidebar-header { + padding: 16px; + border-bottom: 1px solid var(--color-border-2); +} + +.admin-sidebar-nav { + flex: 1; + overflow-y: auto; + padding: 12px; +} + +.admin-nav-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: var(--radius-md); + cursor: pointer; + transition: background 0.2s; + color: var(--color-text-2); + font-size: 14px; + font-weight: 500; + margin-bottom: 2px; +} + +.admin-nav-item:hover { + background: var(--color-bg-2); + color: var(--color-text-1); +} + +.admin-nav-item.active { + background: var(--color-primary-light); + color: var(--color-primary); +} + +.admin-nav-icon { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.admin-nav-text { + flex: 1; +} + +.admin-sidebar-user { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + border-top: 1px solid var(--color-border-2); + background: var(--color-bg-2); + cursor: pointer; + transition: background 0.2s; +} + +.admin-sidebar-user:hover { + background: var(--color-bg-3); +} + +.admin-sidebar-user-info { + flex: 1; + min-width: 0; +} + +.admin-sidebar-user-name { + font-size: 14px; + font-weight: 600; + color: var(--color-text-1); + margin-bottom: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.admin-sidebar-user-role { + font-size: 12px; + color: var(--color-text-3); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 成员选择样式 */ +.member-selection { + border: 1px solid var(--color-border-3); + border-radius: var(--radius-md); + max-height: 240px; + overflow-y: auto; +} + +.member-checkbox-item, +.member-radio-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + cursor: pointer; + transition: background 0.15s; + border-bottom: 1px solid var(--color-border-2); +} + +.member-checkbox-item:last-child, +.member-radio-item:last-child { + border-bottom: none; +} + +.member-checkbox-item:hover, +.member-radio-item:hover { + background: var(--color-bg-2); +} + +.member-checkbox-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: linear-gradient(135deg, var(--color-primary) 0%, #8B5CF6 100%); + color: #FFFFFF; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 600; + flex-shrink: 0; +} diff --git a/src/styles/pages/_console.scss b/src/styles/pages/_console.scss new file mode 100644 index 0000000..f9664ea --- /dev/null +++ b/src/styles/pages/_console.scss @@ -0,0 +1,73 @@ +/* ============ 工作台页面样式 ============ */ +/* 本文件包含工作台(Console)相关的所有页面样式 */ + +/* 聊天布局 */ +.chat-layout { + display: flex; + flex-direction: column; + height: 100%; + background: var(--color-bg-1); +} + +/* 聊天顶部栏 */ +.chat-header { + height: var(--header-height); + background: var(--color-bg-1); + border-bottom: 1px solid var(--color-border-2); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 24px; +} + +/* 聊天侧边栏 */ +.chat-sidebar { + width: 260px; + background: var(--color-bg-2); + border-right: 1px solid var(--color-border-2); + display: flex; + flex-direction: column; + flex-shrink: 0; + height: 100%; + overflow: hidden; +} + +/* 聊天内容区 */ +.chat-content { + flex: 1; + display: flex; + flex-direction: column; + background: var(--color-bg-1); +} + +/* 聊天消息区 */ +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 24px; + min-height: 0; +} + +/* 会话项 */ +.conversation-item { + padding: 12px 14px; + border-radius: var(--radius-md); + cursor: pointer; + margin-bottom: 4px; + transition: all var(--transition); + border: 1px solid transparent; +} + +.conversation-item:hover { + background: var(--color-bg-1); + border-color: var(--color-border-2); +} + +.conversation-item.active { + background: var(--color-bg-1); + border-color: var(--color-primary); + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.08); +} + +/* 更多工作台样式请参考 global.scss 中的对应部分 */ +/* 包括:chat-input-wrapper, message-thinking, instance-stopped 等 */ diff --git a/src/styles/pages/_developer.scss b/src/styles/pages/_developer.scss new file mode 100644 index 0000000..be85e76 --- /dev/null +++ b/src/styles/pages/_developer.scss @@ -0,0 +1,91 @@ +/* ============ 开发台页面样式 ============ */ +/* 本文件包含开发台(Developer)相关的所有页面样式 */ +/* 开发台复用了大量工作台的样式(如 .chat-sidebar 等) */ + +/* 开发台特有样式 */ +.dev-detail-header { + display: flex; + gap: 20px; + align-items: flex-start; +} + +.dev-detail-icon { + width: 72px; + height: 72px; + border-radius: 16px; + background: linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 32px; + flex-shrink: 0; +} + +.dev-detail-main { + flex: 1; +} + +.dev-detail-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 12px 0; +} + +.dev-detail-tag { + padding: 4px 12px; + background: #F1F5F9; + border-radius: 999px; + font-size: 13px; + color: #64748B; +} + +.dev-detail-stats { + display: flex; + gap: 24px; + color: #64748B; + font-size: 14px; +} + +.dev-detail-section { + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid #E2E8F0; +} + +.dev-detail-section h3 { + font-size: 16px; + font-weight: 700; + margin-bottom: 12px; +} + +.dev-info-row { + display: flex; + margin-bottom: 16px; +} + +.dev-info-label { + width: 100px; + flex-shrink: 0; + color: #64748B; + font-size: 14px; + font-weight: 500; +} + +.dev-info-value { + flex: 1; + color: #1E293B; + font-size: 14px; +} + +/* 开发台返回按钮样式 */ +.dev-back-btn { + display: inline-flex; + align-items: center; + gap: 6px; + cursor: pointer; + color: #3B82F6; + font-weight: 600; + margin-bottom: 16px; +} diff --git a/src/styles/pages/_home.scss b/src/styles/pages/_home.scss new file mode 100644 index 0000000..c58f741 --- /dev/null +++ b/src/styles/pages/_home.scss @@ -0,0 +1,222 @@ +/* ============ 首页样式 ============ */ +/* 本文件包含首页(Home)相关的所有页面样式 */ + +.home-layout { + min-height: 100vh; + display: flex; + flex-direction: column; + background: linear-gradient(180deg, #F8FAFC 0%, #FFFFFF 100%); + position: relative; + overflow: hidden; +} + +.home-layout::before { + content: ''; + position: absolute; + top: -30%; + right: -20%; + width: 800px; + height: 800px; + background: radial-gradient(circle, rgba(59, 130, 246, 0.08) 0%, transparent 60%); + pointer-events: none; +} + +.home-layout::after { + content: ''; + position: absolute; + bottom: -20%; + left: -10%; + width: 600px; + height: 600px; + background: radial-gradient(circle, rgba(139, 92, 246, 0.06) 0%, transparent 60%); + pointer-events: none; +} + +.home-header { + padding: 0 48px; + height: 68px; + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + z-index: 1; + border-bottom: 1px solid var(--color-border-2); + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(12px); +} + +.home-logo { + font-size: 18px; + font-weight: 800; + color: var(--color-text-1); + display: flex; + align-items: center; + gap: 10px; + letter-spacing: -0.3px; +} + +.home-nav { + display: flex; + gap: 6px; +} + +.home-nav a { + color: var(--color-text-2); + text-decoration: none; + padding: 9px 16px; + border-radius: 8px; + font-weight: 600; + font-size: 14px; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + gap: 6px; +} + +.home-nav a:hover { + color: var(--color-text-1); + background: var(--color-bg-2); +} + +.home-main { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 24px; + text-align: center; + position: relative; + z-index: 1; +} + +.home-badge { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 14px; + background: var(--color-primary-light); + border: 1px solid rgba(59, 130, 246, 0.2); + border-radius: 999px; + color: var(--color-primary); + font-size: 13px; + font-weight: 700; + margin-bottom: 28px; +} + +.home-badge-dot { + width: 8px; + height: 8px; + background: var(--color-success); + border-radius: 50%; +} + +.home-title { + font-size: 56px; + font-weight: 800; + color: var(--color-text-1); + margin-bottom: 14px; + line-height: 1.15; + letter-spacing: -1.2px; +} + +.home-title span { + background: linear-gradient(90deg, #3B82F6 0%, #8B5CF6 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.home-desc { + font-size: 18px; + color: var(--color-text-3); + margin-bottom: 44px; + max-width: 640px; + line-height: 1.7; +} + +.home-buttons { + display: flex; + gap: 14px; +} + +.home-btn { + padding: 13px 30px; + font-size: 15px; + font-weight: 700; + border-radius: 10px; + cursor: pointer; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 8px; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.home-btn.primary { + background: linear-gradient(135deg, #3B82F6 0%, #60A5FA 100%); + color: #FFFFFF; + border: none; + box-shadow: 0 8px 24px rgba(59, 130, 246, 0.25); +} + +.home-btn.primary:hover { + transform: translateY(-2px); + box-shadow: 0 12px 32px rgba(59, 130, 246, 0.35); +} + +.home-features { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; + max-width: 860px; + margin-top: 64px; +} + +.home-feature { + padding: 24px; + background: #FFFFFF; + border: 1px solid var(--color-border-2); + border-radius: 14px; + text-align: left; + box-shadow: 0 2px 8px rgba(15, 23, 42, 0.03); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.home-feature:hover { + border-color: var(--color-border-3); + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06); + transform: translateY(-2px); +} + +.home-feature-icon { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 12px; +} + +.home-feature-title { + font-size: 16px; + font-weight: 800; + color: var(--color-text-1); + margin-bottom: 8px; +} + +.home-feature-desc { + font-size: 14px; + color: var(--color-text-3); + line-height: 1.6; +} + +.home-footer { + padding: 28px; + text-align: center; + color: var(--color-text-4); + font-size: 13px; + font-weight: 500; + position: relative; + z-index: 1; +}