Skip to content

Feat/侧边栏响应式折叠 — 桌面三态切换 + 移动抽屉ponsive collapse - #166

Open
intfoo wants to merge 5 commits into
shy3130:mainfrom
intfoo:feat/sidebar-responsive-collapse
Open

Feat/侧边栏响应式折叠 — 桌面三态切换 + 移动抽屉ponsive collapse#166
intfoo wants to merge 5 commits into
shy3130:mainfrom
intfoo:feat/sidebar-responsive-collapse

Conversation

@intfoo

@intfoo intfoo commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

PR 描述

# feat: 侧边栏响应式折叠 — 桌面三态切换 + 移动抽屉

## 问题与动机

当前侧边栏使用固定 `grid-cols-[14rem_1fr]` 布局,宽度恒为 14rem(224px),无任何响应式断点。

- **手机端**(375-414px 视口)侧边栏占去 50%+ 宽度,主区域几乎不可用,无法在手机上正常浏览
- **桌面端**无法主动控制侧边栏占位,14rem 在小窗口笔记本上也偏宽

## 解决方案

### 桌面端(≥768px)三态切换

| 状态 | 宽度 | 内容 |
|------|------|------|
| 展开 expanded | 14rem (224px) | 完整侧边栏(Logo + 档位卡 + AI 卡 + 导航 + 数据源条 + 实时行情开关 + 指数行情 + 设置) |
| 图标条 collapsed | 3.5rem (56px) | 仅 icon 导航 + CSS tooltip + 指数轮播 + RadioTower 实时行情按钮 |
| 隐藏 hidden | 0 | 主区域全屏,左边缘悬浮按钮可 hover/click 展开 |

- 单按钮循环切换:展开 → 图标条 → 隐藏 → 展开
- 状态持久化到 localStorage(`sidebar_collapse_state`- 隐藏态悬浮按钮:hover 1s 自动展开(overlay 模式不挤压主区域)、点击固定展开(push 模式挤压主区域)
- 图标条态:导航仅 icon + fixed 定位 tooltip(绕开 aside overflow-hidden)、指数行情 3 秒轮播 + hover 暂停

### 移动端(<768px)抽屉模式

- 默认隐藏侧边栏,左上角 FAB 汉堡按钮(`bg-surface/70` + `backdrop-blur`- 点击呼出抽屉(80vw / max 320px),遮罩 + ESC + 选导航关闭,body 滚动锁
- 抽屉显示完整侧边栏内容

### 架构改造`Layout.tsx`(698 行)拆分到 `components/sidebar/` 下 9 个文件,Layout 瘦身为 ~80 行壳:

frontend/src/
├── lib/
│   ├── useMediaQuery.ts          # 响应式断点 hook
│   └── useSidebarState.ts        # 三态管理 + localStorage
├── components/
│   ├── Layout.tsx                # 瘦身壳:useIsDesktop + SSE/监控徽标/全局浮层
│   └── sidebar/
│       ├── shared.ts             # 共享常量/工具/useDataSyncStatus/useVisibleNavItems/useRealtimeToggle
│       ├── components.tsx        # TierBadge/AIConfigBadge/MonitorBadge/SidebarIndexQuotes/ThemeToggle
│       ├── IndexQuoteCarousel.tsx # 图标条态指数轮播
│       ├── SidebarContent.tsx    # 完整侧边栏内容(展开态+移动抽屉复用)
│       ├── IconRailContent.tsx   # 图标条态精简内容
│       ├── DesktopSidebar.tsx    # 桌面三态容器 + 切换按钮 + 内容延迟挂载
│       └── MobileDrawer.tsx      # 移动抽屉 + FAB

### 数据归属

- Layout 保留:`useQuoteStream`(SSE 全局订阅)、`useQuoteStreamStatus`(断线提示)、`usePreferences`(SSE 参数)、`alertsTotalQuery`(监控徽标全局轮询)
- SidebarContent/IconRailContent 下沉:`useCapabilities`/`useSettings`/`usePreferences`/`useQuoteStatus`/`useVersion`/`useQuery(QK.dataSources)`/`useQuery(QK.analysisMenus)`/`useQuery(QK.pipelineJobs)`/`useQuery(QK.indexQuotes)`/`useToggleRealtimeQuotes`
- 共享 hook 抽取:`useDataSyncStatus`(数据同步瞬时反馈)、`useVisibleNavItems`(导航合并/排序/隐藏)、`useRealtimeToggle`(实时行情开关逻辑)

## 修改范围

| 文件 | 改动 |
|------|------|
| `frontend/src/components/Layout.tsx` | 698 行 → ~80 行,侧边栏内容下沉 |
| `frontend/src/components/sidebar/*.tsx` (9 文件) | 新建 |
| `frontend/src/lib/useMediaQuery.ts` | 新建 |
| `frontend/src/lib/useSidebarState.ts` | 新建 |
| `.gitignore` | 忽略 `docs/superpowers/``.codebuddy.md``package-lock.json` |

**统计**:11 files changed, 1353 insertions(+), 640 deletions(-)

## 兼容性

- **原 Layout 所有功能保留**:SSE 断线提示、数据同步完成反馈(绿色对勾 3 秒)、监控徽标、实时行情开关、档位卡、AI 配置卡、指数行情、导航排序/隐藏、主题切换、版本号
- **实时行情开关逻辑变更**:移除前端档位校验(`qc.fetchQuery` + `tierRank < 0` return),改为直接调 API 让后端校验,与设置页 `Monitoring.tsx``handleToggleQuote` 逻辑一致。None 档点击开关时后端返回 `realtime_allowed: false`,UI 自动更新为关闭态——有响应而非无反应
- **react-query 同 key 去重**:多个组件调用相同 queryKey 不会重复请求
- **向后兼容**:localStorage 无值时默认 `expanded`,刷新恢复上次状态

## 性能

- **hover 展开 overlay 模式**:aside 用 `absolute` 浮在主区域上方,不挤压 main 布局,主区域内容不抖动
- **内容延迟挂载**:state 变化时先卸载内容(`contentVisible=null`),收起方向等宽度动画完成(300ms)再挂载新内容,展开方向延迟 150ms——避免宽度过渡期间内容布局变形
- **首次渲染优化**`isFirstRender` 跳过首次 effect,避免页面加载时内容闪烁
- **不进入实时热路径**:侧边栏组件不参与列表渲染、不发起额外数据请求(共享 Layout 已有的 query 缓存)

## 验证结果

| 验证项 | 命令/方式 | 结果 |
|--------|-----------|------|
| TypeScript 类型检查 | `npx tsc --noEmit` | 通过,零错误 |
| 前端构建 | `pnpm build` | 通过,exitCode 0 |
| Lints | IDE 诊断 | 零错误 |
| 功能完整性 | 对照原 Layout 逐项检查 | 13 项功能全部保留 |
| 数据契约一致性 | `useRealtimeToggle` vs `Monitoring.tsx handleToggleQuote` | 逻辑一致 |
| Query 去重 | queryKey 检查 | 同 key 自动去重 |
| Timer 清理 | `contentTimer`/`hoverTimer`/`setInterval` cleanup | 正确清理 |
| SSR 安全 | `useMediaQuery` `typeof window` 检查 | 通过 |
| localStorage 容错 | `useSidebarState` try-catch | 通过 |
| overlay/push 模式 | `expanded !pinned` + `hidden` = absolute;`pinned expanded` + `collapsed` = relative | 无布局跳变 |

## 界面证据

建议测试矩阵(需在浏览器手动验证):

| 场景 | 预期 |
|------|------|
| 桌面展开态(默认) | 跟改造前一致,14rem,所有内容显示 |
| 桌面点切换按钮 → 图标条 | 宽度变 3.5rem,仅 icon,tooltip hover 显示,轮播启动 |
| 桌面点切换按钮 → 隐藏 | aside 宽度 0,主区域左边缘出现蓝色悬浮按钮 |
| 桌面隐藏态 hover 悬浮按钮 | 1s 后展开(overlay,主区域不抖动),鼠标移出自动隐藏 |
| 桌面隐藏态点击悬浮按钮 | 立即展开(push,主区域让位),不自动隐藏 |
| 桌面刷新页面 | 恢复 localStorage 存的态 |
| 移动端(<768px)默认 | 侧边栏不可见,左上角蓝色 FAB 按钮 |
| 移动端点 FAB | 抽屉滑入,遮罩出现 |
| 移动端点遮罩/导航项 | 抽屉关闭 |
| 移动端按 ESC | 抽屉关闭 |
| 断点跨越(缩放窗口) | 桌面三态 ↔ 移动抽屉切换 |

## 风险与回滚

- **hover 展开的交互边界**:hover 1s 展开后,鼠标移动路径可能触发 `handleAsideLeave`。需手动测试 overlay 展开态下点击导航链接的交互
- **None 档实时行情开关**:移除前端校验后依赖后端返回 `realtime_allowed: false`。如后端 API 异常,UI 可能短暂显示开启态
- **移动端 FAB 遮挡**:FAB `fixed left-2 top-2` 可能遮挡页面左上角内容,需在各页面检查
- **回滚方式**`git revert` 4 个 commit 即可完全回退,无数据迁移、无配置变更

## 测试

项目无自动化测试框架(无 jest/vitest 配置),验证依赖 `tsc --noEmit` + `pnpm build` + 手动测试矩阵。

## 不做的事(YAGNI)

- 不做拖拽调宽度(本次只三态固定宽度)
- 不做移动端三态(移动端只抽屉二态:隐藏/打开)
- 不做后端 prefs 持久化(localStorage 足够,三态是设备本地偏好)
- 不做 a11y 焦点陷阱(纯 CSS tooltip + 简单抽屉)
- 不重构现有 TierBadge/AIConfigBadge/SidebarIndexQuotes 内部逻辑(只搬位置)
PixPin_2026-08-03_19-36-46 PixPin_2026-08-03_19-36-58

intfoo added 5 commits August 3, 2026 17:16
- 桌面端(≥768px)三态:展开(14rem)↔图标条(3.5rem)↔隐藏(0)
  单按钮循环切换,localStorage 持久化
  图标条态:导航仅 icon + CSS tooltip,指数轮播,RadioTower 实时行情按钮
- 移动端(<768px)抽屉模式:FAB 汉堡按钮,80vw/max 320px
  点遮罩/选导航/ESC 关闭,body 滚动锁
- Layout.tsx 从 698 行瘦身为 80 行壳
  侧边栏内容下沉到 components/sidebar/ 8 个文件
  SSE 订阅/断线提示/监控徽标轮询保留在 Layout
- 共享逻辑抽取:useVisibleNavItems/useRealtimeToggle/useDataSyncStatus
- .gitignore 忽略 docs/superpowers/ 和 .codebuddy.md
- tooltip 改用 fixed 定位,绕开 aside overflow-hidden 限制
- nav 加 overflow-x-hidden 消除底部横向滚动条
- 指数轮播名称改为前2字简写,加 overflow-hidden 防溢出
None 档时前端 tierRank<0 直接 return 不调 API,导致开关无反应。
设置页直接调 API 让后端校验(返回 realtime_allowed: false),有响应。
现统一为直接调 toggleQuote.mutateAsync,后端校验档位。
P2: 首次渲染内容闪烁 — isFirstRender 跳过首次 effect
P3: hoverTimer 卸载未清理 — 加独立 cleanup effect
P3: active 态图标颜色不一致 — text-foreground 改 text-accent
P3: useRealtimeToggle _prefs 未使用参数 — 移除
P3: package-lock.json npm 产物 — 删除并加 .gitignore
P3: widthMap 硬编码 — 加注释说明对应 WIDTH_CLASS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant