refactor: 页面状态由 URL 路由驱动 - 移除 usePageState/useNavigation/hooks/constants,改用嵌套路由 + useParams

This commit is contained in:
2026-03-27 18:46:34 +08:00
parent 9feb62da3f
commit 1455cc850d
44 changed files with 587 additions and 1048 deletions

109
README.md
View File

@@ -96,13 +96,11 @@ pnpm build # 验证打包不运行pnpm dev会挂起流程
src/
├── components/ # 组件库
│ ├── common/ # 通用组件 (Modal, Toast, EmptyState等)
│ ├── layout/ # 布局组件 (AppHeader, AppLayout, UserDropdown等)
│ ├── layout/ # 布局组件 (AppHeader, AppLayout, UserDropdown, ConsoleLayout, AdminLayout, DeveloperLayout等)
│ ├── Layout.jsx # 主布局组件sidebar + content
│ └── ListSelector.jsx # 列表选择器
├── contexts/ # 全局状态 (UserContext)
├── hooks/ # 自定义Hook (usePageState, useNavigation)
├── constants/ # 常量配置 (pages, storageKeys)
├── services/ # 数据访问层 (api.js)
├── data/ # 模拟数据
@@ -279,7 +277,9 @@ export default Example;
## 路由规范
### 顶层路由
### 嵌套路由结构
所有页面通过正式路由导航,使用 HashRouter + 嵌套路由。
```jsx
// App.jsx
@@ -288,9 +288,29 @@ export default Example;
<Route path="/login" element={<LoginPage />} />
<Route element={<AppLayout />}>
<Route path="/" element={<HomePage />} />
<Route path="/console" element={<ConsolePage />} />
<Route path="/admin" element={<AdminPage />} />
<Route path="/developer" element={<DeveloperPage />} />
<Route path="/console" element={<ConsoleLayout />}>
<Route index element={<Navigate to="chat/welcome" replace />} />
<Route path="chat" element={<ChatPage />} />
<Route path="chat/:scene" element={<ChatPage />} />
<Route path="skills" element={<SkillsPage />} />
<Route path="skills/:skillId" element={<SkillDetailPage />} />
{/* ...更多子路由 */}
</Route>
<Route path="/admin" element={<AdminLayout />}>
<Route index element={<Navigate to="overview" replace />} />
<Route path="overview" element={<OverviewPage />} />
<Route path="departments" element={<DepartmentsPage />} />
<Route path="departments/add" element={<AddDepartmentPage />} />
<Route path="departments/:id/edit" element={<AddDepartmentPage />} />
{/* ...更多子路由 */}
</Route>
<Route path="/developer" element={<DeveloperLayout />}>
<Route index element={<Navigate to="overview" replace />} />
{/* ...子路由 */}
</Route>
</Route>
</Routes>
</HashRouter>
@@ -299,41 +319,33 @@ export default Example;
**说明**
- `AppLayout` 包裹所有需要统一 Header 的页面
- 登录页独立,不使用 `AppLayout`
- 每个模块Console/Admin/Developer使用独立的 Layout 组件包裹 `<Outlet />`
- 模块根路径自动重定向到默认子页面
- 新增/编辑表单通过 URL 参数区分:`/admin/departments/add` vs `/admin/departments/:id/edit`
### 子页面路由
每个主页面内部管理子页面:
### 子页面参数获取
```jsx
// ConsolePage.jsx示例
const [currentPage, setCurrentPage] = useState('chat');
const renderPage = () => {
switch (currentPage) {
case 'chat': return <ChatPage />;
case 'skills': return <SkillsPage />;
// ...
}
};
```
### 页面配置
```javascript
// constants/pages.js
export const CONSOLE_PAGES = {
chat: { title: '智能助手', icon: 'FiMessageSquare' },
skills: { title: '技能市场', icon: 'FaPuzzlePiece' },
// 使用 useParams 获取 URL 参数
function SkillDetailPage() {
const { skillId } = useParams();
const skill = api.skills.getById(Number(skillId));
// ...
};
}
// 使用 useNavigate 进行导航
function SkillsPage() {
const navigate = useNavigate();
return <button onClick={() => navigate('/console/skills/1')}>查看</button>;
}
```
### 新增页面流程
1.`constants/pages.js` 添加页面配置
2. 在父页面组件导入新页面
3. `renderPage()` 添加case分支
4. 添加导航项(如需要)
1.`App.jsx` 添加路由定义
2. 创建页面组件(使用 `useParams` / `useNavigate`
3.对应 Layout 的 sidebar 添加导航项
4. 确保页面返回按钮使用固定路径导航
---
@@ -351,28 +363,17 @@ function Component() {
}
```
### 页面状态持久化
### 页面状态由 URL 驱动
| 模块 | localStorage键 | 默认值 |
|------|---------------|--------|
| 工作台 | `console_currentPage` | `'chat'` |
| 管理台 | `admin_currentPage` | `'overview'` |
| 开发台 | `developer_currentPage` | `'overview'` |
所有页面状态(当前页面、场景名、实体 ID通过 URL 参数驱动,不依赖 localStorage
### 自定义Hook
```javascript
// usePageState - 页面状态持久化
const { currentPage, setCurrentPage } = usePageState({
storageKey: CONSOLE_KEYS.CURRENT_PAGE,
defaultPage: 'chat',
pageTitles: CONSOLE_PAGES,
});
// useNavigation - 导航逻辑
const { navigateToPage, extraData } = useNavigation(setCurrentPage);
navigateToPage('skillDetail', { skillId: '1' });
```
| 状态类型 | 驱动方式 | 示例 |
|---------|---------|------|
| 当前页面 | URL 路径 | `/console/skills` |
| 实体 ID | URL 参数 | `/console/skills/:skillId` |
| 场景名 | URL 参数 | `/console/chat/:scene` |
| 新增/编辑模式 | URL 参数有无 | `/admin/users/add` vs `/admin/users/:id/edit` |
| 编辑数据 | `api.getById(Number(id))` | 通过 ID 重新获取 |
---

View File

@@ -14,3 +14,7 @@ context: |
- **优先阅读README.md**README.md文档是项目的开发文档记录代码结构和关键开发模式优先读取获取上下文
- 涉及页面/路由/组件/功能模块变更或技术栈调整时同步更新README.md
- Git提交: 仅中文; 格式为"类型: 简短描述",类型可选: feat(新功能)/fix(修复)/refactor(重构)/docs(文档)/style(格式)/test(测试)/chore(构建/工具); 多行描述空行后加详细说明; 禁创建git操作task
rules:
proposal:
- 仔细审查每一个过往spec判断是否存在Modified Capabilities

View File

@@ -2,7 +2,7 @@
## Purpose
提供统一的状态管理方案,包括全局用户信息上下文、页面状态持久化、导航逻辑管理,确保应用状态的一致性和可维护性
提供统一的全局用户信息上下文,确保应用状态的一致性和可维护性。页面状态和导航逻辑由 URL 路由驱动
## Requirements
@@ -20,33 +20,3 @@
#### Scenario: UserContext 提供默认值
- **WHEN** 应用启动且没有提供用户信息
- **THEN** 系统使用默认用户信息name: '张三', avatar: '张', role: 'AI 产品部'
### Requirement: 页面状态持久化 Hook
系统 SHALL 提供 usePageState Hook封装页面状态持久化逻辑自动处理 localStorage 同步和主页跳转重置。
#### Scenario: usePageState 初始化从 localStorage 恢复状态
- **WHEN** 页面使用 usePageState Hook 并传入 storageKey 和 defaultPage
- **THEN** 系统从 localStorage 读取之前保存的页面状态,若无则使用 defaultPage
#### Scenario: usePageState 自动同步状态到 localStorage
- **WHEN** 调用 usePageState 返回的 setCurrentPage 函数
- **THEN** 系统更新状态并自动保存到 localStorage
#### Scenario: usePageState 处理主页跳转重置
- **WHEN** 从主页跳转到页面location.state.fromHome 为 true
- **THEN** 系统重置页面状态为默认值,并清除路由 state
#### Scenario: usePageState 提供 getPageTitle 函数
- **WHEN** 调用 usePageState 返回的 getPageTitle 函数
- **THEN** 系统根据当前页面 ID 从配置中查找并返回对应的页面标题
### Requirement: 导航逻辑 Hook
系统 SHALL 提供 useNavigation Hook统一处理页面导航和路由状态管理。
#### Scenario: useNavigation 提供页面切换函数
- **WHEN** 调用 useNavigation 返回的 navigateToPage 函数并传入目标页面 ID
- **THEN** 系统更新当前页面状态并执行相应导航逻辑
#### Scenario: useNavigation 处理带数据的页面切换
- **WHEN** 调用 navigateToPage 并传入目标页面 ID 和附加数据(如 skillId
- **THEN** 系统更新页面状态和附加数据状态

View File

@@ -1,11 +1,47 @@
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
import { HashRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { UserProvider } from './contexts/UserContext.jsx';
import AppLayout from './components/layout/AppLayout.jsx';
import ConsoleLayout from './components/layout/ConsoleLayout.jsx';
import AdminLayout from './components/layout/AdminLayout.jsx';
import DeveloperLayout from './components/layout/DeveloperLayout.jsx';
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';
// Console 子页面
import ChatPage from './pages/console/ChatPage.jsx';
import SkillsPage from './pages/console/SkillsPage.jsx';
import SkillDetailPage from './pages/console/SkillDetailPage.jsx';
import ConsoleMySkillsPage from './pages/console/MySkillsPage.jsx';
import SkillConfigPage from './pages/console/SkillConfigPage.jsx';
import LogsPage from './pages/console/LogsPage.jsx';
import TasksPage from './pages/console/TasksPage.jsx';
import TaskDetailPage from './pages/console/TaskDetailPage.jsx';
import ProjectsPage from './pages/console/ProjectsPage.jsx';
import MemberConfigPage from './pages/console/MemberConfigPage.jsx';
import AddMemberPage from './pages/console/AddMemberPage.jsx';
import ConsoleReviewListPage from './pages/console/ConsoleReviewListPage.jsx';
import ConsoleReviewDetailPage from './pages/console/ConsoleReviewDetailPage.jsx';
// Admin 子页面
import OverviewPage from './pages/admin/OverviewPage.jsx';
import DepartmentsPage from './pages/admin/DepartmentsPage.jsx';
import AddDepartmentPage from './pages/admin/AddDepartmentPage.jsx';
import UsersPage from './pages/admin/UsersPage.jsx';
import AddUserPage from './pages/admin/AddUserPage.jsx';
import AdminProjectsPage from './pages/admin/AdminProjectsPage.jsx';
import AddProjectPage from './pages/admin/AddProjectPage.jsx';
import AdminLogsPage from './pages/admin/AdminLogsPage.jsx';
import ModelConfigsPage from './pages/admin/ModelConfigsPage.jsx';
import AddModelConfigPage from './pages/admin/AddModelConfigPage.jsx';
// Developer 子页面
import DevOverviewPage from './pages/developer/DevOverviewPage.jsx';
import DeveloperMySkillsPage from './pages/developer/MySkillsPage.jsx';
import UploadSkillPage from './pages/developer/UploadSkillPage.jsx';
import SkillEditorPage from './pages/developer/SkillEditorPage.jsx';
import UploadVersionPage from './pages/developer/UploadVersionPage.jsx';
import UpdateSkillInfoPage from './pages/developer/UpdateSkillInfoPage.jsx';
import DevDocsPage from './pages/developer/DevDocsPage.jsx';
function App() {
return (
@@ -15,9 +51,53 @@ function App() {
<Route path="/login" element={<LoginPage />} />
<Route element={<AppLayout />}>
<Route path="/" element={<HomePage />} />
<Route path="/console" element={<ConsolePage />} />
<Route path="/admin" element={<AdminPage />} />
<Route path="/developer" element={<DeveloperPage />} />
<Route path="/console" element={<ConsoleLayout />}>
<Route index element={<Navigate to="chat/welcome" replace />} />
<Route path="chat" element={<ChatPage />} />
<Route path="chat/:scene" element={<ChatPage />} />
<Route path="skills" element={<SkillsPage />} />
<Route path="skills/:skillId" element={<SkillDetailPage />} />
<Route path="my-skills" element={<ConsoleMySkillsPage />} />
<Route path="my-skills/:subscriptionId/config" element={<SkillConfigPage />} />
<Route path="logs" element={<LogsPage />} />
<Route path="tasks" element={<TasksPage />} />
<Route path="tasks/:taskId" element={<TaskDetailPage />} />
<Route path="projects" element={<ProjectsPage />} />
<Route path="projects/members/add" element={<AddMemberPage />} />
<Route path="projects/members/:memberId/config" element={<MemberConfigPage />} />
</Route>
<Route path="/admin" element={<AdminLayout />}>
<Route index element={<Navigate to="overview" replace />} />
<Route path="overview" element={<OverviewPage />} />
<Route path="reviews" element={<ConsoleReviewListPage />} />
<Route path="reviews/:type/:reviewId" element={<ConsoleReviewDetailPage />} />
<Route path="departments" element={<DepartmentsPage />} />
<Route path="departments/add" element={<AddDepartmentPage />} />
<Route path="departments/:id/edit" element={<AddDepartmentPage />} />
<Route path="users" element={<UsersPage />} />
<Route path="users/add" element={<AddUserPage />} />
<Route path="users/:id/edit" element={<AddUserPage />} />
<Route path="projects" element={<AdminProjectsPage />} />
<Route path="projects/add" element={<AddProjectPage />} />
<Route path="projects/:id/edit" element={<AddProjectPage />} />
<Route path="logs" element={<AdminLogsPage />} />
<Route path="models" element={<ModelConfigsPage />} />
<Route path="models/add" element={<AddModelConfigPage />} />
<Route path="models/:id/edit" element={<AddModelConfigPage />} />
</Route>
<Route path="/developer" element={<DeveloperLayout />}>
<Route index element={<Navigate to="overview" replace />} />
<Route path="overview" element={<DevOverviewPage />} />
<Route path="my-skills" element={<DeveloperMySkillsPage />} />
<Route path="my-skills/upload" element={<UploadSkillPage />} />
<Route path="my-skills/:skillId/editor" element={<SkillEditorPage />} />
<Route path="my-skills/:skillId/new-version" element={<UploadVersionPage />} />
<Route path="my-skills/:skillId/update-info" element={<UpdateSkillInfoPage />} />
<Route path="docs" element={<DevDocsPage />} />
</Route>
</Route>
</Routes>
</Router>
@@ -25,4 +105,4 @@ function App() {
);
}
export default App;
export default App;

View File

@@ -0,0 +1,92 @@
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { FiHome, FiBarChart2, FiUsers, FiList, FiCheckCircle, FiActivity, FiSettings } from 'react-icons/fi';
import Layout from '../Layout.jsx';
import SidebarNavItem from './SidebarNavItem.jsx';
function AdminLayout() {
const location = useLocation();
const navigate = useNavigate();
const isPathActive = (basePath) => location.pathname.startsWith(basePath);
const sidebar = (
<>
<nav className="admin-sidebar-nav">
<SidebarNavItem
icon={<FiHome />}
label="总览"
active={location.pathname === '/admin/overview'}
onClick={() => navigate('/admin/overview')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiCheckCircle />}
label="审核管理"
active={isPathActive('/admin/reviews')}
onClick={() => navigate('/admin/reviews')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiBarChart2 />}
label="部门管理"
active={isPathActive('/admin/departments')}
onClick={() => navigate('/admin/departments')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiUsers />}
label="用户管理"
active={isPathActive('/admin/users')}
onClick={() => navigate('/admin/users')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiList />}
label="项目管理"
active={isPathActive('/admin/projects')}
onClick={() => navigate('/admin/projects')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiActivity />}
label="日志查询"
active={location.pathname === '/admin/logs'}
onClick={() => navigate('/admin/logs')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiSettings />}
label="模型配置"
active={isPathActive('/admin/models')}
onClick={() => navigate('/admin/models')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
</nav>
</>
);
return (
<Layout
sidebar={sidebar}
sidebarClassName="admin-sidebar"
>
<Outlet />
</Layout>
);
}
export default AdminLayout;

View File

@@ -0,0 +1,99 @@
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { FiPlus, FiClock, FiList, FiUsers, FiBox } from 'react-icons/fi';
import { FaPuzzlePiece } from 'react-icons/fa';
import Layout from '../Layout.jsx';
import SidebarNavItem from './SidebarNavItem.jsx';
import api from '../../services/api.js';
function ConsoleLayout() {
const location = useLocation();
const navigate = useNavigate();
// 从 URL 提取当前 scene
const sceneMatch = location.pathname.match(/\/console\/chat\/(.+)$/);
const currentScene = sceneMatch ? sceneMatch[1] : null;
// 判断是否在 chat 页面(需要全宽布局)
const isChatPage = location.pathname === '/console/chat' ||
location.pathname.startsWith('/console/chat/');
// sidebar 高亮判断
const isPathActive = (basePath) => location.pathname.startsWith(basePath);
const sidebar = (
<>
<div className="chat-sidebar-header">
<button className="btn btn-primary" style={{ width: '100%' }}
onClick={() => navigate('/console/chat')}>
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}>
<FiPlus /> 新建对话
</span>
</button>
</div>
<div className="chat-sidebar-content">
{api.conversations.list().map(conv => (
<div
key={conv.id}
className={`conversation-item ${conv.scene === currentScene ? 'active' : ''}`}
onClick={() => navigate(`/console/chat/${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">
<SidebarNavItem
icon={<FaPuzzlePiece />}
label="技能市场"
active={isPathActive('/console/skills')}
onClick={() => navigate('/console/skills')}
/>
<SidebarNavItem
icon={<FiBox />}
label="我的技能"
active={isPathActive('/console/my-skills')}
onClick={() => navigate('/console/my-skills')}
/>
<SidebarNavItem
icon={<FiClock />}
label="定时任务"
active={isPathActive('/console/tasks')}
onClick={() => navigate('/console/tasks')}
/>
<SidebarNavItem
icon={<FiList />}
label="日志查询"
active={isPathActive('/console/logs')}
onClick={() => navigate('/console/logs')}
/>
<SidebarNavItem
icon={<FiUsers />}
label="项目管理"
active={isPathActive('/console/projects')}
onClick={() => navigate('/console/projects')}
/>
</div>
</>
);
return (
<Layout
sidebar={sidebar}
sidebarClassName="chat-sidebar"
contentClassName={isChatPage ? 'page-content-full' : ''}
>
<Outlet />
</Layout>
);
}
export default ConsoleLayout;

View File

@@ -0,0 +1,84 @@
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { FiPlus, FiTerminal, FiHome } from 'react-icons/fi';
import { FaPuzzlePiece } from 'react-icons/fa';
import Layout from '../Layout.jsx';
import SidebarNavItem from './SidebarNavItem.jsx';
import api from '../../services/api.js';
const skillStatusMap = {
dev: { text: '开发中', className: 'status-stopped' },
published: { text: '已上架', className: 'status-running' },
unlisting: { text: '下架审核中', className: 'status-warning' },
unlisted: { text: '已下架', className: 'status-stopped' }
};
function DeveloperLayout() {
const location = useLocation();
const navigate = useNavigate();
const isPathActive = (basePath) => location.pathname.startsWith(basePath);
// 获取当前技能编辑器中的 skillId
const editorMatch = location.pathname.match(/\/developer\/my-skills\/(\d+)\/editor$/);
const activeSkillId = editorMatch ? parseInt(editorMatch[1]) : null;
const sidebar = (
<>
<div className="chat-sidebar-header">
<button className="btn btn-primary" style={{ width: '100%' }}
onClick={() => navigate('/developer/my-skills/upload')}>
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}>
<FiPlus /> 创建技能
</span>
</button>
</div>
<div className="chat-sidebar-content">
{api.developer.getMySkills().map(skill => (
<div
key={skill.id}
className={`conversation-item ${activeSkillId === skill.id ? 'active' : ''}`}
onClick={() => navigate(`/developer/my-skills/${skill.id}/editor`)}
>
<div className="conversation-title">{skill.name}</div>
<div className="conversation-time">
<span className={`status ${skillStatusMap[skill.status]?.className || 'status-stopped'}`}>
{skillStatusMap[skill.status]?.text || skill.status}
</span>
</div>
</div>
))}
</div>
<div className="chat-sidebar-nav">
<SidebarNavItem
icon={<FiHome />}
label="总览"
active={location.pathname === '/developer/overview'}
onClick={() => navigate('/developer/overview')}
/>
<SidebarNavItem
icon={<FaPuzzlePiece />}
label="我的技能"
active={isPathActive('/developer/my-skills')}
onClick={() => navigate('/developer/my-skills')}
/>
<SidebarNavItem
icon={<FiTerminal />}
label="开发文档"
active={location.pathname === '/developer/docs'}
onClick={() => navigate('/developer/docs')}
/>
</div>
</>
);
return (
<Layout
sidebar={sidebar}
sidebarClassName="chat-sidebar"
>
<Outlet />
</Layout>
);
}
export default DeveloperLayout;

View File

@@ -1,61 +0,0 @@
// 页面配置常量
/**
* 工作台页面配置
*/
export const CONSOLE_PAGES = {
chat: { title: '智能助手', icon: 'FiMessageSquare' },
skills: { title: '技能市场', icon: 'FaPuzzlePiece' },
skillDetail: { title: '技能详情', icon: null },
mySkills: { title: '我的技能', icon: 'FiBox' },
skillConfig: { title: '技能配置', icon: null },
logs: { title: '日志查询', icon: 'FiList' },
scheduledTasks: { title: '定时任务', icon: 'FiClock' },
taskDetail: { title: '任务详情', icon: null },
account: { title: '账号管理', icon: 'FiUser' },
projects: { title: '项目管理', icon: 'FiUsers' },
memberConfig: { title: '成员配置', icon: null },
addMember: { title: '增加成员', icon: null },
};
/**
* 管理台页面配置
*/
export const ADMIN_PAGES = {
overview: { title: '总览', icon: 'FiHome' },
departments: { title: '部门管理', icon: 'FiBarChart2' },
users: { title: '用户管理', icon: 'FiUsers' },
projects: { title: '项目管理', icon: 'FiList' },
modelConfigs: { title: '模型配置', icon: 'FiSettings' },
adminLogs: { title: '日志查询', icon: 'FiActivity' },
reviewList: { title: '审核管理', icon: 'FiCheckCircle' },
reviewDetail: { title: '审核详情', icon: null },
addDepartment: { title: '新增部门', icon: null },
addUser: { title: '新增用户', icon: null },
addProject: { title: '新增项目', icon: null },
addModelConfig: { title: '新增配置', icon: null },
account: { title: '账号管理', icon: 'FiUser' },
};
/**
* 开发台页面配置
*/
export const DEVELOPER_PAGES = {
overview: { title: '总览', icon: 'FiHome' },
mySkills: { title: '我的技能', icon: 'FaPuzzlePiece' },
uploadSkill: { title: '创建技能', icon: 'FiPlus' },
newVersion: { title: '上传新版本', icon: null },
devDocs: { title: '开发文档', icon: 'FiTerminal' },
devAccount: { title: '账号管理', icon: 'FiSettings' },
skillEditor: { title: '技能详情', icon: null },
};
/**
* 获取页面标题
* @param {string} pageId - 页面ID
* @param {Object} pagesConfig - 页面配置对象
* @returns {string} 页面标题
*/
export function getPageTitle(pageId, pagesConfig) {
return pagesConfig[pageId]?.title || '';
}

View File

@@ -1,25 +0,0 @@
// localStorage 键名常量
/**
* 工作台相关键名
*/
export const CONSOLE_KEYS = {
CURRENT_PAGE: 'console_currentPage',
CURRENT_SCENE: 'console_currentScene',
};
/**
* 管理台相关键名
*/
export const ADMIN_KEYS = {
CURRENT_PAGE: 'admin_currentPage',
MODEL_CONFIG_EDIT_DATA: 'admin_modelConfigEditData',
};
/**
* 开发台相关键名
*/
export const DEVELOPER_KEYS = {
CURRENT_PAGE: 'developer_currentPage',
CURRENT_SKILL_ID: 'developer_currentSkillId',
};

View File

@@ -1,25 +0,0 @@
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;

View File

@@ -1,50 +0,0 @@
import { useState, useCallback } from 'react';
/**
* 导航逻辑 Hook
* 统一处理页面导航和附加数据状态管理
*
* @param {Function} setPageCallback - 设置当前页面的回调函数
* @returns {Object} 导航操作函数
*/
function useNavigation(setPageCallback) {
const [extraData, setExtraData] = useState({});
/**
* 导航到指定页面
* @param {string} pageId - 目标页面 ID
* @param {Object} data - 附加数据(如 skillId、taskId 等)
*/
const navigateToPage = useCallback((pageId, data = {}) => {
setPageCallback(pageId);
// 如果有附加数据,更新 extraData
if (Object.keys(data).length > 0) {
setExtraData(data);
}
}, [setPageCallback]);
/**
* 设置附加数据
* @param {Object} data - 附加数据对象
*/
const setExtraDataValue = useCallback((data) => {
setExtraData(prev => ({ ...prev, ...data }));
}, []);
/**
* 清除附加数据
*/
const clearExtraData = useCallback(() => {
setExtraData({});
}, []);
return {
extraData,
navigateToPage,
setExtraData: setExtraDataValue,
clearExtraData,
};
}
export default useNavigation;

View File

@@ -1,58 +0,0 @@
import { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
/**
* 页面状态持久化 Hook
* 封装页面状态管理、localStorage 同步和主页跳转重置逻辑
*
* @param {Object} options - 配置选项
* @param {string} options.storageKey - localStorage 存储键名
* @param {string} options.defaultPage - 默认页面 ID
* @param {Object} options.pageTitles - 页面标题映射对象
* @param {Function} options.getPageTitle - 自定义获取页面标题函数(可选)
* @returns {Object} 状态和操作函数
*/
function usePageState({
storageKey,
defaultPage,
pageTitles,
getPageTitle: customGetPageTitle,
}) {
const location = useLocation();
const navigate = useNavigate();
// 从 localStorage 恢复或使用默认值
const [currentPage, setCurrentPage] = useState(() => {
const saved = localStorage.getItem(`${storageKey}_currentPage`);
return saved || defaultPage;
});
// 处理主页跳转重置
useEffect(() => {
if (location.state?.fromHome) {
setCurrentPage(defaultPage);
navigate('.', { replace: true, state: {} });
}
}, [location.state, navigate, defaultPage]);
// 同步到 localStorage
useEffect(() => {
localStorage.setItem(`${storageKey}_currentPage`, currentPage);
}, [storageKey, currentPage]);
// 获取页面标题
const getPageTitle = (pageId = currentPage) => {
if (customGetPageTitle) {
return customGetPageTitle(pageId, currentPage);
}
return pageTitles[pageId] || '';
};
return {
currentPage,
setCurrentPage,
getPageTitle,
};
}
export default usePageState;

View File

@@ -1,192 +0,0 @@
import { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { FiHome, FiBarChart2, FiUsers, FiList, FiCheckCircle, FiActivity, FiSettings } from 'react-icons/fi';
import Layout from '../components/Layout.jsx';
import SidebarNavItem from '../components/layout/SidebarNavItem.jsx';
import usePageState from '../hooks/usePageState.js';
import { ADMIN_PAGES } from '../constants/pages.js';
import { ADMIN_KEYS } from '../constants/storageKeys.js';
import OverviewPage from './admin/OverviewPage.jsx';
import DepartmentsPage from './admin/DepartmentsPage.jsx';
import UsersPage from './admin/UsersPage.jsx';
import AdminProjectsPage from './admin/AdminProjectsPage.jsx';
import AddDepartmentPage from './admin/AddDepartmentPage.jsx';
import AddUserPage from './admin/AddUserPage.jsx';
import AddProjectPage from './admin/AddProjectPage.jsx';
import AdminLogsPage from './admin/AdminLogsPage.jsx';
import ConsoleReviewListPage from './console/ConsoleReviewListPage.jsx';
import ConsoleReviewDetailPage from './console/ConsoleReviewDetailPage.jsx';
import ModelConfigsPage from './admin/ModelConfigsPage.jsx';
import AddModelConfigPage from './admin/AddModelConfigPage.jsx';
function AdminPage() {
const location = useLocation();
const navigate = useNavigate();
const { currentPage, setCurrentPage } = usePageState({
storageKey: ADMIN_KEYS.CURRENT_PAGE,
defaultPage: 'overview',
pageTitles: ADMIN_PAGES,
});
const [editData, setEditData] = useState(null);
const [reviewType, setReviewType] = useState(null);
const [reviewId, setReviewId] = useState(null);
const navigateTo = (page, data) => {
setEditData(data || null);
setCurrentPage(page);
};
const handleReviewClick = (type, id) => {
setReviewType(type);
setReviewId(id);
navigateTo('reviewDetail');
};
const handleReviewBack = () => {
setReviewType(null);
setReviewId(null);
navigateTo('reviewList');
};
const renderPage = () => {
switch (currentPage) {
case 'overview':
return <OverviewPage />;
case 'departments':
return <DepartmentsPage
onAdd={() => navigateTo('addDepartment')}
onEdit={(dept) => navigateTo('addDepartment', dept)}
/>;
case 'users':
return <UsersPage
onAdd={() => navigateTo('addUser')}
onEdit={(user) => navigateTo('addUser', user)}
/>;
case 'projects':
return <AdminProjectsPage
onAdd={() => navigateTo('addProject')}
onEdit={(project) => navigateTo('addProject', project)}
/>;
case 'adminLogs':
return <AdminLogsPage />;
case 'reviewList':
return <ConsoleReviewListPage onReviewClick={handleReviewClick} />;
case 'reviewDetail':
return <ConsoleReviewDetailPage
type={reviewType}
reviewId={reviewId}
onBack={handleReviewBack}
/>;
case 'addDepartment':
return <AddDepartmentPage
onBack={() => navigateTo('departments')}
editData={editData}
/>;
case 'addUser':
return <AddUserPage
onBack={() => navigateTo('users')}
editData={editData}
/>;
case 'addProject':
return <AddProjectPage
onBack={() => navigateTo('projects')}
editData={editData}
/>;
case 'modelConfigs':
return <ModelConfigsPage
onAdd={() => navigateTo('addModelConfig')}
onEdit={(config) => navigateTo('addModelConfig', config)}
/>;
case 'addModelConfig':
return <AddModelConfigPage
onBack={() => navigateTo('modelConfigs')}
editData={editData}
/>;
default:
return <div>Page not found</div>;
}
};
const sidebar = (
<>
<nav className="admin-sidebar-nav">
<SidebarNavItem
icon={<FiHome />}
label="总览"
active={currentPage === 'overview'}
onClick={() => navigateTo('overview')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiCheckCircle />}
label="审核管理"
active={currentPage === 'reviewList' || currentPage === 'reviewDetail'}
onClick={() => navigateTo('reviewList')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiBarChart2 />}
label="部门管理"
active={currentPage === 'departments'}
onClick={() => navigateTo('departments')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiUsers />}
label="用户管理"
active={currentPage === 'users'}
onClick={() => navigateTo('users')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiList />}
label="项目管理"
active={currentPage === 'projects'}
onClick={() => navigateTo('projects')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiActivity />}
label="日志查询"
active={currentPage === 'adminLogs'}
onClick={() => navigateTo('adminLogs')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
<SidebarNavItem
icon={<FiSettings />}
label="模型配置"
active={currentPage === 'modelConfigs' || currentPage === 'addModelConfig'}
onClick={() => navigateTo('modelConfigs')}
itemClassName="admin-nav-item"
iconClassName="admin-nav-icon"
textClassName="admin-nav-text"
/>
</nav>
</>
);
return (
<Layout
sidebar={sidebar}
sidebarClassName="admin-sidebar"
>
{renderPage()}
</Layout>
);
}
export default AdminPage;

View File

@@ -1,219 +0,0 @@
import { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { FiPlus, FiClock, FiList, FiUsers, FiBox } from 'react-icons/fi';
import { FaPuzzlePiece } from 'react-icons/fa';
import Layout from '../components/Layout.jsx';
import SidebarNavItem from '../components/layout/SidebarNavItem.jsx';
import usePageState from '../hooks/usePageState.js';
import { CONSOLE_PAGES } from '../constants/pages.js';
import { CONSOLE_KEYS } from '../constants/storageKeys.js';
import api from '../services/api.js';
import ChatPage from './console/ChatPage.jsx';
import SkillsPage from './console/SkillsPage.jsx';
import SkillDetailPage from './console/SkillDetailPage.jsx';
import MySkillsPage from './console/MySkillsPage.jsx';
import SkillConfigPage from './console/SkillConfigPage.jsx';
import LogsPage from './console/LogsPage.jsx';
import TasksPage from './console/TasksPage.jsx';
import TaskDetailPage from './console/TaskDetailPage.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();
// 使用 usePageState 管理 currentPage不使用其返回的 getPageTitle因为需要访问组件局部变量
const { currentPage, setCurrentPage } = usePageState({
storageKey: CONSOLE_KEYS.CURRENT_PAGE,
defaultPage: 'chat',
pageTitles: CONSOLE_PAGES,
});
// 保留额外的状态scene 和 skillId 等需要特殊处理)
const [currentScene, setCurrentScene] = useState(() => {
return localStorage.getItem(CONSOLE_KEYS.CURRENT_SCENE) || 'welcome';
});
const [currentSkillId, setCurrentSkillId] = useState(null);
const [currentTaskId, setCurrentTaskId] = useState(null);
const [currentSubscriptionId, setCurrentSubscriptionId] = useState(null);
// 处理主页跳转重置
useEffect(() => {
if (location.state?.fromHome) {
setCurrentPage('chat');
setCurrentScene('welcome');
navigate('.', { replace: true, state: {} });
}
}, [location.state, navigate, setCurrentPage, setCurrentScene]);
// 同步 currentScene 到 localStorage
useEffect(() => {
localStorage.setItem(CONSOLE_KEYS.CURRENT_SCENE, currentScene);
}, [currentScene]);
const switchPage = (pageId, data = {}) => {
setCurrentPage(pageId);
if (data.skillId !== undefined) {
setCurrentSkillId(data.skillId);
}
if (data.subscriptionId !== undefined) {
setCurrentSubscriptionId(data.subscriptionId);
}
};
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 'mySkills':
return <MySkillsPage
onConfig={(subscriptionId) => switchPage('skillConfig', { subscriptionId })}
onBack={() => switchPage('skills')}
/>;
case 'skillConfig':
return <SkillConfigPage
subscriptionId={currentSubscriptionId}
onBack={() => switchPage('mySkills')}
/>;
case 'logs':
return <LogsPage />;
case 'scheduledTasks':
return <TasksPage
onViewDetail={(taskId) => {
setCurrentTaskId(taskId);
switchPage('taskDetail');
}}
/>;
case 'taskDetail':
return <TaskDetailPage
taskId={currentTaskId}
onBack={() => switchPage('scheduledTasks')}
/>;
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 = () => {
let title = CONSOLE_PAGES[currentPage]?.title || '';
if (currentPage === 'chat') {
const conv = api.conversations.list().find(c => c.scene === currentScene);
title = conv?.title || '智能助手';
}
if (currentPage === 'skillDetail' && currentSkillId) {
const skill = api.skills.getById(currentSkillId);
title = skill?.name || '技能详情';
}
return title;
};
const sidebar = (
<>
<div className="chat-sidebar-header">
<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">
{api.conversations.list().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">
<SidebarNavItem
icon={<FaPuzzlePiece />}
label="技能市场"
active={currentPage === 'skills'}
onClick={() => switchPage('skills')}
/>
<SidebarNavItem
icon={<FiBox />}
label="我的技能"
active={currentPage === 'mySkills'}
onClick={() => switchPage('mySkills')}
/>
<SidebarNavItem
icon={<FiClock />}
label="定时任务"
active={currentPage === 'scheduledTasks'}
onClick={() => switchPage('scheduledTasks')}
/>
<SidebarNavItem
icon={<FiList />}
label="日志查询"
active={currentPage === 'logs'}
onClick={() => switchPage('logs')}
/>
<SidebarNavItem
icon={<FiUsers />}
label="项目管理"
active={currentPage === 'projects'}
onClick={() => switchPage('projects')}
/>
</div>
</>
);
return (
<Layout
sidebar={sidebar}
sidebarClassName="chat-sidebar"
contentClassName={currentPage === 'chat' ? 'page-content-full' : ''}
>
{renderPage()}
</Layout>
);
}
export default ConsolePage;

View File

@@ -1,180 +0,0 @@
import { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { FiPlus, FiTerminal, FiHome } from 'react-icons/fi';
import { FaPuzzlePiece } from 'react-icons/fa';
import Layout from '../components/Layout.jsx';
import SidebarNavItem from '../components/layout/SidebarNavItem.jsx';
import usePageState from '../hooks/usePageState.js';
import { DEVELOPER_PAGES } from '../constants/pages.js';
import { DEVELOPER_KEYS } from '../constants/storageKeys.js';
import api from '../services/api.js';
import DevOverviewPage from './developer/DevOverviewPage.jsx';
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 SkillEditorPage from './developer/SkillEditorPage.jsx';
import UpdateSkillInfoPage from './developer/UpdateSkillInfoPage.jsx';
import UploadVersionPage from './developer/UploadVersionPage.jsx';
const skillStatusMap = {
dev: { text: '开发中', className: 'status-stopped' },
published: { text: '已上架', className: 'status-running' },
unlisting: { text: '下架审核中', className: 'status-warning' },
unlisted: { text: '已下架', className: 'status-stopped' }
};
function DeveloperPage() {
const location = useLocation();
const navigate = useNavigate();
// 使用 usePageState 管理页面状态
const { currentPage, setCurrentPage } = usePageState({
storageKey: DEVELOPER_KEYS.CURRENT_PAGE,
defaultPage: 'overview',
pageTitles: DEVELOPER_PAGES,
});
// 保留额外的状态currentSkillId 需要持久化到 localStorage
const [currentSkillId, setCurrentSkillId] = useState(() => {
const saved = localStorage.getItem(DEVELOPER_KEYS.CURRENT_SKILL_ID);
return saved ? JSON.parse(saved) : null;
});
const [newVersionSkillName, setNewVersionSkillName] = useState('');
useEffect(() => {
if (location.state?.fromHome) {
setCurrentPage('overview');
setCurrentSkillId(null);
navigate('.', { replace: true, state: {} });
}
}, [location.state, navigate, setCurrentPage, setCurrentSkillId]);
useEffect(() => {
localStorage.setItem(DEVELOPER_KEYS.CURRENT_SKILL_ID, JSON.stringify(currentSkillId));
}, [DEVELOPER_KEYS.CURRENT_SKILL_ID, 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 openUpdateInfoPage = (skillId) => {
setCurrentSkillId(skillId);
setCurrentPage('updateInfo');
};
const handleBack = () => {
setCurrentPage('mySkills');
setCurrentSkillId(null);
};
const handleEditorBack = () => {
setCurrentPage('skillEditor');
setNewVersionSkillName('');
};
const renderPage = () => {
switch (currentPage) {
case 'overview':
return <DevOverviewPage onSkillClick={openSkillEditor} />;
case 'mySkills':
return <MySkillsPage onSkillClick={openSkillEditor} />;
case 'uploadSkill':
return <UploadSkillPage onBack={() => switchPage('mySkills')} />;
case 'devDocs':
return <DevDocsPage />;
case 'skillEditor':
return <SkillEditorPage
skillId={currentSkillId}
onBack={handleBack}
onUploadNewVersion={openNewVersionPage}
onUpdateInfo={openUpdateInfoPage}
/>;
case 'newVersion':
return <UploadVersionPage skillName={newVersionSkillName} onBack={handleEditorBack} />;
case 'updateInfo':
return <UpdateSkillInfoPage
skill={api.developer.getSkillById(currentSkillId)}
onBack={handleEditorBack}
/>;
default:
return <div>Page not found</div>;
}
};
const sidebar = (
<>
<div className="chat-sidebar-header">
<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">
{api.developer.getMySkills().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">
<span className={`status ${skillStatusMap[skill.status]?.className || 'status-stopped'}`}>
{skillStatusMap[skill.status]?.text || skill.status}
</span>
</div>
</div>
))}
</div>
<div className="chat-sidebar-nav">
<SidebarNavItem
icon={<FiHome />}
label="总览"
active={currentPage === 'overview'}
onClick={() => switchPage('overview')}
/>
<SidebarNavItem
icon={<FaPuzzlePiece />}
label="我的技能"
active={currentPage === 'mySkills'}
onClick={() => switchPage('mySkills')}
/>
<SidebarNavItem
icon={<FiTerminal />}
label="开发文档"
active={currentPage === 'devDocs'}
onClick={() => switchPage('devDocs')}
/>
</div>
</>
);
return (
<Layout
sidebar={sidebar}
sidebarClassName="chat-sidebar"
>
{renderPage()}
</Layout>
);
}
export default DeveloperPage;

View File

@@ -15,7 +15,7 @@ function HomePage() {
基于容器化实例的 智能助手平台提供租户隔离技能市场安全审计等核心能力
</p>
<div className="home-buttons">
<Link to="/console" className="home-btn primary" state={{ fromHome: true }}>
<Link to="/console" className="home-btn primary">
<FaRobot /> 进入工作台
</Link>
</div>

View File

@@ -112,7 +112,7 @@ function LoginPage() {
<button
className="btn btn-primary"
style={{ width: '100%', padding: '11px', fontSize: '14px', fontWeight: '600' }}
onClick={() => navigate('/console', { state: { fromHome: true } })}
onClick={() => navigate('/console')}
>
登录
</button>

View File

@@ -1,8 +1,13 @@
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import ListSelector from '../../components/ListSelector.jsx';
import { availableLeaders } from '../../data/adminData.js';
import { api } from '../../services/api.js';
function AddDepartmentPage({ onBack, editData }) {
function AddDepartmentPage() {
const { id } = useParams();
const navigate = useNavigate();
const editData = id ? api.admin.departments.getById(Number(id)) : null;
const isEdit = !!editData;
const [name, setName] = useState(editData?.name || '');
const [description, setDescription] = useState(editData?.description || '');
@@ -22,7 +27,7 @@ function AddDepartmentPage({ onBack, editData }) {
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate('/admin/departments')}>
<span></span>
<span>返回部门列表</span>
</div>
@@ -53,7 +58,7 @@ function AddDepartmentPage({ onBack, editData }) {
/>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn" onClick={() => navigate('/admin/departments')}>取消</button>
<button className="btn btn-primary">确定</button>
</div>
</div>

View File

@@ -1,9 +1,13 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { FiEye, FiEyeOff } from 'react-icons/fi';
import { api } from '../../services/api.js';
import { MODEL_CONFIG_TYPES, getConfigFields, getConfigTypeList } from '../../data/configTypes.js';
function AddModelConfigPage({ onBack, editData }) {
function AddModelConfigPage() {
const { id } = useParams();
const navigate = useNavigate();
const editData = id ? api.admin.modelConfigs.getById(id) : null;
const isEdit = !!editData;
// 基础信息
@@ -98,7 +102,7 @@ function AddModelConfigPage({ onBack, editData }) {
api.admin.modelConfigs.create(configData);
}
onBack();
navigate('/admin/models');
};
// 获取当前类型的字段定义
@@ -107,7 +111,7 @@ function AddModelConfigPage({ onBack, editData }) {
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate('/admin/models')}>
<span></span>
<span>返回配置列表</span>
</div>
@@ -215,7 +219,7 @@ function AddModelConfigPage({ onBack, editData }) {
{/* 操作按钮 */}
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn" onClick={() => navigate('/admin/models')}>取消</button>
<button className="btn btn-primary" onClick={handleSave}>保存</button>
</div>
</div>

View File

@@ -1,8 +1,13 @@
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import ListSelector from '../../components/ListSelector.jsx';
import { availableLeaders } from '../../data/adminData.js';
import { api } from '../../services/api.js';
function AddProjectPage({ onBack, editData }) {
function AddProjectPage() {
const { id } = useParams();
const navigate = useNavigate();
const editData = id ? api.admin.projects.getById(Number(id)) : null;
const isEdit = !!editData;
const [name, setName] = useState(editData?.name || '');
const [description, setDescription] = useState(editData?.description || '');
@@ -22,7 +27,7 @@ function AddProjectPage({ onBack, editData }) {
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate('/admin/projects')}>
<span></span>
<span>返回项目列表</span>
</div>
@@ -53,7 +58,7 @@ function AddProjectPage({ onBack, editData }) {
/>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn" onClick={() => navigate('/admin/projects')}>取消</button>
<button className="btn btn-primary">确定</button>
</div>
</div>

View File

@@ -1,8 +1,13 @@
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import ListSelector from '../../components/ListSelector.jsx';
import { availableDepartments } from '../../data/adminData.js';
import { api } from '../../services/api.js';
function AddUserPage({ onBack, editData }) {
function AddUserPage() {
const { id } = useParams();
const navigate = useNavigate();
const editData = id ? api.admin.users.getById(Number(id)) : null;
const isEdit = !!editData;
const [name, setName] = useState(editData?.name || '');
const [role, setRole] = useState(editData?.role || '成员');
@@ -24,7 +29,7 @@ function AddUserPage({ onBack, editData }) {
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate('/admin/users')}>
<span></span>
<span>返回用户列表</span>
</div>
@@ -67,7 +72,7 @@ function AddUserPage({ onBack, editData }) {
<input type="tel" className="form-control" placeholder="请输入手机号" value={phone} onChange={e => setPhone(e.target.value)} />
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn" onClick={() => navigate('/admin/users')}>取消</button>
<button className="btn btn-primary">确定</button>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../../services/api.js';
import Modal from '../../components/common/Modal.jsx';
@@ -7,7 +8,8 @@ function StatusTag({ status }) {
return <span className={`status ${statusClass}`}>{status}</span>;
}
function AdminProjectsPage({ onAdd, onEdit }) {
function AdminProjectsPage() {
const navigate = useNavigate();
const sourceData = api.admin.projects.list();
const [filters, setFilters] = useState({ keyword: '', status: '' });
const [deleteTarget, setDeleteTarget] = useState(null);
@@ -71,7 +73,7 @@ function AdminProjectsPage({ onAdd, onEdit }) {
<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>
<button className="btn btn-primary btn-sm" onClick={() => navigate('/admin/projects/add')}>新增项目</button>
</div>
<div className="card-body">
<div className="table-wrapper">
@@ -99,7 +101,7 @@ function AdminProjectsPage({ onAdd, onEdit }) {
<td className="col-actions">
<div className="table-actions">
<button className={`text-btn ${project.status === '正常' ? 'text-btn-danger' : 'text-btn-primary'}`}>{project.status === '正常' ? '禁用' : '启用'}</button>
<button className="text-btn text-btn-primary" onClick={() => onEdit(project)}>编辑</button>
<button className="text-btn text-btn-primary" onClick={() => navigate(`/admin/projects/${project.id}/edit`)}>编辑</button>
<button className="text-btn text-btn-danger" onClick={() => setDeleteTarget(project)}>删除</button>
</div>
</td>

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../../services/api.js';
import Modal from '../../components/common/Modal.jsx';
@@ -7,7 +8,8 @@ function StatusTag({ status }) {
return <span className={`status ${statusClass}`}>{status}</span>;
}
function DepartmentsPage({ onAdd, onEdit }) {
function DepartmentsPage() {
const navigate = useNavigate();
const sourceData = api.admin.departments.list();
const [filters, setFilters] = useState({ keyword: '', status: '' });
const [deleteTarget, setDeleteTarget] = useState(null);
@@ -71,7 +73,7 @@ function DepartmentsPage({ onAdd, onEdit }) {
<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>
<button className="btn btn-primary btn-sm" onClick={() => navigate('/admin/departments/add')}>新增部门</button>
</div>
<div className="card-body">
<div className="table-wrapper">
@@ -99,7 +101,7 @@ function DepartmentsPage({ onAdd, onEdit }) {
<td className="col-actions">
<div className="table-actions">
<button className={`text-btn ${dept.status === '正常' ? 'text-btn-danger' : 'text-btn-primary'}`}>{dept.status === '正常' ? '禁用' : '启用'}</button>
<button className="text-btn text-btn-primary" onClick={() => onEdit(dept)}>编辑</button>
<button className="text-btn text-btn-primary" onClick={() => navigate(`/admin/departments/${dept.id}/edit`)}>编辑</button>
<button className="text-btn text-btn-danger" onClick={() => setDeleteTarget(dept)}>删除</button>
</div>
</td>

View File

@@ -1,10 +1,12 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { FiPlus } from 'react-icons/fi';
import { api } from '../../services/api.js';
import { MODEL_CONFIG_TYPES, getConfigSummary } from '../../data/configTypes.js';
import Modal from '../../components/common/Modal.jsx';
function ModelConfigsPage({ onAdd, onEdit }) {
function ModelConfigsPage() {
const navigate = useNavigate();
const [configs, setConfigs] = useState(api.admin.modelConfigs.list());
const [showSetActiveModal, setShowSetActiveModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
@@ -44,7 +46,7 @@ function ModelConfigsPage({ onAdd, onEdit }) {
<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 className="btn btn-primary btn-sm" onClick={() => navigate('/admin/models/add')}>
<FiPlus /> 新增配置
</button>
</div>
@@ -85,7 +87,7 @@ function ModelConfigsPage({ onAdd, onEdit }) {
)}
<button
className="text-btn text-btn-primary"
onClick={() => onEdit(config)}
onClick={() => navigate(`/admin/models/${config.id}/edit`)}
disabled={config.isActive}
title={config.isActive ? '生效中的配置不可编辑' : ''}
style={config.isActive ? { opacity: 0.5, cursor: 'not-allowed' } : {}}

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../../services/api.js';
import Modal from '../../components/common/Modal.jsx';
@@ -12,7 +13,8 @@ function RoleTag({ role }) {
return <span className={`status ${roleClass}`}>{role}</span>;
}
function UsersPage({ onAdd, onEdit }) {
function UsersPage() {
const navigate = useNavigate();
const sourceData = api.admin.users.list();
const [filters, setFilters] = useState({ keyword: '', department: '', status: '' });
const [deleteTarget, setDeleteTarget] = useState(null);
@@ -94,7 +96,7 @@ function UsersPage({ onAdd, onEdit }) {
<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>
<button className="btn btn-primary btn-sm" onClick={() => navigate('/admin/users/add')}>新增用户</button>
</div>
<div className="card-body">
<div className="table-wrapper">
@@ -124,7 +126,7 @@ function UsersPage({ onAdd, onEdit }) {
<td className="col-actions">
<div className="table-actions">
<button className={`text-btn ${user.status === '正常' ? 'text-btn-danger' : 'text-btn-primary'}`}>{user.status === '正常' ? '禁用' : '启用'}</button>
<button className="text-btn text-btn-primary" onClick={() => onEdit(user)}>编辑</button>
<button className="text-btn text-btn-primary" onClick={() => navigate(`/admin/users/${user.id}/edit`)}>编辑</button>
<button className="text-btn text-btn-danger" onClick={() => setDeleteTarget(user)}>删除</button>
</div>
</td>

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import ListSelector from '../../components/ListSelector.jsx';
const availableMembers = [
@@ -12,7 +13,8 @@ const availableMembers = [
{ id: 8, name: '杨十八', department: '数据分析部', email: 'yangshiba@example.com' }
];
function AddMemberPage({ onBack }) {
function AddMemberPage() {
const navigate = useNavigate();
const [selectedMembers, setSelectedMembers] = useState([]);
const memberColumns = [
@@ -31,7 +33,7 @@ function AddMemberPage({ onBack }) {
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate('/console/projects')}>
<span></span>
<span>返回成员列表</span>
</div>
@@ -51,7 +53,7 @@ function AddMemberPage({ onBack }) {
onClearSelected={() => setSelectedMembers([])}
/>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', marginTop: '16px' }}>
<button className="btn btn-secondary" onClick={onBack}>取消</button>
<button className="btn btn-secondary" onClick={() => navigate('/console/projects')}>取消</button>
<button className="btn btn-primary" onClick={handleAdd} disabled={selectedMembers.length === 0}>
添加选中成员 ({selectedMembers.length})
</button>

View File

@@ -1,10 +1,13 @@
import { useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { getChatScenes } from '../../data/conversations.js';
import { FiPaperclip, FiCode, FiSend } from 'react-icons/fi';
function ChatPage({ scene }) {
function ChatPage() {
const { scene } = useParams();
const currentScene = scene || 'welcome';
const chatScenes = getChatScenes();
const html = chatScenes[scene] || '';
const html = chatScenes[currentScene] || '';
const chatMessagesRef = useRef(null);
useEffect(() => {

View File

@@ -1,14 +1,17 @@
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { FiFile } from 'react-icons/fi';
import { pendingVersionReviews, pendingUnlistReviews, skillFiles } from '../../data/skills.js';
import Toast from '../../components/common/Toast.jsx';
function ConsoleReviewDetailPage({ type, reviewId, onBack }) {
function ConsoleReviewDetailPage() {
const { type, reviewId } = useParams();
const navigate = useNavigate();
const [showToast, setShowToast] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const versionReview = type === 'version' ? pendingVersionReviews.find(r => r.id === reviewId) : null;
const unlistReview = type === 'unlist' ? pendingUnlistReviews.find(r => r.id === reviewId) : null;
const versionReview = type === 'version' ? pendingVersionReviews.find(r => r.id === Number(reviewId)) : null;
const unlistReview = type === 'unlist' ? pendingUnlistReviews.find(r => r.id === Number(reviewId)) : null;
const review = versionReview || unlistReview;
@@ -20,7 +23,7 @@ function ConsoleReviewDetailPage({ type, reviewId, onBack }) {
setToastMessage('审核通过');
setShowToast(true);
setTimeout(() => {
onBack && onBack();
onBack && navigate('/admin/reviews');
}, 1000);
};
@@ -28,13 +31,13 @@ function ConsoleReviewDetailPage({ type, reviewId, onBack }) {
setToastMessage('已拒绝');
setShowToast(true);
setTimeout(() => {
onBack && onBack();
onBack && navigate('/admin/reviews');
}, 1000);
};
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate('/admin/reviews')}>
<span></span>
<span>返回审核列表</span>
</div>

View File

@@ -1,7 +1,9 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { pendingVersionReviews, pendingUnlistReviews } from '../../data/skills.js';
function ConsoleReviewListPage({ onReviewClick }) {
function ConsoleReviewListPage() {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState('version');
return (
@@ -50,7 +52,7 @@ function ConsoleReviewListPage({ onReviewClick }) {
<td>
<button
className="text-btn text-btn-primary"
onClick={() => onReviewClick('version', review.id)}
onClick={() => navigate(`/admin/reviews/version/${review.id}`)}
>
审核
</button>
@@ -84,7 +86,7 @@ function ConsoleReviewListPage({ onReviewClick }) {
<td>
<button
className="text-btn text-btn-primary"
onClick={() => onReviewClick('unlist', review.id)}
onClick={() => navigate(`/admin/reviews/unlist/${review.id}`)}
>
审核
</button>

View File

@@ -1,7 +1,12 @@
function MemberConfigPage({ onBack }) {
import { useNavigate, useParams } from 'react-router-dom';
function MemberConfigPage() {
const { memberId } = useParams();
const navigate = useNavigate();
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate('/console/projects')}>
<span></span>
<span>返回成员列表</span>
</div>
@@ -10,11 +15,11 @@ function MemberConfigPage({ onBack }) {
<div className="card-title">成员配置</div>
</div>
<div className="card-body">
<p>成员配置页面内容</p>
<p>成员配置页面内容 (成员 ID: {memberId})</p>
</div>
</div>
</>
);
}
export default MemberConfigPage;
export default MemberConfigPage;

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { FiSearch } from 'react-icons/fi';
import { FaBoxOpen } from 'react-icons/fa';
import { skills, userSubscriptions } from '../../data/skills.js';
@@ -6,7 +7,8 @@ import EmptyState from '../../components/common/EmptyState.jsx';
import Modal from '../../components/common/Modal.jsx';
import Toast from '../../components/common/Toast.jsx';
function MySkillsPage({ onConfig, onBack }) {
function MySkillsPage() {
const navigate = useNavigate();
const [filters, setFilters] = useState({ keyword: '', category: '', status: '' });
const [subscriptions, setSubscriptions] = useState(userSubscriptions);
const [actionTarget, setActionTarget] = useState(null);
@@ -249,7 +251,7 @@ function MySkillsPage({ onConfig, onBack }) {
)}
<button
className="text-btn text-btn-primary"
onClick={() => onConfig(item.id)}
onClick={() => navigate(`/console/my-skills/${item.id}/config`)}
>
配置
</button>

View File

@@ -1,10 +1,12 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { FiUsers, FiSearch } from 'react-icons/fi';
import { projectMembers } from '../../data/members.js';
import EmptyState from '../../components/common/EmptyState.jsx';
import Modal from '../../components/common/Modal.jsx';
function ProjectsPage({ onAddMember }) {
function ProjectsPage() {
const navigate = useNavigate();
const [members, setMembers] = useState(projectMembers);
const [removeTarget, setRemoveTarget] = useState(null);
const [filters, setFilters] = useState({
@@ -82,7 +84,7 @@ function ProjectsPage({ onAddMember }) {
<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>
<button className="btn btn-primary btn-sm" onClick={() => navigate('/console/projects/members/add')}>增加成员</button>
</div>
<div className="card-body">
{filteredMembers.length > 0 ? (
@@ -110,7 +112,7 @@ function ProjectsPage({ onAddMember }) {
<td><span className={`status ${member.role === '管理员' ? 'role-admin' : 'role-member'}`}>{member.role}</span></td>
<td className="col-actions--narrow">
<div className="table-actions">
<button className="text-btn text-btn-primary">配置</button>
<button className="text-btn text-btn-primary" onClick={() => navigate(`/console/projects/members/${member.id}/config`)}>配置</button>
<button className="text-btn text-btn-danger" onClick={() => handleRemoveClick(member)}>移除</button>
</div>
</td>

View File

@@ -1,9 +1,12 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { FiPlus, FiX, FiUsers, FiStar, FiPackage } from 'react-icons/fi';
import { skills, userSubscriptions } from '../../data/skills.js';
import Toast from '../../components/common/Toast.jsx';
function SkillConfigPage({ subscriptionId, onBack }) {
function SkillConfigPage() {
const { subscriptionId } = useParams();
const navigate = useNavigate();
const [subscriptions, setSubscriptions] = useState(userSubscriptions);
const [subscription, setSubscription] = useState(null);
const [skill, setSkill] = useState(null);
@@ -12,7 +15,7 @@ function SkillConfigPage({ subscriptionId, onBack }) {
const [toast, setToast] = useState({ visible: false, type: 'success', message: '' });
useEffect(() => {
const sub = subscriptions.find(s => s.id === subscriptionId);
const sub = subscriptions.find(s => s.id === Number(subscriptionId));
if (sub) {
setSubscription(sub);
const skillData = skills.find(s => s.id === sub.skillId);
@@ -82,7 +85,7 @@ function SkillConfigPage({ subscriptionId, onBack }) {
// 延迟返回
setTimeout(() => {
onBack();
navigate('/console/my-skills');
}, 500);
};
@@ -94,7 +97,7 @@ function SkillConfigPage({ subscriptionId, onBack }) {
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate('/console/my-skills')}>
<span></span>
<span>返回我的技能</span>
</div>
@@ -207,7 +210,7 @@ function SkillConfigPage({ subscriptionId, onBack }) {
</div>
)}
<div style={{ marginTop: '16px', textAlign: 'right', display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
<button className="btn btn-secondary" onClick={onBack}>
<button className="btn btn-secondary" onClick={() => navigate('/console/my-skills')}>
取消
</button>
<button className="btn btn-primary" onClick={handleSave}>

View File

@@ -1,8 +1,11 @@
import { useNavigate, useParams } from 'react-router-dom';
import { FiChevronLeft, FiFile, FiUsers, FiStar, FiPackage } from 'react-icons/fi';
import { skills, skillFiles } from '../../data/skills.js';
function SkillDetailPage({ skillId, onBack }) {
const skill = skills.find(s => s.id === skillId);
function SkillDetailPage() {
const { skillId } = useParams();
const navigate = useNavigate();
const skill = skills.find(s => s.id === Number(skillId));
if (!skill) {
return <div>Skill not found</div>;
@@ -12,7 +15,7 @@ function SkillDetailPage({ skillId, onBack }) {
return (
<>
<div className="dev-back-btn" onClick={onBack} style={{ marginBottom: '16px' }}>
<div className="dev-back-btn" onClick={() => navigate('/console/skills')} style={{ marginBottom: '16px' }}>
<FiChevronLeft /> 返回技能市场
</div>
{cv ? (

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { FiUser, FiStar, FiSearch } from 'react-icons/fi';
import { FaBoxOpen } from 'react-icons/fa';
import { skills, userSubscriptions } from '../../data/skills.js';
@@ -46,7 +47,8 @@ function SkillCard({ skill, onClick, onSubscribe }) {
);
}
function SkillsPage({ onSkillClick }) {
function SkillsPage() {
const navigate = useNavigate();
const [sort, setSort] = useState('subs');
const [searchQuery, setSearchQuery] = useState('');
const [subscriptions, setSubscriptions] = useState(userSubscriptions);
@@ -140,7 +142,7 @@ function SkillsPage({ onSkillClick }) {
<SkillCard
key={skill.id}
skill={skill}
onClick={() => onSkillClick(skill.id)}
onClick={() => navigate(`/console/skills/${skill.id}`)}
onSubscribe={handleSubscribeClick}
/>
))}

View File

@@ -1,12 +1,15 @@
import { useParams, useNavigate } from 'react-router-dom';
import { scheduledTasks } from '../../data/tasks.js';
function TaskDetailPage({ taskId, onBack }) {
const task = scheduledTasks.find(t => t.id === taskId);
function TaskDetailPage() {
const { taskId } = useParams();
const navigate = useNavigate();
const task = scheduledTasks.find(t => t.id === Number(taskId));
if (!task) {
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate('/console/tasks')}>
<span></span>
<span>返回任务列表</span>
</div>
@@ -24,7 +27,7 @@ function TaskDetailPage({ taskId, onBack }) {
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate('/console/tasks')}>
<span></span>
<span>返回任务列表</span>
</div>

View File

@@ -1,10 +1,12 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { FiClock } from 'react-icons/fi';
import { scheduledTasks } from '../../data/tasks.js';
import EmptyState from '../../components/common/EmptyState.jsx';
import Modal from '../../components/common/Modal.jsx';
function TasksPage({ onViewDetail }) {
function TasksPage() {
const navigate = useNavigate();
const [tasks, setTasks] = useState(scheduledTasks);
const [deleteTarget, setDeleteTarget] = useState(null);
@@ -66,7 +68,7 @@ function TasksPage({ onViewDetail }) {
<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-primary" style={{ marginLeft: '8px' }} onClick={() => navigate(`/console/tasks/${task.id}`)}>详情</button>
<button className="text-btn text-btn-danger" style={{ marginLeft: '8px' }} onClick={() => handleDeleteClick(task)}>删除</button>
</td>
</tr>

View File

@@ -1,7 +1,9 @@
import { useNavigate } from 'react-router-dom';
import { FiAlertTriangle, FiInfo } from 'react-icons/fi';
import { api } from '../../services/api.js';
function DevOverviewPage({ onSkillClick }) {
function DevOverviewPage() {
const navigate = useNavigate();
const data = api.developer.getOverview();
return (
@@ -36,7 +38,7 @@ function DevOverviewPage({ onSkillClick }) {
key={index}
className={`anomaly-item ${item.status === 'rejected' ? 'anomaly-warning' : 'anomaly-info'}`}
style={{ cursor: 'pointer' }}
onClick={() => onSkillClick && onSkillClick(item.skillId)}
onClick={() => navigate(`/developer/my-skills/${item.skillId}/editor`)}
>
<span className="anomaly-icon">
{item.status === 'rejected' ? <FiAlertTriangle /> : <FiInfo />}

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../../services/api.js';
import Modal from '../../components/common/Modal.jsx';
import Toast from '../../components/common/Toast.jsx';
@@ -10,7 +11,8 @@ const skillStatusMap = {
unlisted: { text: '已下架', className: 'status-stopped' }
};
function MySkillsPage({ onSkillClick }) {
function MySkillsPage() {
const navigate = useNavigate();
const sourceData = api.developer.getMySkills();
const [filters, setFilters] = useState({ keyword: '', status: '' });
const [deleteTarget, setDeleteTarget] = useState(null);
@@ -100,7 +102,7 @@ function MySkillsPage({ onSkillClick }) {
</thead>
<tbody>
{filteredList.map(skill => (
<tr key={skill.id} className="tr-clickable" onClick={() => onSkillClick(skill.id)}>
<tr key={skill.id} className="tr-clickable" onClick={() => navigate(`/developer/my-skills/${skill.id}/editor`)}>
<td>{skill.name}</td>
<td>{skill.desc}</td>
<td>
@@ -110,7 +112,7 @@ function MySkillsPage({ onSkillClick }) {
</td>
<td>
<div className="table-actions">
<button className="text-btn text-btn-primary" onClick={e => { e.stopPropagation(); onSkillClick(skill.id); }}>
<button className="text-btn text-btn-primary" onClick={e => { e.stopPropagation(); navigate(`/developer/my-skills/${skill.id}/editor`); }}>
编辑
</button>
{skill.status === 'published' && (

View File

@@ -1,58 +0,0 @@
import { useState } from 'react';
import { FiUpload } from 'react-icons/fi';
import Toast from '../../components/common/Toast.jsx';
function NewVersionPage({ skillName, onBack }) {
const [showToast, setShowToast] = useState(false);
const handleSubmit = () => {
setShowToast(true);
setTimeout(() => {
onBack();
}, 1000);
};
return (
<>
<div className="page-back-btn" onClick={onBack}>
<span></span>
<span>返回技能详情</span>
</div>
<div className="card">
<div className="card-header">
<div className="card-title">上传新版本 {skillName}</div>
</div>
<div className="card-body">
<div className="form-group">
<label className="form-label required">版本说明</label>
<textarea
className="form-control"
rows="3"
placeholder="请描述本次版本更新的内容"
/>
</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" onClick={onBack}>取消</button>
<button className="btn btn-primary" onClick={handleSubmit}>提交审核</button>
</div>
</div>
</div>
<Toast
visible={showToast}
type="success"
message="已提交审核"
onClose={() => setShowToast(false)}
/>
</>
);
}
export default NewVersionPage;

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { FiUpload, FiUsers, FiPackage, FiStar, FiRotateCcw } from 'react-icons/fi';
import { api } from '../../services/api.js';
import Modal from '../../components/common/Modal.jsx';
@@ -18,8 +19,10 @@ const skillStatusMap = {
unlisted: { text: '已下架', className: 'status-stopped' }
};
function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo }) {
const skill = api.developer.getSkillById(skillId);
function SkillEditorPage() {
const { skillId } = useParams();
const navigate = useNavigate();
const skill = api.developer.getSkillById(Number(skillId));
const [deleteSkillModal, setDeleteSkillModal] = useState(false);
const [unlistSkillModal, setUnlistSkillModal] = useState(false);
const [deleteVersionTarget, setDeleteVersionTarget] = useState(null);
@@ -50,7 +53,7 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate('/developer/my-skills')}>
<span></span>
<span>返回我的技能</span>
</div>
@@ -64,7 +67,7 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
{skillStatusMap[skill.status]?.text || skill.status}
</span>
<div className="skill-actions">
<button className="btn btn-primary btn-sm" onClick={() => onUpdateInfo && onUpdateInfo(skill.id)}>编辑内部信息</button>
<button className="btn btn-primary btn-sm" onClick={() => navigate(`/developer/my-skills/${skillId}/update-info`)}>编辑内部信息</button>
</div>
</div>
<div className="skill-desc-row">
@@ -121,7 +124,7 @@ function SkillEditorPage({ skillId, onBack, onUploadNewVersion, onUpdateInfo })
<div className="card-title">版本历史</div>
<button
className="btn btn-primary btn-sm"
onClick={() => onUploadNewVersion(skill)}
onClick={() => navigate(`/developer/my-skills/${skillId}/new-version`)}
disabled={skill.status === 'unlisting' || skill.status === 'unlisted' || skill.hasPendingReview}
title={skill.status === 'unlisted' ? '已下架的技能不能上传新版本' : (skill.status === 'unlisting' ? '下架审核中的技能不能上传新版本' : (skill.hasPendingReview ? '存在审核中的版本,请先撤回后再上传新版本' : ''))}
>

View File

@@ -1,7 +1,12 @@
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { api } from '../../services/api.js';
import Toast from '../../components/common/Toast.jsx';
function UpdateSkillInfoPage({ skill, onBack }) {
function UpdateSkillInfoPage() {
const { skillId } = useParams();
const navigate = useNavigate();
const skill = api.developer.getSkillById(Number(skillId));
const [name, setName] = useState(skill?.name || '');
const [desc, setDesc] = useState(skill?.desc || '');
const [showToast, setShowToast] = useState(false);
@@ -9,13 +14,13 @@ function UpdateSkillInfoPage({ skill, onBack }) {
const handleSave = () => {
setShowToast(true);
setTimeout(() => {
onBack();
navigate(`/developer/my-skills/${skillId}/editor`);
}, 1000);
};
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate(`/developer/my-skills/${skillId}/editor`)}>
<span></span>
<span>返回技能详情</span>
</div>
@@ -48,7 +53,7 @@ function UpdateSkillInfoPage({ skill, onBack }) {
/>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn" onClick={() => navigate(`/developer/my-skills/${skillId}/editor`)}>取消</button>
<button className="btn btn-primary" onClick={handleSave}>保存修改</button>
</div>
</div>

View File

@@ -1,7 +1,9 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Toast from '../../components/common/Toast.jsx';
function UploadSkillPage({ onBack }) {
function UploadSkillPage() {
const navigate = useNavigate();
const [showToast, setShowToast] = useState(false);
const handleCreate = () => {
@@ -10,7 +12,7 @@ function UploadSkillPage({ onBack }) {
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate('/developer/my-skills')}>
<span></span>
<span>返回技能管理</span>
</div>
@@ -31,7 +33,7 @@ function UploadSkillPage({ onBack }) {
<textarea className="form-control" rows="3" placeholder="请输入开发者内部技能描述" />
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn" onClick={() => navigate('/developer/my-skills')}>取消</button>
<button className="btn btn-primary" onClick={handleCreate}>创建技能</button>
</div>
</div>

View File

@@ -1,11 +1,15 @@
import { FiUpload, FiX } from 'react-icons/fi';
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { api } from '../../services/api.js';
import Toast from '../../components/common/Toast.jsx';
const ICON_OPTIONS = ['🌤️', '📊', '📝', '🔧', '💻', '📋', '🔍', '📈', '🎯', '⚡', '🌐', '🤖'];
function UploadVersionPage({ skill, onBack }) {
function UploadVersionPage() {
const { skillId } = useParams();
const navigate = useNavigate();
const skill = api.developer.getSkillById(Number(skillId));
const categories = api.developer.getCategories();
const [showToast, setShowToast] = useState(false);
@@ -38,13 +42,13 @@ function UploadVersionPage({ skill, onBack }) {
const handleSubmit = () => {
setShowToast(true);
setTimeout(() => {
onBack();
navigate(`/developer/my-skills/${skillId}/editor`);
}, 1000);
};
return (
<>
<div className="page-back-btn" onClick={onBack}>
<div className="page-back-btn" onClick={() => navigate(`/developer/my-skills/${skillId}/editor`)}>
<span></span>
<span>返回技能详情</span>
</div>
@@ -162,7 +166,7 @@ function UploadVersionPage({ skill, onBack }) {
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
<button className="btn" onClick={onBack}>取消</button>
<button className="btn" onClick={() => navigate(`/developer/my-skills/${skillId}/editor`)}>取消</button>
<button className="btn btn-primary" onClick={handleSubmit}>提交审核</button>
</div>
</div>