refactor: 代码架构重构 - 提取组件、统一状态管理和数据访问层

- 新增布局组件(SidebarBrand、SidebarUser、SidebarNavItem)
- 新增通用UI组件(EmptyState、StatusBadge、TagInput、SearchBar)
- 新增全局状态管理(UserContext)
- 新增自定义Hooks(usePageState、useNavigation)
- 新增统一数据访问层(src/services/api.js)
- 新增常量配置(constants/pages.js、constants/storageKeys.js)
- 样式文件模块化,拆分页面特定样式
- 更新README文档,添加组件和使用说明
- 同步OpenSpec规范到主specs目录
This commit is contained in:
2026-03-20 10:19:31 +08:00
parent f2e0ec047e
commit 56c08a34ff
27 changed files with 1812 additions and 199 deletions

View File

@@ -3,8 +3,13 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { FiPlus, FiClock, FiList, FiUsers } from 'react-icons/fi';
import { FaPuzzlePiece } from 'react-icons/fa';
import Layout from '../components/Layout.jsx';
import { conversations, getChatScenes } from '../data/conversations.js';
import { skills } from '../data/skills.js';
import SidebarBrand from '../components/layout/SidebarBrand.jsx';
import SidebarUser from '../components/layout/SidebarUser.jsx';
import SidebarNavItem from '../components/layout/SidebarNavItem.jsx';
import usePageState from '../hooks/usePageState.js';
import { CONSOLE_PAGES } from '../constants/pages.js';
import { CONSOLE_KEYS } from '../constants/storageKeys.js';
import api from '../services/api.js';
import ChatPage from './console/ChatPage.jsx';
import SkillsPage from './console/SkillsPage.jsx';
import SkillDetailPage from './console/SkillDetailPage.jsx';
@@ -19,15 +24,22 @@ import AddMemberPage from './console/AddMemberPage.jsx';
function ConsolePage() {
const location = useLocation();
const navigate = useNavigate();
const [currentPage, setCurrentPage] = useState(() => {
return localStorage.getItem('console_currentPage') || 'chat';
// 使用 usePageState 管理 currentPage不使用其返回的 getPageTitle因为需要访问组件局部变量
const { currentPage, setCurrentPage } = usePageState({
storageKey: CONSOLE_KEYS.CURRENT_PAGE,
defaultPage: 'chat',
pageTitles: CONSOLE_PAGES,
});
// 保留额外的状态scene 和 skillId 等需要特殊处理)
const [currentScene, setCurrentScene] = useState(() => {
return localStorage.getItem('console_currentScene') || 'welcome';
return localStorage.getItem(CONSOLE_KEYS.CURRENT_SCENE) || 'welcome';
});
const [currentSkillId, setCurrentSkillId] = useState(null);
const [currentTaskId, setCurrentTaskId] = useState(null);
// 处理主页跳转重置
useEffect(() => {
if (location.state?.fromHome) {
setCurrentPage('chat');
@@ -36,12 +48,9 @@ function ConsolePage() {
}
}, [location.state, navigate, setCurrentPage, setCurrentScene]);
// 同步 currentScene 到 localStorage
useEffect(() => {
localStorage.setItem('console_currentPage', currentPage);
}, [currentPage]);
useEffect(() => {
localStorage.setItem('console_currentScene', currentScene);
localStorage.setItem(CONSOLE_KEYS.CURRENT_SCENE, currentScene);
}, [currentScene]);
const switchPage = (pageId, data = {}) => {
@@ -109,25 +118,13 @@ function ConsolePage() {
};
const getPageTitle = () => {
const pageTitles = {
chat: '智能助手',
skills: '技能市场',
skillDetail: '技能详情',
logs: '日志查询',
scheduledTasks: '定时任务',
taskDetail: '任务详情',
account: '账号管理',
projects: '项目管理',
memberConfig: '成员配置',
addMember: '增加成员'
};
let title = pageTitles[currentPage] || '';
let title = CONSOLE_PAGES[currentPage]?.title || '';
if (currentPage === 'chat') {
const conv = conversations.find(c => c.scene === currentScene);
const conv = api.conversations.list().find(c => c.scene === currentScene);
title = conv?.title || '智能助手';
}
if (currentPage === 'skillDetail' && currentSkillId) {
const skill = skills.find(s => s.id === currentSkillId);
const skill = api.skills.getById(currentSkillId);
title = skill?.name || '技能详情';
}
return title;
@@ -136,16 +133,7 @@ function ConsolePage() {
const sidebar = (
<>
<div className="chat-sidebar-header">
<div className="sidebar-brand">
<div className="sidebar-logo-icon">
<span></span>
<span></span>
</div>
<div className="sidebar-brand-text">
<div className="sidebar-logo">GrandClaw</div>
<div className="sidebar-subtitle">企业级AI平台</div>
</div>
</div>
<SidebarBrand subtitle="企业级AI平台" />
<div className="sidebar-divider"></div>
<button className="btn btn-primary" style={{ width: '100%' }} onClick={createNewChat}>
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}>
@@ -154,7 +142,7 @@ function ConsolePage() {
</button>
</div>
<div className="chat-sidebar-content">
{conversations.map(conv => (
{api.conversations.list().map(conv => (
<div
key={conv.id}
className={`conversation-item ${conv.scene === activeScene ? 'active' : ''}`}
@@ -174,42 +162,32 @@ function ConsolePage() {
</select>
</div>
<div className="chat-sidebar-nav">
<div
className={`chat-nav-item ${currentPage === 'skills' ? 'active' : ''}`}
<SidebarNavItem
icon={<FaPuzzlePiece />}
label="技能市场"
active={currentPage === 'skills'}
onClick={() => switchPage('skills')}
>
<span className="chat-nav-icon"><FaPuzzlePiece /></span>
<span className="chat-nav-text">技能市场</span>
</div>
<div
className={`chat-nav-item ${currentPage === 'scheduledTasks' ? 'active' : ''}`}
/>
<SidebarNavItem
icon={<FiClock />}
label="定时任务"
active={currentPage === 'scheduledTasks'}
onClick={() => switchPage('scheduledTasks')}
>
<span className="chat-nav-icon"><FiClock /></span>
<span className="chat-nav-text">定时任务</span>
</div>
<div
className={`chat-nav-item ${currentPage === 'logs' ? 'active' : ''}`}
/>
<SidebarNavItem
icon={<FiList />}
label="日志查询"
active={currentPage === 'logs'}
onClick={() => switchPage('logs')}
>
<span className="chat-nav-icon"><FiList /></span>
<span className="chat-nav-text">日志查询</span>
</div>
<div
className={`chat-nav-item ${currentPage === 'projects' ? 'active' : ''}`}
/>
<SidebarNavItem
icon={<FiUsers />}
label="项目管理"
active={currentPage === 'projects'}
onClick={() => switchPage('projects')}
>
<span className="chat-nav-icon"><FiUsers /></span>
<span className="chat-nav-text">项目管理</span>
</div>
</div>
<div className="chat-sidebar-user" onClick={() => switchPage('account')}>
<div className="user-avatar"></div>
<div className="chat-sidebar-user-info">
<div className="chat-sidebar-user-name">张三</div>
<div className="chat-sidebar-user-role">AI 产品部</div>
</div>
/>
</div>
<SidebarUser onClick={() => switchPage('account')} />
</>
);