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:
@@ -2,6 +2,12 @@ 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 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 { 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';
|
||||
@@ -13,21 +19,14 @@ import AddProjectPage from './admin/AddProjectPage.jsx';
|
||||
function AdminPage() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [currentPage, setCurrentPage] = useState(() => {
|
||||
return localStorage.getItem('admin_currentPage') || 'overview';
|
||||
|
||||
// 使用 usePageState 管理页面状态
|
||||
const { currentPage, setCurrentPage } = usePageState({
|
||||
storageKey: ADMIN_KEYS.CURRENT_PAGE,
|
||||
defaultPage: 'overview',
|
||||
pageTitles: ADMIN_PAGES,
|
||||
});
|
||||
|
||||
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':
|
||||
@@ -50,69 +49,59 @@ function AdminPage() {
|
||||
};
|
||||
|
||||
const getPageTitle = () => {
|
||||
const titles = {
|
||||
overview: '总览',
|
||||
departments: '部门管理',
|
||||
users: '用户管理',
|
||||
projects: '项目管理',
|
||||
addDepartment: '新增部门',
|
||||
addUser: '新增用户',
|
||||
addProject: '新增项目'
|
||||
};
|
||||
return titles[currentPage] || '';
|
||||
return ADMIN_PAGES[currentPage]?.title || '';
|
||||
};
|
||||
|
||||
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>
|
||||
<SidebarBrand subtitle="运营管理台" />
|
||||
</div>
|
||||
<nav className="admin-sidebar-nav">
|
||||
<div
|
||||
className={`admin-nav-item ${currentPage === 'overview' ? 'active' : ''}`}
|
||||
<SidebarNavItem
|
||||
icon={<FiHome />}
|
||||
label="总览"
|
||||
active={currentPage === 'overview'}
|
||||
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' : ''}`}
|
||||
itemClassName="admin-nav-item"
|
||||
iconClassName="admin-nav-icon"
|
||||
textClassName="admin-nav-text"
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiBarChart2 />}
|
||||
label="部门管理"
|
||||
active={currentPage === 'departments'}
|
||||
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' : ''}`}
|
||||
itemClassName="admin-nav-item"
|
||||
iconClassName="admin-nav-icon"
|
||||
textClassName="admin-nav-text"
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiUsers />}
|
||||
label="用户管理"
|
||||
active={currentPage === 'users'}
|
||||
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' : ''}`}
|
||||
itemClassName="admin-nav-item"
|
||||
iconClassName="admin-nav-icon"
|
||||
textClassName="admin-nav-text"
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiList />}
|
||||
label="项目管理"
|
||||
active={currentPage === 'projects'}
|
||||
onClick={() => setCurrentPage('projects')}
|
||||
>
|
||||
<span className="admin-nav-icon"><FiList /></span>
|
||||
<span className="admin-nav-text">项目管理</span>
|
||||
</div>
|
||||
itemClassName="admin-nav-item"
|
||||
iconClassName="admin-nav-icon"
|
||||
textClassName="admin-nav-text"
|
||||
/>
|
||||
</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>
|
||||
<SidebarUser
|
||||
onClick={() => {}}
|
||||
wrapperClassName="admin-sidebar-user"
|
||||
infoClassName="admin-sidebar-user-info"
|
||||
nameClassName="admin-sidebar-user-name"
|
||||
roleClassName="admin-sidebar-user-role"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -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')} />
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -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')} />
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user