refactor: 项目管理菜单改造为下拉导航组
- 新增 SidebarNavGroup 组件支持可展开导航组 - 路由从 /console/projects 调整为 /console/project/* - 成员管理页面独立为子菜单 - 新增权限配置、技能配置占位页面 - URL 驱动展开状态,刷新保持 - 更新 README.md 和 specs
This commit is contained in:
13
src/App.jsx
13
src/App.jsx
@@ -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 />}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
44
src/components/layout/SidebarNavGroup.jsx
Normal file
44
src/components/layout/SidebarNavGroup.jsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
21
src/pages/console/PermissionsPage.jsx
Normal file
21
src/pages/console/PermissionsPage.jsx
Normal 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;
|
||||
21
src/pages/console/SkillsConfigPage.jsx
Normal file
21
src/pages/console/SkillsConfigPage.jsx
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user