refactor: 项目管理菜单改造为下拉导航组
- 新增 SidebarNavGroup 组件支持可展开导航组 - 路由从 /console/projects 调整为 /console/project/* - 成员管理页面独立为子菜单 - 新增权限配置、技能配置占位页面 - URL 驱动展开状态,刷新保持 - 更新 README.md 和 specs
This commit is contained in:
11
README.md
11
README.md
@@ -128,7 +128,7 @@ src/
|
||||
| 模块 | 路由 | 功能 |
|
||||
|------|------|------|
|
||||
| 首页 | `/` | 品牌展示、登录入口 |
|
||||
| 工作台 | `/console` | 聊天、技能市场、定时任务、项目管理 |
|
||||
| 工作台 | `/console` | 聊天、技能市场、定时任务、项目管理(成员/权限/技能配置) |
|
||||
| 管理台 | `/admin` | 部门/用户/项目管理、模型配置 |
|
||||
| 开发台 | `/developer` | 技能开发、版本管理 |
|
||||
|
||||
@@ -272,6 +272,7 @@ export default Example;
|
||||
| SearchBar | `components/common/SearchBar.jsx` | 搜索框 |
|
||||
| StatusBadge | `components/common/StatusBadge.jsx` | 状态标签 |
|
||||
| SidebarNavItem | `components/layout/SidebarNavItem.jsx` | 侧边栏导航项 |
|
||||
| SidebarNavGroup | `components/layout/SidebarNavGroup.jsx` | 可展开侧边栏导航组 |
|
||||
|
||||
---
|
||||
|
||||
@@ -295,6 +296,12 @@ export default Example;
|
||||
<Route path="chat/:scene" element={<ChatPage />} />
|
||||
<Route path="skills" element={<SkillsPage />} />
|
||||
<Route path="skills/:skillId" element={<SkillDetailPage />} />
|
||||
<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>
|
||||
|
||||
@@ -434,4 +441,4 @@ api.logs.filter({ user, type, status });
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026-03-27*
|
||||
*最后更新:2026-03-30*
|
||||
|
||||
@@ -182,3 +182,33 @@
|
||||
#### Scenario: 页面样式文件内容结构
|
||||
- **WHEN** 查看页面样式文件
|
||||
- **THEN** 该文件包含页面特定的布局、组件、状态等样式,使用清晰的注释分节
|
||||
|
||||
### Requirement: 可展开导航组组件
|
||||
系统 SHALL 提供可展开的导航组组件 `SidebarNavGroup`,用于组织多个相关导航项。
|
||||
|
||||
#### Scenario: 导航组基本结构
|
||||
- **WHEN** 开发者需要创建可展开的导航组
|
||||
- **THEN** 系统 SHALL 提供 `SidebarNavGroup` 组件
|
||||
- **AND** 组件接受 `icon`、`label`、`children` 属性
|
||||
- **AND** 组件内部使用 `SidebarNavItem` 渲染子菜单项
|
||||
|
||||
#### Scenario: 导航组头部交互
|
||||
- **WHEN** 用户点击导航组头部
|
||||
- **THEN** 系统切换展开/收起状态
|
||||
- **AND** 头部显示展开/收起箭头图标
|
||||
|
||||
#### Scenario: 导航组样式
|
||||
- **WHEN** 导航组渲染时
|
||||
- **THEN** 系统 SHALL 提供以下 BEM 类名:
|
||||
- `.nav-group` 容器类
|
||||
- `.nav-group__header` 头部类
|
||||
- `.nav-group__icon` 图标类
|
||||
- `.nav-group__label` 标签类
|
||||
- `.nav-group__arrow` 箭头类
|
||||
- `.nav-group__children` 子菜单容器类
|
||||
- `.nav-group--expanded` 展开状态修饰符
|
||||
|
||||
#### Scenario: 子菜单项缩进
|
||||
- **WHEN** 导航组展开显示子菜单
|
||||
- **THEN** 子菜单项相对于父级有左侧缩进
|
||||
- **AND** 子菜单项使用 `SidebarNavItem` 组件渲染
|
||||
|
||||
53
openspec/specs/project-management-nav/spec.md
Normal file
53
openspec/specs/project-management-nav/spec.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Capability: 项目管理导航
|
||||
|
||||
## Purpose
|
||||
|
||||
项目管理导航提供工作台侧边栏中项目管理功能的下拉导航组,包含成员管理、权限配置、技能配置等子菜单入口,支持 URL 驱动的展开状态。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 项目管理下拉导航组
|
||||
系统 SHALL 提供可展开的项目管理导航组,包含成员管理、权限配置、技能配置三个子菜单入口。
|
||||
|
||||
#### Scenario: 默认收起状态
|
||||
- **WHEN** 用户访问非项目管理相关页面(如聊天、技能市场)
|
||||
- **THEN** 项目管理导航组显示为收起状态
|
||||
- **AND** 仅显示项目管理的图标和标签
|
||||
|
||||
#### Scenario: URL 驱动自动展开
|
||||
- **WHEN** 用户访问 `/console/project/members`、`/console/project/permissions` 或 `/console/project/skills` 及其子路径
|
||||
- **THEN** 项目管理导航组自动展开
|
||||
- **AND** 显示三个子菜单项:成员管理、权限配置、技能配置
|
||||
- **AND** 当前访问的子菜单项高亮
|
||||
|
||||
#### Scenario: 点击头部展开收起
|
||||
- **WHEN** 用户点击项目管理导航组的头部区域
|
||||
- **THEN** 切换展开/收起状态
|
||||
|
||||
#### Scenario: 子菜单项导航
|
||||
- **WHEN** 用户点击"成员管理"子菜单项
|
||||
- **THEN** 导航至 `/console/project/members`
|
||||
- **WHEN** 用户点击"权限配置"子菜单项
|
||||
- **THEN** 导航至 `/console/project/permissions`
|
||||
- **WHEN** 用户点击"技能配置"子菜单项
|
||||
- **THEN** 导航至 `/console/project/skills`
|
||||
|
||||
#### Scenario: 页面刷新保持展开状态
|
||||
- **WHEN** 用户在项目管理子页面刷新浏览器
|
||||
- **THEN** 项目管理导航组保持展开状态
|
||||
- **AND** 当前子菜单项保持高亮
|
||||
|
||||
### Requirement: 项目管理子菜单高亮
|
||||
系统 SHALL 在导航组中高亮当前访问的子菜单项。
|
||||
|
||||
#### Scenario: 成员管理高亮
|
||||
- **WHEN** 用户访问 `/console/project/members` 或其子路径(如 `/console/project/members/add`)
|
||||
- **THEN** "成员管理"子菜单项显示激活状态
|
||||
|
||||
#### Scenario: 权限配置高亮
|
||||
- **WHEN** 用户访问 `/console/project/permissions`
|
||||
- **THEN** "权限配置"子菜单项显示激活状态
|
||||
|
||||
#### Scenario: 技能配置高亮
|
||||
- **WHEN** 用户访问 `/console/project/skills`
|
||||
- **THEN** "技能配置"子菜单项显示激活状态
|
||||
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