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,7 +3,13 @@ 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 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 { DEVELOPER_PAGES } from '../constants/pages.js';
import { DEVELOPER_KEYS } from '../constants/storageKeys.js';
import api from '../services/api.js';
import MySkillsPage from './developer/MySkillsPage.jsx';
import UploadSkillPage from './developer/UploadSkillPage.jsx';
import NewVersionPage from './developer/NewVersionPage.jsx';
@@ -14,11 +20,17 @@ import SkillEditorPage from './developer/SkillEditorPage.jsx';
function DeveloperPage() {
const location = useLocation();
const navigate = useNavigate();
const [currentPage, setCurrentPage] = useState(() => {
return localStorage.getItem('developer_currentPage') || 'mySkills';
// 使用 usePageState 管理页面状态
const { currentPage, setCurrentPage } = usePageState({
storageKey: DEVELOPER_KEYS.CURRENT_PAGE,
defaultPage: 'mySkills',
pageTitles: DEVELOPER_PAGES,
});
// 保留额外的状态currentSkillId 需要持久化到 localStorage
const [currentSkillId, setCurrentSkillId] = useState(() => {
const saved = localStorage.getItem('developer_currentSkillId');
const saved = localStorage.getItem(DEVELOPER_KEYS.CURRENT_SKILL_ID);
return saved ? JSON.parse(saved) : null;
});
const [newVersionSkillName, setNewVersionSkillName] = useState('');
@@ -32,12 +44,8 @@ function DeveloperPage() {
}, [location.state, navigate, setCurrentPage, setCurrentSkillId]);
useEffect(() => {
localStorage.setItem('developer_currentPage', currentPage);
}, [currentPage]);
useEffect(() => {
localStorage.setItem('developer_currentSkillId', JSON.stringify(currentSkillId));
}, [currentSkillId]);
localStorage.setItem(DEVELOPER_KEYS.CURRENT_SKILL_ID, JSON.stringify(currentSkillId));
}, [DEVELOPER_KEYS.CURRENT_SKILL_ID, currentSkillId]);
const switchPage = (pageId, data = {}) => {
setCurrentPage(pageId);
@@ -90,30 +98,13 @@ function DeveloperPage() {
};
const getPageTitle = () => {
const titles = {
mySkills: '我的技能',
uploadSkill: '创建技能',
newVersion: '上传新版本',
devDocs: '开发文档',
devAccount: '开发者设置',
skillEditor: '技能详情'
};
return titles[currentPage] || '';
return DEVELOPER_PAGES[currentPage]?.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">技能开发台</div>
</div>
</div>
<SidebarBrand subtitle="技能开发台" />
<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' }}>
@@ -122,7 +113,7 @@ function DeveloperPage() {
</button>
</div>
<div className="chat-sidebar-content">
{mySkills.map(skill => (
{api.developer.getMySkills().map(skill => (
<div
key={skill.id}
className={`conversation-item ${currentSkillId === skill.id && currentPage === 'skillEditor' ? 'active' : ''}`}
@@ -134,28 +125,20 @@ function DeveloperPage() {
))}
</div>
<div className="chat-sidebar-nav">
<div
className={`chat-nav-item ${currentPage === 'mySkills' ? 'active' : ''}`}
<SidebarNavItem
icon={<FaPuzzlePiece />}
label="我的技能"
active={currentPage === 'mySkills'}
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' : ''}`}
/>
<SidebarNavItem
icon={<FiTerminal />}
label="开发文档"
active={currentPage === 'devDocs'}
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>
<SidebarUser onClick={() => switchPage('devAccount')} />
</>
);