From 9aaa8f6656123e5f40cd137db2dcf0f535920d56 Mon Sep 17 00:00:00 2001 From: zcode Date: Mon, 15 Jun 2026 15:58:48 +0800 Subject: [PATCH] docs: add mobile phase 1 infrastructure implementation plan --- ...2026-06-15-mobile-phase1-infrastructure.md | 846 ++++++++++++++++++ 1 file changed, 846 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-15-mobile-phase1-infrastructure.md diff --git a/docs/superpowers/plans/2026-06-15-mobile-phase1-infrastructure.md b/docs/superpowers/plans/2026-06-15-mobile-phase1-infrastructure.md new file mode 100644 index 0000000..30cf3bb --- /dev/null +++ b/docs/superpowers/plans/2026-06-15-mobile-phase1-infrastructure.md @@ -0,0 +1,846 @@ +# 手机端阶段 1:基础设施 实施计划 + +> **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:** 搭建手机端基础设施——引入 Vant 组件库、设备检测、路由分流、MobileLayout(底部 Tab + 顶部栏)、主题适配,使手机浏览器访问能自动进入手机版框架页。 + +**Architecture:** 在现有 Vue 3 项目内新增手机端视图层,路由层根据设备检测结果动态 import 桌面或手机视图。Vant 全量引入(与现有 ElementPlus 全量引入风格一致),主题 CSS 变量映射到现有 `--gg-*` 设计系统。本阶段不改任何桌面端代码,仅新增文件 + 改造路由与入口。 + +**Tech Stack:** Vue 3 + TypeScript + Vite + Vue Router 4 + Vant 4(新增)+ 现有 PocketBase SDK / Pinia(复用) + +**Spec 参考:** `docs/superpowers/specs/2026-06-15-mobile-frontend-design.md` 第 2-5 节 + +**项目约定(执行前必读):** +- 构建走 Docker,**不要**本地启动 vite dev server。测试通过 `deploy-dev.sh` 部署后访问 `http://192.168.1.14:7033` +- Windows 环境,命令行用 cmd 语法 +- 现有 `main.ts` 全量引入 ElementPlus,手机端 Vant 同样全量引入保持一致 +- 提交用 `git -c user.name="zcode" -c user.email="zcode@local" commit` + +--- + +## 文件结构(本阶段涉及) + +**新增:** +- `frontend/src/mobile/useDevice.ts` — 设备检测 composable(UA + 屏宽 + localStorage) +- `frontend/src/mobile/MobileLayout.vue` — 手机端主布局(顶部栏 + 底部 Tab + router-view) +- `frontend/src/assets/mobile.css` — Vant 主题变量覆盖(映射到 `--gg-*`) +- `frontend/src/views-mobile/Placeholder.vue` — 占位页(未实现的页面暂用) + +**修改:** +- `frontend/package.json` — 新增 vant 依赖 +- `frontend/src/main.ts` — 注册 Vant + 引入 mobile.css +- `frontend/src/router/index.ts` — 路由分流(设备检测 + 动态 import) +- `frontend/src/App.vue` — viewport meta 适配(已在 index.html 处理,本阶段确认) + +**不改:** +- 所有 `frontend/src/views/*`、`frontend/src/components/*`(桌面端零改动) +- `frontend/src/api/*`、`frontend/src/stores/*`、`frontend/src/types/*`、`frontend/src/composables/*`(共享层零改动) +- `frontend/src/assets/design.css`(设计系统零改动,手机端只在其之上叠加 mobile.css) + +--- + +## Task 1: 安装 Vant 依赖 + +**Files:** +- Modify: `frontend/package.json` + +- [ ] **Step 1: 进入 frontend 目录安装 vant** + +Run(在 `D:\ZcodeProj\FT\frontend` 下): +```cmd +npm install vant@^4 +``` +Expected: package.json 的 dependencies 出现 `"vant": "^4.x.x"`,生成/更新 package-lock.json + +- [ ] **Step 2: 验证安装成功** + +Run: +```cmd +node -e "console.log(require('vant/package.json').version)" +``` +Expected: 输出 4.x.x 版本号,无报错 + +- [ ] **Step 3: 提交** + +```cmd +git add frontend/package.json frontend/package-lock.json +git -c user.name="zcode" -c user.email="zcode@local" commit -m "feat(mobile): add vant 4 dependency" +``` + +--- + +## Task 2: 创建设备检测 composable + +**Files:** +- Create: `frontend/src/mobile/useDevice.ts` + +- [ ] **Step 1: 创建 useDevice.ts** + +创建 `frontend/src/mobile/useDevice.ts`: + +```ts +// src/mobile/useDevice.ts +// 设备检测:判断当前是否移动端,结果存 localStorage 避免重复检测 + +const STORAGE_KEY = 'device_mode' + +/** UA 关键字匹配移动设备 */ +const MOBILE_UA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i + +/** 屏幕宽度阈值(含平板竖屏) */ +const MOBILE_WIDTH = 768 + +/** + * 原始检测:综合 UA 和屏幕宽度判断 + * - UA 命中移动设备关键字 → 手机 + * - 屏宽 <= 768 → 手机 + * - 否则 → 桌面 + */ +function detectRaw(): 'mobile' | 'desktop' { + if (typeof navigator === 'undefined') return 'desktop' + if (MOBILE_UA.test(navigator.userAgent)) return 'mobile' + if (typeof window !== 'undefined' && window.innerWidth <= MOBILE_WIDTH) return 'mobile' + return 'desktop' +} + +/** 当前设备模式(读取 localStorage,无则检测并存入) */ +export function getDeviceMode(): 'mobile' | 'desktop' { + if (typeof localStorage === 'undefined') return detectRaw() + const stored = localStorage.getItem(STORAGE_KEY) + if (stored === 'mobile' || stored === 'desktop') return stored + const detected = detectRaw() + localStorage.setItem(STORAGE_KEY, detected) + return detected +} + +/** 是否移动端(路由分流用,同步函数) */ +export function isMobile(): boolean { + return getDeviceMode() === 'mobile' +} + +/** + * 手动切换设备模式(设置页"切换到桌面版/手机版"用) + * 切换后需整页刷新以重新走路由解析 + */ +export function setDeviceMode(mode: 'mobile' | 'desktop') { + localStorage.setItem(STORAGE_KEY, mode) +} + +/** 重置为自动检测(登出时调用,避免下个用户沿用上个用户的偏好) */ +export function resetDeviceMode() { + localStorage.removeItem(STORAGE_KEY) +} +``` + +- [ ] **Step 2: 提交** + +```cmd +git add frontend/src/mobile/useDevice.ts +git -c user.name="zcode" -c user.email="zcode@local" commit -m "feat(mobile): add device detection composable" +``` + +--- + +## Task 3: 创建 Vant 主题覆盖 CSS + +**Files:** +- Create: `frontend/src/assets/mobile.css` + +- [ ] **Step 1: 创建 mobile.css** + +创建 `frontend/src/assets/mobile.css`,将 Vant 的 CSS 变量映射到现有 `--gg-*` 设计系统(参考 `frontend/src/assets/design.css` 的变量定义): + +```css +/* src/assets/mobile.css */ +/* Vant 4 主题变量覆盖 —— 映射到项目 design.css 的 --gg-* 设计系统 */ + +:root { + /* 主色(Vant primary → gg-primary 绿色) */ + --van-primary-color: var(--gg-primary); /* #059669 */ + --van-success-color: var(--gg-success); /* #10b981 */ + --van-danger-color: var(--gg-danger); /* #ef4444 */ + --van-warning-color: var(--gg-warning); /* #f59e0b */ + + /* 文字色 */ + --van-text-color: var(--gg-text); /* #1e293b */ + --van-text-color-2: var(--gg-text-secondary); /* #475569 */ + --van-text-color-3: var(--gg-text-muted); /* #94a3b8 */ + + /* 背景 */ + --van-background: var(--gg-bg); /* #f0fdf4 */ + --van-background-2: var(--gg-bg-card); /* #ffffff */ + + /* 边框 */ + --van-border-color: var(--gg-border); /* #e2e8f0 */ + + /* 圆角 */ + --van-radius-sm: var(--gg-radius-sm); + --van-radius-md: var(--gg-radius-md); + --van-radius-lg: var(--gg-radius-lg); + + /* Tabbar(底部导航) */ + --van-tabbar-background: var(--gg-bg-card); + --van-tabbar-item-active-color: var(--gg-primary); + --van-tabbar-item-text-color: var(--gg-text-muted); + --van-tabbar-height: 56px; + + /* NavBar(顶部栏) */ + --van-nav-bar-background: var(--gg-bg-card); + --van-nav-bar-title-font-size: 17px; + --van-nav-bar-height: 52px; + --van-nav-bar-icon-color: var(--gg-text); + + /* 按钮主色渐变 */ + --van-button-primary-background: var(--gg-primary); + --van-button-primary-border-color: var(--gg-primary); +} + +/* 手机端全局:安全区域适配(刘海屏底部 Tab 不被遮挡) */ +.mobile-app { + padding-bottom: env(safe-area-inset-bottom, 0px); +} + +/* 禁止移动端点击高亮 */ +.mobile-app * { + -webkit-tap-highlight-color: transparent; +} +``` + +- [ ] **Step 2: 提交** + +```cmd +git add frontend/src/assets/mobile.css +git -c user.name="zcode" -c user.email="zcode@local" commit -m "feat(mobile): add vant theme override css" +``` + +--- + +## Task 4: 注册 Vant 并引入主题 CSS + +**Files:** +- Modify: `frontend/src/main.ts` + +- [ ] **Step 1: 修改 main.ts** + +将 `frontend/src/main.ts` 改为(在现有 ElementPlus 之后注册 Vant): + +```ts +// src/main.ts +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import Vant from 'vant' +import 'vant/lib/index.css' +import './assets/design.css' +import './assets/mobile.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import App from './App.vue' +import router from './router' + +const app = createApp(App) +const pinia = createPinia() + +// 注册所有 Element Plus 图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.use(pinia) +app.use(router) +app.use(ElementPlus) +app.use(Vant) + +app.mount('#app') +``` + +- [ ] **Step 2: 提交** + +```cmd +git add frontend/src/main.ts +git -c user.name="zcode" -c user.email="zcode@local" commit -m "feat(mobile): register vant and import theme css" +``` + +--- + +## Task 5: 创建占位页(未实现的手机页面暂用) + +**Files:** +- Create: `frontend/src/views-mobile/Placeholder.vue` + +- [ ] **Step 1: 创建占位组件** + +创建 `frontend/src/views-mobile/Placeholder.vue`: + +```vue + + + + + + + +``` + +- [ ] **Step 2: 提交** + +```cmd +git add frontend/src/views-mobile/Placeholder.vue +git -c user.name="zcode" -c user.email="zcode@local" commit -m "feat(mobile): add placeholder view for unimplemented pages" +``` + +--- + +## Task 6: 创建 MobileLayout 主布局 + +**Files:** +- Create: `frontend/src/mobile/MobileLayout.vue` + +- [ ] **Step 1: 创建 MobileLayout.vue** + +创建 `frontend/src/mobile/MobileLayout.vue`。这是手机端主布局:顶部栏(返回 + 标题 + 通知铃铛)+ 底部 5 Tab + 内容区。复用现有 `useUserStore`、`useGroupStore`、`useTeamStore`、`useNotificationStore`(与桌面 Layout.vue 相同的初始化逻辑)。 + +底部 Tab 对应路由:首页 `/`、群组 `/mobile-groups`、游戏 `/games`、通知 `/mobile-notifications`、我的 `/profile`。其中群组和通知在后续阶段才真正实现,本阶段先指向占位页。 + +```vue + + + + + + + +``` + +- [ ] **Step 2: 提交** + +```cmd +git add frontend/src/mobile/MobileLayout.vue +git -c user.name="zcode" -c user.email="zcode@local" commit -m "feat(mobile): add MobileLayout with navbar and tabbar" +``` + +--- + +## Task 7: 改造路由实现设备分流 + +**Files:** +- Modify: `frontend/src/router/index.ts` + +这是本阶段核心。改造路由:同一 URL,根据 `isMobile()` 动态 import 桌面或手机视图。手机端用 `MobileLayout` 作为布局容器(替代桌面的 `Layout.vue`),子路由指向手机视图(本阶段除 Home 外先用 Placeholder 占位)。 + +- [ ] **Step 1: 重写 router/index.ts** + +将 `frontend/src/router/index.ts` 完整替换为: + +```ts +// src/router/index.ts +import { createRouter, createWebHistory } from 'vue-router' +import type { RouteRecordRaw } from 'vue-router' +import { isAuthenticated } from '@/api/pocketbase' +import { isMobile } from '@/mobile/useDevice' + +// 动态选择布局:手机端用 MobileLayout,桌面端用 Layout +const LayoutComponent = () => + isMobile() + ? import('@/mobile/MobileLayout.vue') + : import('@/views/Layout.vue') + +// 动态选择视图:同一路由名,根据设备加载桌面/手机视图 +function view(desktop: () => Promise, mobile: () => Promise) { + return isMobile() ? mobile : desktop +} + +// 路由配置 +const routes: RouteRecordRaw[] = [ + { + path: '/login', + name: 'Login', + component: view( + () => import('@/views/Login.vue'), + () => import('@/views-mobile/Placeholder.vue') // 阶段 2 替换为 LoginMobile + ), + meta: { requiresGuest: true } + }, + { + path: '/register', + name: 'Register', + component: view( + () => import('@/views/Register.vue'), + () => import('@/views-mobile/Placeholder.vue') // 阶段 2 替换为 RegisterMobile + ), + meta: { requiresGuest: true } + }, + { + path: '/', + component: LayoutComponent, + meta: { requiresAuth: true }, + children: [ + { + path: '', + name: 'Home', + component: view( + () => import('@/views/Home.vue'), + () => import('@/views-mobile/Placeholder.vue') // 阶段 3 替换为 HomeMobile + ) + }, + { + path: 'mobile-groups', + name: 'MobileGroups', + component: view( + () => import('@/views/Home.vue'), // 桌面端无此路由,回退首页 + () => import('@/views-mobile/Placeholder.vue') // 阶段 3 替换为 GroupsMobile + ) + }, + { + path: 'mobile-notifications', + name: 'MobileNotifications', + component: view( + () => import('@/views/Home.vue'), + () => import('@/views-mobile/Placeholder.vue') // 阶段 3 替换为 NotificationsMobile + ) + }, + { + path: 'group/:id', + name: 'GroupView', + component: view( + () => import('@/views/GroupView.vue'), + () => import('@/views-mobile/Placeholder.vue') // 阶段 4 替换为 GroupViewMobile + ), + props: true + }, + { + path: 'group/:groupId/ledger', + name: 'LedgerView', + component: view( + () => import('@/views/LedgerView.vue'), + () => import('@/views-mobile/Placeholder.vue') // 阶段 8 替换为 LedgerMobile + ), + props: true + }, + { + path: 'group/:groupId/assets', + name: 'AssetView', + component: view( + () => import('@/views/AssetView.vue'), + () => import('@/views-mobile/Placeholder.vue') // 阶段 8 替换为 AssetMobile + ), + props: true + }, + { + path: 'group/:groupId/blacklist', + name: 'BlacklistView', + component: view( + () => import('@/views/BlacklistView.vue'), + () => import('@/views-mobile/Placeholder.vue') // 阶段 8 替换为 BlacklistMobile + ), + props: true + }, + { + path: 'group/:groupId/voice/:sessionId', + name: 'VoiceRoom', + component: view( + () => import('@/views/VoiceRoom.vue'), + () => import('@/views-mobile/Placeholder.vue') // 阶段 5 替换为 VoiceRoomMobile + ), + props: true + }, + { + path: 'games', + name: 'GamesLibrary', + component: view( + () => import('@/views/GamesLibrary.vue'), + () => import('@/views-mobile/Placeholder.vue') // 阶段 7 替换为 GamesLibraryMobile + ) + }, + { + path: 'profile', + name: 'Profile', + component: view( + () => import('@/views/Profile.vue'), + () => import('@/views-mobile/Placeholder.vue') // 阶段 9 替换为 ProfileMobile + ) + }, + { + path: 'settings', + name: 'Settings', + component: view( + () => import('@/views/Settings.vue'), + () => import('@/views-mobile/Placeholder.vue') // 阶段 9 替换为 SettingsMobile + ) + }, + { + path: 'changelog', + name: 'Changelog', + component: view( + () => import('@/views/Changelog.vue'), + () => import('@/views-mobile/Placeholder.vue') // 阶段 9 替换为 ChangelogMobile + ) + } + ] + }, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: () => import('@/views/NotFound.vue') + } +] + +// 创建路由实例 +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes +}) + +// 路由守卫(与原逻辑一致) +router.beforeEach((to, _from, next) => { + const authenticated = isAuthenticated() + + if (to.meta.requiresAuth && !authenticated) { + next({ name: 'Login', query: { redirect: to.fullPath } }) + return + } + + if (to.meta.requiresGuest && authenticated) { + next({ name: 'Home' }) + return + } + + next() +}) + +export default router +``` + +- [ ] **Step 2: 提交** + +```cmd +git add frontend/src/router/index.ts +git -c user.name="zcode" -c user.email="zcode@local" commit -m "feat(mobile): add device-based route splitting" +``` + +--- + +## Task 8: 确认 viewport meta 标签 + +**Files:** +- Check/Modify: `frontend/index.html` + +手机端必须禁止缩放、适配视口宽度。检查 `index.html` 的 ``。 + +- [ ] **Step 1: 查看现有 index.html** + +Run: +```cmd +type frontend\index.html +``` + +- [ ] **Step 2: 确认/修改 viewport meta** + +如果 `` 内没有 viewport meta,或不是以下内容,将其改为: + +```html + +``` + +`viewport-fit=cover` 配合 Task 3 CSS 里的 `env(safe-area-inset-bottom)` 处理刘海屏。保留现有 `` 等其他内容不变。 + +> 注意:如果 index.html 已有正确 viewport,此 Task 跳过修改直接进入 Step 3。 + +- [ ] **Step 3: 提交(如有修改)** + +```cmd +git add frontend/index.html +git -c user.name="zcode" -c user.email="zcode@local" commit -m "fix(mobile): ensure viewport meta for mobile scaling" +``` + +--- + +## Task 9: TypeScript 类型检查与构建验证 + +**Files:** +- 无修改(仅验证) + +- [ ] **Step 1: 运行 TypeScript 类型检查** + +Run(在 `D:\ZcodeProj\FT\frontend` 下): +```cmd +npx vue-tsc --noEmit +``` +Expected: 无类型错误。如果报错,常见原因: +- Vant 组件类型未识别 → 确认 `vant` 已安装且 `main.ts` 引入 +- `@/mobile/useDevice` 路径找不到 → 确认 `tsconfig.json` 的 paths 配置包含 `@/*`(现有项目已有) +- 修复后重新运行直到通过 + +- [ ] **Step 2: 运行生产构建** + +Run: +```cmd +npm run build +``` +Expected: 构建成功,`dist/` 目录生成。如果失败,根据报错修复(通常是 import 路径或类型问题)。 + +- [ ] **Step 3: 提交(如有修复)** + +如果构建过程中修复了任何问题: +```cmd +git add -A +git -c user.name="zcode" -c user.email="zcode@local" commit -m "fix(mobile): resolve type/build errors" +``` + +--- + +## Task 10: 部署到 Dev 环境并验证 + +**Files:** +- 无修改(部署与验收测试) + +- [ ] **Step 1: 部署到 Dev 环境** + +Run(在项目根 `D:\ZcodeProj\FT` 下): +```cmd +bash deploy-dev.sh +``` +> 如果 Windows 无 bash,参考 `deploy-dev.sh` 内容手动执行等价命令(通常是 `cd frontend && npm run build` 后用 docker-compose 重建 `gamegroup-frontend-dev` 容器)。 +Expected: 容器重建成功,监听 7033 端口。 + +- [ ] **Step 2: 桌面浏览器验证(回归不破坏)** + +在桌面浏览器打开 `http://192.168.1.14:7033`: +- [ ] 登录页正常显示(桌面版 Login.vue) +- [ ] 登录后进入桌面版首页(左侧边栏 + 右侧主区) +- [ ] 桌面端布局与改动前完全一致 +- [ ] 打开浏览器 DevTools Console 无报错 + +- [ ] **Step 3: 手机端验证(模拟移动设备)** + +在桌面浏览器 DevTools 切换到移动设备模拟(如 Chrome 的 Toggle device toolbar,选 iPhone 12): +- [ ] 刷新页面 → 自动进入手机版(顶部 NavBar + 底部 Tabbar) +- [ ] 顶部栏显示标题 + 右侧通知铃铛图标 +- [ ] 底部 5 个 Tab 显示:首页/群组/游戏/通知/我的 +- [ ] 点击底部各 Tab → 路由切换,内容区显示 Placeholder 占位页("该页面手机版开发中") +- [ ] 顶部栏在非 Tab 根页面(如点群组进入详情占位)显示返回箭头 +- [ ] 整体绿色主题(按钮、Tab 激活态为 #059669) +- [ ] Console 无报错 + +- [ ] **Step 4: 真机验证(可选但推荐)** + +用手机浏览器打开 `http://192.168.1.14:7033`(需与电脑同 WiFi): +- [ ] 自动进入手机版 +- [ ] 底部 Tab 单手可达,无横向滚动条 +- [ ] 顶部栏和底部 Tab 不被刘海/Home 指示条遮挡 + +- [ ] **Step 5: 设备切换验证** + +在手机版页面的浏览器 Console 执行: +```js +localStorage.setItem('device_mode', 'desktop'); location.reload() +``` +- [ ] 刷新后切换到桌面版 +- [ ] 执行 `localStorage.removeItem('device_mode'); location.reload()` 恢复自动检测 + +--- + +## 验收标准(本阶段完成标志) + +- [ ] 桌面端完全不受影响(回归测试通过) +- [ ] 手机端自动识别设备并加载 MobileLayout +- [ ] 底部 5 Tab 导航可切换,路由跳转正常 +- [ ] 顶部栏标题、返回箭头、通知铃铛显示正常 +- [ ] 绿色主题统一(Vant 变量映射成功) +- [ ] 未实现的页面显示 Placeholder 占位 +- [ ] TypeScript 类型检查通过,生产构建成功 +- [ ] 已部署到 Dev(7033)并可访问 + +--- + +## 后续阶段预告(本计划不实现) + +本阶段完成后,后续按以下顺序逐步将各 Placeholder 替换为真实手机视图: + +| 阶段 | 内容 | 替换的占位路由 | +|------|------|--------------| +| 2 | 登录/注册 | Login, Register | +| 3 | 首页 + 群组列表 + 通知 | Home, MobileGroups, MobileNotifications | +| 4 | 群组详情核心(动态 + 成员) | GroupView | +| 5 | 组队 + 语音房 UI | VoiceRoom | +| 6 | 投票 + 竞猜 | GroupView 内标签 | +| 7 | 游戏库 | GamesLibrary | +| 8 | 账本 + 资产 + 黑名单 | LedgerView, AssetView, BlacklistView | +| 9 | 回忆 + 统计 + 个人/设置/更新日志 | Profile, Settings, Changelog | + +每个阶段一份独立实施计划,每阶段产出可测试的手机页面。