# 首页移动端适配实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 改造首页在 `<768px` 视口下的布局与交互:左侧栏改为抽屉触发,header 三行布局,瀑布流列数与现有阈值一致,桌面端 100% 回归。 **Architecture:** 新增 `bolt-ui/components/Drawer` 通用抽屉组件(基于现有 `BoMask`),新增 `bolt-ui/hooks/useMediaQuery` 响应式 composable;`IndexLeftSidebar` 增加 `mode: 'rail' | 'drawer'` 切换;首页用 `useMediaQuery` 驱动 mode 与 drawer 状态。 **Tech Stack:** Vue 3.5, Nuxt 4.4, bolt-ui (SCSS + useNamespace pattern), @nuxt/icon, vue3-toastify **Conventions to follow:** - bolt-ui 组件:`components//index.ts` + `src/.vue` + `style/{index.ts,css.ts}` + `theme-chalk/src/.scss` - 命名空间:`useNamespace('drawer')` → `bo-drawer`, `bo-drawer__x`, `bo-drawer--x` - 注册:`withInstall(_Drawer)` → `` 全局可用(由 `bolt-ui/nuxt` 模块自动注册) - Composable:`hooks//index.ts` → 自动导入为 `useXxx` - 锁定版本:禁止升级依赖;如需新增,手动 `bun add` --- ## 文件结构 ``` packages/bolt-ui/ ├── components/ │ ├── Drawer/ [NEW] │ │ ├── index.ts │ │ ├── src/ │ │ │ └── Drawer.vue │ │ └── style/ │ │ ├── index.ts │ │ └── css.ts │ └── index.ts [MODIFY] 添加 export * from './Drawer' ├── hooks/ │ └── useMediaQuery/ [NEW] │ └── index.ts ├── theme-chalk/src/ │ ├── components/drawer/ [NEW] │ │ ├── _index.scss (主入口 @use 子文件) │ │ ├── _var.scss │ │ ├── panel.scss │ │ ├── panel-left.scss │ │ ├── panel-right.scss │ │ ├── mask.scss │ │ └── anim/ │ │ ├── _var.scss │ │ ├── panel-left-fade.scss │ │ ├── panel-right-fade.scss │ │ └── mask-fade.scss │ └── drawer.scss [NEW] 主入口(被 bolt-ui/nuxt 自动注入) └── README.md [MODIFY] 追加 Drawer 章节 app/ ├── components/ │ └── index/LeftSidebar.vue [MODIFY] 新增 mode/drawerOpen 等 props └── pages/index/index.vue [MODIFY] drawerOpen 状态、header 三行、断点重写 ``` --- ## Task 1: useMediaQuery composable **Files:** - Create: `packages/bolt-ui/hooks/useMediaQuery/index.ts` - [ ] **Step 1: 写 composable** ```ts import { onBeforeUnmount, onMounted, ref, type Ref } from 'vue' /** * 响应式地跟踪 CSS 媒体查询匹配状态。 * * SSR 安全:服务端渲染期间始终返回 `false`,避免 hydration mismatch; * 客户端 `onMounted` 之后才订阅 `matchMedia`。 * * @param query - 合法 CSS media query 字符串 * @returns Ref - 当前是否匹配 */ export function useMediaQuery(query: string): Ref { const matches = ref(false) let mql: MediaQueryList | null = null const handler = (e: MediaQueryListEvent) => { matches.value = e.matches } onMounted(() => { if (typeof window === 'undefined' || !window.matchMedia) return mql = window.matchMedia(query) matches.value = mql.matches mql.addEventListener('change', handler) }) onBeforeUnmount(() => { mql?.removeEventListener('change', handler) }) return matches } ``` - [ ] **Step 2: 提交** ```bash git add packages/bolt-ui/hooks/useMediaQuery/index.ts git -c commit.gpgsign=false commit -m "feat(bolt-ui): add useMediaQuery composable" ``` > 备注:bolt-ui/nuxt 模块会自动扫描 `hooks//index.ts` 并把 `useXxx` 导出注册为 auto-import,所以无需修改 `nuxt.ts`。 --- ## Task 2: BoDrawer 组件脚手架(props/emits/template/状态) **Files:** - Create: `packages/bolt-ui/components/Drawer/src/Drawer.vue` - Create: `packages/bolt-ui/components/Drawer/index.ts` - [ ] **Step 1: 写 Drawer.vue 模板 + props + emits + 基础状态** `packages/bolt-ui/components/Drawer/src/Drawer.vue`: ```vue ``` - [ ] **Step 2: 写 index.ts** `packages/bolt-ui/components/Drawer/index.ts`: ```ts import _Drawer from './src/Drawer.vue' import { withInstall } from 'bolt-ui/utils/vue/install' export type BoDrawer = InstanceType export const BoDrawer = withInstall(_Drawer) export default BoDrawer ``` - [ ] **Step 3: 在 components/index.ts 注册导出** 编辑 `packages/bolt-ui/components/index.ts`,在末尾追加: ```ts export * from './Drawer' ``` - [ ] **Step 4: 提交** ```bash git add packages/bolt-ui/components/Drawer packages/bolt-ui/components/index.ts git -c commit.gpgsign=false commit -m "feat(bolt-ui): add BoDrawer component scaffold" ``` --- ## Task 3: BoDrawer 主题样式(panel + mask + 动画) **Files:** - Create: `packages/bolt-ui/theme-chalk/src/components/drawer/_var.scss` - Create: `packages/bolt-ui/theme-chalk/src/components/drawer/panel.scss` - Create: `packages/bolt-ui/theme-chalk/src/components/drawer/panel-left.scss` - Create: `packages/bolt-ui/theme-chalk/src/components/drawer/panel-right.scss` - Create: `packages/bolt-ui/theme-chalk/src/components/drawer/mask.scss` - Create: `packages/bolt-ui/theme-chalk/src/components/drawer/anim/_var.scss` - Create: `packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-left-fade.scss` - Create: `packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-right-fade.scss` - Create: `packages/bolt-ui/theme-chalk/src/components/drawer/anim/mask-fade.scss` - Create: `packages/bolt-ui/theme-chalk/src/components/drawer/_index.scss` - Create: `packages/bolt-ui/theme-chalk/src/drawer.scss` - Create: `packages/bolt-ui/components/Drawer/style/index.ts` - Create: `packages/bolt-ui/components/Drawer/style/css.ts` - [ ] **Step 1: 写 SCSS 变量与动画基础** `packages/bolt-ui/theme-chalk/src/components/drawer/anim/_var.scss`: ```scss $drawer-time: 0.28s !default; ``` `packages/bolt-ui/theme-chalk/src/components/drawer/anim/mask-fade.scss`: ```scss @use "_var" as *; .bo-drawer-mask-fade-enter-active, .bo-drawer-mask-fade-leave-active { transition: opacity $drawer-time ease; } .bo-drawer-mask-fade-enter-from, .bo-drawer-mask-fade-leave-to { opacity: 0; } ``` `packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-left-fade.scss`: ```scss @use "_var" as *; .bo-drawer-panel-left-fade-enter-active, .bo-drawer-panel-left-fade-leave-active { transition: transform $drawer-time cubic-bezier(0.4, 0, 0.2, 1); } .bo-drawer-panel-left-fade-enter-from, .bo-drawer-panel-left-fade-leave-to { transform: translateX(-100%); } ``` `packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-right-fade.scss`: ```scss @use "_var" as *; .bo-drawer-panel-right-fade-enter-active, .bo-drawer-panel-right-fade-leave-active { transition: transform $drawer-time cubic-bezier(0.4, 0, 0.2, 1); } .bo-drawer-panel-right-fade-enter-from, .bo-drawer-panel-right-fade-leave-to { transform: translateX(100%); } ``` - [ ] **Step 2: 写 panel / mask 样式** `packages/bolt-ui/theme-chalk/src/components/drawer/_var.scss`: ```scss $bo-drawer-bg: var(--color-canvas) !default; $bo-drawer-shadow: 0 8px 32px rgba(20, 20, 19, 0.12) !default; $bo-drawer-mask-bg: rgba(20, 20, 19, 0.32) !default; ``` `packages/bolt-ui/theme-chalk/src/components/drawer/panel.scss`: ```scss @use "./_var" as *; .bo-drawer { position: fixed; top: 0; bottom: 0; background: $bo-drawer-bg; box-shadow: $bo-drawer-shadow; display: flex; flex-direction: column; overflow: hidden; outline: none; -webkit-overflow-scrolling: touch; } ``` `packages/bolt-ui/theme-chalk/src/components/drawer/panel-left.scss`: ```scss .bo-drawer--left { left: 0; } ``` `packages/bolt-ui/theme-chalk/src/components/drawer/panel-right.scss`: ```scss .bo-drawer--right { right: 0; } ``` `packages/bolt-ui/theme-chalk/src/components/drawer/mask.scss`: ```scss @use "./_var" as *; .bo-drawer__mask { position: fixed; inset: 0; background: $bo-drawer-mask-bg; z-index: 199; // 比 panel 低 1 backdrop-filter: blur(2px); } ``` - [ ] **Step 3: 写组件目录入口** `packages/bolt-ui/theme-chalk/src/components/drawer/_index.scss`: ```scss @use "./anim/mask-fade.scss"; @use "./anim/panel-left-fade.scss"; @use "./anim/panel-right-fade.scss"; @use "./panel.scss"; @use "./panel-left.scss"; @use "./panel-right.scss"; @use "./mask.scss"; ``` - [ ] **Step 4: 写主题根入口(被 bolt-ui/nuxt 自动注入)** `packages/bolt-ui/theme-chalk/src/drawer.scss`: ```scss @use "components/drawer/_index.scss"; ``` - [ ] **Step 5: 写 css bridge 文件** `packages/bolt-ui/components/Drawer/style/index.ts`: ```ts import 'bolt-ui/theme-chalk/src/drawer.scss' ``` `packages/bolt-ui/components/Drawer/style/css.ts`: ```ts import 'bolt-ui/theme-chalk/src/drawer.scss' ``` - [ ] **Step 6: 提交** ```bash git add packages/bolt-ui/theme-chalk/src/components/drawer packages/bolt-ui/theme-chalk/src/drawer.scss packages/bolt-ui/components/Drawer/style git -c commit.gpgsign=false commit -m "feat(bolt-ui): add Drawer theme styles and css bridge" ``` > 备注:bolt-ui/nuxt 模块会扫描 `theme-chalk/src/drawer.scss` 并按需注入到使用 `BoDrawer` 的 .vue/.ts/.tsx 文件。无需手动 import。 --- ## Task 4: LeftSidebar 增加 mode 与 drawer 模式分支 **Files:** - Modify: `app/components/index/LeftSidebar.vue`(props、emits、template 顶部) - [ ] **Step 1: 扩展 props 类型** 编辑 `app/components/index/LeftSidebar.vue` 的 `