chore: 添加 .gitignore 规则,包含前端、pnpm、Node.js 和 AI 工具

This commit is contained in:
2026-03-20 09:14:58 +08:00
parent bf294f9f50
commit 176a727f6e
50 changed files with 6436 additions and 0 deletions

22
src/App.jsx Normal file
View File

@@ -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 (
<Router>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/console" element={<ConsolePage />} />
<Route path="/admin" element={<AdminPage />} />
<Route path="/developer" element={<DeveloperPage />} />
</Routes>
</Router>
);
}
export default App;

36
src/components/Layout.jsx Normal file
View File

@@ -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 (
<div className="layout">
{sidebarOpen && (
<div className="sidebar-overlay show" onClick={toggleSidebar}></div>
)}
<aside className={`${sidebarClassName} ${sidebarOpen ? 'show' : ''}`}>
{sidebar}
</aside>
<main className="main-content">
<header className="header">
<div className="header-left">
<div className="mobile-menu-btn" onClick={toggleSidebar}>
<FiMenu />
</div>
<div className="header-title">{headerTitle}</div>
</div>
</header>
<div className={`page-content ${contentClassName}`}>
{children}
</div>
</main>
</div>
);
}
export default Layout;

View File

@@ -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 (
<div style={{ marginBottom: '16px' }}>
<input
type="text"
className="form-control"
style={{ padding: '6px 10px', fontSize: '13px', marginBottom: '10px' }}
placeholder={searchPlaceholder}
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
{selectedLabel && (
<div style={{ padding: '6px 10px', background: 'var(--color-primary-light)', borderRadius: '6px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', color: 'var(--color-primary)', fontWeight: '500', fontSize: '13px', marginBottom: '10px' }}>
<span>{selectedLabel}</span>
<span
style={{ cursor: 'pointer', color: 'var(--color-text-3)' }}
onClick={onClearSelected}
>×</span>
</div>
)}
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
<table className="table" style={{ marginBottom: '0' }}>
<thead>
<tr>
<th style={{ width: '50px', padding: '8px' }}></th>
{columns.map(col => (
<th key={col.key} style={{ padding: '8px' }}>{col.label}</th>
))}
</tr>
</thead>
<tbody>
{filteredData.map(item => (
<tr key={item.id}>
<td style={{ padding: '8px' }}>
<input
type={multiSelect ? 'checkbox' : 'radio'}
checked={isSelected(item)}
onChange={() => handleSelect(item)}
style={{ width: '16px', height: '16px', cursor: 'pointer' }}
/>
</td>
{columns.map(col => (
<td key={col.key} style={{ padding: '8px' }}>
{col.render ? col.render(item) : item[col.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
export default ListSelector;

218
src/data/conversations.js Normal file
View File

@@ -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: `
<div class="welcome-section">
<h1 class="welcome-title">你好,我是 GrandClaw</h1>
<p class="welcome-desc">企业级智能助手已为你接通3个技能</p>
<div class="welcome-actions">
<div class="welcome-action">
<div class="welcome-action-title">💻 代码生成</div>
<div class="welcome-action-desc">帮我生成一段 Python 代码</div>
</div>
<div class="welcome-action">
<div class="welcome-action-title">📊 数据分析</div>
<div class="welcome-action-desc">分析一下这份数据</div>
</div>
<div class="welcome-action">
<div class="welcome-action-title">📄 文档撰写</div>
<div class="welcome-action-desc">帮我写一份项目周报</div>
</div>
<div class="welcome-action">
<div class="welcome-action-title">👥 业务查询</div>
<div class="welcome-action-desc">查询一下客户信息</div>
</div>
</div>
</div>
`,
starting: `
<div class="conversation-starting-state">
<div class="starting-state-icon-wrapper">
<div class="spinner" style="width: 56px; height: 56px; border-width: 4px;"></div>
</div>
<h2 class="starting-state-title">正在启动对话实例...</h2>
<p class="starting-state-desc">预计需要 3-5 秒,请稍候</p>
<div class="starting-state-progress">
<div class="starting-state-progress-item">
<span class="starting-state-progress-icon">✓</span>
<span class="starting-state-progress-text">加载模型配置</span>
</div>
<div class="starting-state-progress-item">
<span class="starting-state-progress-icon">✓</span>
<span class="starting-state-progress-text">初始化技能</span>
</div>
<div class="starting-state-progress-item active">
<span class="starting-state-progress-icon">
<span class="starting-state-spinner" style="width: 14px; height: 14px; border-width: 2px;"></span>
</span>
<span class="starting-state-progress-text">恢复对话上下文</span>
</div>
</div>
</div>
`,
text: `
<div class="message user">
<div class="message-avatar user">张</div>
<div class="message-content">
<div class="message-bubble">帮我重构这段代码,让它更简洁高效</div>
<div class="message-time">14:30</div>
</div>
</div>
<div class="message assistant">
<div class="message-avatar assistant">🤖</div>
<div class="message-content">
<div class="message-thinking">
<div class="message-thinking-header">
<span class="message-thinking-icon">▶</span>
<span>已深度思考</span>
</div>
<div class="message-thinking-content">
<p>让我分析一下代码重构的思路:</p>
<ul>
<li>首先识别代码中的重复模式和冗余逻辑</li>
<li>考虑使用 Python 的列表推导式和生成器表达式来简化循环</li>
<li>评估是否可以引入高阶函数如 map/filter/replace</li>
<li>检查异常处理是否可以统一抽象</li>
<li>评估类型提示的添加位置,确保不影响性能</li>
</ul>
</div>
</div>
<div class="message-bubble">
<p>好的,我来帮你重构这段代码。首先让我分析一下现有代码的问题,然后提供优化方案。</p>
<p style="margin-top: 12px;"><strong>优化建议:</strong></p>
<ul style="margin-top: 8px; padding-left: 20px;">
<li>使用列表推导式替代循环</li>
<li>添加类型提示提高可读性</li>
<li>提取公共逻辑为独立函数</li>
</ul>
</div>
<div class="message-time">14:31</div>
</div>
</div>
`,
skill: `
<div class="message user">
<div class="message-avatar user">张</div>
<div class="message-content">
<div class="message-bubble">帮我查询一下客户 "张三" 的最近订单</div>
<div class="message-time">10:15</div>
</div>
</div>
<div class="message assistant">
<div class="message-avatar assistant">🤖</div>
<div class="message-content">
<div class="message-bubble">
<p style="display: flex; align-items: center; gap: 8px; color: #3B82F6;">
🧩 已加载CRM客户查询技能
</p>
</div>
<div class="message-time">10:15</div>
</div>
</div>
<div class="message assistant">
<div class="message-avatar assistant">🤖</div>
<div class="message-content">
<div class="message-thinking">
<div class="message-thinking-header">
<span class="message-thinking-icon">▶</span>
<span>已深度思考</span>
</div>
<div class="message-thinking-content">
<p>正在调用CRM客户查询技能...</p>
<ul>
<li>识别用户意图:查询客户"张三"的订单信息</li>
<li>匹配技能CRM 客户查询 v1.2.0</li>
<li>构建查询参数:姓名="张三",时间范围=最近一年</li>
<li>执行查询,获取结果</li>
<li>格式化输出,突出关键信息(等级、订单数、金额)</li>
</ul>
</div>
</div>
<div class="message-bubble">
<p><strong>客户信息查询结果:</strong></p>
<div style="margin-top: 12px; padding: 12px; background: #F8FAFC; border-radius: 8px;">
<p><strong>客户姓名:</strong>张三</p>
<p><strong>客户等级:</strong>VIP</p>
<p><strong>最近订单:</strong>3笔总计 ¥28,560</p>
<p><strong>最后联系:</strong>2026-03-10</p>
</div>
</div>
<div class="message-time">10:16</div>
</div>
</div>
`,
file: `
<div class="message user">
<div class="message-avatar user">张</div>
<div class="message-content">
<div class="message-bubble">
<p>帮我分析一下这个 Excel 文件里的销售数据</p>
<div style="margin-top: 12px; padding: 10px 14px; background: #F8FAFC; border-radius: 8px; display: inline-flex; align-items: center; gap: 10px; color: #475569;">
<span style="font-size: 20px;">📊</span>
<div>
<div style="font-weight: 600;">Q1销售数据.xlsx</div>
<div style="font-size: 12px; color: #94A3B8;">2.4 MB</div>
</div>
</div>
</div>
<div class="message-time">16:20</div>
</div>
</div>
<div class="message assistant">
<div class="message-avatar assistant">🤖</div>
<div class="message-content">
<div class="message-bubble">
<p>文件已接收!正在分析数据...</p>
<div style="margin-top: 14px;">
<div style="height: 6px; background: #E2E8F0; border-radius: 3px; overflow: hidden;">
<div style="width: 65%; height: 100%; background: linear-gradient(90deg, #3B82F6, #8B5CF6); border-radius: 3px;"></div>
</div>
</div>
</div>
<div class="message-time">16:20</div>
</div>
</div>
<div class="message assistant">
<div class="message-avatar assistant">🤖</div>
<div class="message-content">
<div class="message-thinking">
<div class="message-thinking-header">
<span class="message-thinking-icon">▶</span>
<span>已深度思考</span>
</div>
<div class="message-thinking-content">
<p>正在分析 Excel 文件中的销售数据...</p>
<ul>
<li>文件格式识别Excel (.xlsx),大小 2.4MB</li>
<li>数据结构解析:包含日期、产品、销售额等字段</li>
<li>时间范围2026年 Q11-3月</li>
<li>计算指标:总销售额、同比增长、月度趋势、产品占比</li>
<li>异常检测:无异常值或缺失数据</li>
<li>关键洞察提取3月表现突出产品C增长势头强劲</li>
</ul>
</div>
</div>
<div class="message-bubble">
<p><strong>数据分析完成!以下是关键发现:</strong></p>
<ul style="margin-top: 12px; padding-left: 20px;">
<li>Q1 总销售额:<strong>¥128.5 万</strong>,同比增长 18%</li>
<li>3 月份表现最佳,单月突破 50 万</li>
<li>产品 A 占比最高42%),产品 C 增长最快</li>
</ul>
<p style="margin-top: 12px;">需要生成可视化图表吗?</p>
</div>
<div class="message-time">16:22</div>
</div>
</div>
`
};
}

92
src/data/developerData.js Normal file
View File

@@ -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: '技能发布后的审核和上线流程...' }
];

14
src/data/logs.js Normal file
View File

@@ -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条记录' }
];

12
src/data/members.js Normal file
View File

@@ -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 客户查询', '财务数据同步', '网络故障排查'];

31
src/data/skills.js Normal file
View File

@@ -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];
}

62
src/data/tasks.js Normal file
View File

@@ -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' }
]
}
];

View File

@@ -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;

10
src/main.jsx Normal file
View File

@@ -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(
<StrictMode>
<App />
</StrictMode>,
)

130
src/pages/AdminPage.jsx Normal file
View File

@@ -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 <OverviewPage />;
case 'departments':
return <DepartmentsPage onAdd={() => setCurrentPage('addDepartment')} />;
case 'users':
return <UsersPage onAdd={() => setCurrentPage('addUser')} />;
case 'projects':
return <AdminProjectsPage onAdd={() => setCurrentPage('addProject')} />;
case 'addDepartment':
return <AddDepartmentPage onBack={() => setCurrentPage('departments')} />;
case 'addUser':
return <AddUserPage onBack={() => setCurrentPage('users')} />;
case 'addProject':
return <AddProjectPage onBack={() => setCurrentPage('projects')} />;
default:
return <div>Page not found</div>;
}
};
const getPageTitle = () => {
const titles = {
overview: '总览',
departments: '部门管理',
users: '用户管理',
projects: '项目管理',
addDepartment: '新增部门',
addUser: '新增用户',
addProject: '新增项目'
};
return titles[currentPage] || '';
};
const sidebar = (
<>
<div className="admin-sidebar-header">
<div className="sidebar-brand">
<div className="sidebar-logo-icon">
<span></span>
<span></span>
</div>
<div className="sidebar-brand-text">
<div className="sidebar-logo">GrandClaw</div>
<div className="sidebar-subtitle">运营管理台</div>
</div>
</div>
</div>
<nav className="admin-sidebar-nav">
<div
className={`admin-nav-item ${currentPage === 'overview' ? 'active' : ''}`}
onClick={() => setCurrentPage('overview')}
>
<span className="admin-nav-icon"><FiHome /></span>
<span className="admin-nav-text">总览</span>
</div>
<div
className={`admin-nav-item ${currentPage === 'departments' ? 'active' : ''}`}
onClick={() => setCurrentPage('departments')}
>
<span className="admin-nav-icon"><FiBarChart2 /></span>
<span className="admin-nav-text">部门管理</span>
</div>
<div
className={`admin-nav-item ${currentPage === 'users' ? 'active' : ''}`}
onClick={() => setCurrentPage('users')}
>
<span className="admin-nav-icon"><FiUsers /></span>
<span className="admin-nav-text">用户管理</span>
</div>
<div
className={`admin-nav-item ${currentPage === 'projects' ? 'active' : ''}`}
onClick={() => setCurrentPage('projects')}
>
<span className="admin-nav-icon"><FiList /></span>
<span className="admin-nav-text">项目管理</span>
</div>
</nav>
<div className="admin-sidebar-user">
<div className="user-avatar"></div>
<div className="admin-sidebar-user-info">
<div className="admin-sidebar-user-name">张三</div>
<div className="admin-sidebar-user-role">系统管理员</div>
</div>
</div>
</>
);
return (
<Layout
sidebar={sidebar}
headerTitle={getPageTitle()}
sidebarClassName="admin-sidebar"
>
{renderPage()}
</Layout>
);
}
export default AdminPage;

228
src/pages/ConsolePage.jsx Normal file
View File

@@ -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 <ChatPage scene={currentScene} />;
case 'skills':
return <SkillsPage onSkillClick={handleSkillClick} />;
case 'skillDetail':
return <SkillDetailPage skillId={currentSkillId} onBack={handleBack} />;
case 'logs':
return <LogsPage />;
case 'scheduledTasks':
return <TasksPage
onViewDetail={(taskId) => {
setCurrentTaskId(taskId);
switchPage('taskDetail');
}}
/>;
case 'taskDetail':
return <TaskDetailPage
taskId={currentTaskId}
onBack={() => switchPage('scheduledTasks')}
/>;
case 'account':
return <AccountPage />;
case 'projects':
return <ProjectsPage onAddMember={() => switchPage('addMember')} />;
case 'memberConfig':
return <MemberConfigPage onBack={() => switchPage('projects')} />;
case 'addMember':
return <AddMemberPage onBack={() => switchPage('projects')} />;
default:
return <div>Page not found</div>;
}
};
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 = (
<>
<div className="chat-sidebar-header">
<div className="sidebar-brand">
<div className="sidebar-logo-icon">
<span></span>
<span></span>
</div>
<div className="sidebar-brand-text">
<div className="sidebar-logo">GrandClaw</div>
<div className="sidebar-subtitle">企业级AI平台</div>
</div>
</div>
<div className="sidebar-divider"></div>
<button className="btn btn-primary" style={{ width: '100%' }} onClick={createNewChat}>
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}>
<FiPlus /> 新建对话
</span>
</button>
</div>
<div className="chat-sidebar-content">
{conversations.map(conv => (
<div
key={conv.id}
className={`conversation-item ${conv.scene === activeScene ? 'active' : ''}`}
onClick={() => switchChatScene(conv.scene)}
>
<div className="conversation-title">{conv.title}</div>
<div className="conversation-time">{conv.time}</div>
</div>
))}
</div>
<div className="chat-sidebar-project">
<label className="chat-sidebar-project-label">当前项目</label>
<select className="form-control chat-sidebar-project-select">
<option>企业 AI 智算平台</option>
<option>知识库管理系统</option>
<option>数据分析平台</option>
</select>
</div>
<div className="chat-sidebar-nav">
<div
className={`chat-nav-item ${currentPage === 'skills' ? 'active' : ''}`}
onClick={() => switchPage('skills')}
>
<span className="chat-nav-icon"><FaPuzzlePiece /></span>
<span className="chat-nav-text">技能市场</span>
</div>
<div
className={`chat-nav-item ${currentPage === 'scheduledTasks' ? 'active' : ''}`}
onClick={() => switchPage('scheduledTasks')}
>
<span className="chat-nav-icon"><FiClock /></span>
<span className="chat-nav-text">定时任务</span>
</div>
<div
className={`chat-nav-item ${currentPage === 'logs' ? 'active' : ''}`}
onClick={() => switchPage('logs')}
>
<span className="chat-nav-icon"><FiList /></span>
<span className="chat-nav-text">日志查询</span>
</div>
<div
className={`chat-nav-item ${currentPage === 'projects' ? 'active' : ''}`}
onClick={() => switchPage('projects')}
>
<span className="chat-nav-icon"><FiUsers /></span>
<span className="chat-nav-text">项目管理</span>
</div>
</div>
<div className="chat-sidebar-user" onClick={() => switchPage('account')}>
<div className="user-avatar"></div>
<div className="chat-sidebar-user-info">
<div className="chat-sidebar-user-name">张三</div>
<div className="chat-sidebar-user-role">AI 产品部</div>
</div>
</div>
</>
);
return (
<Layout
sidebar={sidebar}
headerTitle={getPageTitle()}
sidebarClassName="chat-sidebar"
contentClassName={currentPage === 'chat' ? 'page-content-full' : ''}
>
{renderPage()}
</Layout>
);
}
export default ConsolePage;

173
src/pages/DeveloperPage.jsx Normal file
View File

@@ -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 <MySkillsPage onSkillClick={openSkillEditor} />;
case 'uploadSkill':
return <UploadSkillPage />;
case 'devDocs':
return <DevDocsPage />;
case 'devAccount':
return <DevAccountPage />;
case 'skillEditor':
return <SkillEditorPage skillId={currentSkillId} onBack={handleBack} onUploadNewVersion={openNewVersionPage} />;
case 'newVersion':
return <NewVersionPage skillName={newVersionSkillName} onBack={handleNewVersionBack} />;
default:
return <div>Page not found</div>;
}
};
const getPageTitle = () => {
const titles = {
mySkills: '我的技能',
uploadSkill: '创建技能',
newVersion: '上传新版本',
devDocs: '开发文档',
devAccount: '开发者设置',
skillEditor: '技能详情'
};
return titles[currentPage] || '';
};
const sidebar = (
<>
<div className="chat-sidebar-header">
<div className="sidebar-brand">
<div className="sidebar-logo-icon">
<span></span>
<span></span>
</div>
<div className="sidebar-brand-text">
<div className="sidebar-logo">GrandClaw</div>
<div className="sidebar-subtitle">技能开发台</div>
</div>
</div>
<div className="sidebar-divider"></div>
<button className="btn btn-primary" style={{ width: '100%' }} onClick={createNewProject}>
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}>
<FiPlus /> 创建技能
</span>
</button>
</div>
<div className="chat-sidebar-content">
{mySkills.map(skill => (
<div
key={skill.id}
className={`conversation-item ${currentSkillId === skill.id && currentPage === 'skillEditor' ? 'active' : ''}`}
onClick={() => openSkillEditor(skill.id)}
>
<div className="conversation-title">{skill.name}</div>
<div className="conversation-time">{skill.status === 'published' ? '已发布' : '草稿'}</div>
</div>
))}
</div>
<div className="chat-sidebar-nav">
<div
className={`chat-nav-item ${currentPage === 'mySkills' ? 'active' : ''}`}
onClick={() => switchPage('mySkills')}
>
<span className="chat-nav-icon"><FaPuzzlePiece /></span>
<span className="chat-nav-text">我的技能</span>
</div>
<div
className={`chat-nav-item ${currentPage === 'devDocs' ? 'active' : ''}`}
onClick={() => switchPage('devDocs')}
>
<span className="chat-nav-icon"><FiTerminal /></span>
<span className="chat-nav-text">开发文档</span>
</div>
</div>
<div className="chat-sidebar-user" onClick={() => switchPage('devAccount')}>
<div className="user-avatar"></div>
<div className="chat-sidebar-user-info">
<div className="chat-sidebar-user-name">张三</div>
<div className="chat-sidebar-user-role">开发者</div>
</div>
</div>
</>
);
return (
<Layout
sidebar={sidebar}
headerTitle={getPageTitle()}
sidebarClassName="chat-sidebar"
>
{renderPage()}
</Layout>
);
}
export default DeveloperPage;

76
src/pages/HomePage.jsx Normal file
View File

@@ -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 (
<div className="home-layout">
<header className="home-header">
<div className="home-logo">
<div className="sidebar-logo-icon">
<span></span>
<span></span>
</div>
GrandClaw
</div>
<nav className="home-nav">
<Link to="/console" state={{ fromHome: true }}>
<FiSettings /> 工作台
</Link>
<Link to="/developer?init=true" state={{ fromHome: true }}>
<FiCode /> 开发台
</Link>
<Link to="/admin" state={{ fromHome: true }}>
<FiUsers /> 管理台
</Link>
<Link to="/login" className="home-nav-login">
<FiLogIn /> 登录
</Link>
</nav>
</header>
<main className="home-main">
<div className="home-badge">
<span className="home-badge-dot"></span>
企业级 AI 平台
</div>
<h1 className="home-title">智能 <span>企业助手</span></h1>
<p className="home-desc">
基于容器化实例的 智能助手平台提供租户隔离技能市场安全审计等核心能力
</p>
<div className="home-buttons">
<Link to="/console" className="home-btn primary" state={{ fromHome: true }}>
<FaRobot /> 进入工作台
</Link>
</div>
<div className="home-features">
<div className="home-feature">
<div className="home-feature-icon">
<FiMonitor />
</div>
<div className="home-feature-title">容器化隔离</div>
<div className="home-feature-desc">每租户独享独立容器实例天然物理隔离数据安全有保障</div>
</div>
<div className="home-feature">
<div className="home-feature-icon">
<FaPuzzlePiece />
</div>
<div className="home-feature-title">技能市场</div>
<div className="home-feature-desc">丰富的技能生态按需订阅动态挂载无缝扩展能力</div>
</div>
<div className="home-feature">
<div className="home-feature-icon">
<FiList />
</div>
<div className="home-feature-title">安全审计</div>
<div className="home-feature-desc">完整的操作日志运行日志计费统计满足企业合规要求</div>
</div>
</div>
</main>
<footer className="home-footer">
© 2026 GrandClaw Team · 前端原型演示
</footer>
</div>
);
}
export default HomePage;

141
src/pages/LoginPage.jsx Normal file
View File

@@ -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 (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#F8FAFC',
padding: '20px'
}}>
<div style={{ width: '100%', maxWidth: '380px' }}>
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '12px',
marginBottom: '20px'
}}>
<div className="sidebar-logo-icon home-logo-icon">
<span></span>
<span></span>
</div>
<span style={{ fontSize: '24px', fontWeight: '800', color: '#1E293B' }}>GrandClaw</span>
</div>
</div>
<div style={{
background: '#FFFFFF',
borderRadius: '16px',
padding: '32px',
boxShadow: '0 4px 24px rgba(15, 23, 42, 0.08)',
border: '1px solid #E2E8F0'
}}>
<h2 style={{ fontSize: '20px', fontWeight: '700', color: '#1E293B', marginBottom: '6px', textAlign: 'center' }}>
欢迎回来
</h2>
<p style={{ color: '#64748B', fontSize: '14px', textAlign: 'center', marginBottom: '28px' }}>
请登录您的账号以继续
</p>
<div className="form-group">
<input type="text" className="form-control" placeholder="用户名 / 邮箱" />
</div>
<div className="form-group">
<input type="password" className="form-control" placeholder="密码" />
</div>
<div className="form-group">
<div style={{ display: 'flex', gap: '12px' }}>
<input
type="text"
className="form-control"
placeholder="验证码"
value={captchaInput}
onChange={(e) => setCaptchaInput(e.target.value)}
style={{ flex: 1 }}
/>
<div
style={{
width: '100px',
height: '38px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
fontWeight: '700',
color: '#FFFFFF',
letterSpacing: '6px',
cursor: 'pointer',
userSelect: 'none'
}}
onClick={refreshCaptcha}
title="点击刷新验证码"
>
{captchaCode}
</div>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '13px', color: '#64748B', cursor: 'pointer' }}>
<input type="checkbox" style={{ width: '14px', height: '14px', accentColor: '#3B82F6' }} />
记住我
</label>
<a href="#" style={{ fontSize: '13px', color: '#3B82F6', textDecoration: 'none' }}>
忘记密码
</a>
</div>
<button
className="btn btn-primary"
style={{ width: '100%', padding: '11px', fontSize: '14px', fontWeight: '600' }}
onClick={() => navigate('/console', { state: { fromHome: true } })}
>
登录
</button>
</div>
<div style={{ textAlign: 'center', marginTop: '24px' }}>
<Link
to="/"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
color: '#64748B',
fontSize: '14px',
textDecoration: 'none'
}}
>
<FiArrowLeft /> 返回首页
</Link>
</div>
</div>
</div>
);
}
export default LoginPage;

View File

@@ -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 (
<div className="card">
<div className="card-header">
<div className="card-title">新增部门</div>
</div>
<div className="card-body">
<div className="form-group">
<label className="form-label required">部门名称</label>
<input type="text" className="form-control" placeholder="请输入部门名称" />
</div>
<div className="form-group">
<label className="form-label">部门描述</label>
<textarea className="form-control" rows="3" placeholder="请输入部门描述"></textarea>
</div>
<div className="form-group">
<label className="form-label required">负责人</label>
<ListSelector
data={availableLeaders}
selectedIds={selectedLeader}
onChange={setSelectedLeader}
searchPlaceholder="搜索负责人..."
columns={leaderColumns}
multiSelect={false}
selectedLabel={selectedLabel}
onClearSelected={() => setSelectedLeader(null)}
/>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn btn-primary">确定</button>
</div>
</div>
</div>
);
}
export default AddDepartmentPage;

View File

@@ -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 (
<div className="card">
<div className="card-header">
<div className="card-title">新增项目</div>
</div>
<div className="card-body">
<div className="form-group">
<label className="form-label required">项目名称</label>
<input type="text" className="form-control" placeholder="请输入项目名称" />
</div>
<div className="form-group">
<label className="form-label">项目描述</label>
<textarea className="form-control" rows="3" placeholder="请输入项目描述"></textarea>
</div>
<div className="form-group">
<label className="form-label required">负责人</label>
<ListSelector
data={availableLeaders}
selectedIds={selectedLeader}
onChange={setSelectedLeader}
searchPlaceholder="搜索负责人..."
columns={leaderColumns}
multiSelect={false}
selectedLabel={selectedLabel}
onClearSelected={() => setSelectedLeader(null)}
/>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn btn-primary">确定</button>
</div>
</div>
</div>
);
}
export default AddProjectPage;

View File

@@ -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 (
<div className="card">
<div className="card-header">
<div className="card-title">新增用户</div>
</div>
<div className="card-body">
<div className="form-group">
<label className="form-label required">姓名</label>
<input type="text" className="form-control" placeholder="请输入用户姓名" />
</div>
<div className="form-group">
<label className="form-label required">部门</label>
<ListSelector
data={availableDepartments}
selectedIds={selectedDepartment}
onChange={setSelectedDepartment}
searchPlaceholder="搜索部门..."
columns={departmentColumns}
multiSelect={false}
selectedLabel={selectedLabel}
onClearSelected={() => setSelectedDepartment(null)}
/>
</div>
<div className="form-group">
<label className="form-label required">角色</label>
<select className="form-control">
<option>成员</option>
<option>管理员</option>
<option>开发者</option>
</select>
</div>
<div className="form-group">
<label className="form-label required">邮箱</label>
<input type="email" className="form-control" placeholder="请输入邮箱" />
</div>
<div className="form-group">
<label className="form-label">手机号</label>
<input type="tel" className="form-control" placeholder="请输入手机号" />
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn btn-primary">确定</button>
</div>
</div>
</div>
);
}
export default AddUserPage;

View File

@@ -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 <span className={`status ${statusClass}`}>{status}</span>;
}
function AdminProjectsPage({ onAdd }) {
return (
<>
<div className="card">
<div className="card-body">
<div className="search-bar">
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
<label>关键词</label>
<input type="text" className="form-control" placeholder="搜索项目名称、描述..." />
</div>
<div className="search-item">
<label>状态</label>
<select className="form-control">
<option>全部</option>
<option>正常</option>
<option>禁用</option>
</select>
</div>
</div>
<div className="search-actions">
<button className="btn btn-primary">查询</button>
<button className="btn">重置</button>
</div>
</div>
</div>
<div className="card">
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="card-title">项目列表</div>
<button className="btn btn-primary btn-sm" onClick={onAdd}>新增项目</button>
</div>
<div className="card-body">
<div className="table-wrapper">
<table className="table">
<thead>
<tr>
<th>项目名称</th>
<th>项目描述</th>
<th>负责人</th>
<th>成员数</th>
<th>状态</th>
<th>创建时间</th>
<th style={{ width: '200px' }}>操作</th>
</tr>
</thead>
<tbody>
{adminProjects.map(project => (
<tr key={project.id}>
<td><strong>{project.name}</strong></td>
<td>{project.description}</td>
<td>{project.owner}</td>
<td>{project.members} </td>
<td><StatusTag status={project.status} /></td>
<td>{project.createTime}</td>
<td style={{ width: '200px' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<button className={`text-btn ${project.status === '正常' ? 'text-btn-danger' : 'text-btn-primary'}`}>{project.status === '正常' ? '禁用' : '启用'}</button>
<button className="text-btn text-btn-primary">编辑</button>
<button className="text-btn text-btn-danger">删除</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="pagination">
<div className="pagination-item"></div>
<div className="pagination-item active">1</div>
<div className="pagination-item">2</div>
<div className="pagination-item">3</div>
<div className="pagination-item"></div>
</div>
</div>
</div>
</>
);
}
export default AdminProjectsPage;

View File

@@ -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 <span className={`status ${statusClass}`}>{status}</span>;
}
function DepartmentsPage({ onAdd }) {
return (
<>
<div className="card">
<div className="card-body">
<div className="search-bar">
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
<label>关键词</label>
<input type="text" className="form-control" placeholder="搜索部门名称、描述..." />
</div>
<div className="search-item">
<label>状态</label>
<select className="form-control">
<option>全部</option>
<option>正常</option>
<option>禁用</option>
</select>
</div>
</div>
<div className="search-actions">
<button className="btn btn-primary">查询</button>
<button className="btn">重置</button>
</div>
</div>
</div>
<div className="card">
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="card-title">部门列表</div>
<button className="btn btn-primary btn-sm" onClick={onAdd}>新增部门</button>
</div>
<div className="card-body">
<div className="table-wrapper">
<table className="table">
<thead>
<tr>
<th>部门名称</th>
<th>部门描述</th>
<th>负责人</th>
<th>成员数</th>
<th>状态</th>
<th>创建时间</th>
<th style={{ width: '200px' }}>操作</th>
</tr>
</thead>
<tbody>
{departments.map(dept => (
<tr key={dept.id}>
<td><strong>{dept.name}</strong></td>
<td>{dept.description}</td>
<td>{dept.head}</td>
<td>{dept.members} </td>
<td><StatusTag status={dept.status} /></td>
<td>{dept.createTime}</td>
<td style={{ width: '200px' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<button className={`text-btn ${dept.status === '正常' ? 'text-btn-danger' : 'text-btn-primary'}`}>{dept.status === '正常' ? '禁用' : '启用'}</button>
<button className="text-btn text-btn-primary">编辑</button>
<button className="text-btn text-btn-danger">删除</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="pagination">
<div className="pagination-item"></div>
<div className="pagination-item active">1</div>
<div className="pagination-item">2</div>
<div className="pagination-item">3</div>
<div className="pagination-item"></div>
</div>
</div>
</div>
</>
);
}
export default DepartmentsPage;

View File

@@ -0,0 +1,18 @@
function OverviewPage() {
return (
<div className="card">
<div className="card-header">
<div className="card-title">运营总览</div>
</div>
<div className="card-body">
<div style={{ textAlign: 'center', padding: '60px 20px', color: 'var(--color-text-3)' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}></div>
<div style={{ fontSize: '18px', fontWeight: 600, marginBottom: '8px' }}>运营总览页面</div>
<div>此处展示平台运营数据概览</div>
</div>
</div>
</div>
);
}
export default OverviewPage;

View File

@@ -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 <span className={`status ${statusClass}`}>{status}</span>;
}
function RoleTag({ role }) {
const roleClass = role === '管理员' ? 'role-admin' : role === '开发者' ? 'role-developer' : 'role-member';
return <span className={`status ${roleClass}`}>{role}</span>;
}
function UsersPage({ onAdd }) {
return (
<>
<div className="card">
<div className="card-body">
<div className="search-bar">
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
<label>关键词</label>
<input type="text" className="form-control" placeholder="搜索用户姓名、邮箱..." />
</div>
<div className="search-item">
<label>部门</label>
<select className="form-control">
<option>全部部门</option>
<option>AI 产品部</option>
<option>技术研发部</option>
<option>数据分析部</option>
<option>运营部</option>
<option>测试部</option>
</select>
</div>
<div className="search-item">
<label>状态</label>
<select className="form-control">
<option>全部</option>
<option>正常</option>
<option>禁用</option>
</select>
</div>
</div>
<div className="search-actions">
<button className="btn btn-primary">查询</button>
<button className="btn">重置</button>
</div>
</div>
</div>
<div className="card">
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="card-title">用户列表</div>
<button className="btn btn-primary btn-sm" onClick={onAdd}>新增用户</button>
</div>
<div className="card-body">
<div className="table-wrapper">
<table className="table">
<thead>
<tr>
<th>姓名</th>
<th>部门</th>
<th>角色</th>
<th>邮箱</th>
<th>手机号</th>
<th>状态</th>
<th>最后登录</th>
<th style={{ width: '200px' }}>操作</th>
</tr>
</thead>
<tbody>
{adminUsers.map(user => (
<tr key={user.id}>
<td><strong>{user.name}</strong></td>
<td>{user.department}</td>
<td><RoleTag role={user.role} /></td>
<td>{user.email}</td>
<td>{user.phone}</td>
<td><StatusTag status={user.status} /></td>
<td>{user.lastLogin}</td>
<td style={{ width: '200px' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<button className={`text-btn ${user.status === '正常' ? 'text-btn-danger' : 'text-btn-primary'}`}>{user.status === '正常' ? '禁用' : '启用'}</button>
<button className="text-btn text-btn-primary">编辑</button>
<button className="text-btn text-btn-danger">删除</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="pagination">
<div className="pagination-item"></div>
<div className="pagination-item active">1</div>
<div className="pagination-item">2</div>
<div className="pagination-item">3</div>
<div className="pagination-item"></div>
</div>
</div>
</div>
</>
);
}
export default UsersPage;

View File

@@ -0,0 +1,85 @@
function AccountPage() {
return (
<>
<div className="card">
<div className="card-header">
<div className="card-title">账号信息</div>
</div>
<div className="card-body">
{/* 头像区域 */}
<div style={{ textAlign: 'center', marginBottom: '24px', paddingBottom: '24px', borderBottom: '1px solid var(--color-border-2)' }}>
<div style={{
width: '100px',
height: '100px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #3B82F6, #8B5CF6)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: '36px',
margin: '0 auto 12px'
}}></div>
<button className="btn btn-sm">更换头像</button>
</div>
{/* 表单区域 */}
<div className="form-row">
<div className="form-col">
<div className="form-group">
<label className="form-label">用户名</label>
<input type="text" className="form-control" defaultValue="zhangsan" readOnly style={{ background: '#F8FAFC' }} />
</div>
</div>
<div className="form-col">
<div className="form-group">
<label className="form-label">姓名</label>
<input type="text" className="form-control" defaultValue="张三" />
</div>
</div>
</div>
<div className="form-row">
<div className="form-col">
<div className="form-group">
<label className="form-label">邮箱</label>
<input type="email" className="form-control" defaultValue="zhangsan@example.com" />
</div>
</div>
<div className="form-col">
<div className="form-group">
<label className="form-label">手机号</label>
<input type="text" className="form-control" defaultValue="138****8888" />
</div>
</div>
</div>
<div className="form-group">
<label className="form-label">所属部门</label>
<input type="text" className="form-control" defaultValue="AI 产品部" readOnly style={{ background: '#F8FAFC' }} />
</div>
<button className="btn btn-primary">保存修改</button>
</div>
</div>
<div className="card">
<div className="card-header">
<div className="card-title">修改密码</div>
</div>
<div className="card-body">
<div className="form-group">
<label className="form-label">当前密码</label>
<input type="password" className="form-control" placeholder="请输入当前密码" />
</div>
<div className="form-group">
<label className="form-label">新密码</label>
<input type="password" className="form-control" placeholder="请输入新密码" />
</div>
<div className="form-group">
<label className="form-label">确认新密码</label>
<input type="password" className="form-control" placeholder="请再次输入新密码" />
</div>
<button className="btn btn-primary">更新密码</button>
</div>
</div>
</>
);
}
export default AccountPage;

View File

@@ -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 (
<div className="card">
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="card-title">增加成员</div>
<button className="btn btn-primary btn-sm" onClick={onBack}>返回列表</button>
</div>
<div className="card-body">
<ListSelector
data={availableMembers}
selectedIds={selectedMembers}
onChange={setSelectedMembers}
searchPlaceholder="搜索成员姓名、部门、邮箱..."
columns={memberColumns}
multiSelect={true}
selectedLabel={selectedLabel}
onClearSelected={() => setSelectedMembers([])}
/>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '16px' }}>
<button className="btn btn-primary" onClick={handleAdd} disabled={selectedMembers.length === 0}>
添加选中成员 ({selectedMembers.length})
</button>
</div>
</div>
</div>
);
}
export default AddMemberPage;

View File

@@ -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 (
<div className="chat-layout" style={{ height: '100%' }}>
<div className="chat-content">
<div className="chat-messages" style={{ padding: '16px 24px 8px' }}>
<div ref={chatMessagesRef} dangerouslySetInnerHTML={{ __html: html }} />
</div>
<div className="chat-input-wrapper" style={{ padding: '12px 24px 20px' }}>
<div className="chat-input-container">
<div className="chat-input-box">
<div className="chat-input-main">
<textarea
className="chat-input"
placeholder="输入消息... Enter 发送Shift+Enter 换行"
rows="1"
/>
<div className="chat-input-actions">
<div className="chat-input-tools">
<div className="chat-input-tool" title="上传文件">
<FiPaperclip />
</div>
<div className="chat-input-tool" title="代码块">
<FiCode />
</div>
</div>
<button className="chat-send-btn">
<FiSend />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default ChatPage;

View File

@@ -0,0 +1,94 @@
import { logs } from '../../data/logs.js';
function LogsPage() {
return (
<>
<div className="card">
<div className="card-body">
<div className="search-bar">
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
<label>关键词</label>
<input type="text" className="form-control" placeholder="搜索操作、详情..." />
</div>
<div className="search-item">
<label>用户</label>
<select className="form-control">
<option>全部用户</option>
<option>张三</option>
<option>李四</option>
<option>王五</option>
</select>
</div>
<div className="search-item">
<label>类型</label>
<select className="form-control">
<option>全部类型</option>
<option>登录</option>
<option>实例操作</option>
<option>技能</option>
<option>配置修改</option>
<option>文件上传</option>
</select>
</div>
<div className="search-item">
<label>状态</label>
<select className="form-control">
<option>全部</option>
<option>成功</option>
<option>失败</option>
<option>警告</option>
</select>
</div>
</div>
<div className="search-actions">
<button className="btn btn-primary">查询</button>
<button className="btn">重置</button>
</div>
</div>
</div>
<div className="card">
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="card-title">日志列表</div>
<button className="btn btn-primary btn-sm">导出日志</button>
</div>
<div className="card-body">
<div className="table-wrapper">
<table className="table">
<thead>
<tr>
<th>时间</th>
<th>用户</th>
<th>类型</th>
<th>操作</th>
<th>状态</th>
<th>详情</th>
</tr>
</thead>
<tbody>
{logs.map((log, index) => (
<tr key={index}>
<td>{log.time}</td>
<td>{log.user}</td>
<td>{log.type}</td>
<td>{log.action}</td>
<td><span className={`status ${log.status === '成功' ? 'status-running' : log.status === '失败' ? 'status-error' : 'status-warning'}`}>{log.status}</span></td>
<td>{log.detail}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="pagination">
<div className="pagination-item"></div>
<div className="pagination-item active">1</div>
<div className="pagination-item">2</div>
<div className="pagination-item">3</div>
<div className="pagination-item"></div>
</div>
</div>
</div>
</>
);
}
export default LogsPage;

View File

@@ -0,0 +1,17 @@
function MemberConfigPage({ onBack }) {
return (
<div className="card">
<div className="card-header">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="card-title">成员配置</div>
<button className="btn btn-primary" onClick={onBack}>返回列表</button>
</div>
</div>
<div className="card-body">
<p>成员配置页面内容</p>
</div>
</div>
);
}
export default MemberConfigPage;

View File

@@ -0,0 +1,45 @@
import { projectMembers } from '../../data/members.js';
function ProjectsPage({ onAddMember }) {
return (
<div className="card">
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="card-title">项目管理</div>
<button className="btn btn-primary btn-sm" onClick={onAddMember}>增加成员</button>
</div>
<div className="card-body">
<div className="table-wrapper">
<table className="table">
<thead>
<tr>
<th>成员</th>
<th style={{ width: '100px' }}>角色</th>
<th style={{ width: '80px' }}>操作</th>
</tr>
</thead>
<tbody>
{projectMembers.map(member => (
<tr key={member.id}>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div className="user-avatar" style={{ width: '32px', height: '32px', fontSize: '13px' }}>
{member.name.charAt(0)}
</div>
{member.name}
</div>
</td>
<td><span className={`status ${member.role === '管理员' ? 'role-admin' : 'role-member'}`}>{member.role}</span></td>
<td style={{ width: '80px' }}>
<button className="text-btn text-btn-primary">配置</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
export default ProjectsPage;

View File

@@ -0,0 +1,77 @@
import { FiChevronLeft, FiFile } from 'react-icons/fi';
import { skills, getSkillIcon, skillFiles, skillVersions } from '../../data/skills.js';
function SkillDetailPage({ skillId, onBack }) {
const skill = skills.find(s => s.id === skillId);
if (!skill) {
return <div>Skill not found</div>;
}
return (
<>
<div className="skill-back-btn" onClick={onBack}>
<FiChevronLeft /> 返回技能市场
</div>
<div className="card">
<div className="card-body">
<div className="skill-detail-header">
<div className="skill-detail-icon">{getSkillIcon(skill.id)}</div>
<div className="skill-detail-main">
<h2 style={{ fontSize: '22px', fontWeight: 800, marginBottom: '6px' }}>{skill.name}</h2>
<p style={{ color: '#64748B', marginBottom: '8px' }}>by {skill.author}</p>
<div className="skill-detail-tags">
{skill.tags.map(tag => (
<span key={tag} className="skill-detail-tag">{tag}</span>
))}
</div>
<div className="skill-detail-stats">
<span>👤 {skill.subs.toLocaleString()} 订阅</span>
<span> {skill.rating} 评分</span>
</div>
</div>
<div style={{ flexShrink: 0 }}>
<button className={`btn ${skill.subscribed ? '' : 'btn-primary'}`}>
{skill.subscribed ? '取消订阅' : '立即订阅'}
</button>
</div>
</div>
<div className="skill-detail-section">
<h3>使用说明</h3>
<p style={{ color: '#475569', lineHeight: 1.8 }}>
{skill.desc}安装后您可以在对话中直接调用该技能例如您可以说
</p>
<div style={{ marginTop: '12px', padding: '12px 16px', background: '#F8FAFC', borderRadius: '8px', color: '#3B82F6', fontFamily: 'monospace' }}>
"帮我用这个技能 查询一下数据"
</div>
</div>
<div className="skill-detail-section">
<h3>文件列表</h3>
{skillFiles.map(file => (
<div key={file.name} className="file-list-item">
<div className="file-icon"><FiFile /></div>
<div className="file-info">
<div className="file-name">{file.name}</div>
<div className="file-size">{file.type} · {file.size}</div>
</div>
</div>
))}
</div>
<div className="skill-detail-section">
<h3>版本历史</h3>
{skillVersions.map(ver => (
<div key={ver.version} className="version-list-item">
<div className="version-info">
<span className={`version-tag ${ver.current ? 'current' : ''}`}>{ver.version}</span>
<span className="version-desc">{ver.desc}</span>
</div>
<div className="version-date">{ver.date}</div>
</div>
))}
</div>
</div>
</div>
</>
);
}
export default SkillDetailPage;

View File

@@ -0,0 +1,101 @@
import { useState } from 'react';
import { FiUser, FiStar, FiSearch } from 'react-icons/fi';
import { skills, getSkillIcon } from '../../data/skills.js';
function SkillCard({ skill, onClick }) {
return (
<div className="skill-card" onClick={onClick}>
<div className="skill-header">
<div className="skill-icon">{getSkillIcon(skill.id)}</div>
<div className="skill-info">
<div className="skill-name">{skill.name}</div>
<div className="skill-author">{skill.author}</div>
</div>
</div>
<div className="skill-desc">{skill.desc}</div>
<div className="skill-tags">
{skill.tags.map(tag => (
<span key={tag} className="skill-tag">{tag}</span>
))}
</div>
<div className="skill-footer">
<div className="skill-stats">
<span style={{ display: 'flex', alignItems: 'center', gap: '4px', color: '#94A3B8', fontSize: '13px' }}>
<FiUser /> {skill.subs}
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px', color: '#94A3B8', fontSize: '13px' }}>
<FiStar /> {skill.rating}
</span>
</div>
<button className={`btn ${skill.subscribed ? 'btn-primary' : ''} btn-sm`} onClick={e => e.stopPropagation()}>
{skill.subscribed ? '已订阅' : '订阅'}
</button>
</div>
</div>
);
}
function SkillsPage({ onSkillClick }) {
const [filter, setFilter] = useState('all');
const [sort, setSort] = useState('subs');
const filteredSkills = filter === 'subscribed'
? skills.filter(s => s.subscribed)
: [...skills];
filteredSkills.sort((a, b) => {
if (sort === 'subs') return b.subs - a.subs;
if (sort === 'rating') return b.rating - a.rating;
return 0;
});
return (
<>
<div className="card">
<div className="card-body">
<div className="search-bar">
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
<label>关键词</label>
<input type="text" className="form-control" placeholder="搜索技能..." />
</div>
<div className="search-item">
<label>分类</label>
<select className="form-control">
<option>全部分类</option>
<option>开发工具</option>
<option>数据分析</option>
<option>办公效率</option>
<option>业务系统</option>
</select>
</div>
<div className="search-item">
<label>排序</label>
<select className="form-control" value={sort} onChange={e => setSort(e.target.value)}>
<option value="subs">订阅数</option>
<option value="rating">评分</option>
</select>
</div>
</div>
<div className="search-actions">
<button className="btn btn-primary"><FiSearch /> 查询</button>
</div>
</div>
</div>
<div style={{ marginBottom: '16px' }}>
<div className="btn-group">
<button className={`btn ${filter === 'all' ? 'btn-primary' : ''}`} onClick={() => setFilter('all')}>全部技能</button>
<button className={`btn ${filter === 'subscribed' ? 'btn-primary' : ''}`} onClick={() => setFilter('subscribed')}>
已订阅 ({skills.filter(s => s.subscribed).length})
</button>
</div>
</div>
<div className="skill-grid">
{filteredSkills.map(skill => (
<SkillCard key={skill.id} skill={skill} onClick={() => onSkillClick(skill.id)} />
))}
</div>
</>
);
}
export default SkillsPage;

View File

@@ -0,0 +1,89 @@
import { scheduledTasks } from '../../data/tasks.js';
function TaskDetailPage({ taskId, onBack }) {
const task = scheduledTasks.find(t => t.id === taskId);
if (!task) {
return (
<div className="card">
<div className="card-header">
<div className="card-title">任务详情</div>
</div>
<div className="card-body">
<p>任务不存在</p>
<button className="btn btn-primary btn-sm" onClick={onBack} style={{ marginTop: '16px' }}>返回列表</button>
</div>
</div>
);
}
return (
<div className="card">
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="card-title">任务详情</div>
<button className="btn btn-primary btn-sm" onClick={onBack}>返回列表</button>
</div>
<div className="card-body">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '20px', marginBottom: '24px' }}>
<div>
<div style={{ fontSize: '13px', color: '#64748B', marginBottom: '6px' }}>任务名称</div>
<div style={{ fontSize: '15px', fontWeight: '600', color: '#1E293B' }}>{task.name}</div>
</div>
<div>
<div style={{ fontSize: '13px', color: '#64748B', marginBottom: '6px' }}>执行频率</div>
<div style={{ fontSize: '15px', fontWeight: '600', color: '#1E293B' }}>{task.frequency}</div>
</div>
<div>
<div style={{ fontSize: '13px', color: '#64748B', marginBottom: '6px' }}>上次触发</div>
<div style={{ fontSize: '15px', fontWeight: '600', color: '#1E293B' }}>{task.lastTriggered}</div>
</div>
<div>
<div style={{ fontSize: '13px', color: '#64748B', marginBottom: '6px' }}>下次触发</div>
<div style={{ fontSize: '15px', fontWeight: '600', color: '#1E293B' }}>{task.nextTrigger}</div>
</div>
</div>
<div style={{ marginBottom: '24px' }}>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#1E293B', marginBottom: '12px' }}>执行内容</div>
<textarea
className="form-control"
rows="4"
style={{ background: '#F8FAFC', color: '#475569' }}
value={task.prompt || '请分析本月的销售数据,生成一份可视化报表。'}
readOnly
/>
</div>
<div>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#1E293B', marginBottom: '12px' }}>执行日志</div>
<div style={{ border: '1px solid #E2E8F0', borderRadius: '8px', overflow: 'hidden' }}>
<table className="table" style={{ marginBottom: '0' }}>
<thead>
<tr>
<th style={{ width: '180px', whiteSpace: 'nowrap' }}>执行时间</th>
<th style={{ width: '100px', whiteSpace: 'nowrap' }}>状态</th>
<th>执行信息</th>
</tr>
</thead>
<tbody>
{task.logs.map((log, index) => (
<tr key={index}>
<td style={{ color: '#64748B', fontSize: '13px', whiteSpace: 'nowrap' }}>{log.time}</td>
<td>
<span className={`status ${log.status === '成功' ? 'status-running' : 'status-error'}`}>
{log.status}
</span>
</td>
<td style={{ fontSize: '14px', color: '#475569' }}>{log.message}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
}
export default TaskDetailPage;

View File

@@ -0,0 +1,60 @@
import { useState } from 'react';
import { scheduledTasks } from '../../data/tasks.js';
function TasksPage({ onViewDetail }) {
const [tasks, setTasks] = useState(scheduledTasks);
const toggleTask = (taskId) => {
setTasks(prev => prev.map(task =>
task.id === taskId ? { ...task, enabled: !task.enabled } : task
));
};
return (
<div className="card">
<div className="card-header">
<div className="card-title">定时任务</div>
</div>
<div className="card-body">
<table className="table">
<thead>
<tr>
<th>任务名称</th>
<th>频率</th>
<th>上次触发</th>
<th>下次触发</th>
<th>上次运行状态</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{tasks.map(task => (
<tr key={task.id}>
<td>{task.name}</td>
<td>{task.frequency}</td>
<td>{task.lastTriggered}</td>
<td>{task.nextTrigger}</td>
<td>
<span className={`status ${task.lastStatus === '成功' ? 'status-running' : task.lastStatus === '失败' ? 'status-error' : 'status-warning'}`}>
{task.lastStatus}
</span>
</td>
<td><span className={`status ${task.enabled ? 'status-running' : 'status-stopped'}`}>{task.enabled ? '启用' : '禁用'}</span></td>
<td>
<button className={`text-btn ${task.enabled ? 'text-btn-danger' : 'text-btn-primary'}`} onClick={() => toggleTask(task.id)}>
{task.enabled ? '禁用' : '启用'}
</button>
<button className="text-btn text-btn-primary" style={{ marginLeft: '8px' }} onClick={() => onViewDetail(task.id)}>详情</button>
<button className="text-btn text-btn-danger" style={{ marginLeft: '8px' }}>删除</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
export default TasksPage;

View File

@@ -0,0 +1,24 @@
function DevAccountPage() {
return (
<div className="card">
<div className="card-header">
<div className="card-title">开发者设置</div>
</div>
<div className="card-body">
<div style={{ display: 'flex', gap: '20px', alignItems: 'flex-start' }}>
<div className="user-avatar" style={{ width: '72px', height: '72px', fontSize: '32px' }}></div>
<div>
<h2 style={{ marginBottom: '8px' }}>张三</h2>
<div style={{ color: '#64748B', marginBottom: '12px' }}>开发者</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button className="btn btn-primary">编辑资料</button>
<button className="btn">API 密钥管理</button>
</div>
</div>
</div>
</div>
</div>
);
}
export default DevAccountPage;

View File

@@ -0,0 +1,29 @@
import { devDocs } from '../../data/developerData.js';
function DevDocsPage() {
const categories = [...new Set(devDocs.map(doc => doc.category))];
return (
<div className="card">
<div className="card-header">
<div className="card-title">开发文档</div>
</div>
<div className="card-body">
{categories.map(category => (
<div key={category} style={{ marginBottom: '24px' }}>
<h3 style={{ marginBottom: '12px', fontSize: '16px', fontWeight: 600 }}>{category}</h3>
<div style={{ display: 'grid', gap: '12px' }}>
{devDocs.filter(doc => doc.category === category).map(doc => (
<div key={doc.id} style={{ padding: '12px', background: '#F8FAFC', borderRadius: '8px', cursor: 'pointer' }}>
<div style={{ fontWeight: 600, marginBottom: '4px' }}>{doc.title}</div>
<div style={{ fontSize: '14px', color: '#64748B' }}>{doc.content}</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
}
export default DevDocsPage;

View File

@@ -0,0 +1,50 @@
import { mySkills } from '../../data/developerData.js';
function MySkillsPage({ onSkillClick }) {
return (
<div className="card">
<div className="card-header">
<div className="card-title">我的技能</div>
</div>
<div className="card-body">
<table className="table">
<thead>
<tr>
<th>技能名称</th>
<th>分类</th>
<th>版本</th>
<th>状态</th>
<th>安装量</th>
<th>评分</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{mySkills.map(skill => (
<tr key={skill.id} onClick={() => onSkillClick(skill.id)} style={{ cursor: 'pointer' }}>
<td>
<div style={{ fontWeight: 600 }}>{skill.name}</div>
<div style={{ fontSize: '12px', color: '#94A3B8' }}>{skill.desc}</div>
</td>
<td>{skill.category}</td>
<td>{skill.version}</td>
<td><span className={`status ${skill.status === 'published' ? 'status-running' : 'status-stopped'}`}>
{skill.status === 'published' ? '已发布' : '草稿'}
</span></td>
<td>{skill.installs}</td>
<td>{skill.rating || '-'}</td>
<td>
<button className="text-btn text-btn-primary" onClick={e => { e.stopPropagation(); onSkillClick(skill.id); }}>
编辑
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
export default MySkillsPage;

View File

@@ -0,0 +1,43 @@
import { FiUpload } from 'react-icons/fi';
function NewVersionPage({ skillName, onBack }) {
return (
<div className="card">
<div className="card-header">
<div className="card-title">上传新版本</div>
</div>
<div className="card-body">
<div className="form-group">
<label className="form-label">技能名称</label>
<input type="text" className="form-control" defaultValue={skillName} />
</div>
<div className="form-group">
<label className="form-label">技能描述</label>
<textarea className="form-control" rows="3" placeholder="请输入技能描述"></textarea>
</div>
<div className="form-group">
<label className="form-label">技能分类</label>
<select className="form-control">
<option>信息查询</option>
<option>效率工具</option>
<option>开发工具</option>
</select>
</div>
<div className="form-group">
<label className="form-label">技能包上传</label>
<div style={{ border: '2px dashed #E2E8F0', borderRadius: '8px', padding: '40px', textAlign: 'center', color: '#94A3B8' }}>
<FiUpload size={48} style={{ marginBottom: '16px' }} />
<div>点击或拖拽文件到此处上传</div>
<div style={{ fontSize: '12px', marginTop: '8px' }}>支持 .zip 格式</div>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn btn-primary">提交审核</button>
</div>
</div>
</div>
);
}
export default NewVersionPage;

View File

@@ -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 <div>Skill not found</div>;
}
return (
<>
<div className="dev-back-btn" onClick={onBack}>
<FiChevronLeft /> 返回我的技能
</div>
<div className="card">
<div className="card-header">
<div className="card-title">配置信息</div>
</div>
<div className="card-body">
<div className="dev-detail-header">
<div className="dev-detail-icon">{skill.name.charAt(0)}</div>
<div className="dev-detail-main">
<h2 style={{ marginBottom: '8px' }}>{skill.name}</h2>
<div style={{ color: '#64748B', marginBottom: '12px' }}>{skill.category}</div>
<div className="dev-detail-tags">
{skill.tags.map(tag => (
<span key={tag} className="dev-detail-tag">{tag}</span>
))}
</div>
<div className="dev-detail-stats">
<span>版本: {skill.version}</span>
<span>安装量: {skill.installs}</span>
<span>评分: {skill.rating || '-'}</span>
</div>
</div>
</div>
<div className="dev-detail-section">
<h3>基本信息</h3>
<div className="dev-info-row">
<span className="dev-info-label">技能名称</span>
<span className="dev-info-value">{skill.name}</span>
</div>
<div className="dev-info-row">
<span className="dev-info-label">技能描述</span>
<span className="dev-info-value">{skill.desc}</span>
</div>
</div>
</div>
</div>
<div className="card" style={{ marginTop: '24px' }}>
<div className="card-header">
<div className="card-title">技能包管理</div>
</div>
<div className="card-body">
<div style={{ display: 'flex', gap: '12px', marginBottom: '24px' }}>
<button className="btn btn-primary" onClick={() => onUploadNewVersion(skill.name)}><FiUpload /> 上传新版本</button>
</div>
<h4 style={{ marginBottom: '12px' }}>版本历史</h4>
<div className="table-wrapper" style={{ margin: 0, padding: 0 }}>
<table className="table" style={{ tableLayout: 'fixed' }}>
<colgroup>
<col style={{ width: '100px' }} />
<col />
<col style={{ width: '120px' }} />
<col style={{ width: '120px' }} />
<col style={{ width: '100px' }} />
<col style={{ width: '160px' }} />
</colgroup>
<thead>
<tr>
<th>版本号</th>
<th>版本说明</th>
<th>状态</th>
<th>更新时间</th>
<th>是否启用</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{skill.versions.map(ver => (
<tr key={ver.version}>
<td>{ver.version}</td>
<td>{ver.desc}</td>
<td>
{ver.status === 'pending' ? (
<span className="status status-warning">审核中</span>
) : ver.status === 'rejected' ? (
<span className="status status-error">审核拒绝</span>
) : (
<span className="status status-running">审核通过</span>
)}
</td>
<td>{ver.date}</td>
<td>
{ver.enabled ? (
<span className="status status-running">已启用</span>
) : (
<span className="status status-stopped">未启用</span>
)}
</td>
<td>
<div className="btn-group">
{!ver.enabled && <button className="text-btn text-btn-success">启用</button>}
<button className="text-btn">下载</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</>
);
}
export default SkillEditorPage;

View File

@@ -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 (
<div className="card">
<div className="card-header">
<div className="card-title">创建技能</div>
</div>
<div className="card-body">
<div className="form-group">
<label className="form-label required">技能名称</label>
<input type="text" className="form-control" placeholder="请输入技能名称" required />
</div>
<div className="form-group">
<label className="form-label required">技能描述</label>
<textarea className="form-control" rows="3" placeholder="请输入技能描述" required></textarea>
</div>
<div className="form-group">
<label className="form-label">技能分类</label>
<select className="form-control">
<option>信息查询</option>
<option>效率工具</option>
<option>开发工具</option>
</select>
</div>
<div className="form-group">
<label className="form-label">标签</label>
<div className="tag-input-container">
{tags.map(tag => (
<span key={tag} className="tag-item">
{tag}
<span className="tag-remove" onClick={() => removeTag(tag)}><FiX /></span>
</span>
))}
<input
type="text"
className="tag-input"
placeholder={tags.length === 0 ? '输入标签后按回车添加' : ''}
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleTagKeyDown}
/>
</div>
<div style={{ fontSize: '12px', color: '#94A3B8', marginTop: '6px' }}>按回车添加标签最多5个</div>
</div>
<div className="form-group">
<label className="form-label required">技能包上传</label>
<div style={{ border: '2px dashed #E2E8F0', borderRadius: '8px', padding: '40px', textAlign: 'center', color: '#94A3B8' }}>
<FiUpload size={48} style={{ marginBottom: '16px' }} />
<div>点击或拖拽文件到此处上传</div>
<div style={{ fontSize: '12px', marginTop: '8px' }}>支持 .zip 格式</div>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button className="btn">取消</button>
<button className="btn btn-primary">创建技能</button>
</div>
</div>
</div>
);
}
export default UploadSkillPage;

71
src/styles/_base.scss Normal file
View File

@@ -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;
}

188
src/styles/_components.scss Normal file
View File

@@ -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);
}

37
src/styles/_layout.scss Normal file
View File

@@ -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;
}
// 其他布局样式...

70
src/styles/_mixins.scss Normal file
View File

@@ -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;
}
}

52
src/styles/_pages.scss Normal file
View File

@@ -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);
}
// 其他页面样式将逐步添加...

View File

@@ -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;

2541
src/styles/global.scss Normal file

File diff suppressed because it is too large Load Diff