chore: 添加 .gitignore 规则,包含前端、pnpm、Node.js 和 AI 工具
This commit is contained in:
22
src/App.jsx
Normal file
22
src/App.jsx
Normal 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
36
src/components/Layout.jsx
Normal 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;
|
||||
93
src/components/ListSelector.jsx
Normal file
93
src/components/ListSelector.jsx
Normal 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
218
src/data/conversations.js
Normal 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年 Q1(1-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
92
src/data/developerData.js
Normal 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
14
src/data/logs.js
Normal 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
12
src/data/members.js
Normal 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
31
src/data/skills.js
Normal 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
62
src/data/tasks.js
Normal 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' }
|
||||
]
|
||||
}
|
||||
];
|
||||
25
src/hooks/useLocalStorage.js
Normal file
25
src/hooks/useLocalStorage.js
Normal 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
10
src/main.jsx
Normal 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
130
src/pages/AdminPage.jsx
Normal 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
228
src/pages/ConsolePage.jsx
Normal 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
173
src/pages/DeveloperPage.jsx
Normal 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
76
src/pages/HomePage.jsx
Normal 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
141
src/pages/LoginPage.jsx
Normal 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;
|
||||
61
src/pages/admin/AddDepartmentPage.jsx
Normal file
61
src/pages/admin/AddDepartmentPage.jsx
Normal 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;
|
||||
61
src/pages/admin/AddProjectPage.jsx
Normal file
61
src/pages/admin/AddProjectPage.jsx
Normal 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;
|
||||
74
src/pages/admin/AddUserPage.jsx
Normal file
74
src/pages/admin/AddUserPage.jsx
Normal 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;
|
||||
93
src/pages/admin/AdminProjectsPage.jsx
Normal file
93
src/pages/admin/AdminProjectsPage.jsx
Normal 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;
|
||||
93
src/pages/admin/DepartmentsPage.jsx
Normal file
93
src/pages/admin/DepartmentsPage.jsx
Normal 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;
|
||||
18
src/pages/admin/OverviewPage.jsx
Normal file
18
src/pages/admin/OverviewPage.jsx
Normal 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;
|
||||
113
src/pages/admin/UsersPage.jsx
Normal file
113
src/pages/admin/UsersPage.jsx
Normal 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;
|
||||
85
src/pages/console/AccountPage.jsx
Normal file
85
src/pages/console/AccountPage.jsx
Normal 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;
|
||||
59
src/pages/console/AddMemberPage.jsx
Normal file
59
src/pages/console/AddMemberPage.jsx
Normal 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;
|
||||
69
src/pages/console/ChatPage.jsx
Normal file
69
src/pages/console/ChatPage.jsx
Normal 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;
|
||||
94
src/pages/console/LogsPage.jsx
Normal file
94
src/pages/console/LogsPage.jsx
Normal 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;
|
||||
17
src/pages/console/MemberConfigPage.jsx
Normal file
17
src/pages/console/MemberConfigPage.jsx
Normal 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;
|
||||
45
src/pages/console/ProjectsPage.jsx
Normal file
45
src/pages/console/ProjectsPage.jsx
Normal 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;
|
||||
77
src/pages/console/SkillDetailPage.jsx
Normal file
77
src/pages/console/SkillDetailPage.jsx
Normal 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;
|
||||
101
src/pages/console/SkillsPage.jsx
Normal file
101
src/pages/console/SkillsPage.jsx
Normal 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;
|
||||
89
src/pages/console/TaskDetailPage.jsx
Normal file
89
src/pages/console/TaskDetailPage.jsx
Normal 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;
|
||||
60
src/pages/console/TasksPage.jsx
Normal file
60
src/pages/console/TasksPage.jsx
Normal 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;
|
||||
24
src/pages/developer/DevAccountPage.jsx
Normal file
24
src/pages/developer/DevAccountPage.jsx
Normal 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;
|
||||
29
src/pages/developer/DevDocsPage.jsx
Normal file
29
src/pages/developer/DevDocsPage.jsx
Normal 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;
|
||||
50
src/pages/developer/MySkillsPage.jsx
Normal file
50
src/pages/developer/MySkillsPage.jsx
Normal 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;
|
||||
43
src/pages/developer/NewVersionPage.jsx
Normal file
43
src/pages/developer/NewVersionPage.jsx
Normal 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;
|
||||
118
src/pages/developer/SkillEditorPage.jsx
Normal file
118
src/pages/developer/SkillEditorPage.jsx
Normal 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;
|
||||
81
src/pages/developer/UploadSkillPage.jsx
Normal file
81
src/pages/developer/UploadSkillPage.jsx
Normal 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
71
src/styles/_base.scss
Normal 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
188
src/styles/_components.scss
Normal 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
37
src/styles/_layout.scss
Normal 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
70
src/styles/_mixins.scss
Normal 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
52
src/styles/_pages.scss
Normal 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);
|
||||
}
|
||||
|
||||
// 其他页面样式将逐步添加...
|
||||
54
src/styles/_variables.scss
Normal file
54
src/styles/_variables.scss
Normal 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
2541
src/styles/global.scss
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user