From 4f2faa3e8de64414a75fe28d2f9436e91ed905e9 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Mon, 30 Mar 2026 14:11:31 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=A1=B9=E7=9B=AE=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E8=8F=9C=E5=8D=95=E6=94=B9=E9=80=A0=E4=B8=BA=E4=B8=8B?= =?UTF-8?q?=E6=8B=89=E5=AF=BC=E8=88=AA=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 SidebarNavGroup 组件支持可展开导航组 - 路由从 /console/projects 调整为 /console/project/* - 成员管理页面独立为子菜单 - 新增权限配置、技能配置占位页面 - URL 驱动展开状态,刷新保持 - 更新 README.md 和 specs --- README.md | 11 +++- openspec/specs/layout-system/spec.md | 30 ++++++++++ openspec/specs/project-management-nav/spec.md | 53 +++++++++++++++++ src/App.jsx | 13 ++-- src/components/layout/ConsoleLayout.jsx | 31 ++++++++-- src/components/layout/SidebarNavGroup.jsx | 44 ++++++++++++++ src/pages/console/AddMemberPage.jsx | 4 +- src/pages/console/MemberConfigPage.jsx | 2 +- .../{ProjectsPage.jsx => MembersPage.jsx} | 8 +-- src/pages/console/PermissionsPage.jsx | 21 +++++++ src/pages/console/SkillsConfigPage.jsx | 21 +++++++ src/styles/components/nav/_index.scss | 59 +++++++++++++++++++ 12 files changed, 278 insertions(+), 19 deletions(-) create mode 100644 openspec/specs/project-management-nav/spec.md create mode 100644 src/components/layout/SidebarNavGroup.jsx rename src/pages/console/{ProjectsPage.jsx => MembersPage.jsx} (97%) create mode 100644 src/pages/console/PermissionsPage.jsx create mode 100644 src/pages/console/SkillsConfigPage.jsx diff --git a/README.md b/README.md index bbdf1d4..fad282e 100644 --- a/README.md +++ b/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; } /> } /> } /> + } /> + } /> + } /> + } /> + } /> + } /> {/* ...更多子路由 */} @@ -434,4 +441,4 @@ api.logs.filter({ user, type, status }); --- -*最后更新:2026-03-27* +*最后更新:2026-03-30* diff --git a/openspec/specs/layout-system/spec.md b/openspec/specs/layout-system/spec.md index 997af01..cee8fef 100644 --- a/openspec/specs/layout-system/spec.md +++ b/openspec/specs/layout-system/spec.md @@ -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` 组件渲染 diff --git a/openspec/specs/project-management-nav/spec.md b/openspec/specs/project-management-nav/spec.md new file mode 100644 index 0000000..48b7620 --- /dev/null +++ b/openspec/specs/project-management-nav/spec.md @@ -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** "技能配置"子菜单项显示激活状态 diff --git a/src/App.jsx b/src/App.jsx index 1245922..240aa10 100644 --- a/src/App.jsx +++ b/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() { } /> } /> } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> }> diff --git a/src/components/layout/ConsoleLayout.jsx b/src/components/layout/ConsoleLayout.jsx index 9c24058..2a89632 100644 --- a/src/components/layout/ConsoleLayout.jsx +++ b/src/components/layout/ConsoleLayout.jsx @@ -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')} /> - } + } label="项目管理" - active={isPathActive('/console/projects')} - onClick={() => navigate('/console/projects')} - /> + paths={['/console/project']} + > + } + label="成员管理" + active={isPathActive('/console/project/members')} + onClick={() => navigate('/console/project/members')} + /> + } + label="权限配置" + active={isPathActive('/console/project/permissions')} + onClick={() => navigate('/console/project/permissions')} + /> + } + label="技能配置" + active={isPathActive('/console/project/skills')} + onClick={() => navigate('/console/project/skills')} + /> + } 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 ( +
+
+ {icon} + {label} + + {isExpanded ? : } + +
+ {isExpanded && ( +
+ {children} +
+ )} +
+ ); +} + +export default SidebarNavGroup; diff --git a/src/pages/console/AddMemberPage.jsx b/src/pages/console/AddMemberPage.jsx index 70d005c..6292fec 100644 --- a/src/pages/console/AddMemberPage.jsx +++ b/src/pages/console/AddMemberPage.jsx @@ -33,7 +33,7 @@ function AddMemberPage() { return ( <> -
navigate('/console/projects')}> +
navigate('/console/project/members')}> 返回成员列表
@@ -53,7 +53,7 @@ function AddMemberPage() { onClearSelected={() => setSelectedMembers([])} />
- + diff --git a/src/pages/console/MemberConfigPage.jsx b/src/pages/console/MemberConfigPage.jsx index 7eb276f..eaf2205 100644 --- a/src/pages/console/MemberConfigPage.jsx +++ b/src/pages/console/MemberConfigPage.jsx @@ -6,7 +6,7 @@ function MemberConfigPage() { return ( <> -
navigate('/console/projects')}> +
navigate('/console/project/members')}> 返回成员列表
diff --git a/src/pages/console/ProjectsPage.jsx b/src/pages/console/MembersPage.jsx similarity index 97% rename from src/pages/console/ProjectsPage.jsx rename to src/pages/console/MembersPage.jsx index 850a18c..45b65a1 100644 --- a/src/pages/console/ProjectsPage.jsx +++ b/src/pages/console/MembersPage.jsx @@ -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() {
成员列表
- +
{filteredMembers.length > 0 ? ( @@ -112,7 +112,7 @@ function ProjectsPage() { {member.role}
- +
@@ -150,4 +150,4 @@ function ProjectsPage() { ); } -export default ProjectsPage; \ No newline at end of file +export default MembersPage; \ No newline at end of file diff --git a/src/pages/console/PermissionsPage.jsx b/src/pages/console/PermissionsPage.jsx new file mode 100644 index 0000000..411e792 --- /dev/null +++ b/src/pages/console/PermissionsPage.jsx @@ -0,0 +1,21 @@ +import { FiShield } from 'react-icons/fi'; +import EmptyState from '../../components/common/EmptyState.jsx'; + +function PermissionsPage() { + return ( +
+
+
权限配置
+
+
+ } + message="权限配置功能开发中" + description="该功能即将上线,敬请期待" + /> +
+
+ ); +} + +export default PermissionsPage; diff --git a/src/pages/console/SkillsConfigPage.jsx b/src/pages/console/SkillsConfigPage.jsx new file mode 100644 index 0000000..47ee0f8 --- /dev/null +++ b/src/pages/console/SkillsConfigPage.jsx @@ -0,0 +1,21 @@ +import { FiSettings } from 'react-icons/fi'; +import EmptyState from '../../components/common/EmptyState.jsx'; + +function SkillsConfigPage() { + return ( +
+
+
技能配置
+
+
+ } + message="技能配置功能开发中" + description="该功能即将上线,敬请期待" + /> +
+
+ ); +} + +export default SkillsConfigPage; diff --git a/src/styles/components/nav/_index.scss b/src/styles/components/nav/_index.scss index 33ec698..7a5f1b2 100644 --- a/src/styles/components/nav/_index.scss +++ b/src/styles/components/nav/_index.scss @@ -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); + } +}