diff --git a/README.md b/README.md new file mode 100644 index 0000000..067218a --- /dev/null +++ b/README.md @@ -0,0 +1,460 @@ +# GrandClaw 原型项目 + +> 企业级AI智算平台前端原型,专注于展示页面布局和内容,使用React + Vite构建。 + +## 文档编写原则 + +本文档遵循以下编写原则,以确保内容的准确性、可维护性和可读性: + +### 内容原则 +- **准确性优先**:所有技术描述、代码示例、路径配置必须与实际代码保持一致 +- **简洁明了**:使用清晰简洁的语言,避免冗余描述 +- **实用导向**:提供开发者实际需要的配置和开发指南 + +### 结构原则 +- **分层组织**:按功能模块和技术领域分层组织内容 +- **逻辑清晰**:从项目概述 → 技术栈 → 功能 → 开发指南 → 维护的顺序组织 +- **易于检索**:使用明确的标题层级和目录结构 + +### 代码原则 +- **代码即文档**:代码示例使用实际可运行的代码 +- **注释克制**:仅在必要时添加注释,代码本身应自解释 + +## 文档更新原则 + +本文档随项目迭代持续更新,遵循以下更新策略: + +### 更新触发条件 +- 新增或移除核心功能模块 +- 更改技术栈或依赖版本 +- 添加新的页面或路由 +- 新增通用组件或工具 +- 重大架构调整 + +### 更新内容 +- **版本对齐**:技术栈版本号必须与 package.json 保持一致 +- **路径同步**:文件路径必须与实际项目结构一致 +- **功能同步**:核心功能描述必须覆盖实际实现的所有功能 +- **示例验证**:代码示例必须经过验证可正常运行 + +### 更新记录 +- 每次重要更新在更新日志中记录 +- 记录内容包括:日期、更新类型、具体变更 +- 保持更新日志按时间倒序排列 + +--- + +## 项目概述 + +GrandClaw 是一个企业级AI智能助手平台的前端原型项目,主要用于展示平台的主要页面布局、交互流程和视觉设计。项目采用现代化的前端技术栈,实现了四大核心模块: + +- **首页**:平台入口,包含登录功能 +- **工作台**:用户与AI助手交互的主要界面,包含聊天、技能市场、日志查询、定时任务等功能 +- **管理台**:运营管理界面,包含总览、部门管理、用户管理、项目管理 +- **开发台**:技能开发界面,包含我的技能、技能编辑、开发文档等 + +## 技术栈 + +### 核心框架 +- **React 19.2.4**:UI组件库 +- **React Router 7.13.1**:路由管理(使用HashRouter) +- **Vite 8.0.1**:构建工具和开发服务器 + +### UI与样式 +- **react-icons 5.5.0**:图标库(Feather + FontAwesome图标集) +- **Sass 1.98.0**:CSS预处理器 +- **SCSS模块化**:变量、混入、组件、布局、页面分离 + +### 构建优化 +- **vite-plugin-singlefile 2.3.2**:单文件打包,解决CORS问题 +- **相对路径配置**:支持直接打开HTML文件运行 +- **注意**:构建时会显示弃用警告 `WARN inlineDynamicImports option is deprecated, please use codeSplitting: false instead.`,这是由 `vite-plugin-singlefile` 插件内部使用弃用选项导致的,不影响功能,可以忽略。 + +### 包管理 +- **pnpm**:高效的包管理器 + +## 项目结构 + +``` +grandclaw-archtype/ +├── dist/ # 构建输出目录(单个index.html) +├── public/ # 静态资源 +├── src/ # 源代码 +│ ├── App.jsx # 主路由配置 +│ ├── main.jsx # 应用入口 +│ ├── components/ # 通用组件 +│ │ ├── Layout.jsx # 通用布局组件(侧边栏+主内容) +│ │ └── ListSelector.jsx # 列表选择器组件(支持单选/多选) +│ ├── hooks/ # 自定义Hook +│ │ └── useLocalStorage.js # localStorage状态管理Hook +│ ├── data/ # 模拟数据 +│ │ ├── conversations.js # 聊天场景数据 +│ │ ├── developerData.js # 开发台数据 +│ │ ├── logs.js # 日志数据 +│ │ ├── members.js # 成员数据 +│ │ ├── skills.js # 技能数据 +│ │ └── tasks.js # 定时任务数据 +│ ├── pages/ # 页面组件 +│ │ ├── HomePage.jsx # 首页 +│ │ ├── LoginPage.jsx # 登录页面 +│ │ ├── ConsolePage.jsx # 工作台主页面 +│ │ ├── AdminPage.jsx # 管理台主页面 +│ │ ├── DeveloperPage.jsx # 开发台主页面 +│ │ ├── console/ # 工作台子页面 +│ │ │ ├── ChatPage.jsx # 聊天页面 +│ │ │ ├── SkillsPage.jsx # 技能市场 +│ │ │ ├── SkillDetailPage.jsx # 技能详情 +│ │ │ ├── LogsPage.jsx # 日志查询 +│ │ │ ├── TasksPage.jsx # 定时任务 +│ │ │ ├── TaskDetailPage.jsx # 任务详情 +│ │ │ ├── AccountPage.jsx # 账号管理 +│ │ │ ├── ProjectsPage.jsx # 项目管理 +│ │ │ ├── MemberConfigPage.jsx # 成员配置 +│ │ │ └── AddMemberPage.jsx # 增加成员 +│ │ ├── admin/ # 管理台子页面 +│ │ │ ├── OverviewPage.jsx # 运营总览 +│ │ │ ├── DepartmentsPage.jsx # 部门管理 +│ │ │ ├── AddDepartmentPage.jsx # 新增部门 +│ │ │ ├── UsersPage.jsx # 用户管理 +│ │ │ ├── AddUserPage.jsx # 新增用户 +│ │ │ ├── AdminProjectsPage.jsx # 项目管理 +│ │ │ └── AddProjectPage.jsx # 新增项目 +│ │ └── developer/ # 开发台子页面 +│ │ ├── MySkillsPage.jsx +│ │ ├── UploadSkillPage.jsx +│ │ ├── DevDocsPage.jsx +│ │ ├── DevAccountPage.jsx +│ │ └── SkillEditorPage.jsx +│ └── styles/ # SCSS样式模块 +│ ├── _variables.scss # 设计系统变量 +│ ├── _mixins.scss # 可复用混入 +│ ├── _base.scss # 基础重置和全局样式 +│ ├── _components.scss # 通用组件样式 +│ ├── _layout.scss # 布局相关样式 +│ ├── _pages.scss # 页面特定样式 +│ └── global.scss # 主样式文件,导入所有模块 +├── index.html # HTML入口文件 +├── package.json # 项目配置和依赖 +├── vite.config.js # Vite配置 +└── pnpm-lock.yaml # 依赖锁定文件 +``` + +## 开发指南 + +### 环境要求 +- Node.js 18+ +- pnpm(推荐)或 npm + +### 安装依赖 +```bash +pnpm install +``` + +### 开发模式 +```bash +pnpm dev +# 访问 http://localhost:5173 +``` + +### 构建生产版本 +```bash +pnpm build +# 生成 dist/index.html(单个HTML文件) +``` + +### 预览生产构建 +直接双击 `dist/index.html` 文件,或在浏览器中打开。 + +## 核心功能 + +### 1. 首页 +- 平台品牌展示 +- 导航入口(工作台、开发台、管理台) +- 登录入口 + +### 2. 登录页面 +- 用户名/邮箱登录 +- 密码输入 +- 验证码(防爆破) +- 记住我功能 +- 忘记密码链接 + +### 3. 工作台(Console) +- **聊天界面**:支持多种聊天场景(欢迎页、普通对话、技能调用、文件上传) +- **技能市场**:浏览、订阅、查看技能详情 +- **日志查询**:支持按用户、类型、状态筛选 +- **定时任务**:管理定时任务,支持启用/禁用,查看任务详情 +- **项目管理**:成员列表,增加成员 +- **账号管理**:个人信息和密码修改 + +### 4. 管理台(Admin) +- **运营总览**:平台运营数据概览 +- **部门管理**:部门列表,支持新增、编辑、启用/禁用、删除 +- **用户管理**:用户列表,支持新增、编辑、启用/禁用、删除,角色区分(管理员/开发者/成员) +- **项目管理**:项目列表,支持新增、编辑、启用/禁用、删除 + +### 5. 开发台(Developer) +- **我的技能**:已开发的技能列表 +- **创建技能**:上传新技能 +- **技能编辑**:编辑技能配置、版本管理 +- **开发文档**:技能开发相关文档 +- **开发者设置**:开发者账号信息 + +## 路由结构 + +项目使用 **HashRouter**,所有路由基于哈希路径,支持直接打开HTML文件运行。 + +### 主要路由 +``` +/ # 首页 +/login # 登录页面 +/console # 工作台 +/admin # 管理台 +/developer # 开发台 +``` + +### 路由配置(App.jsx) +```jsx +import { HashRouter as Router, Routes, Route } from 'react-router-dom'; + + + + } /> + } /> + } /> + } /> + } /> + + +``` + +## 通用组件 + +### ListSelector 列表选择器 +通用的列表选择器组件,支持单选和多选模式。 + +```jsx +import ListSelector from '../components/ListSelector.jsx'; + + {}} // 清除已选回调 +/> +``` + +## 状态管理 + +### 1. 路由状态 +- **顶级路由**:由React Router的URL哈希管理 +- **子页面状态**:使用`localStorage`持久化 + +### 2. 导航状态持久化策略 +每个主要页面(工作台、管理台、开发台)都有独立的`localStorage`键: + +```javascript +// 工作台 +localStorage.setItem('console_currentPage', 'chat'); +localStorage.setItem('console_currentScene', 'welcome'); + +// 管理台 +localStorage.setItem('admin_currentPage', 'overview'); + +// 开发台 +localStorage.setItem('developer_currentPage', 'mySkills'); +localStorage.setItem('developer_currentSkillId', '1'); +``` + +### 3. 主页跳转 vs 刷新浏览器 +通过`location.state.fromHome`区分两种导航来源: + +```javascript +// 从主页跳转:显示默认页面 +useEffect(() => { + if (location.state?.fromHome) { + setCurrentPage('chat'); // 默认页面 + navigate('.', { replace: true, state: {} }); // 清除state + } +}, [location.state]); + +// 刷新浏览器:从localStorage恢复 +const [currentPage, setCurrentPage] = useState(() => { + return localStorage.getItem('console_currentPage') || 'chat'; +}); +``` + +## 样式系统 + +### SCSS模块化结构 +``` +src/styles/ +├── _variables.scss # 设计系统变量(颜色、间距、阴影等) +├── _mixins.scss # 可复用混入(媒体查询、弹性布局等) +├── _base.scss # 基础重置、CSS变量定义、body样式 +├── _components.scss # 通用组件(按钮、卡片、表单、状态标签) +├── _layout.scss # 布局相关(侧边栏、主内容区) +├── _pages.scss # 页面特定样式(首页、管理台等) +└── global.scss # 主文件,导入所有模块 +``` + +### 设计系统变量(_variables.scss) +```scss +// 品牌主色 +$primary: #3B82F6; +$primary-light: #EFF6FF; +$primary-dark: #2563EB; + +// 功能色 +$success: #10B981; +$warning: #F59E0B; +$danger: #EF4444; + +// 中性色 +$text-1: #1E293B; +$text-2: #475569; +$text-3: #94A3B8; + +// 布局尺寸 +$sidebar-width: 240px; +$header-height: 60px; +$radius-md: 8px; +``` + +### 状态标签样式 +支持多种状态标签: +- `status-running` - 成功/运行中(绿色) +- `status-stopped` - 停止/禁用(灰色) +- `status-warning` - 警告(黄色) +- `status-error` - 失败/错误(红色) + +### 角色标签样式 +- `role-admin` - 管理员(蓝色) +- `role-member` - 成员(灰色) +- `role-developer` - 开发者(橙色) + +## 数据模拟 + +所有数据都存储在 `src/data/` 目录下的JavaScript文件中,作为静态模拟数据。 + +### 数据文件说明 +- `conversations.js`:聊天场景和对话历史 +- `skills.js`:技能市场数据,包含技能详情、文件列表、版本历史 +- `developerData.js`:开发台数据,包含我的技能、技能分类、开发文档 +- `logs.js`:操作日志数据(成功/失败/警告状态) +- `tasks.js`:定时任务数据(包含任务配置和执行日志) +- `members.js`:项目成员数据 + +## 构建和部署 + +### 构建配置(vite.config.js) +```javascript +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { viteSingleFile } from 'vite-plugin-singlefile'; + +export default defineConfig({ + base: './', // 相对路径,支持直接打开HTML文件 + plugins: [ + react(), + viteSingleFile() // 单文件打包 + ], +}); +``` + +### 构建产物 +运行 `pnpm build` 后生成: +- `dist/index.html`:包含所有JavaScript和CSS的单个HTML文件 +- 支持直接双击打开,无需服务器 +- 使用HashRouter,路由正常工作 + +### 部署方式 +1. **本地运行**:直接双击 `dist/index.html` +2. **静态服务器**:将 `dist/` 目录部署到任何静态文件服务器 +3. **CDN部署**:上传单个HTML文件到CDN即可 + +## 已知问题和注意事项 + +### 1. 聊天场景渲染 +- 聊天内容使用 `dangerouslySetInnerHTML` 渲染HTML字符串 +- 深度思考区域支持点击展开/折叠 +- 聊天输入框仅为展示,无实际功能 + +### 2. 浏览器兼容性 +- 需要现代浏览器(Chrome、Firefox、Safari、Edge) +- 不支持IE +- file://协议下可能存在某些限制 + +### 3. 状态持久化 +- 使用localStorage存储子页面状态 +- 清除浏览器数据会丢失导航状态 +- 不同浏览器标签页共享localStorage + +### 4. 性能考虑 +- 单个HTML文件较大,首次加载可能稍慢 +- 所有图标已通过react-icons优化,按需加载 +- SCSS样式已编译为CSS,无运行时开销 + +### 5. 构建警告 +- 构建时会显示弃用警告:`WARN inlineDynamicImports option is deprecated, please use codeSplitting: false instead.` +- 这个警告来自 `vite-plugin-singlefile` 插件内部,无法通过配置消除 +- 警告不影响功能,可以安全忽略 + +## 开发建议 + +### 添加新页面 +1. 在 `src/pages/` 目录下创建页面组件 +2. 在父页面组件(如ConsolePage、AdminPage)中添加路由逻辑 +3. 如果需要持久化状态,添加localStorage逻辑 +4. 在 `src/styles/global.scss` 中添加页面特定样式 + +### 添加新组件 +1. 在 `src/components/` 目录下创建组件 +2. 在 `src/styles/global.scss` 中添加组件样式 +3. 使用设计系统变量保持一致性 + +### 修改样式 +1. 优先修改 `src/styles/_variables.scss` 中的变量 +2. 添加新的混入到 `src/styles/_mixins.scss` +3. 组件样式添加到 `_components.scss` +4. 页面特定样式添加到 `global.scss` + +### 调试技巧 +1. 使用 `pnpm dev` 启动开发服务器 +2. 检查浏览器控制台的localStorage操作 +3. 使用React Developer Tools检查组件状态 +4. 检查网络面板确认资源加载 + +## 更新日志 + +### 2026-03-19 +- 完成从静态HTML原型到React项目的重构 +- 实现四大核心模块:首页、工作台、管理台、开发台 +- 集成react-icons图标库 +- 实现SCSS样式模块化 +- 配置Vite单文件打包 +- 实现导航状态持久化 +- 区分主页跳转和刷新浏览器行为 + +### 2026-03-19(功能更新) +- 新增登录页面,支持验证码防爆破 +- 工作台定时任务支持查看详情、执行日志 +- 管理台新增用户/部门/项目管理支持新增表单 +- 新增ListSelector通用列表选择器组件 +- 日志查询支持按用户、类型、状态筛选 +- 用户管理支持管理员/开发者/成员角色区分 +- 优化页面布局和样式 + +## 联系方式 + +- 项目原型演示用途 +- 基于GrandClaw团队设计 +- 前端技术栈:React + Vite + SCSS + +--- + +*最后更新:2026-03-19* diff --git a/index.html b/index.html new file mode 100644 index 0000000..ad21b6e --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + grandclaw-archtype + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..1b8c46e --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "grandclaw-archtype", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "dependencies": { + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-icons": "^5.5.0", + "react-router-dom": "^7.13.1" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "globals": "^17.4.0", + "sass": "^1.98.0", + "vite": "^8.0.1", + "vite-plugin-singlefile": "^2.3.2" + } +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..f54f706 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,22 @@ +import { HashRouter as Router, Routes, Route } from 'react-router-dom'; +import HomePage from './pages/HomePage.jsx'; +import LoginPage from './pages/LoginPage.jsx'; +import ConsolePage from './pages/ConsolePage.jsx'; +import AdminPage from './pages/AdminPage.jsx'; +import DeveloperPage from './pages/DeveloperPage.jsx'; + +function App() { + return ( + + + } /> + } /> + } /> + } /> + } /> + + + ); +} + +export default App; \ No newline at end of file diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx new file mode 100644 index 0000000..9f2d53f --- /dev/null +++ b/src/components/Layout.jsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import { FiMenu } from 'react-icons/fi'; + +function Layout({ sidebar, headerTitle, children, sidebarClassName = 'sidebar', contentClassName = '' }) { + const [sidebarOpen, setSidebarOpen] = useState(false); + + const toggleSidebar = () => { + setSidebarOpen(!sidebarOpen); + }; + + return ( +
+ {sidebarOpen && ( +
+ )} + +
+
+
+
+ +
+
{headerTitle}
+
+
+
+ {children} +
+
+
+ ); +} + +export default Layout; \ No newline at end of file diff --git a/src/components/ListSelector.jsx b/src/components/ListSelector.jsx new file mode 100644 index 0000000..7a5ef9b --- /dev/null +++ b/src/components/ListSelector.jsx @@ -0,0 +1,93 @@ +import { useState } from 'react'; + +function ListSelector({ + data = [], + selectedIds = [], + onChange, + searchPlaceholder = '搜索...', + columns = [], + multiSelect = false, + selectedLabel, + onClearSelected +}) { + const [searchKeyword, setSearchKeyword] = useState(''); + + const filteredData = data.filter(item => { + return columns.some(col => { + const value = item[col.key]; + return typeof value === 'string' && value.includes(searchKeyword); + }); + }); + + const handleSelect = (item) => { + if (multiSelect) { + const newSelected = selectedIds.includes(item.id) + ? selectedIds.filter(id => id !== item.id) + : [...selectedIds, item.id]; + onChange(newSelected); + } else { + onChange(item.id); + } + }; + + const isSelected = (item) => { + return multiSelect + ? selectedIds.includes(item.id) + : selectedIds === item.id; + }; + + return ( +
+ setSearchKeyword(e.target.value)} + /> + {selectedLabel && ( +
+ {selectedLabel} + × +
+ )} +
+ + + + + {columns.map(col => ( + + ))} + + + + {filteredData.map(item => ( + + + {columns.map(col => ( + + ))} + + ))} + +
{col.label}
+ handleSelect(item)} + style={{ width: '16px', height: '16px', cursor: 'pointer' }} + /> + + {col.render ? col.render(item) : item[col.key]} +
+
+
+ ); +} + +export default ListSelector; diff --git a/src/data/conversations.js b/src/data/conversations.js new file mode 100644 index 0000000..6a9c7d1 --- /dev/null +++ b/src/data/conversations.js @@ -0,0 +1,218 @@ + + +export const conversations = [ + { id: 'welcome', title: '新对话', time: '欢迎页', scene: 'welcome', status: 'running' }, + { id: 'text', title: '代码重构方案讨论', time: '普通对话', scene: 'text', status: 'running' }, + { id: 'skill', title: '查询客户数据', time: '调用 Skill', scene: 'skill', status: 'running' }, + { id: 'file', title: '分析上传的报表', time: '上传文件', scene: 'file', status: 'running' }, + { id: 'starting', title: '文档生成助手', time: '启动中', scene: 'starting', status: 'starting' } +]; + +export function getChatScenes() { + return { + welcome: ` +
+

你好,我是 GrandClaw

+

企业级智能助手,已为你接通3个技能

+
+
+
💻 代码生成
+
帮我生成一段 Python 代码
+
+
+
📊 数据分析
+
分析一下这份数据
+
+
+
📄 文档撰写
+
帮我写一份项目周报
+
+
+
👥 业务查询
+
查询一下客户信息
+
+
+
+ `, + starting: ` +
+
+
+
+

正在启动对话实例...

+

预计需要 3-5 秒,请稍候

+
+
+ + 加载模型配置 +
+
+ + 初始化技能 +
+
+ + + + 恢复对话上下文 +
+
+
+ `, + text: ` +
+
+
+
帮我重构这段代码,让它更简洁高效
+
14:30
+
+
+
+
🤖
+
+
+
+ + 已深度思考 +
+
+

让我分析一下代码重构的思路:

+
    +
  • 首先识别代码中的重复模式和冗余逻辑
  • +
  • 考虑使用 Python 的列表推导式和生成器表达式来简化循环
  • +
  • 评估是否可以引入高阶函数如 map/filter/replace
  • +
  • 检查异常处理是否可以统一抽象
  • +
  • 评估类型提示的添加位置,确保不影响性能
  • +
+
+
+
+

好的,我来帮你重构这段代码。首先让我分析一下现有代码的问题,然后提供优化方案。

+

优化建议:

+
    +
  • 使用列表推导式替代循环
  • +
  • 添加类型提示提高可读性
  • +
  • 提取公共逻辑为独立函数
  • +
+
+
14:31
+
+
+ `, + skill: ` +
+
+
+
帮我查询一下客户 "张三" 的最近订单
+
10:15
+
+
+
+
🤖
+
+
+

+ 🧩 已加载CRM客户查询技能 +

+
+
10:15
+
+
+
+
🤖
+
+
+
+ + 已深度思考 +
+
+

正在调用CRM客户查询技能...

+
    +
  • 识别用户意图:查询客户"张三"的订单信息
  • +
  • 匹配技能:CRM 客户查询 v1.2.0
  • +
  • 构建查询参数:姓名="张三",时间范围=最近一年
  • +
  • 执行查询,获取结果
  • +
  • 格式化输出,突出关键信息(等级、订单数、金额)
  • +
+
+
+
+

客户信息查询结果:

+
+

客户姓名:张三

+

客户等级:VIP

+

最近订单:3笔(总计 ¥28,560)

+

最后联系:2026-03-10

+
+
+
10:16
+
+
+ `, + file: ` +
+
+
+
+

帮我分析一下这个 Excel 文件里的销售数据

+
+ 📊 +
+
Q1销售数据.xlsx
+
2.4 MB
+
+
+
+
16:20
+
+
+
+
🤖
+
+
+

文件已接收!正在分析数据...

+
+
+
+
+
+
+
16:20
+
+
+
+
🤖
+
+
+
+ + 已深度思考 +
+
+

正在分析 Excel 文件中的销售数据...

+
    +
  • 文件格式识别:Excel (.xlsx),大小 2.4MB
  • +
  • 数据结构解析:包含日期、产品、销售额等字段
  • +
  • 时间范围:2026年 Q1(1-3月)
  • +
  • 计算指标:总销售额、同比增长、月度趋势、产品占比
  • +
  • 异常检测:无异常值或缺失数据
  • +
  • 关键洞察提取:3月表现突出,产品C增长势头强劲
  • +
+
+
+
+

数据分析完成!以下是关键发现:

+
    +
  • Q1 总销售额:¥128.5 万,同比增长 18%
  • +
  • 3 月份表现最佳,单月突破 50 万
  • +
  • 产品 A 占比最高(42%),产品 C 增长最快
  • +
+

需要生成可视化图表吗?

+
+
16:22
+
+
+ ` + }; +} \ No newline at end of file diff --git a/src/data/developerData.js b/src/data/developerData.js new file mode 100644 index 0000000..c381c96 --- /dev/null +++ b/src/data/developerData.js @@ -0,0 +1,92 @@ +export const mySkills = [ + { + id: 1, + name: '天气查询助手', + desc: '根据城市名称查询当前天气和未来预报,支持全国主要城市', + status: 'published', + version: '1.2.0', + category: '信息查询', + tags: ['天气', '查询', '生活'], + modelSupport: ['Doubao-pro', 'GPT-4', 'Claude-3'], + lastModified: '2026-03-18', + installs: 156, + rating: 4.7, + versions: [ + { version: '1.2.0', date: '2026-03-18', desc: '新增支持未来7天预报', current: true, status: 'approved', enabled: true }, + { version: '1.1.0', date: '2026-03-10', desc: '优化响应速度', current: false, status: 'approved', enabled: false }, + { version: '1.0.0', date: '2026-03-01', desc: '初始版本', current: false, status: 'approved', enabled: false } + ], + package: { + name: 'weather-assistant-v1.2.0.zip', + size: '2.4 MB', + uploadDate: '2026-03-18 14:30' + } + }, + { + id: 2, + name: '待办事项管理', + desc: '帮助用户管理日常待办事项,支持添加、完成、删除操作', + status: 'draft', + version: '0.1.0', + category: '效率工具', + tags: ['待办', '管理', '效率'], + modelSupport: ['Doubao-pro', 'Claude-3'], + lastModified: '2026-03-17', + installs: 0, + rating: 0, + versions: [ + { version: '0.1.0', date: '2026-03-17', desc: '开发中版本', current: true, status: 'pending', enabled: false } + ], + package: { + name: 'todo-manager-v0.1.0.zip', + size: '1.8 MB', + uploadDate: '2026-03-17 10:15' + } + }, + { + id: 3, + name: '代码审查助手', + desc: '自动审查代码质量,提供优化建议和潜在问题检测', + status: 'published', + version: '2.0.1', + category: '开发工具', + tags: ['代码', '审查', '开发'], + modelSupport: ['Claude-3', 'GPT-4'], + lastModified: '2026-03-15', + installs: 342, + rating: 4.9, + versions: [ + { version: '2.0.1', date: '2026-03-15', desc: '修复 Python 代码审查问题', current: true, status: 'approved', enabled: true }, + { version: '2.0.0', date: '2026-03-10', desc: '修复安全问题', current: false, status: 'rejected', enabled: false }, + { version: '2.0.0', date: '2026-03-08', desc: '支持多语言审查', current: false, status: 'approved', enabled: false }, + { version: '1.0.0', date: '2026-02-20', desc: '初始版本', current: false, status: 'approved', enabled: false } + ], + package: { + name: 'code-reviewer-v2.0.1.zip', + size: '3.2 MB', + uploadDate: '2026-03-15 16:45' + } + } +]; + +export const skillCategories = ['信息查询', '效率工具', '开发工具', '数据分析', '文档处理', '业务系统']; + +export const supportedModels = [ + { id: 'doubao-pro', name: 'Doubao-pro', provider: '字节跳动' }, + { id: 'doubao-lite', name: 'Doubao-lite', provider: '字节跳动' }, + { id: 'gpt-4', name: 'GPT-4', provider: 'OpenAI' }, + { id: 'gpt-3.5', name: 'GPT-3.5 Turbo', provider: 'OpenAI' }, + { id: 'claude-3', name: 'Claude-3 Opus', provider: 'Anthropic' }, + { id: 'claude-3-haiku', name: 'Claude-3 Haiku', provider: 'Anthropic' } +]; + +export const devDocs = [ + { id: 1, title: '快速开始', category: '入门指南', content: '介绍如何开发并上传第一个技能...' }, + { id: 2, title: '技能包规范', category: '入门指南', content: '技能包的目录结构和必要文件说明...' }, + { id: 3, title: 'skill.json 配置', category: '入门指南', content: '详细说明 skill.json 各配置项...' }, + { id: 4, title: '技能开发 API', category: 'API参考', content: '技能可调用的平台接口...' }, + { id: 5, title: '上下文获取', category: 'API参考', content: '如何获取对话上下文和用户信息...' }, + { id: 6, title: '工具调用规范', category: 'API参考', content: '定义和使用工具函数的规范...' }, + { id: 7, title: '版本管理指南', category: '发布管理', content: '版本号规则和升级策略...' }, + { id: 8, title: '发布审核流程', category: '发布管理', content: '技能发布后的审核和上线流程...' } +]; \ No newline at end of file diff --git a/src/data/logs.js b/src/data/logs.js new file mode 100644 index 0000000..78600af --- /dev/null +++ b/src/data/logs.js @@ -0,0 +1,14 @@ +export const logs = [ + { time: '2026-03-19 16:42:33', user: '张三', type: '实例操作', action: '启动实例', status: '成功', detail: '实例 teleclaw-zhangsan 启动成功' }, + { time: '2026-03-19 15:28:17', user: '张三', type: '技能', action: '调用 代码生成助手', status: '成功', detail: 'Token 消耗: 1,234' }, + { time: '2026-03-19 14:55:02', user: '李四', type: '文件上传', action: '上传数据文件', status: '成功', detail: '文件 sales_2026_q1.xlsx 上传完成' }, + { time: '2026-03-19 12:30:45', user: '王五', type: '实例操作', action: '启动实例', status: '失败', detail: '资源配额不足,请联系管理员' }, + { time: '2026-03-19 11:20:33', user: '张三', type: '配置修改', action: '更新模型偏好', status: '成功', detail: 'Doubao-lite-128k → Doubao-pro-32k' }, + { time: '2026-03-19 10:45:18', user: '李四', type: '技能', action: '订阅 文档智能撰写', status: '成功', detail: 'Skill v1.2.0 已挂载' }, + { time: '2026-03-19 09:15:07', user: '张三', type: '实例操作', action: '停止实例', status: '成功', detail: '实例运行时长: 6小时23分' }, + { time: '2026-03-19 09:10:22', user: '李四', type: '登录', action: '用户登录', status: '成功', detail: 'IP: 192.168.1.105' }, + { time: '2026-03-18 18:32:11', user: '王五', type: '实例操作', action: '重启实例', status: '警告', detail: '实例异常重启,请检查运行状态' }, + { time: '2026-03-18 16:55:40', user: '张三', type: '文件上传', action: '上传数据文件', status: '失败', detail: '文件大小超过限制 (最大 50MB)' }, + { time: '2026-03-18 14:20:05', user: '张三', type: '配置修改', action: '更新 API Key', status: '成功', detail: 'API Key 已更新' }, + { time: '2026-03-18 11:08:55', user: '李四', type: '技能', action: '调用 CRM客户查询', status: '成功', detail: '查询结果: 23条记录' } +]; \ No newline at end of file diff --git a/src/data/members.js b/src/data/members.js new file mode 100644 index 0000000..6ab6e55 --- /dev/null +++ b/src/data/members.js @@ -0,0 +1,12 @@ +export const projectMembers = [ + { id: 1, name: '张三', role: '管理员', skills: ['代码生成助手', '数据分析专家', '文档智能撰写'] }, + { id: 2, name: '李四', role: '成员', skills: ['代码生成助手', '文档智能撰写'] }, + { id: 3, name: '王五', role: '成员', skills: ['数据分析专家'] }, + { id: 4, name: '赵六', role: '管理员', skills: ['代码生成助手', '数据分析专家', '文档智能撰写', 'CRM 客户查询'] }, + { id: 5, name: '钱七', role: '成员', skills: ['文档智能撰写'] }, + { id: 6, name: '孙八', role: '成员', skills: ['代码生成助手', 'CRM 客户查询'] }, + { id: 7, name: '周九', role: '成员', skills: [] }, + { id: 8, name: '吴十', role: '成员', skills: ['数据分析专家', '文档智能撰写'] } +]; + +export const allSkillsList = ['代码生成助手', '数据分析专家', '文档智能撰写', 'CRM 客户查询', '财务数据同步', '网络故障排查']; \ No newline at end of file diff --git a/src/data/skills.js b/src/data/skills.js new file mode 100644 index 0000000..bb9b9e7 --- /dev/null +++ b/src/data/skills.js @@ -0,0 +1,31 @@ +// skills data + +export const skills = [ + { id: 1, name: '代码生成助手', author: 'GrandClaw Team', desc: '根据需求自动生成高质量代码,支持多种编程语言', tags: ['开发', '代码', 'AI'], subs: 1256, rating: 4.8, subscribed: true }, + { id: 2, name: '数据分析专家', author: 'DataLab', desc: '智能分析数据,生成可视化图表和洞察报告', tags: ['数据', '分析', '可视化'], subs: 892, rating: 4.7, subscribed: true }, + { id: 3, name: '文档智能撰写', author: 'DocAI', desc: '帮助撰写各种文档,包括报告、邮件、技术文档等', tags: ['文档', '写作', '办公'], subs: 2103, rating: 4.9, subscribed: true }, + { id: 4, name: 'CRM 客户查询', author: 'Telecom', desc: '对接企业CRM系统,快速查询客户信息和订单状态', tags: ['业务', 'CRM', '客户'], subs: 567, rating: 4.5, subscribed: false }, + { id: 5, name: '财务数据同步', author: 'Finance Team', desc: '自动同步财务系统数据,生成费用报表', tags: ['财务', '报表', '同步'], subs: 432, rating: 4.6, subscribed: false }, + { id: 6, name: '网络故障排查', author: 'NetOps', desc: '智能诊断网络问题,提供故障排除方案', tags: ['运维', '网络', '诊断'], subs: 789, rating: 4.8, subscribed: false } +]; + +export const skillFiles = [ + { name: 'skill.json', size: '2.4 KB', type: '配置文件' }, + { name: 'main.py', size: '8.2 KB', type: '代码文件' }, + { name: 'requirements.txt', size: '1.1 KB', type: '依赖文件' }, + { name: 'README.md', size: '4.5 KB', type: '说明文档' } +]; + +export const skillVersions = [ + { version: 'v1.3.0', date: '2026-03-12', desc: '新增 Python 3.11 支持', current: true }, + { version: 'v1.2.1', date: '2026-03-08', desc: '修复若干已知问题', current: false }, + { version: 'v1.2.0', date: '2026-03-01', desc: '优化性能,提升响应速度 30%', current: false }, + { version: 'v1.1.0', date: '2026-02-15', desc: '新增 JavaScript 支持', current: false } +]; + +// 技能图标映射 +const skillIcons = ['💻', '📊', '📝', '👥', '📈', '🔧']; + +export function getSkillIcon(id) { + return skillIcons[(id - 1) % skillIcons.length]; +} \ No newline at end of file diff --git a/src/data/tasks.js b/src/data/tasks.js new file mode 100644 index 0000000..82d201b --- /dev/null +++ b/src/data/tasks.js @@ -0,0 +1,62 @@ +export const scheduledTasks = [ + { + id: 1, + name: '每日数据同步', + frequency: '每天 02:00', + lastTriggered: '2026-03-18 02:00:15', + nextTrigger: '2026-03-19 02:00:00', + enabled: true, + lastStatus: '失败', + prompt: '请连接数据源服务器,自动同步昨天的销售数据、客户信息和库存记录到本地数据库。同步完成后生成数据同步报告,包括同步记录数、耗时和异常情况。', + logs: [ + { time: '2026-03-18 02:00:15', status: '失败', message: '数据源连接超时' }, + { time: '2026-03-17 02:00:08', status: '成功', message: '数据同步完成,同步记录 1,128 条' }, + { time: '2026-03-16 02:00:22', status: '失败', message: '数据源连接超时' } + ] + }, + { + id: 2, + name: '每周报表生成', + frequency: '每周一 08:00', + lastTriggered: '2026-03-17 08:00:45', + nextTrigger: '2026-03-24 08:00:00', + enabled: true, + lastStatus: '成功', + prompt: '请分析本周的销售数据,生成一份包含销售趋势、区域表现、产品排行的周报。报告需要包含可视化图表和关键指标解读,完成后发送给所有订阅用户。', + logs: [ + { time: '2026-03-17 08:00:45', status: '成功', message: '报表生成完成,已发送至 12 个用户' }, + { time: '2026-03-10 08:01:02', status: '成功', message: '报表生成完成,已发送至 12 个用户' }, + { time: '2026-03-03 08:00:33', status: '成功', message: '报表生成完成,已发送至 10 个用户' } + ] + }, + { + id: 3, + name: '系统备份', + frequency: '每周日 03:00', + lastTriggered: '2026-03-16 03:12:05', + nextTrigger: '2026-03-23 03:00:00', + enabled: false, + lastStatus: '失败', + prompt: '请执行数据库全量备份,包括用户数据、业务数据、日志记录。将备份文件压缩并上传至云存储,同时清理超过30天的旧备份文件。', + logs: [ + { time: '2026-03-16 03:12:05', status: '失败', message: '备份失败,磁盘空间不足' }, + { time: '2026-03-09 03:05:33', status: '成功', message: '备份完成,数据量 2.5GB' }, + { time: '2026-03-02 03:02:18', status: '成功', message: '备份完成,数据量 2.3GB' } + ] + }, + { + id: 4, + name: '日志清理', + frequency: '每月1日 04:00', + lastTriggered: '2026-03-01 04:00:30', + nextTrigger: '2026-04-01 04:00:00', + enabled: true, + lastStatus: '成功', + prompt: '请扫描并清理超过90天的系统日志和操作日志,计算清理的记录数和释放的存储空间,生成清理报告。', + logs: [ + { time: '2026-03-01 04:00:30', status: '成功', message: '清理完成,删除日志 3,456 条,释放空间 1.2GB' }, + { time: '2026-02-01 04:00:15', status: '成功', message: '清理完成,删除日志 2,890 条,释放空间 0.9GB' }, + { time: '2026-01-01 04:00:42', status: '成功', message: '清理完成,删除日志 3,102 条,释放空间 1.1GB' } + ] + } +]; \ No newline at end of file diff --git a/src/hooks/useLocalStorage.js b/src/hooks/useLocalStorage.js new file mode 100644 index 0000000..7c717c4 --- /dev/null +++ b/src/hooks/useLocalStorage.js @@ -0,0 +1,25 @@ +import { useState, useEffect } from 'react'; + +function useLocalStorage(key, initialValue) { + const [value, setValue] = useState(() => { + try { + const saved = localStorage.getItem(key); + return saved ? JSON.parse(saved) : initialValue; + } catch (error) { + console.warn(`Error reading localStorage key "${key}":`, error); + return initialValue; + } + }); + + useEffect(() => { + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch (error) { + console.warn(`Error setting localStorage key "${key}":`, error); + } + }, [key, value]); + + return [value, setValue]; +} + +export default useLocalStorage; \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..05f4142 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './styles/global.scss' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/src/pages/AdminPage.jsx b/src/pages/AdminPage.jsx new file mode 100644 index 0000000..40c049c --- /dev/null +++ b/src/pages/AdminPage.jsx @@ -0,0 +1,130 @@ +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 OverviewPage from './admin/OverviewPage.jsx'; +import DepartmentsPage from './admin/DepartmentsPage.jsx'; +import UsersPage from './admin/UsersPage.jsx'; +import AdminProjectsPage from './admin/AdminProjectsPage.jsx'; +import AddDepartmentPage from './admin/AddDepartmentPage.jsx'; +import AddUserPage from './admin/AddUserPage.jsx'; +import AddProjectPage from './admin/AddProjectPage.jsx'; + +function AdminPage() { + const location = useLocation(); + const navigate = useNavigate(); + const [currentPage, setCurrentPage] = useState(() => { + return localStorage.getItem('admin_currentPage') || 'overview'; + }); + + 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': + return ; + case 'departments': + return setCurrentPage('addDepartment')} />; + case 'users': + return setCurrentPage('addUser')} />; + case 'projects': + return setCurrentPage('addProject')} />; + case 'addDepartment': + return setCurrentPage('departments')} />; + case 'addUser': + return setCurrentPage('users')} />; + case 'addProject': + return setCurrentPage('projects')} />; + default: + return
Page not found
; + } + }; + + const getPageTitle = () => { + const titles = { + overview: '总览', + departments: '部门管理', + users: '用户管理', + projects: '项目管理', + addDepartment: '新增部门', + addUser: '新增用户', + addProject: '新增项目' + }; + return titles[currentPage] || ''; + }; + + const sidebar = ( + <> +
+
+
+ + +
+
+
GrandClaw
+
运营管理台
+
+
+
+ +
+
+
+
张三
+
系统管理员
+
+
+ + ); + + return ( + + {renderPage()} + + ); +} + +export default AdminPage; \ No newline at end of file diff --git a/src/pages/ConsolePage.jsx b/src/pages/ConsolePage.jsx new file mode 100644 index 0000000..8a6f79b --- /dev/null +++ b/src/pages/ConsolePage.jsx @@ -0,0 +1,228 @@ +import { useState, useEffect } from 'react'; +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 ChatPage from './console/ChatPage.jsx'; +import SkillsPage from './console/SkillsPage.jsx'; +import SkillDetailPage from './console/SkillDetailPage.jsx'; +import LogsPage from './console/LogsPage.jsx'; +import TasksPage from './console/TasksPage.jsx'; +import TaskDetailPage from './console/TaskDetailPage.jsx'; +import AccountPage from './console/AccountPage.jsx'; +import ProjectsPage from './console/ProjectsPage.jsx'; +import MemberConfigPage from './console/MemberConfigPage.jsx'; +import AddMemberPage from './console/AddMemberPage.jsx'; + +function ConsolePage() { + const location = useLocation(); + const navigate = useNavigate(); + const [currentPage, setCurrentPage] = useState(() => { + return localStorage.getItem('console_currentPage') || 'chat'; + }); + const [currentScene, setCurrentScene] = useState(() => { + return localStorage.getItem('console_currentScene') || 'welcome'; + }); + const [currentSkillId, setCurrentSkillId] = useState(null); + const [currentTaskId, setCurrentTaskId] = useState(null); + + useEffect(() => { + if (location.state?.fromHome) { + setCurrentPage('chat'); + setCurrentScene('welcome'); + navigate('.', { replace: true, state: {} }); + } + }, [location.state, navigate, setCurrentPage, setCurrentScene]); + + useEffect(() => { + localStorage.setItem('console_currentPage', currentPage); + }, [currentPage]); + + useEffect(() => { + localStorage.setItem('console_currentScene', currentScene); + }, [currentScene]); + + const switchPage = (pageId, data = {}) => { + setCurrentPage(pageId); + if (data.skillId !== undefined) { + setCurrentSkillId(data.skillId); + } + }; + + const handleSkillClick = (skillId) => { + switchPage('skillDetail', { skillId }); + }; + + const handleBack = () => { + switchPage('skills'); + }; + + const switchChatScene = (scene) => { + setCurrentScene(scene); + if (currentPage !== 'chat') { + setCurrentPage('chat'); + } + }; + + const createNewChat = () => { + setCurrentScene('welcome'); + setCurrentPage('chat'); + }; + + const activeScene = currentPage === 'chat' ? currentScene : null; + + const renderPage = () => { + switch (currentPage) { + case 'chat': + return ; + case 'skills': + return ; + case 'skillDetail': + return ; + case 'logs': + return ; + case 'scheduledTasks': + return { + setCurrentTaskId(taskId); + switchPage('taskDetail'); + }} + />; + case 'taskDetail': + return switchPage('scheduledTasks')} + />; + case 'account': + return ; + case 'projects': + return switchPage('addMember')} />; + case 'memberConfig': + return switchPage('projects')} />; + case 'addMember': + return switchPage('projects')} />; + default: + return
Page not found
; + } + }; + + const getPageTitle = () => { + const pageTitles = { + chat: '智能助手', + skills: '技能市场', + skillDetail: '技能详情', + logs: '日志查询', + scheduledTasks: '定时任务', + taskDetail: '任务详情', + account: '账号管理', + projects: '项目管理', + memberConfig: '成员配置', + addMember: '增加成员' + }; + let title = pageTitles[currentPage] || ''; + if (currentPage === 'chat') { + const conv = conversations.find(c => c.scene === currentScene); + title = conv?.title || '智能助手'; + } + if (currentPage === 'skillDetail' && currentSkillId) { + const skill = skills.find(s => s.id === currentSkillId); + title = skill?.name || '技能详情'; + } + return title; + }; + + const sidebar = ( + <> +
+
+
+ + +
+
+
GrandClaw
+
企业级AI平台
+
+
+
+ +
+
+ {conversations.map(conv => ( +
switchChatScene(conv.scene)} + > +
{conv.title}
+
{conv.time}
+
+ ))} +
+
+ + +
+
+
switchPage('skills')} + > + + 技能市场 +
+
switchPage('scheduledTasks')} + > + + 定时任务 +
+
switchPage('logs')} + > + + 日志查询 +
+
switchPage('projects')} + > + + 项目管理 +
+
+
switchPage('account')}> +
+
+
张三
+
AI 产品部
+
+
+ + ); + + return ( + + {renderPage()} + + ); +} + +export default ConsolePage; \ No newline at end of file diff --git a/src/pages/DeveloperPage.jsx b/src/pages/DeveloperPage.jsx new file mode 100644 index 0000000..aca4c38 --- /dev/null +++ b/src/pages/DeveloperPage.jsx @@ -0,0 +1,173 @@ +import { useState, useEffect } from 'react'; +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 MySkillsPage from './developer/MySkillsPage.jsx'; +import UploadSkillPage from './developer/UploadSkillPage.jsx'; +import NewVersionPage from './developer/NewVersionPage.jsx'; +import DevDocsPage from './developer/DevDocsPage.jsx'; +import DevAccountPage from './developer/DevAccountPage.jsx'; +import SkillEditorPage from './developer/SkillEditorPage.jsx'; + +function DeveloperPage() { + const location = useLocation(); + const navigate = useNavigate(); + const [currentPage, setCurrentPage] = useState(() => { + return localStorage.getItem('developer_currentPage') || 'mySkills'; + }); + const [currentSkillId, setCurrentSkillId] = useState(() => { + const saved = localStorage.getItem('developer_currentSkillId'); + return saved ? JSON.parse(saved) : null; + }); + const [newVersionSkillName, setNewVersionSkillName] = useState(''); + + useEffect(() => { + if (location.state?.fromHome) { + setCurrentPage('mySkills'); + setCurrentSkillId(null); + navigate('.', { replace: true, state: {} }); + } + }, [location.state, navigate, setCurrentPage, setCurrentSkillId]); + + useEffect(() => { + localStorage.setItem('developer_currentPage', currentPage); + }, [currentPage]); + + useEffect(() => { + localStorage.setItem('developer_currentSkillId', JSON.stringify(currentSkillId)); + }, [currentSkillId]); + + const switchPage = (pageId, data = {}) => { + setCurrentPage(pageId); + if (data.skillId !== undefined) { + setCurrentSkillId(data.skillId); + } + }; + + const openSkillEditor = (skillId) => { + setCurrentSkillId(skillId); + setCurrentPage('skillEditor'); + }; + + const createNewProject = () => { + setCurrentPage('uploadSkill'); + }; + + const openNewVersionPage = (skillName) => { + setNewVersionSkillName(skillName); + setCurrentPage('newVersion'); + }; + + const handleBack = () => { + setCurrentPage('mySkills'); + setCurrentSkillId(null); + }; + + const handleNewVersionBack = () => { + setCurrentPage('skillEditor'); + setNewVersionSkillName(''); + }; + + const renderPage = () => { + switch (currentPage) { + case 'mySkills': + return ; + case 'uploadSkill': + return ; + case 'devDocs': + return ; + case 'devAccount': + return ; + case 'skillEditor': + return ; + case 'newVersion': + return ; + default: + return
Page not found
; + } + }; + + const getPageTitle = () => { + const titles = { + mySkills: '我的技能', + uploadSkill: '创建技能', + newVersion: '上传新版本', + devDocs: '开发文档', + devAccount: '开发者设置', + skillEditor: '技能详情' + }; + return titles[currentPage] || ''; + }; + + const sidebar = ( + <> +
+
+
+ + +
+
+
GrandClaw
+
技能开发台
+
+
+
+ +
+
+ {mySkills.map(skill => ( +
openSkillEditor(skill.id)} + > +
{skill.name}
+
{skill.status === 'published' ? '已发布' : '草稿'}
+
+ ))} +
+
+
switchPage('mySkills')} + > + + 我的技能 +
+
switchPage('devDocs')} + > + + 开发文档 +
+
+
switchPage('devAccount')}> +
+
+
张三
+
开发者
+
+
+ + ); + + return ( + + {renderPage()} + + ); +} + +export default DeveloperPage; \ No newline at end of file diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx new file mode 100644 index 0000000..12155c3 --- /dev/null +++ b/src/pages/HomePage.jsx @@ -0,0 +1,76 @@ +import { Link } from 'react-router-dom'; +import { FiSettings, FiCode, FiUsers, FiMonitor, FiList, FiLogIn } from 'react-icons/fi'; +import { FaRobot, FaPuzzlePiece } from 'react-icons/fa'; + +function HomePage() { + return ( +
+
+
+
+ + +
+ GrandClaw +
+ +
+
+
+ + 企业级 AI 平台 +
+

智能 企业助手

+

+ 基于容器化实例的 智能助手平台,提供租户隔离、技能市场、安全审计等核心能力 +

+
+ + 进入工作台 + +
+
+
+
+ +
+
容器化隔离
+
每租户独享独立容器实例,天然物理隔离,数据安全有保障
+
+
+
+ +
+
技能市场
+
丰富的技能生态,按需订阅,动态挂载,无缝扩展能力
+
+
+
+ +
+
安全审计
+
完整的操作日志、运行日志、计费统计,满足企业合规要求
+
+
+
+
+ © 2026 GrandClaw Team · 前端原型演示 +
+
+ ); +} + +export default HomePage; \ No newline at end of file diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx new file mode 100644 index 0000000..13bb589 --- /dev/null +++ b/src/pages/LoginPage.jsx @@ -0,0 +1,141 @@ +import { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { FiArrowLeft } from 'react-icons/fi'; + +function generateCode() { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + let code = ''; + for (let i = 0; i < 4; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return code; +} + +function LoginPage() { + const navigate = useNavigate(); + const [captchaCode, setCaptchaCode] = useState(generateCode()); + const [captchaInput, setCaptchaInput] = useState(''); + + const refreshCaptcha = () => { + setCaptchaCode(generateCode()); + setCaptchaInput(''); + }; + + return ( +
+
+
+
+
+ + +
+ GrandClaw +
+
+ +
+

+ 欢迎回来 +

+

+ 请登录您的账号以继续 +

+ +
+ +
+
+ +
+
+
+ setCaptchaInput(e.target.value)} + style={{ flex: 1 }} + /> +
+ {captchaCode} +
+
+
+
+ + + 忘记密码? + +
+ +
+ +
+ + 返回首页 + +
+
+
+ ); +} + +export default LoginPage; diff --git a/src/pages/admin/AddDepartmentPage.jsx b/src/pages/admin/AddDepartmentPage.jsx new file mode 100644 index 0000000..60ff687 --- /dev/null +++ b/src/pages/admin/AddDepartmentPage.jsx @@ -0,0 +1,61 @@ +import { useState } from 'react'; +import ListSelector from '../../components/ListSelector.jsx'; + +const availableLeaders = [ + { id: 1, name: '张三', department: 'AI 产品部', phone: '138****8888', email: 'zhangsan@example.com' }, + { id: 2, name: '李四', department: '技术研发部', phone: '139****1234', email: 'lisi@example.com' }, + { id: 3, name: '王五', department: '数据分析部', phone: '136****5678', email: 'wangwu@example.com' }, + { id: 4, name: '赵六', department: 'AI 产品部', phone: '135****9012', email: 'zhaoliu@example.com' }, + { id: 5, name: '钱七', department: '运营部', phone: '137****3456', email: 'qianqi@example.com' } +]; + +function AddDepartmentPage({ onBack }) { + const [selectedLeader, setSelectedLeader] = useState(null); + + const leaderColumns = [ + { key: 'name', label: '姓名' }, + { key: 'phone', label: '联系电话' }, + { key: 'email', label: '邮箱' } + ]; + + const selectedLabel = selectedLeader + ? `${availableLeaders.find(l => l.id === selectedLeader)?.name} - ${availableLeaders.find(l => l.id === selectedLeader)?.department}` + : null; + + return ( +
+
+
新增部门
+
+
+
+ + +
+
+ + +
+
+ + setSelectedLeader(null)} + /> +
+
+ + +
+
+
+ ); +} + +export default AddDepartmentPage; diff --git a/src/pages/admin/AddProjectPage.jsx b/src/pages/admin/AddProjectPage.jsx new file mode 100644 index 0000000..ba5b764 --- /dev/null +++ b/src/pages/admin/AddProjectPage.jsx @@ -0,0 +1,61 @@ +import { useState } from 'react'; +import ListSelector from '../../components/ListSelector.jsx'; + +const availableLeaders = [ + { id: 1, name: '张三', department: 'AI 产品部', phone: '138****8888', email: 'zhangsan@example.com' }, + { id: 2, name: '李四', department: '技术研发部', phone: '139****1234', email: 'lisi@example.com' }, + { id: 3, name: '王五', department: '数据分析部', phone: '136****5678', email: 'wangwu@example.com' }, + { id: 4, name: '赵六', department: 'AI 产品部', phone: '135****9012', email: 'zhaoliu@example.com' }, + { id: 5, name: '钱七', department: '运营部', phone: '137****3456', email: 'qianqi@example.com' } +]; + +function AddProjectPage({ onBack }) { + const [selectedLeader, setSelectedLeader] = useState(null); + + const leaderColumns = [ + { key: 'name', label: '姓名' }, + { key: 'department', label: '部门' }, + { key: 'phone', label: '联系电话' } + ]; + + const selectedLabel = selectedLeader + ? `${availableLeaders.find(l => l.id === selectedLeader)?.name} - ${availableLeaders.find(l => l.id === selectedLeader)?.department}` + : null; + + return ( +
+
+
新增项目
+
+
+
+ + +
+
+ + +
+
+ + setSelectedLeader(null)} + /> +
+
+ + +
+
+
+ ); +} + +export default AddProjectPage; diff --git a/src/pages/admin/AddUserPage.jsx b/src/pages/admin/AddUserPage.jsx new file mode 100644 index 0000000..2d4e65b --- /dev/null +++ b/src/pages/admin/AddUserPage.jsx @@ -0,0 +1,74 @@ +import { useState } from 'react'; +import ListSelector from '../../components/ListSelector.jsx'; + +const availableDepartments = [ + { id: 1, name: 'AI 产品部', description: '负责AI产品规划与设计', head: '张三', memberCount: 8 }, + { id: 2, name: '技术研发部', description: '负责核心技术研发与实现', head: '李四', memberCount: 15 }, + { id: 3, name: '数据分析部', description: '负责数据分析与挖掘', head: '王五', memberCount: 10 }, + { id: 4, name: '运营部', description: '负责产品运营与推广', head: '钱七', memberCount: 6 }, + { id: 5, name: '测试部', description: '负责产品质量保障', head: '周九', memberCount: 5 }, + { id: 6, name: '客户服务部', description: '负责客户支持与服务', head: '吴十', memberCount: 12 } +]; + +function AddUserPage({ onBack }) { + const [selectedDepartment, setSelectedDepartment] = useState(null); + + const departmentColumns = [ + { key: 'name', label: '部门名称' }, + { key: 'description', label: '部门描述' }, + { key: 'head', label: '负责人' } + ]; + + const selectedLabel = selectedDepartment + ? availableDepartments.find(d => d.id === selectedDepartment)?.name + : null; + + return ( +
+
+
新增用户
+
+
+
+ + +
+
+ + setSelectedDepartment(null)} + /> +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ ); +} + +export default AddUserPage; diff --git a/src/pages/admin/AdminProjectsPage.jsx b/src/pages/admin/AdminProjectsPage.jsx new file mode 100644 index 0000000..b6599e7 --- /dev/null +++ b/src/pages/admin/AdminProjectsPage.jsx @@ -0,0 +1,93 @@ +const adminProjects = [ + { id: 1, name: '企业 AI 智算平台', description: '企业级AI智能助手平台', owner: '张三', members: 8, status: '正常', createTime: '2026-01-15', lastActive: '2026-03-19 10:30' }, + { id: 2, name: '知识库管理系统', description: '智能知识库检索与管理', owner: '李四', members: 5, status: '正常', createTime: '2026-02-10', lastActive: '2026-03-18 16:20' }, + { id: 3, name: '数据分析平台', description: '大数据分析与可视化平台', owner: '王五', members: 12, status: '正常', createTime: '2026-01-20', lastActive: '2026-03-19 09:45' }, + { id: 4, name: '智能客服系统', description: 'AI驱动的客户服务系统', owner: '赵六', members: 6, status: '禁用', createTime: '2026-02-28', lastActive: '2026-03-10 14:15' }, + { id: 5, name: '文档自动化平台', description: '智能文档生成与处理', owner: '钱七', members: 4, status: '正常', createTime: '2026-03-01', lastActive: '2026-03-19 11:20' }, + { id: 6, name: '代码审查助手', description: '自动化代码审查与优化建议', owner: '孙八', members: 7, status: '正常', createTime: '2026-02-15', lastActive: '2026-03-17 15:30' } +]; + +function StatusTag({ status }) { + const statusClass = status === '正常' ? 'status-running' : status === '禁用' ? 'status-error' : ''; + return {status}; +} + +function AdminProjectsPage({ onAdd }) { + return ( + <> +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+
项目列表
+ +
+
+
+ + + + + + + + + + + + + + {adminProjects.map(project => ( + + + + + + + + + + ))} + +
项目名称项目描述负责人成员数状态创建时间操作
{project.name}{project.description}{project.owner}{project.members} 人{project.createTime} +
+ + + +
+
+
+
+
+
1
+
2
+
3
+
+
+
+
+ + ); +} + +export default AdminProjectsPage; \ No newline at end of file diff --git a/src/pages/admin/DepartmentsPage.jsx b/src/pages/admin/DepartmentsPage.jsx new file mode 100644 index 0000000..454dc4f --- /dev/null +++ b/src/pages/admin/DepartmentsPage.jsx @@ -0,0 +1,93 @@ +const departments = [ + { id: 1, name: 'AI 产品部', description: '负责AI产品规划与设计', head: '张三', members: 8, status: '正常', createTime: '2025-06-01' }, + { id: 2, name: '技术研发部', description: '负责核心技术研发与实现', head: '李四', members: 15, status: '正常', createTime: '2025-06-01' }, + { id: 3, name: '数据分析部', description: '负责数据分析与挖掘', head: '王五', members: 10, status: '正常', createTime: '2025-06-01' }, + { id: 4, name: '运营部', description: '负责产品运营与推广', head: '钱七', members: 6, status: '正常', createTime: '2025-08-15' }, + { id: 5, name: '测试部', description: '负责产品质量保障', head: '周九', members: 5, status: '正常', createTime: '2025-07-01' }, + { id: 6, name: '客户服务部', description: '负责客户支持与服务', head: '吴十', members: 12, status: '禁用', createTime: '2025-09-10' } +]; + +function StatusTag({ status }) { + const statusClass = status === '正常' ? 'status-running' : status === '禁用' ? 'status-error' : ''; + return {status}; +} + +function DepartmentsPage({ onAdd }) { + return ( + <> +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+
部门列表
+ +
+
+
+ + + + + + + + + + + + + + {departments.map(dept => ( + + + + + + + + + + ))} + +
部门名称部门描述负责人成员数状态创建时间操作
{dept.name}{dept.description}{dept.head}{dept.members} 人{dept.createTime} +
+ + + +
+
+
+
+
+
1
+
2
+
3
+
+
+
+
+ + ); +} + +export default DepartmentsPage; \ No newline at end of file diff --git a/src/pages/admin/OverviewPage.jsx b/src/pages/admin/OverviewPage.jsx new file mode 100644 index 0000000..a7be047 --- /dev/null +++ b/src/pages/admin/OverviewPage.jsx @@ -0,0 +1,18 @@ +function OverviewPage() { + return ( +
+
+
运营总览
+
+
+
+
+
运营总览页面
+
此处展示平台运营数据概览
+
+
+
+ ); +} + +export default OverviewPage; \ No newline at end of file diff --git a/src/pages/admin/UsersPage.jsx b/src/pages/admin/UsersPage.jsx new file mode 100644 index 0000000..0e6644c --- /dev/null +++ b/src/pages/admin/UsersPage.jsx @@ -0,0 +1,113 @@ +const adminUsers = [ + { id: 1, name: '张三', department: 'AI 产品部', role: '管理员', email: 'zhangsan@example.com', phone: '138****8888', status: '正常', lastLogin: '2026-03-19 10:30' }, + { id: 2, name: '李四', department: '技术研发部', role: '开发者', email: 'lisi@example.com', phone: '139****1234', status: '正常', lastLogin: '2026-03-19 09:15' }, + { id: 3, name: '王五', department: '数据分析部', role: '成员', email: 'wangwu@example.com', phone: '136****5678', status: '禁用', lastLogin: '2026-03-15 18:20' }, + { id: 4, name: '赵六', department: 'AI 产品部', role: '管理员', email: 'zhaoliu@example.com', phone: '135****9012', status: '正常', lastLogin: '2026-03-19 11:45' }, + { id: 5, name: '钱七', department: '技术研发部', role: '开发者', email: 'qianqi@example.com', phone: '137****3456', status: '正常', lastLogin: '2026-03-18 16:30' }, + { id: 6, name: '孙八', department: '技术研发部', role: '成员', email: 'sunba@example.com', phone: '133****7890', status: '正常', lastLogin: '2026-03-19 08:00' }, + { id: 7, name: '周九', department: '测试部', role: '成员', email: 'zhoujiu@example.com', phone: '158****2345', status: '正常', lastLogin: '2026-03-17 14:20' }, + { id: 8, name: '吴十', department: '技术研发部', role: '开发者', email: 'wushi@example.com', phone: '159****6789', status: '正常', lastLogin: '2026-03-19 12:10' } +]; + +function StatusTag({ status }) { + const statusClass = status === '正常' ? 'status-running' : status === '禁用' ? 'status-error' : ''; + return {status}; +} + +function RoleTag({ role }) { + const roleClass = role === '管理员' ? 'role-admin' : role === '开发者' ? 'role-developer' : 'role-member'; + return {role}; +} + +function UsersPage({ onAdd }) { + return ( + <> +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+
用户列表
+ +
+
+
+ + + + + + + + + + + + + + + {adminUsers.map(user => ( + + + + + + + + + + + ))} + +
姓名部门角色邮箱手机号状态最后登录操作
{user.name}{user.department}{user.email}{user.phone}{user.lastLogin} +
+ + + +
+
+
+
+
+
1
+
2
+
3
+
+
+
+
+ + ); +} + +export default UsersPage; \ No newline at end of file diff --git a/src/pages/console/AccountPage.jsx b/src/pages/console/AccountPage.jsx new file mode 100644 index 0000000..b708211 --- /dev/null +++ b/src/pages/console/AccountPage.jsx @@ -0,0 +1,85 @@ +function AccountPage() { + return ( + <> +
+
+
账号信息
+
+
+ {/* 头像区域 */} +
+
+ +
+ {/* 表单区域 */} +
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+ +
+
+
+
+
修改密码
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + ); +} + +export default AccountPage; \ No newline at end of file diff --git a/src/pages/console/AddMemberPage.jsx b/src/pages/console/AddMemberPage.jsx new file mode 100644 index 0000000..969a6c2 --- /dev/null +++ b/src/pages/console/AddMemberPage.jsx @@ -0,0 +1,59 @@ +import { useState } from 'react'; +import ListSelector from '../../components/ListSelector.jsx'; + +const availableMembers = [ + { id: 1, name: '陈十一', department: 'AI 产品部', email: 'chenshiyi@example.com' }, + { id: 2, name: '郑十二', department: '技术研发部', email: 'zhengshier@example.com' }, + { id: 3, name: '冯十三', department: '数据分析部', email: 'fengshisan@example.com' }, + { id: 4, name: '卫十四', department: '运营部', email: 'weishisi@example.com' }, + { id: 5, name: '蒋十五', department: '测试部', email: 'jiangshiwu@example.com' }, + { id: 6, name: '沈十六', department: 'AI 产品部', email: 'shenshiliu@example.com' }, + { id: 7, name: '韩十七', department: '技术研发部', email: 'hanshiqi@example.com' }, + { id: 8, name: '杨十八', department: '数据分析部', email: 'yangshiba@example.com' } +]; + +function AddMemberPage({ onBack }) { + const [selectedMembers, setSelectedMembers] = useState([]); + + const memberColumns = [ + { key: 'name', label: '姓名' }, + { key: 'department', label: '部门' }, + { key: 'email', label: '邮箱' } + ]; + + const selectedLabel = selectedMembers.length > 0 + ? `已选择 ${selectedMembers.length} 位成员` + : null; + + const handleAdd = () => { + onBack(); + }; + + return ( +
+
+
增加成员
+ +
+
+ setSelectedMembers([])} + /> +
+ +
+
+
+ ); +} + +export default AddMemberPage; diff --git a/src/pages/console/ChatPage.jsx b/src/pages/console/ChatPage.jsx new file mode 100644 index 0000000..89dd6f2 --- /dev/null +++ b/src/pages/console/ChatPage.jsx @@ -0,0 +1,69 @@ +import { useEffect, useRef } from 'react'; +import { getChatScenes } from '../../data/conversations.js'; +import { FiPaperclip, FiCode, FiSend } from 'react-icons/fi'; + +function ChatPage({ scene }) { + const chatScenes = getChatScenes(); + const html = chatScenes[scene] || ''; + const chatMessagesRef = useRef(null); + + useEffect(() => { + if (!chatMessagesRef.current) return; + + const thinkingElements = chatMessagesRef.current.querySelectorAll('.message-thinking'); + + const handleClick = (event) => { + const thinkingElement = event.currentTarget; + thinkingElement.classList.toggle('expanded'); + }; + + thinkingElements.forEach(el => { + el.addEventListener('click', handleClick); + el.style.cursor = 'pointer'; + }); + + return () => { + thinkingElements.forEach(el => { + el.removeEventListener('click', handleClick); + }); + }; + }, [scene, html]); // 依赖场景和html内容 + + return ( +
+
+
+
+
+
+
+
+
+ +
+
+ + +
+
+ +
+ +
点击或拖拽文件到此处上传
+
支持 .zip 格式
+
+
+
+ + +
+
+
+ ); +} + +export default NewVersionPage; diff --git a/src/pages/developer/SkillEditorPage.jsx b/src/pages/developer/SkillEditorPage.jsx new file mode 100644 index 0000000..5ffc401 --- /dev/null +++ b/src/pages/developer/SkillEditorPage.jsx @@ -0,0 +1,118 @@ +import { FiChevronLeft, FiUpload, FiDownload } from 'react-icons/fi'; +import { mySkills } from '../../data/developerData.js'; + +function SkillEditorPage({ skillId, onBack, onUploadNewVersion }) { + const skill = mySkills.find(s => s.id === skillId); + if (!skill) { + return
Skill not found
; + } + + return ( + <> +
+ 返回我的技能 +
+
+
+
配置信息
+
+
+
+
{skill.name.charAt(0)}
+
+

{skill.name}

+
{skill.category}
+
+ {skill.tags.map(tag => ( + {tag} + ))} +
+
+ 版本: {skill.version} + 安装量: {skill.installs} + 评分: {skill.rating || '-'} +
+
+
+
+

基本信息

+
+ 技能名称 + {skill.name} +
+
+ 技能描述 + {skill.desc} +
+
+
+
+
+
+
技能包管理
+
+
+
+ +
+

版本历史

+
+ + + + + + + + + + + + + + + + + + + + + {skill.versions.map(ver => ( + + + + + + + + + ))} + +
版本号版本说明状态更新时间是否启用操作
{ver.version}{ver.desc} + {ver.status === 'pending' ? ( + 审核中 + ) : ver.status === 'rejected' ? ( + 审核拒绝 + ) : ( + 审核通过 + )} + {ver.date} + {ver.enabled ? ( + 已启用 + ) : ( + 未启用 + )} + +
+ {!ver.enabled && } + +
+
+
+
+
+ + ); +} + +export default SkillEditorPage; \ No newline at end of file diff --git a/src/pages/developer/UploadSkillPage.jsx b/src/pages/developer/UploadSkillPage.jsx new file mode 100644 index 0000000..5f23970 --- /dev/null +++ b/src/pages/developer/UploadSkillPage.jsx @@ -0,0 +1,81 @@ +import { FiUpload, FiX } from 'react-icons/fi'; +import { useState } from 'react'; + +function UploadSkillPage() { + const [tags, setTags] = useState([]); + const [tagInput, setTagInput] = useState(''); + + const handleTagKeyDown = (e) => { + if (e.key === 'Enter' && tagInput.trim()) { + e.preventDefault(); + if (!tags.includes(tagInput.trim())) { + setTags([...tags, tagInput.trim()]); + } + setTagInput(''); + } + }; + + const removeTag = (tagToRemove) => { + setTags(tags.filter(tag => tag !== tagToRemove)); + }; + + return ( +
+
+
创建技能
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ {tags.map(tag => ( + + {tag} + removeTag(tag)}> + + ))} + setTagInput(e.target.value)} + onKeyDown={handleTagKeyDown} + /> +
+
按回车添加标签,最多5个
+
+
+ +
+ +
点击或拖拽文件到此处上传
+
支持 .zip 格式
+
+
+
+ + +
+
+
+ ); +} + +export default UploadSkillPage; \ No newline at end of file diff --git a/src/styles/_base.scss b/src/styles/_base.scss new file mode 100644 index 0000000..a418151 --- /dev/null +++ b/src/styles/_base.scss @@ -0,0 +1,71 @@ +// 基础重置与全局样式 +@use 'variables' as *; + +// 重置 +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +// CSS变量定义在:root中 +:root { + /* 品牌主色 - 清新科技蓝 */ + --color-primary: #{$primary}; + --color-primary-light: #{$primary-light}; + --color-primary-lighter: #{$primary-lighter}; + --color-primary-dark: #{$primary-dark}; + + /* 功能色 */ + --color-success: #{$success}; + --color-success-light: #{$success-light}; + --color-warning: #{$warning}; + --color-warning-light: #{$warning-light}; + --color-danger: #{$danger}; + --color-danger-light: #{$danger-light}; + + /* 中性色 - 现代简约灰阶 */ + --color-text-1: #{$text-1}; + --color-text-2: #{$text-2}; + --color-text-3: #{$text-3}; + --color-text-4: #{$text-4}; + + /* 边框/分割线 */ + --color-border-1: #{$border-1}; + --color-border-2: #{$border-2}; + --color-border-3: #{$border-3}; + + /* 背景色 */ + --color-bg-1: #{$bg-1}; + --color-bg-2: #{$bg-2}; + --color-bg-3: #{$bg-3}; + --color-bg-4: #{$bg-4}; + + /* 阴影 - 柔和现代 */ + --shadow-1: #{$shadow-1}; + --shadow-2: #{$shadow-2}; + --shadow-3: #{$shadow-3}; + --shadow-card: #{$shadow-card}; + + /* 布局尺寸 */ + --sidebar-width: #{$sidebar-width}; + --header-height: #{$header-height}; + --radius-sm: #{$radius-sm}; + --radius-md: #{$radius-md}; + --radius-lg: #{$radius-lg}; + --radius-xl: #{$radius-xl}; + + /* 过渡动画 */ + --transition: #{$transition}; +} + +// 全局body样式 +body { + font-family: $font-family; + font-size: 14px; + line-height: 1.6; + color: var(--color-text-1); + background-color: var(--color-bg-2); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} \ No newline at end of file diff --git a/src/styles/_components.scss b/src/styles/_components.scss new file mode 100644 index 0000000..eeafa7f --- /dev/null +++ b/src/styles/_components.scss @@ -0,0 +1,188 @@ +// 通用组件样式 +// 按钮、卡片、表单、状态标签等 + +@use 'variables' as *; +@use 'mixins' as *; + +// 按钮 +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + border-radius: var(--radius-md); + border: 1px solid transparent; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + text-decoration: none; + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +// 主要按钮 +.btn-primary { + background: var(--color-primary); + color: white; + border-color: var(--color-primary); + + &:hover { + background: var(--color-primary-dark); + border-color: var(--color-primary-dark); + } +} + +// 小按钮 +.btn-sm { + padding: 6px 12px; + font-size: 13px; +} + +// 按钮组 +.btn-group { + display: flex; + gap: 8px; +} + +// 卡片 +.card { + background: var(--color-bg-1); + border: 1px solid var(--color-border-2); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + overflow: hidden; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid var(--color-border-2); +} + +.card-title { + font-size: 16px; + font-weight: 600; + color: var(--color-text-1); +} + +.card-body { + padding: 20px; +} + +// 表单 +.form-group { + margin-bottom: 16px; +} + +.form-label { + display: block; + margin-bottom: 6px; + font-size: 14px; + font-weight: 500; + color: var(--color-text-1); +} + +.form-control { + width: 100%; + padding: 8px 12px; + font-size: 14px; + border: 1px solid var(--color-border-3); + border-radius: var(--radius-md); + background: var(--color-bg-1); + color: var(--color-text-1); + transition: border-color 0.2s; + + &:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); + } + + &::placeholder { + color: var(--color-text-3); + } +} + +.form-row { + display: flex; + gap: 20px; +} + +.form-col { + flex: 1; +} + +// 状态标签 +.status { + display: inline-flex; + align-items: center; + padding: 2px 8px; + font-size: 12px; + font-weight: 500; + border-radius: 999px; + + &.status-running { + background: var(--color-success-light); + color: var(--color-success); + } + + &.status-stopped { + background: var(--color-bg-3); + color: var(--color-text-3); + } + + &.status-error { + background: var(--color-danger-light); + color: var(--color-danger); + } + + &.status-warning { + background: var(--color-warning-light); + color: var(--color-warning); + } + + &.role-admin { + background: var(--color-primary-light); + color: var(--color-primary); + } + + &.role-member { + background: var(--color-bg-3); + color: var(--color-text-2); + } +} + +// 文本按钮 +.text-btn { + padding: 4px 8px; + font-size: 13px; + font-weight: 500; + border: none; + background: none; + cursor: pointer; + border-radius: var(--radius-sm); + transition: background 0.2s; + + &:hover { + background: var(--color-bg-2); + } +} + +.text-btn-primary { + color: var(--color-primary); +} + +.text-btn-success { + color: var(--color-success); +} + +.text-btn-danger { + color: var(--color-danger); +} \ No newline at end of file diff --git a/src/styles/_layout.scss b/src/styles/_layout.scss new file mode 100644 index 0000000..f4a05f6 --- /dev/null +++ b/src/styles/_layout.scss @@ -0,0 +1,37 @@ +// 布局样式 +// 侧边栏、主内容区、页眉等 + +@use 'variables' as *; +@use 'mixins' as *; + +// 主布局 +.layout { + display: flex; + height: 100vh; + overflow: hidden; +} + +// 侧边栏 +.sidebar { + width: var(--sidebar-width); + background: var(--color-bg-1); + border-right: 1px solid var(--color-border-2); + position: fixed; + height: 100vh; + overflow-y: auto; + overflow-x: hidden; + z-index: 101; + display: flex; + flex-direction: column; +} + +.sidebar-header { + height: var(--header-height); + display: flex; + align-items: center; + padding: 0 20px; + border-bottom: 1px solid var(--color-border-2); + flex-shrink: 0; +} + +// 其他布局样式... \ No newline at end of file diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss new file mode 100644 index 0000000..ffe9534 --- /dev/null +++ b/src/styles/_mixins.scss @@ -0,0 +1,70 @@ +// SCSS Mixins - 可复用代码片段 +@use 'variables' as *; + +// 媒体查询断点 +@mixin mobile { + @media (max-width: 768px) { + @content; + } +} + +@mixin tablet { + @media (min-width: 769px) and (max-width: 1024px) { + @content; + } +} + +@mixin desktop { + @media (min-width: 1025px) { + @content; + } +} + +// 弹性布局 +@mixin flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +@mixin flex-between { + display: flex; + align-items: center; + justify-content: space-between; +} + +// 文本截断 +@mixin text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +// 过渡效果 +@mixin transition($property: all, $duration: 0.2s, $timing: cubic-bezier(0.4, 0, 0.2, 1)) { + transition: $property $duration $timing; +} + +// 阴影 +@mixin shadow($level: 1) { + @if $level == 1 { + box-shadow: $shadow-1; + } @else if $level == 2 { + box-shadow: $shadow-2; + } @else if $level == 3 { + box-shadow: $shadow-3; + } +} + +// 圆角 +@mixin radius($size: md) { + @if $size == sm { + border-radius: $radius-sm; + } @else if $size == md { + border-radius: $radius-md; + } @else if $size == lg { + border-radius: $radius-lg; + } @else if $size == xl { + border-radius: $radius-xl; + } +} \ No newline at end of file diff --git a/src/styles/_pages.scss b/src/styles/_pages.scss new file mode 100644 index 0000000..0a39be2 --- /dev/null +++ b/src/styles/_pages.scss @@ -0,0 +1,52 @@ +// 页面特定样式 +// 首页、管理台、开发台、技能市场等 + +@use 'variables' as *; +@use 'mixins' as *; + +// 首页样式(从内联样式迁移) +.home-layout { + min-height: 100vh; + display: flex; + flex-direction: column; + background: linear-gradient(180deg, #F8FAFC 0%, #FFFFFF 100%); + position: relative; + overflow: hidden; + + &::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; + } + + &::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); +} + +// 其他页面样式将逐步添加... \ No newline at end of file diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss new file mode 100644 index 0000000..6101ff0 --- /dev/null +++ b/src/styles/_variables.scss @@ -0,0 +1,54 @@ +// SCSS Variables - 设计系统变量 +// 注意:这些是SCSS变量,用于开发时引用 +// CSS变量定义在:root中,供运行时使用 + +// 品牌主色 +$primary: #3B82F6; +$primary-light: #EFF6FF; +$primary-lighter: #F8FAFC; +$primary-dark: #2563EB; + +// 功能色 +$success: #10B981; +$success-light: #ECFDF5; +$warning: #F59E0B; +$warning-light: #FFFBEB; +$danger: #EF4444; +$danger-light: #FEF2F2; + +// 中性色 +$text-1: #1E293B; +$text-2: #475569; +$text-3: #94A3B8; +$text-4: #CBD5E1; + +// 边框/分割线 +$border-1: #F8FAFC; +$border-2: #F1F5F9; +$border-3: #E2E8F0; + +// 背景色 +$bg-1: #FFFFFF; +$bg-2: #F8FAFC; +$bg-3: #F1F5F9; +$bg-4: #E2E8F0; + +// 阴影 +$shadow-1: 0 1px 3px rgba(15, 23, 42, 0.04); +$shadow-2: 0 4px 12px rgba(15, 23, 42, 0.06); +$shadow-3: 0 8px 24px rgba(15, 23, 42, 0.08); +$shadow-card: 0 2px 8px rgba(15, 23, 42, 0.04); + +// 布局尺寸 +$sidebar-width: 240px; +$header-height: 60px; +$radius-sm: 6px; +$radius-md: 8px; +$radius-lg: 12px; +$radius-xl: 16px; + +// 过渡动画 +$transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1); + +// 字体 +$font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Inter', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif; \ No newline at end of file diff --git a/src/styles/global.scss b/src/styles/global.scss new file mode 100644 index 0000000..434f952 --- /dev/null +++ b/src/styles/global.scss @@ -0,0 +1,2541 @@ +// GrandClaw 设计系统 - 主样式文件 +// 导入设计系统模块 + +@use 'variables' as *; +@use 'mixins' as *; +@use 'base' as *; +@use 'components' as *; +@use 'layout' as *; +@use 'pages' as *; + +/* ============================================ + 原始样式内容(待进一步拆分) + ============================================ */ + +/* ============ 布局 - 管理控制台 ============ */ +.layout { + display: flex; + height: 100vh; + overflow: hidden; +} + +/* 侧边栏 - 现代白色 */ +.sidebar { + width: var(--sidebar-width); + background: var(--color-bg-1); + border-right: 1px solid var(--color-border-2); + position: fixed; + height: 100vh; + overflow-y: auto; + overflow-x: hidden; + z-index: 101; + display: flex; + flex-direction: column; +} + +.sidebar-header { + height: var(--header-height); + display: flex; + align-items: center; + padding: 0 20px; + border-bottom: 1px solid var(--color-border-2); + flex-shrink: 0; +} + +.sidebar-brand { + display: flex; + align-items: center; + gap: 10px; +} + +.sidebar-logo-icon { + width: 28px; + height: 28px; + background: linear-gradient(135deg, var(--color-primary) 0%, #8B5CF6 100%); + border-radius: 6px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} + +.sidebar-logo-icon::before { + content: ''; + position: absolute; + width: 14px; + height: 10px; + background: rgba(255, 255, 255, 0.95); + border-radius: 4px 4px 2px 2px; + top: 5px; +} + +.sidebar-logo-icon::after { + content: ''; + position: absolute; + width: 18px; + height: 10px; + background: rgba(255, 255, 255, 0.85); + border-radius: 2px 2px 4px 4px; + bottom: 4px; +} + +.sidebar-logo-icon span { + position: absolute; + width: 3px; + height: 3px; + background: rgba(59, 130, 246, 0.9); + border-radius: 50%; + top: 9px; + z-index: 1; +} + +.sidebar-logo-icon span:nth-child(1) { + left: 9px; +} + +.sidebar-logo-icon span:nth-child(2) { + right: 9px; +} + +.sidebar-brand-text { + display: flex; + flex-direction: column; + gap: 2px; +} + +.sidebar-logo { + font-size: 18px; + font-weight: 700; + color: var(--color-text-1); + letter-spacing: -0.3px; + line-height: 1.2; +} + +.chat-logo::before { + content: ''; + width: 28px; + height: 28px; + background: linear-gradient(135deg, var(--color-primary) 0%, #8B5CF6 100%); + border-radius: 6px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} + +.chat-logo::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.9); + border-radius: 4px; + transform: rotate(45deg); +} + +.chat-logo > span::before, +.chat-logo + span::before { + content: ''; + position: absolute; + width: 6px; + height: 6px; + background: rgba(255, 255, 255, 0.95); + border-radius: 50%; + box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.3); + z-index: 1; +} + +.sidebar-menu { + padding: 16px 12px; + flex: 1; +} + +.sidebar-divider { + height: 1px; + background: var(--color-border-2); + margin: 12px 0; +} + +.sidebar-subtitle { + font-size: 13px; + color: var(--color-text-3); + font-weight: 600; + line-height: 1.2; +} + +.menu-item { + display: flex; + align-items: center; + padding: 11px 14px; + margin: 2px 0; + color: var(--color-text-2); + cursor: pointer; + border-radius: var(--radius-md); + transition: all var(--transition); + position: relative; + font-weight: 500; +} + +.menu-item:hover { + color: var(--color-text-1); + background: var(--color-bg-2); +} + +.menu-item.active { + color: var(--color-primary); + background: var(--color-primary-light); + font-weight: 600; +} + +.menu-item.active::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 20px; + background: var(--color-primary); + border-radius: 0 4px 4px 0; +} + +.menu-item-icon { + margin-right: 12px; + width: 20px; + text-align: center; + font-size: 18px; +} + +/* 主内容区 */ +.main-content { + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +/* 顶部栏 */ +.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; + position: sticky; + top: 0; + z-index: 100; +} + +.header-left { + display: flex; + align-items: center; + gap: 16px; +} + +.header-title { + font-size: 15px; + font-weight: 600; + color: var(--color-text-1); +} + +.user-avatar { + width: 34px; + height: 34px; + 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: 14px; + font-weight: 600; + flex-shrink: 0; +} + +/* 页面内容 */ +.page-content { + flex: 1; + padding: 24px; + overflow-y: auto; +} + +.page-content-full { + flex: 1; + padding: 0; + overflow: hidden; + display: flex; + flex-direction: column; +} + +/* ============ 卡片组件 ============ */ +.card { + background: var(--color-bg-1); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + margin-bottom: 20px; + border: 1px solid var(--color-border-2); + overflow: hidden; +} + +.card-header { + padding: 18px 22px; + border-bottom: 1px solid var(--color-border-2); + display: flex; + align-items: center; + justify-content: space-between; + background: var(--color-bg-1); +} + +.card-title { + font-size: 15px; + font-weight: 700; + color: var(--color-text-1); + display: flex; + align-items: center; + gap: 8px; +} + +.card-body { + padding: 22px; +} + +/* ============ 按钮组件 ============ */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 9px 16px; + font-size: 14px; + font-weight: 600; + border-radius: var(--radius-md); + border: 1px solid var(--color-border-3); + background: var(--color-bg-1); + color: var(--color-text-1); + cursor: pointer; + transition: all var(--transition); + white-space: nowrap; +} + +.btn:hover { + background: var(--color-bg-2); + border-color: var(--color-border-3); + transform: translateY(-1px); +} + +.btn:active { + transform: translateY(0); +} + +.btn-primary { + background: var(--color-primary); + border-color: var(--color-primary); + color: #FFFFFF; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.25); +} + +.btn-primary:hover { + background: var(--color-primary-dark); + border-color: var(--color-primary-dark); + color: #FFFFFF; + box-shadow: 0 6px 16px rgba(59, 130, 246, 0.35); +} + +.btn-success { + background: var(--color-success); + border-color: var(--color-success); + color: #FFFFFF; +} + +.btn-success:hover { + background: #059669; + border-color: #059669; + color: #FFFFFF; +} + +.btn-danger { + background: var(--color-danger); + border-color: var(--color-danger); + color: #FFFFFF; +} + +.btn-danger:hover { + background: #DC2626; + border-color: #DC2626; + color: #FFFFFF; +} + +.btn-sm { + padding: 6px 12px; + font-size: 13px; + font-weight: 600; +} + +/* 文字按钮 */ +.text-btn { + padding: 2px 6px; + font-size: 13px; + background: transparent; + border: none; + color: #475569; + cursor: pointer; + font-weight: 500; + transition: color 0.2s; +} + +.text-btn:hover { + color: #3B82F6; +} + +.text-btn-primary { + color: #3B82F6; +} + +.text-btn-primary:hover { + color: #2563EB; +} + +.text-btn-success { + color: #10B981; +} + +.text-btn-success:hover { + color: #059669; +} + +.text-btn-danger { + color: #EF4444; +} + +.text-btn-danger:hover { + color: #DC2626; +} + +.btn-group { + display: flex; + gap: 8px; + align-items: center; +} + +/* ============ 表格组件 ============ */ +.table-wrapper { + overflow-x: auto; + margin: -22px; + padding: 22px; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 14px; +} + +.table th, +.table td { + padding: 14px 16px; + text-align: left; +} + +.table th { + background: var(--color-bg-2); + font-weight: 700; + color: var(--color-text-2); + font-size: 13px; + letter-spacing: 0.2px; + border-bottom: 1px solid var(--color-border-3); +} + +.table td { + border-bottom: 1px solid var(--color-border-2); + color: var(--color-text-1); +} + +.table tr:last-child td { + border-bottom: none; +} + +.table tbody tr { + transition: background var(--transition); +} + +.table tbody tr:hover td { + background: var(--color-bg-2); +} + +/* ============ 状态标签组件 ============ */ +.status { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; +} + +.status::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; +} + +.status-running { + background: var(--color-success-light); + color: var(--color-success); +} + +.status-running::before { + background: var(--color-success); +} + +.status-stopped { + background: var(--color-bg-3); + color: var(--color-text-3); +} + +.status-stopped::before { + background: var(--color-text-4); +} + +.status-starting { + background: var(--color-warning-light); + color: var(--color-warning); +} + +.status-starting::before { + background: var(--color-warning); + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(0.8); } +} + +.status-error { + background: var(--color-danger-light); + color: var(--color-danger); +} + +.status-error::before { + background: var(--color-danger); +} + +.status-warning { + background: var(--color-warning-light); + color: var(--color-warning); +} + +.status-warning::before { + background: var(--color-warning); +} + +/* 成员角色标签 */ +.role-admin { + background: var(--color-primary-light); + color: var(--color-primary); +} + +.role-admin::before { + background: var(--color-primary); +} + +.role-member { + background: var(--color-bg-3); + color: var(--color-text-2); +} + +.role-member::before { + background: var(--color-text-3); +} + +.role-developer { + background: #FEF3C7; + color: #D97706; +} + +.role-developer::before { + background: #D97706; +} + +/* 开关按钮 */ +.switch { + position: relative; + display: inline-block; + width: 44px; + height: 22px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--color-text-4); + transition: 0.3s; + border-radius: 22px; +} + +.slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 2px; + bottom: 2px; + background-color: white; + transition: 0.3s; + border-radius: 50%; +} + +input:checked + .slider { + background-color: var(--color-primary); +} + +input:checked + .slider:before { + transform: translateX(22px); +} + +/* 列表选择器 */ +.list-selector { + margin-bottom: 16px; +} + +.list-selector-input { + padding: 6px 10px; + font-size: 13px; + margin-bottom: 10px; +} + +.list-selector-tag { + padding: 6px 10px; + background: var(--color-primary-light); + border-radius: 6px; + display: flex; + align-items: center; + justify-content: space-between; + color: var(--color-primary); + font-weight: 500; + font-size: 13px; + margin-bottom: 10px; +} + +.list-selector-tag-close { + cursor: pointer; + color: var(--color-text-3); +} + +.list-selector-tag-close:hover { + color: var(--color-text-1); +} + +.list-selector-table { + max-height: 200px; + overflow-y: auto; +} + +/* ============ 表单组件 ============ */ +.form-group { + margin-bottom: 22px; +} + +.form-label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: var(--color-text-2); + font-size: 14px; +} + +.form-label.required::after { + content: ' *'; + color: var(--color-danger); +} + +.form-control { + width: 100%; + padding: 9px 12px; + font-size: 14px; + line-height: 1.6; + border: 1px solid var(--color-border-3); + border-radius: var(--radius-md); + background: var(--color-bg-1); + color: var(--color-text-1); + transition: all var(--transition); +} + +.form-control:hover { + border-color: #94A3B8; +} + +.form-control:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.form-control::placeholder { + color: var(--color-text-4); +} + +.form-control[readonly] { + background: var(--color-bg-2); + color: var(--color-text-3); + cursor: not-allowed; +} + +.form-row { + display: flex; + gap: 20px; +} + +.form-col { + flex: 1; +} + +.tag-input-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 8px 12px; + border: 1px solid var(--color-border-3); + border-radius: var(--radius-md); + background: var(--color-bg-1); + min-height: 42px; + align-items: center; +} + +.tag-input-container:focus-within { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.tag-item { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + background: var(--color-primary-light); + color: var(--color-primary); + border-radius: 999px; + font-size: 13px; + font-weight: 500; +} + +.tag-remove { + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border-radius: 50%; + transition: background 0.2s; +} + +.tag-remove:hover { + background: rgba(59, 130, 246, 0.2); +} + +.tag-input { + flex: 1; + min-width: 120px; + border: none; + outline: none; + font-size: 14px; + background: transparent; + color: var(--color-text-1); +} + +.tag-input::placeholder { + color: var(--color-text-4); +} + +/* 技能多选列表 */ +.skill-checkbox-list { + border: 1px solid var(--color-border-3); + border-radius: var(--radius-md); + max-height: 320px; + overflow-y: auto; +} + +.skill-checkbox-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + cursor: pointer; + transition: background 0.15s; + border-bottom: 1px solid var(--color-border-2); +} + +.skill-checkbox-item:last-child { + border-bottom: none; +} + +.skill-checkbox-item:hover { + background: var(--color-bg-2); +} + +.skill-checkbox-item input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; + flex-shrink: 0; +} + +.skill-checkbox-label { + flex: 1; + font-weight: 500; + font-size: 14px; + color: var(--color-text-1); +} + +/* ============ 搜索栏 ============ */ +.search-bar { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + margin-bottom: 8px; + align-items: flex-end; +} + +.search-bar-row { + display: flex; + gap: 12px; + flex-wrap: wrap; + align-items: flex-end; +} + +.search-item { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.search-item-inline { + display: flex; + align-items: center; + gap: 10px; +} + +.search-item label { + color: var(--color-text-2); + font-size: 13px; + font-weight: 600; +} + +.search-actions { + display: flex; + gap: 10px; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--color-border-2); +} + +/* ============ 分页组件 ============ */ +.pagination { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + margin-top: 20px; +} + +.pagination-item { + min-width: 34px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--color-border-3); + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition); + font-size: 13px; + font-weight: 500; + color: var(--color-text-2); + background: var(--color-bg-1); +} + +.pagination-item:hover { + border-color: var(--color-primary); + color: var(--color-primary); +} + +.pagination-item.active { + background: var(--color-primary); + border-color: var(--color-primary); + color: #FFFFFF; +} + +/* ============ 技能 卡片 ============ */ +.skill-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 16px; +} + +.skill-card { + background: var(--color-bg-1); + border: 1px solid var(--color-border-2); + border-radius: var(--radius-lg); + padding: 20px; + transition: all var(--transition); + cursor: pointer; + display: flex; + flex-direction: column; + height: 100%; +} + +.skill-card:hover { + border-color: var(--color-border-3); + box-shadow: var(--shadow-2); + transform: translateY(-2px); +} + +.skill-header { + display: flex; + align-items: flex-start; + gap: 14px; + margin-bottom: 14px; +} + +.skill-icon { + width: 52px; + height: 52px; + border-radius: 12px; + background: linear-gradient(135deg, var(--color-primary) 0%, #8B5CF6 100%); + display: flex; + align-items: center; + justify-content: center; + color: #FFFFFF; + font-size: 24px; + flex-shrink: 0; +} + +.skill-info { + flex: 1; + min-width: 0; +} + +.skill-name { + font-size: 16px; + font-weight: 700; + margin-bottom: 4px; + color: var(--color-text-1); +} + +.skill-author { + font-size: 13px; + color: var(--color-text-3); +} + +.skill-desc { + color: var(--color-text-2); + font-size: 14px; + margin-bottom: 14px; + line-height: 1.6; + height: 42px; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.skill-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 14px; + max-height: 60px; + overflow: hidden; +} + +.skill-tag { + padding: 3px 10px; + background: var(--color-bg-2); + border-radius: 999px; + font-size: 12px; + color: var(--color-text-3); + font-weight: 500; +} + +.skill-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 14px; + border-top: 1px solid var(--color-border-2); + margin-top: auto; +} + +.skill-stats { + display: flex; + gap: 16px; + font-size: 13px; + color: var(--color-text-3); + font-weight: 500; +} + +/* ============ 统计卡片 ============ */ +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 20px; +} + +.stat-card { + background: var(--color-bg-1); + border-radius: var(--radius-lg); + padding: 22px; + border: 1px solid var(--color-border-2); + position: relative; + overflow: hidden; +} + +.stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, var(--color-primary) 0%, #8B5CF6 100%); +} + +.stat-title { + font-size: 14px; + color: var(--color-text-3); + margin-bottom: 8px; + font-weight: 600; +} + +.stat-value { + font-size: 32px; + font-weight: 800; + color: var(--color-text-1); + line-height: 1.2; + letter-spacing: -0.5px; +} + +.stat-trend { + font-size: 13px; + margin-top: 10px; + display: flex; + align-items: center; + gap: 4px; + font-weight: 600; +} + +.stat-trend.up { + color: var(--color-success); +} + +/* ============ 实例交互页面样式 ============ */ +.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-header-left { + display: flex; + align-items: center; + gap: 16px; +} + +.chat-logo { + font-size: 17px; + font-weight: 700; + color: var(--color-text-1); + display: flex; + align-items: center; + gap: 10px; +} + +/* 聊天主区域 */ +.chat-main { + flex: 1; + display: flex; + overflow: hidden; +} + +/* 会话列表 */ +.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-sidebar-header { + padding: 16px; + border-bottom: 1px solid var(--color-border-2); +} + +.chat-sidebar-content { + flex: 1; + overflow-y: auto; + padding: 12px; +} + +/* 聊天侧边栏底部导航 */ +.chat-sidebar-nav { + border-top: 1px solid var(--color-border-2); + padding: 8px 12px; + background: var(--color-bg-1); +} + +.chat-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; +} + +.chat-nav-item:hover { + background: var(--color-bg-2); + color: var(--color-text-1); +} + +.chat-nav-item.active { + background: var(--color-primary-light); + color: var(--color-primary); + font-weight: 600; +} + +.chat-nav-item:hover { + background: var(--color-bg-2); + color: var(--color-text-1); +} + +.chat-nav-item.active { + background: var(--color-primary-light); + color: var(--color-primary); + font-weight: 600; +} + +/* 侧边栏用户状态区域 */ +.chat-sidebar-user { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + border-top: 1px solid var(--color-border-2); + background: var(--color-bg-1); + cursor: pointer; + transition: background 0.2s; +} + +.chat-sidebar-user:hover { + background: var(--color-bg-2); +} + +.chat-sidebar-user-info { + flex: 1; + min-width: 0; +} + +.chat-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; +} + +.chat-sidebar-user-role { + font-size: 12px; + color: var(--color-text-3); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 侧边栏项目切换区域 */ +.chat-sidebar-project { + padding: 16px; + border-top: 1px solid var(--color-border-2); + background: var(--color-bg-1); +} + +.chat-sidebar-project-label { + display: block; + font-size: 12px; + font-weight: 600; + color: var(--color-text-3); + margin-bottom: 6px; +} + +.chat-sidebar-project-select { + width: 100%; + font-size: 13px; +} + +.chat-nav-icon { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + font-size: 16px; +} + +.chat-nav-text { + flex: 1; +} + +.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); +} + +.conversation-title { + font-size: 14px; + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 600; + color: var(--color-text-1); +} + +.conversation-time { + font-size: 12px; + color: var(--color-text-3); +} + +/* 聊天内容区 */ +.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; +} + +/* 欢迎消息 */ +.welcome-section { + max-width: 820px; + margin: 40px auto 0; + text-align: center; +} + +.welcome-title { + font-size: 32px; + font-weight: 800; + margin-bottom: 10px; + color: var(--color-text-1); +} + +.welcome-desc { + color: var(--color-text-3); + font-size: 16px; + margin-bottom: 40px; +} + +.welcome-actions { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 14px; + max-width: 640px; + margin: 0 auto; +} + +.welcome-action { + padding: 18px 20px; + background: var(--color-bg-2); + border: 1px solid var(--color-border-2); + border-radius: var(--radius-lg); + text-align: left; + cursor: pointer; + transition: all var(--transition); +} + +.welcome-action:hover { + border-color: var(--color-primary); + background: var(--color-primary-light); + transform: translateY(-2px); +} + +.welcome-action-title { + font-weight: 700; + margin-bottom: 4px; + font-size: 15px; + color: var(--color-text-1); +} + +.welcome-action-desc { + font-size: 13px; + color: var(--color-text-3); +} + +/* 消息气泡 */ +.message { + display: flex; + gap: 14px; + margin-bottom: 28px; + max-width: 900px; + margin-left: auto; + margin-right: auto; +} + +.message.user { + flex-direction: row-reverse; +} + +.message-avatar { + width: 40px; + height: 40px; + border-radius: 10px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; +} + +.message-avatar.assistant { + background: linear-gradient(135deg, var(--color-primary) 0%, #8B5CF6 100%); + color: #FFFFFF; +} + +.message-avatar.user { + background: linear-gradient(135deg, var(--color-primary) 0%, #8B5CF6 100%); + color: #FFFFFF; +} + +.message-content { + max-width: 72%; +} + +.message-bubble { + padding: 14px 18px; + border-radius: 14px; + line-height: 1.7; + font-size: 15px; +} + +.message.assistant .message-bubble { + background: var(--color-bg-2); + border-bottom-left-radius: 4px; +} + +.message.user .message-bubble { + background: linear-gradient(135deg, var(--color-primary) 0%, #60A5FA 100%); + color: #FFFFFF; + border-bottom-right-radius: 4px; +} + +.message-time { + font-size: 12px; + color: var(--color-text-4); + margin-top: 8px; + padding: 0 4px; +} + +/* AI 思考过程 */ +.message-thinking { + margin-bottom: 12px; + border: 1px solid var(--color-border-3); + border-radius: 12px; + overflow: hidden; + background: #FFFBEB; +} + +.message-thinking-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + cursor: pointer; + user-select: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + background: rgba(255, 255, 255, 0.5); + font-size: 13px; + color: #92400E; + font-weight: 500; +} + +.message-thinking-header:hover { + background: rgba(255, 255, 255, 0.8); +} + +.message-thinking-icon { + transition: transform 0.2s ease; + font-size: 12px; +} + +.message-thinking.expanded .message-thinking-icon { + transform: rotate(90deg); +} + +.message-thinking-content { + padding: 12px 14px; + font-size: 14px; + line-height: 1.6; + color: #78350F; + display: none; +} + +.message-thinking.expanded .message-thinking-content { + display: block; +} + +.message-thinking-content ul { + margin: 8px 0 0 0; + padding-left: 20px; +} + +.message-thinking-content li { + margin-bottom: 4px; +} + +/* 输入区 - 现代设计 */ +.chat-input-wrapper { + padding: 16px 24px 24px; + border-top: none; + background: linear-gradient(180deg, transparent 0%, rgba(248, 250, 252, 0.95) 15%, #F8FAFC 100%); + position: relative; + flex-shrink: 0; +} + +.chat-input-wrapper::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 60%; + height: 1px; + background: linear-gradient(90deg, transparent, var(--color-border-3), transparent); +} + +.chat-input-container { + max-width: 860px; + margin: 0 auto; +} + +.chat-input-box { + border: 1px solid var(--color-border-3); + border-radius: 16px; + overflow: hidden; + background: var(--color-bg-1); + transition: all var(--transition); + box-shadow: 0 2px 12px rgba(15, 23, 42, 0.04); +} + +.chat-input-box:hover { + border-color: #CBD5E1; + box-shadow: 0 4px 16px rgba(15, 23, 42, 0.06); +} + +.chat-input-box:focus-within { + border-color: var(--color-primary); + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1), 0 6px 20px rgba(15, 23, 42, 0.08); +} + +.chat-input-main { + display: flex; + align-items: flex-end; + gap: 8px; + padding: 10px 12px 10px 14px; +} + +.chat-input { + flex: 1; + padding: 6px 2px; + border: none; + outline: none; + font-size: 15px; + resize: none; + min-height: 24px; + max-height: 200px; + line-height: 1.6; + background: transparent; + color: var(--color-text-1); +} + +.chat-input::placeholder { + color: var(--color-text-4); +} + +.chat-input-actions { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.chat-input-tools { + display: flex; + gap: 2px; + padding-right: 6px; + border-right: 1px solid var(--color-border-2); +} + +.chat-input-tool { + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + cursor: pointer; + color: var(--color-text-3); + transition: all var(--transition); + font-size: 16px; +} + +.chat-input-tool:hover { + background: var(--color-bg-2); + color: var(--color-text-1); +} + +.chat-send-btn { + width: 34px; + height: 34px; + border-radius: 50%; + border: none; + background: linear-gradient(135deg, var(--color-primary) 0%, #60A5FA 100%); + color: #FFFFFF; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + transition: all var(--transition); + flex-shrink: 0; + box-shadow: 0 3px 10px rgba(59, 130, 246, 0.25); +} + +.chat-send-btn:hover { + transform: scale(1.06); + box-shadow: 0 5px 14px rgba(59, 130, 246, 0.35); +} + +.chat-send-btn:active { + transform: scale(0.97); +} + +.chat-input-footer { + display: flex; + align-items: center; + justify-content: center; + padding: 6px 16px 10px; + background: transparent; + border-top: none; +} + +.chat-input-hint { + font-size: 11px; + color: var(--color-text-4); +} + +/* 实例未启动状态 */ +.instance-stopped { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 40px; +} + +.instance-stopped-icon { + width: 96px; + height: 96px; + border-radius: 24px; + background: var(--color-bg-2); + display: flex; + align-items: center; + justify-content: center; + font-size: 40px; + color: var(--color-text-3); + margin-bottom: 28px; +} + +.instance-stopped-title { + font-size: 22px; + font-weight: 700; + margin-bottom: 10px; + color: var(--color-text-1); +} + +.instance-stopped-desc { + color: var(--color-text-3); + margin-bottom: 28px; + font-size: 15px; +} + +/* 对话未运行状态 */ +.conversation-stopped-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 60px 40px; + text-align: center; +} + +.stopped-state-icon-wrapper { + position: relative; + margin-bottom: 28px; +} + +.stopped-state-icon { + width: 88px; + height: 88px; + border-radius: 22px; + background: linear-gradient(135deg, var(--color-bg-2) 0%, var(--color-bg-3) 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 36px; + color: var(--color-text-3); + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08); + border: 2px solid var(--color-border-2); +} + +.stopped-state-badge { + position: absolute; + bottom: -8px; + left: 50%; + transform: translateX(-50%); + background: var(--color-text-3); + color: #FFFFFF; + font-size: 11px; + font-weight: 600; + padding: 4px 10px; + border-radius: 999px; + white-space: nowrap; +} + +.stopped-state-title { + font-size: 22px; + font-weight: 700; + margin-bottom: 10px; + color: var(--color-text-1); +} + +.stopped-state-desc { + color: var(--color-text-3); + margin-bottom: 28px; + font-size: 15px; + line-height: 1.6; +} + +.stopped-state-info { + display: flex; + gap: 32px; + margin-bottom: 32px; + padding: 20px 28px; + background: var(--color-bg-2); + border-radius: 12px; + border: 1px solid var(--color-border-2); +} + +.stopped-state-info-item { + display: flex; + flex-direction: column; + gap: 6px; +} + +.stopped-state-info-label { + font-size: 12px; + color: var(--color-text-3); + font-weight: 500; +} + +.stopped-state-info-value { + font-size: 18px; + font-weight: 700; + color: var(--color-text-1); +} + +.stopped-state-btn { + padding: 12px 32px; + font-size: 15px; + display: inline-flex; + align-items: center; + gap: 8px; +} + +/* 对话启动中状态 */ +.conversation-starting-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 60px 40px; + text-align: center; +} + +.starting-state-icon-wrapper { + margin-bottom: 28px; +} + +.starting-state-title { + font-size: 22px; + font-weight: 700; + margin-bottom: 10px; + color: var(--color-text-1); +} + +.starting-state-desc { + color: var(--color-text-3); + margin-bottom: 32px; + font-size: 15px; +} + +.starting-state-progress { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + max-width: 320px; +} + +.starting-state-progress-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: var(--color-bg-2); + border-radius: 10px; + border: 1px solid var(--color-border-2); +} + +.starting-state-progress-item.active { + background: var(--color-primary-light); + border-color: var(--color-primary); +} + +.starting-state-progress-icon { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--color-success); + font-size: 14px; + font-weight: 700; +} + +.starting-state-progress-item.active .starting-state-progress-icon { + color: var(--color-primary); +} + +.starting-state-progress-text { + flex: 1; + text-align: left; + font-size: 14px; + font-weight: 500; + color: var(--color-text-2); +} + +.starting-state-progress-item.active .starting-state-progress-text { + color: var(--color-primary); + font-weight: 600; +} + +.starting-state-spinner { + display: inline-block; + border: 2px solid var(--color-primary); + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +/* 启动中动画 */ +.starting-animation { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +/* 启动中状态 */ +.instance-starting { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 40px; +} + +.spinner { + width: 44px; + height: 44px; + border: 3px solid var(--color-border-3); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin-bottom: 24px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ============ 响应式设计 ============ */ +@media (max-width: 768px) { + :root { + --header-height: 56px; + } + + /* 管理控制台 - 移动端 */ + .sidebar { + transform: translateX(-100%); + transition: transform var(--transition); + z-index: 1001; + } + + .sidebar.show { + transform: translateX(0); + } + + .page-content { + padding: 16px; + } + + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .stat-value { + font-size: 26px; + } + + /* 聊天页面 - 移动端 */ + .chat-sidebar { + position: fixed; + left: 0; + top: 0; + height: 100%; + z-index: 101; + transform: translateX(-100%); + transition: transform var(--transition); + } + + .chat-sidebar.show { + transform: translateX(0); + } + + .chat-header { + padding: 0 16px; + } + + .chat-messages { + padding: 16px; + } + + .message-content { + max-width: 82%; + } + + .welcome-actions { + grid-template-columns: 1fr; + } + + .chat-input-wrapper { + padding: 12px 16px 16px; + } + + .skill-grid { + grid-template-columns: 1fr; + } + + .form-row { + flex-direction: column; + gap: 0; + } + + .header-title { + display: none; + } + + .mobile-menu-btn { + display: flex !important; + } + + .card-body { + padding: 16px; + } + + .table-wrapper { + margin: -16px; + padding: 16px; + } +} + +.mobile-menu-btn { + display: none; + width: 40px; + height: 40px; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: var(--radius-md); +} + +.mobile-menu-btn:hover { + background: var(--color-bg-2); +} + +/* 侧边栏遮罩 */ +.sidebar-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(15, 23, 42, 0.45); + z-index: 1000; + backdrop-filter: blur(2px); +} + +.sidebar-overlay.show { + display: block; +} + +/* 空状态 */ +.empty-state { + text-align: center; + padding: 60px 24px; + color: var(--color-text-3); +} + +.empty-state-icon { + font-size: 56px; + margin-bottom: 16px; + opacity: 0.6; +} + +.empty-state-text { + font-size: 14px; +} + +/* ===== Console page inline styles ===== */ +.instance-actions { display: flex; gap: 6px; } +.skill-back-btn { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; color: #3B82F6; font-weight: 600; margin-bottom: 16px; } +.skill-detail-header { display: flex; gap: 20px; align-items: flex-start; } +.skill-detail-icon { width: 72px; height: 72px; border-radius: 16px; background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%); display: flex; align-items: center; justify-content: center; color: #fff; font-size: 32px; flex-shrink: 0; } +.skill-detail-main { flex: 1; } +.skill-detail-tags { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; } +.skill-detail-tag { padding: 4px 12px; background: #F1F5F9; border-radius: 999px; font-size: 13px; color: #64748B; } +.skill-detail-stats { display: flex; gap: 24px; color: #64748B; font-size: 14px; } +.skill-detail-section { margin-top: 24px; padding-top: 24px; border-top: 1px solid #E2E8F0; } +.skill-detail-section h3 { font-size: 16px; font-weight: 700; margin-bottom: 12px; } +.file-list-item { display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: #F8FAFC; border-radius: 8px; margin-bottom: 8px; } +.file-icon { width: 32px; height: 32px; background: #E8F3FF; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: #3B82F6; } +.file-info { flex: 1; } +.file-name { font-weight: 600; font-size: 14px; } +.file-size { font-size: 12px; color: #94A3B8; } +.version-list-item { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; border: 1px solid #E2E8F0; border-radius: 8px; margin-bottom: 8px; } +.version-info { display: flex; align-items: center; gap: 12px; } +.version-tag { padding: 3px 10px; background: #F1F5F9; border-radius: 6px; font-size: 12px; font-weight: 600; } +.version-tag.current { background: #E8F3FF; color: #3B82F6; } +.version-desc { color: #64748B; font-size: 14px; } +.version-date { color: #94A3B8; font-size: 13px; } + +/* ===== Admin page inline styles ===== */ +.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-item input[type="checkbox"], +.member-radio-item input[type="radio"] { + width: 18px; + height: 18px; + cursor: pointer; + flex-shrink: 0; +} + +.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; +} + +.member-checkbox-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; +} + +.member-checkbox-name { + font-weight: 600; + font-size: 14px; + color: var(--color-text-1); +} + +.member-checkbox-dept { + font-size: 12px; + color: var(--color-text-3); +} + +/* ===== Developer page inline styles ===== */ +.dev-back-btn { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; color: #3B82F6; font-weight: 600; margin-bottom: 16px; } +.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; } + +/* ===== Home page inline styles ===== */ +.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-logo .sidebar-logo-icon { + width: 28px; + height: 28px; + background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%); + border-radius: 6px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} + +.home-logo .sidebar-logo-icon::before { + content: ''; + position: absolute; + width: 14px; + height: 10px; + background: rgba(255, 255, 255, 0.95); + border-radius: 4px 4px 2px 2px; + top: 5px; +} + +.home-logo .sidebar-logo-icon::after { + content: ''; + position: absolute; + width: 18px; + height: 10px; + background: rgba(255, 255, 255, 0.85); + border-radius: 2px 2px 4px 4px; + bottom: 4px; +} + +.home-logo .sidebar-logo-icon span { + position: absolute; + width: 3px; + height: 3px; + background: rgba(59, 130, 246, 0.9); + border-radius: 50%; + top: 9px; + z-index: 1; +} + +.home-logo .sidebar-logo-icon span:nth-child(1) { + left: 9px; +} + +.home-logo .sidebar-logo-icon span:nth-child(2) { + right: 9px; +} + +.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-nav a svg, .home-icon { + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; +} + +.home-nav a.home-nav-login { + color: #3B82F6; + font-weight: 600; +} + +.home-nav a.home-nav-login:hover { + color: #2563EB; + background: #EFF6FF; +} + +.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 svg { + width: 18px; + height: 18px; +} + +.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-btn.secondary { + background: #FFFFFF; + color: var(--color-text-1); + border: 1px solid var(--color-border-3); + box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); +} + +.home-btn.secondary:hover { + background: var(--color-bg-2); + border-color: #94A3B8; + transform: translateY(-1px); +} + +.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-icon svg { + width: 24px; + height: 24px; + color: var(--color-primary); +} + +.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; +} + +@media (max-width: 768px) { + .home-header { + padding: 0 16px; + } + + .home-title { + font-size: 36px; + } + + .home-desc { + font-size: 16px; + } + + .home-buttons { + flex-direction: column; + width: 100%; + max-width: 280px; + } + + .home-btn { + width: 100%; + justify-content: center; + } + + .home-features { + grid-template-columns: 1fr; + } + + .home-nav { + display: none; + } +} + +/* 添加深度思考区域的可点击样式 */ +.message-thinking { + cursor: pointer; +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..f5052fd --- /dev/null +++ b/vite.config.js @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { viteSingleFile } from 'vite-plugin-singlefile' + +// https://vite.dev/config/ +export default defineConfig({ + base: './', + plugins: [react(), viteSingleFile()], +})