refactor: 代码架构重构 - 提取组件、统一状态管理和数据访问层
- 新增布局组件(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目录
This commit is contained in:
228
README.md
228
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';
|
||||
|
||||
<SidebarBrand subtitle="企业级AI平台" />
|
||||
```
|
||||
|
||||
#### SidebarUser 用户信息
|
||||
侧边栏底部的用户信息展示组件,使用全局用户上下文。
|
||||
|
||||
```jsx
|
||||
import { SidebarUser } from '../components/layout/SidebarUser.jsx';
|
||||
|
||||
<SidebarUser
|
||||
onClick={() => 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';
|
||||
|
||||
<SidebarNavItem
|
||||
icon={<FiMessageSquare />}
|
||||
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';
|
||||
|
||||
<EmptyState
|
||||
icon={<FiInbox size={48} />}
|
||||
title="暂无数据"
|
||||
description="当前没有可显示的内容"
|
||||
/>
|
||||
```
|
||||
|
||||
#### StatusBadge 状态标签
|
||||
用于显示状态(成功、失败、警告等)的标签组件。
|
||||
|
||||
```jsx
|
||||
import StatusBadge from '../components/common/StatusBadge.jsx';
|
||||
|
||||
<StatusBadge status="success" text="运行中" />
|
||||
<StatusBadge status="error" text="失败" />
|
||||
<StatusBadge status="warning" text="警告" />
|
||||
<StatusBadge status="stopped" text="已停止" />
|
||||
```
|
||||
|
||||
#### TagInput 标签输入
|
||||
支持输入标签的输入框组件。
|
||||
|
||||
```jsx
|
||||
import TagInput from '../components/common/TagInput.jsx';
|
||||
|
||||
<TagInput
|
||||
tags={tags}
|
||||
onChange={setTags}
|
||||
placeholder="输入标签后按回车添加"
|
||||
/>
|
||||
```
|
||||
|
||||
#### SearchBar 搜索框
|
||||
通用的搜索输入框组件。
|
||||
|
||||
```jsx
|
||||
import SearchBar from '../components/common/SearchBar.jsx';
|
||||
|
||||
<SearchBar
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholder="搜索..."
|
||||
/>
|
||||
```
|
||||
|
||||
### 列表组件
|
||||
|
||||
#### 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 中包裹
|
||||
<UserProvider user={{ name: '张三', avatar: '张', role: 'AI 产品部' }}>
|
||||
<App />
|
||||
</UserProvider>
|
||||
|
||||
// 在组件中使用
|
||||
function Component() {
|
||||
const { user } = useUserContext();
|
||||
return <div>{user.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义 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
|
||||
- 文档同步:补充开发台功能描述(上传新版本)
|
||||
|
||||
|
||||
49
openspec/specs/data-service-layer/spec.md
Normal file
49
openspec/specs/data-service-layer/spec.md
Normal file
@@ -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** 系统返回符合约定格式的数据(如对象、数组),无论底层存储格式如何
|
||||
52
openspec/specs/modular-styles/spec.md
Normal file
52
openspec/specs/modular-styles/spec.md
Normal file
@@ -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** 文件名使用 _<page-name>.scss 格式(如 _console.scss、_admin.scss)
|
||||
|
||||
#### Scenario: 页面样式文件内容结构
|
||||
- **WHEN** 查看页面样式文件
|
||||
- **THEN** 该文件包含页面特定的布局、组件、状态等样式,使用清晰的注释分节
|
||||
41
openspec/specs/reusable-components/spec.md
Normal file
41
openspec/specs/reusable-components/spec.md
Normal file
@@ -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 回调
|
||||
52
openspec/specs/unified-state-management/spec.md
Normal file
52
openspec/specs/unified-state-management/spec.md
Normal file
@@ -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** 系统更新页面状态和附加数据状态
|
||||
21
src/App.jsx
21
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 (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/console" element={<ConsolePage />} />
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
<Route path="/developer" element={<DeveloperPage />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
<UserProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/console" element={<ConsolePage />} />
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
<Route path="/developer" element={<DeveloperPage />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</UserProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
39
src/components/common/EmptyState.jsx
Normal file
39
src/components/common/EmptyState.jsx
Normal file
@@ -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 (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon">{icon}</div>
|
||||
<div className="empty-state-text">
|
||||
<div style={{ fontSize: '15px', fontWeight: 600, marginBottom: '4px' }}>
|
||||
{message}
|
||||
</div>
|
||||
{description && (
|
||||
<div style={{ fontSize: '13px', color: 'var(--color-text-3)' }}>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
{actionText && onAction && (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
style={{ marginTop: '16px' }}
|
||||
onClick={onAction}
|
||||
>
|
||||
{actionText}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmptyState;
|
||||
76
src/components/common/SearchBar.jsx
Normal file
76
src/components/common/SearchBar.jsx
Normal file
@@ -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 (
|
||||
<div className="search-bar">
|
||||
{filters.map(filter => (
|
||||
<div key={filter.key} className="search-item">
|
||||
<label>{filter.label}</label>
|
||||
{filter.type === 'select' ? (
|
||||
<select
|
||||
className="form-control"
|
||||
value={filterValues[filter.key] || ''}
|
||||
onChange={(e) => handleFilterChange(filter.key, e.target.value)}
|
||||
>
|
||||
<option value="">{filter.placeholder || '全部'}</option>
|
||||
{filter.options.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={filter.placeholder || '搜索...'}
|
||||
onChange={(e) => handleFilterChange(filter.key, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="search-item" style={{ flex: 1 }}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="搜索..."
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
{showFilter && (
|
||||
<button className="btn btn-secondary btn-sm">
|
||||
<FiFilter /> 筛选
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchBar;
|
||||
34
src/components/common/StatusBadge.jsx
Normal file
34
src/components/common/StatusBadge.jsx
Normal file
@@ -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 (
|
||||
<span
|
||||
className={`status ${config.className} ${small ? 'status-small' : ''}`}
|
||||
style={small ? { fontSize: '11px', padding: '2px 8px' } : {}}
|
||||
>
|
||||
{displayText}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatusBadge;
|
||||
52
src/components/common/TagInput.jsx
Normal file
52
src/components/common/TagInput.jsx
Normal file
@@ -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 (
|
||||
<div className="tag-input-container">
|
||||
{tags.map(tag => (
|
||||
<span key={tag} className="tag-item">
|
||||
{tag}
|
||||
<span
|
||||
className="tag-remove"
|
||||
onClick={() => handleRemove(tag)}
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
className="tag-input"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TagInput;
|
||||
23
src/components/layout/SidebarBrand.jsx
Normal file
23
src/components/layout/SidebarBrand.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* SidebarBrand - 侧边栏品牌区域组件
|
||||
* 统一显示 GrandClaw 品牌标识和副标题
|
||||
*
|
||||
* @param {Object} props - 组件属性
|
||||
* @param {string} [props.subtitle] - 副标题文本(如"企业级AI平台"、"运营管理台"、"技能开发台")
|
||||
*/
|
||||
function SidebarBrand({ subtitle = '企业级AI平台' }) {
|
||||
return (
|
||||
<div className="sidebar-brand">
|
||||
<div className="sidebar-logo-icon">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div className="sidebar-brand-text">
|
||||
<div className="sidebar-logo">GrandClaw</div>
|
||||
<div className="sidebar-subtitle">{subtitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SidebarBrand;
|
||||
34
src/components/layout/SidebarNavItem.jsx
Normal file
34
src/components/layout/SidebarNavItem.jsx
Normal file
@@ -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 (
|
||||
<div
|
||||
className={`${itemClassName} ${active ? 'active' : ''}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className={iconClassName}>{icon}</span>
|
||||
<span className={textClassName}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SidebarNavItem;
|
||||
34
src/components/layout/SidebarUser.jsx
Normal file
34
src/components/layout/SidebarUser.jsx
Normal file
@@ -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 (
|
||||
<div className={wrapperClassName} onClick={onClick}>
|
||||
<div className="user-avatar">{user.avatar}</div>
|
||||
<div className={infoClassName}>
|
||||
<div className={nameClassName}>{user.name}</div>
|
||||
<div className={roleClassName}>{user.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SidebarUser;
|
||||
52
src/constants/pages.js
Normal file
52
src/constants/pages.js
Normal file
@@ -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 || '';
|
||||
}
|
||||
24
src/constants/storageKeys.js
Normal file
24
src/constants/storageKeys.js
Normal file
@@ -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',
|
||||
};
|
||||
49
src/contexts/UserContext.jsx
Normal file
49
src/contexts/UserContext.jsx
Normal file
@@ -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 <UserContext.Provider value={value}>{children}</UserContext.Provider>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用用户上下文的 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;
|
||||
50
src/hooks/useNavigation.js
Normal file
50
src/hooks/useNavigation.js
Normal file
@@ -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;
|
||||
58
src/hooks/usePageState.js
Normal file
58
src/hooks/usePageState.js
Normal file
@@ -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;
|
||||
@@ -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 = (
|
||||
<>
|
||||
<div className="admin-sidebar-header">
|
||||
<div className="sidebar-brand">
|
||||
<div className="sidebar-logo-icon">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div className="sidebar-brand-text">
|
||||
<div className="sidebar-logo">GrandClaw</div>
|
||||
<div className="sidebar-subtitle">运营管理台</div>
|
||||
</div>
|
||||
</div>
|
||||
<SidebarBrand subtitle="运营管理台" />
|
||||
</div>
|
||||
<nav className="admin-sidebar-nav">
|
||||
<div
|
||||
className={`admin-nav-item ${currentPage === 'overview' ? 'active' : ''}`}
|
||||
<SidebarNavItem
|
||||
icon={<FiHome />}
|
||||
label="总览"
|
||||
active={currentPage === 'overview'}
|
||||
onClick={() => setCurrentPage('overview')}
|
||||
>
|
||||
<span className="admin-nav-icon"><FiHome /></span>
|
||||
<span className="admin-nav-text">总览</span>
|
||||
</div>
|
||||
<div
|
||||
className={`admin-nav-item ${currentPage === 'departments' ? 'active' : ''}`}
|
||||
itemClassName="admin-nav-item"
|
||||
iconClassName="admin-nav-icon"
|
||||
textClassName="admin-nav-text"
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiBarChart2 />}
|
||||
label="部门管理"
|
||||
active={currentPage === 'departments'}
|
||||
onClick={() => setCurrentPage('departments')}
|
||||
>
|
||||
<span className="admin-nav-icon"><FiBarChart2 /></span>
|
||||
<span className="admin-nav-text">部门管理</span>
|
||||
</div>
|
||||
<div
|
||||
className={`admin-nav-item ${currentPage === 'users' ? 'active' : ''}`}
|
||||
itemClassName="admin-nav-item"
|
||||
iconClassName="admin-nav-icon"
|
||||
textClassName="admin-nav-text"
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiUsers />}
|
||||
label="用户管理"
|
||||
active={currentPage === 'users'}
|
||||
onClick={() => setCurrentPage('users')}
|
||||
>
|
||||
<span className="admin-nav-icon"><FiUsers /></span>
|
||||
<span className="admin-nav-text">用户管理</span>
|
||||
</div>
|
||||
<div
|
||||
className={`admin-nav-item ${currentPage === 'projects' ? 'active' : ''}`}
|
||||
itemClassName="admin-nav-item"
|
||||
iconClassName="admin-nav-icon"
|
||||
textClassName="admin-nav-text"
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiList />}
|
||||
label="项目管理"
|
||||
active={currentPage === 'projects'}
|
||||
onClick={() => setCurrentPage('projects')}
|
||||
>
|
||||
<span className="admin-nav-icon"><FiList /></span>
|
||||
<span className="admin-nav-text">项目管理</span>
|
||||
</div>
|
||||
itemClassName="admin-nav-item"
|
||||
iconClassName="admin-nav-icon"
|
||||
textClassName="admin-nav-text"
|
||||
/>
|
||||
</nav>
|
||||
<div className="admin-sidebar-user">
|
||||
<div className="user-avatar">张</div>
|
||||
<div className="admin-sidebar-user-info">
|
||||
<div className="admin-sidebar-user-name">张三</div>
|
||||
<div className="admin-sidebar-user-role">系统管理员</div>
|
||||
</div>
|
||||
</div>
|
||||
<SidebarUser
|
||||
onClick={() => {}}
|
||||
wrapperClassName="admin-sidebar-user"
|
||||
infoClassName="admin-sidebar-user-info"
|
||||
nameClassName="admin-sidebar-user-name"
|
||||
roleClassName="admin-sidebar-user-role"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
<>
|
||||
<div className="chat-sidebar-header">
|
||||
<div className="sidebar-brand">
|
||||
<div className="sidebar-logo-icon">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div className="sidebar-brand-text">
|
||||
<div className="sidebar-logo">GrandClaw</div>
|
||||
<div className="sidebar-subtitle">企业级AI平台</div>
|
||||
</div>
|
||||
</div>
|
||||
<SidebarBrand subtitle="企业级AI平台" />
|
||||
<div className="sidebar-divider"></div>
|
||||
<button className="btn btn-primary" style={{ width: '100%' }} onClick={createNewChat}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}>
|
||||
@@ -154,7 +142,7 @@ function ConsolePage() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="chat-sidebar-content">
|
||||
{conversations.map(conv => (
|
||||
{api.conversations.list().map(conv => (
|
||||
<div
|
||||
key={conv.id}
|
||||
className={`conversation-item ${conv.scene === activeScene ? 'active' : ''}`}
|
||||
@@ -174,42 +162,32 @@ function ConsolePage() {
|
||||
</select>
|
||||
</div>
|
||||
<div className="chat-sidebar-nav">
|
||||
<div
|
||||
className={`chat-nav-item ${currentPage === 'skills' ? 'active' : ''}`}
|
||||
<SidebarNavItem
|
||||
icon={<FaPuzzlePiece />}
|
||||
label="技能市场"
|
||||
active={currentPage === 'skills'}
|
||||
onClick={() => switchPage('skills')}
|
||||
>
|
||||
<span className="chat-nav-icon"><FaPuzzlePiece /></span>
|
||||
<span className="chat-nav-text">技能市场</span>
|
||||
</div>
|
||||
<div
|
||||
className={`chat-nav-item ${currentPage === 'scheduledTasks' ? 'active' : ''}`}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiClock />}
|
||||
label="定时任务"
|
||||
active={currentPage === 'scheduledTasks'}
|
||||
onClick={() => switchPage('scheduledTasks')}
|
||||
>
|
||||
<span className="chat-nav-icon"><FiClock /></span>
|
||||
<span className="chat-nav-text">定时任务</span>
|
||||
</div>
|
||||
<div
|
||||
className={`chat-nav-item ${currentPage === 'logs' ? 'active' : ''}`}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiList />}
|
||||
label="日志查询"
|
||||
active={currentPage === 'logs'}
|
||||
onClick={() => switchPage('logs')}
|
||||
>
|
||||
<span className="chat-nav-icon"><FiList /></span>
|
||||
<span className="chat-nav-text">日志查询</span>
|
||||
</div>
|
||||
<div
|
||||
className={`chat-nav-item ${currentPage === 'projects' ? 'active' : ''}`}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiUsers />}
|
||||
label="项目管理"
|
||||
active={currentPage === 'projects'}
|
||||
onClick={() => switchPage('projects')}
|
||||
>
|
||||
<span className="chat-nav-icon"><FiUsers /></span>
|
||||
<span className="chat-nav-text">项目管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="chat-sidebar-user" onClick={() => switchPage('account')}>
|
||||
<div className="user-avatar">张</div>
|
||||
<div className="chat-sidebar-user-info">
|
||||
<div className="chat-sidebar-user-name">张三</div>
|
||||
<div className="chat-sidebar-user-role">AI 产品部</div>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
<SidebarUser onClick={() => switchPage('account')} />
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
<>
|
||||
<div className="chat-sidebar-header">
|
||||
<div className="sidebar-brand">
|
||||
<div className="sidebar-logo-icon">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div className="sidebar-brand-text">
|
||||
<div className="sidebar-logo">GrandClaw</div>
|
||||
<div className="sidebar-subtitle">技能开发台</div>
|
||||
</div>
|
||||
</div>
|
||||
<SidebarBrand subtitle="技能开发台" />
|
||||
<div className="sidebar-divider"></div>
|
||||
<button className="btn btn-primary" style={{ width: '100%' }} onClick={createNewProject}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}>
|
||||
@@ -122,7 +113,7 @@ function DeveloperPage() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="chat-sidebar-content">
|
||||
{mySkills.map(skill => (
|
||||
{api.developer.getMySkills().map(skill => (
|
||||
<div
|
||||
key={skill.id}
|
||||
className={`conversation-item ${currentSkillId === skill.id && currentPage === 'skillEditor' ? 'active' : ''}`}
|
||||
@@ -134,28 +125,20 @@ function DeveloperPage() {
|
||||
))}
|
||||
</div>
|
||||
<div className="chat-sidebar-nav">
|
||||
<div
|
||||
className={`chat-nav-item ${currentPage === 'mySkills' ? 'active' : ''}`}
|
||||
<SidebarNavItem
|
||||
icon={<FaPuzzlePiece />}
|
||||
label="我的技能"
|
||||
active={currentPage === 'mySkills'}
|
||||
onClick={() => switchPage('mySkills')}
|
||||
>
|
||||
<span className="chat-nav-icon"><FaPuzzlePiece /></span>
|
||||
<span className="chat-nav-text">我的技能</span>
|
||||
</div>
|
||||
<div
|
||||
className={`chat-nav-item ${currentPage === 'devDocs' ? 'active' : ''}`}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiTerminal />}
|
||||
label="开发文档"
|
||||
active={currentPage === 'devDocs'}
|
||||
onClick={() => switchPage('devDocs')}
|
||||
>
|
||||
<span className="chat-nav-icon"><FiTerminal /></span>
|
||||
<span className="chat-nav-text">开发文档</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="chat-sidebar-user" onClick={() => switchPage('devAccount')}>
|
||||
<div className="user-avatar">张</div>
|
||||
<div className="chat-sidebar-user-info">
|
||||
<div className="chat-sidebar-user-name">张三</div>
|
||||
<div className="chat-sidebar-user-role">开发者</div>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
<SidebarUser onClick={() => switchPage('devAccount')} />
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
200
src/services/api.js
Normal file
200
src/services/api.js
Normal file
@@ -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;
|
||||
@@ -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 *;
|
||||
|
||||
/* ============================================
|
||||
原始样式内容(待进一步拆分)
|
||||
|
||||
142
src/styles/pages/_admin.scss
Normal file
142
src/styles/pages/_admin.scss
Normal file
@@ -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;
|
||||
}
|
||||
73
src/styles/pages/_console.scss
Normal file
73
src/styles/pages/_console.scss
Normal file
@@ -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 等 */
|
||||
91
src/styles/pages/_developer.scss
Normal file
91
src/styles/pages/_developer.scss
Normal file
@@ -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;
|
||||
}
|
||||
222
src/styles/pages/_home.scss
Normal file
222
src/styles/pages/_home.scss
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user