refactor: 项目管理菜单改造为下拉导航组

- 新增 SidebarNavGroup 组件支持可展开导航组
- 路由从 /console/projects 调整为 /console/project/*
- 成员管理页面独立为子菜单
- 新增权限配置、技能配置占位页面
- URL 驱动展开状态,刷新保持
- 更新 README.md 和 specs
This commit is contained in:
2026-03-30 14:11:31 +08:00
parent ea81a714bb
commit 4f2faa3e8d
12 changed files with 278 additions and 19 deletions

View File

@@ -16,9 +16,11 @@ 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 MembersPage from './pages/console/MembersPage.jsx';
import MemberConfigPage from './pages/console/MemberConfigPage.jsx';
import AddMemberPage from './pages/console/AddMemberPage.jsx';
import PermissionsPage from './pages/console/PermissionsPage.jsx';
import SkillsConfigPage from './pages/console/SkillsConfigPage.jsx';
import ConsoleReviewListPage from './pages/console/ConsoleReviewListPage.jsx';
import ConsoleReviewDetailPage from './pages/console/ConsoleReviewDetailPage.jsx';
@@ -63,9 +65,12 @@ function App() {
<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 path="project" element={<Navigate to="members" replace />} />
<Route path="project/members" element={<MembersPage />} />
<Route path="project/members/add" element={<AddMemberPage />} />
<Route path="project/members/:memberId/config" element={<MemberConfigPage />} />
<Route path="project/permissions" element={<PermissionsPage />} />
<Route path="project/skills" element={<SkillsConfigPage />} />
</Route>
<Route path="/admin" element={<AdminLayout />}>

View File

@@ -1,10 +1,11 @@
import { useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { FiPlus, FiClock, FiList, FiUsers, FiBox } from 'react-icons/fi';
import { FiPlus, FiClock, FiList, FiUsers, FiBox, FiFolder, FiShield, FiSettings } from 'react-icons/fi';
import { FiTrash2 } from 'react-icons/fi';
import { FaPuzzlePiece } from 'react-icons/fa';
import Layout from '../Layout.jsx';
import SidebarNavItem from './SidebarNavItem.jsx';
import SidebarNavGroup from './SidebarNavGroup.jsx';
import Modal from '../common/Modal.jsx';
import api from '../../services/api.js';
@@ -101,12 +102,30 @@ function ConsoleLayout() {
active={isPathActive('/console/logs')}
onClick={() => navigate('/console/logs')}
/>
<SidebarNavItem
icon={<FiUsers />}
<SidebarNavGroup
icon={<FiFolder />}
label="项目管理"
active={isPathActive('/console/projects')}
onClick={() => navigate('/console/projects')}
/>
paths={['/console/project']}
>
<SidebarNavItem
icon={<FiUsers />}
label="成员管理"
active={isPathActive('/console/project/members')}
onClick={() => navigate('/console/project/members')}
/>
<SidebarNavItem
icon={<FiShield />}
label="权限配置"
active={isPathActive('/console/project/permissions')}
onClick={() => navigate('/console/project/permissions')}
/>
<SidebarNavItem
icon={<FiSettings />}
label="技能配置"
active={isPathActive('/console/project/skills')}
onClick={() => navigate('/console/project/skills')}
/>
</SidebarNavGroup>
</div>
<Modal
visible={!!deleteTarget}

View File

@@ -0,0 +1,44 @@
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { FiChevronDown, FiChevronRight } from 'react-icons/fi';
/**
* SidebarNavGroup - 可展开的侧边栏导航组组件
* 用于组织多个相关导航项
*
* @param {Object} props - 组件属性
* @param {React.ReactNode} props.icon - 主图标组件
* @param {string} props.label - 导航组标签文本
* @param {Array<string>} props.paths - 用于判断展开状态的路径前缀数组
* @param {React.ReactNode} props.children - 子菜单项SidebarNavItem 组件)
*/
function SidebarNavGroup({ icon, label, paths = [], children }) {
const location = useLocation();
const [manualExpanded, setManualExpanded] = useState(null);
const isUrlActive = paths.some(path => location.pathname.startsWith(path));
const isExpanded = manualExpanded !== null ? manualExpanded : isUrlActive;
const handleHeaderClick = () => {
setManualExpanded(!isExpanded);
};
return (
<div className={`nav-group ${isExpanded ? 'nav-group--expanded' : ''}`}>
<div className="nav-group__header" onClick={handleHeaderClick}>
<span className="nav-group__icon">{icon}</span>
<span className="nav-group__label">{label}</span>
<span className="nav-group__arrow">
{isExpanded ? <FiChevronDown /> : <FiChevronRight />}
</span>
</div>
{isExpanded && (
<div className="nav-group__children">
{children}
</div>
)}
</div>
);
}
export default SidebarNavGroup;

View File

@@ -33,7 +33,7 @@ function AddMemberPage() {
return (
<>
<div className="page-back-btn" onClick={() => navigate('/console/projects')}>
<div className="page-back-btn" onClick={() => navigate('/console/project/members')}>
<span></span>
<span>返回成员列表</span>
</div>
@@ -53,7 +53,7 @@ function AddMemberPage() {
onClearSelected={() => setSelectedMembers([])}
/>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', marginTop: '16px' }}>
<button className="btn btn-secondary" onClick={() => navigate('/console/projects')}>取消</button>
<button className="btn btn-secondary" onClick={() => navigate('/console/project/members')}>取消</button>
<button className="btn btn-primary" onClick={handleAdd} disabled={selectedMembers.length === 0}>
添加选中成员 ({selectedMembers.length})
</button>

View File

@@ -6,7 +6,7 @@ function MemberConfigPage() {
return (
<>
<div className="page-back-btn" onClick={() => navigate('/console/projects')}>
<div className="page-back-btn" onClick={() => navigate('/console/project/members')}>
<span></span>
<span>返回成员列表</span>
</div>

View File

@@ -5,7 +5,7 @@ import { projectMembers } from '../../data/members.js';
import EmptyState from '../../components/common/EmptyState.jsx';
import Modal from '../../components/common/Modal.jsx';
function ProjectsPage() {
function MembersPage() {
const navigate = useNavigate();
const [members, setMembers] = useState(projectMembers);
const [removeTarget, setRemoveTarget] = useState(null);
@@ -84,7 +84,7 @@ function ProjectsPage() {
<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={() => navigate('/console/projects/members/add')}>增加成员</button>
<button className="btn btn-primary btn-sm" onClick={() => navigate('/console/project/members/add')}>增加成员</button>
</div>
<div className="card-body">
{filteredMembers.length > 0 ? (
@@ -112,7 +112,7 @@ function ProjectsPage() {
<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" onClick={() => navigate(`/console/projects/members/${member.id}/config`)}>配置</button>
<button className="text-btn text-btn-primary" onClick={() => navigate(`/console/project/members/${member.id}/config`)}>配置</button>
<button className="text-btn text-btn-danger" onClick={() => handleRemoveClick(member)}>移除</button>
</div>
</td>
@@ -150,4 +150,4 @@ function ProjectsPage() {
);
}
export default ProjectsPage;
export default MembersPage;

View File

@@ -0,0 +1,21 @@
import { FiShield } from 'react-icons/fi';
import EmptyState from '../../components/common/EmptyState.jsx';
function PermissionsPage() {
return (
<div className="card">
<div className="card-header">
<div className="card-title">权限配置</div>
</div>
<div className="card-body">
<EmptyState
icon={<FiShield size={48} />}
message="权限配置功能开发中"
description="该功能即将上线,敬请期待"
/>
</div>
</div>
);
}
export default PermissionsPage;

View File

@@ -0,0 +1,21 @@
import { FiSettings } from 'react-icons/fi';
import EmptyState from '../../components/common/EmptyState.jsx';
function SkillsConfigPage() {
return (
<div className="card">
<div className="card-header">
<div className="card-title">技能配置</div>
</div>
<div className="card-body">
<EmptyState
icon={<FiSettings size={48} />}
message="技能配置功能开发中"
description="该功能即将上线,敬请期待"
/>
</div>
</div>
);
}
export default SkillsConfigPage;

View File

@@ -46,3 +46,62 @@
font-size: $font-size-sm;
color: var(--color-text-3);
}
// 可展开导航组
.nav-group {
margin-bottom: 4px;
}
.nav-group__header {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: var(--radius-md);
cursor: pointer;
transition: background 0.2s;
color: var(--color-text-2);
font-size: $font-size-base;
font-weight: $font-weight-medium;
&:hover {
background: var(--color-bg-2);
color: var(--color-text-1);
}
}
.nav-group__icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
font-size: $font-size-lg;
flex-shrink: 0;
}
.nav-group__label {
flex: 1;
}
.nav-group__arrow {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
font-size: $font-size-sm;
color: var(--color-text-3);
transition: transform 0.2s;
}
.nav-group__children {
margin-top: 4px;
padding-left: 12px;
}
.nav-group--expanded {
.nav-group__header {
color: var(--color-text-1);
}
}