Files
gamegroup2/docs/superpowers/plans/2026-06-15-mobile-phase1-infrastructure.md
T

25 KiB
Raw Blame History

手机端阶段 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 — 设备检测 composableUA + 屏宽 + 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 下):

npm install vant@^4

Expected: package.json 的 dependencies 出现 "vant": "^4.x.x",生成/更新 package-lock.json

  • Step 2: 验证安装成功

Run:

node -e "console.log(require('vant/package.json').version)"

Expected: 输出 4.x.x 版本号,无报错

  • Step 3: 提交
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

// 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: 提交
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 的变量定义):

/* 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: 提交
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):

// 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: 提交
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

<!-- src/views-mobile/Placeholder.vue -->
<!-- 占位页手机端尚未实现的页面暂时显示此组件 -->
<script setup lang="ts">
import { useRoute } from 'vue-router'

const route = useRoute()
</script>

<template>
  <div class="placeholder-page">
    <van-empty description="该页面手机版开发中">
      <p class="placeholder-hint">当前页面{{ route.name }}</p>
    </van-empty>
  </div>
</template>

<style scoped>
.placeholder-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 60vh;
  padding: 24px;
}

.placeholder-hint {
  margin-top: 8px;
  font-size: 12px;
  color: var(--gg-text-muted);
}
</style>
  • Step 2: 提交
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 + 内容区。复用现有 useUserStoreuseGroupStoreuseTeamStoreuseNotificationStore(与桌面 Layout.vue 相同的初始化逻辑)。

底部 Tab 对应路由:首页 /、群组 /mobile-groups、游戏 /games、通知 /mobile-notifications、我的 /profile。其中群组和通知在后续阶段才真正实现,本阶段先指向占位页。

<!-- src/mobile/MobileLayout.vue -->
<!-- 手机端主布局顶部栏 + 底部 Tab + 内容区 -->
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { useGroupStore } from '@/stores/group'
import { useTeamStore } from '@/stores/team'
import { useNotificationStore } from '@/stores/notification'
import { displayName } from '@/types'
import { resetDeviceMode } from '@/mobile/useDevice'

const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const groupStore = useGroupStore()
const teamStore = useTeamStore()
const notificationStore = useNotificationStore()

// 与桌面 Layout 相同的初始化
let refreshTimer: ReturnType<typeof setInterval> | null = null

onMounted(async () => {
  await userStore.initUser()
  await groupStore.loadGroups()
  await teamStore.loadActiveSession()
  await notificationStore.loadPendingInvitations()
  await notificationStore.startListening()

  refreshTimer = setInterval(async () => {
    await groupStore.loadGroups()
    await teamStore.loadActiveSession()
  }, 30000)
})

onUnmounted(() => {
  notificationStore.stopListening()
  if (refreshTimer) {
    clearInterval(refreshTimer)
    refreshTimer = null
  }
})

// 当前激活的底部 Tab(根据路由匹配)
const activeTab = computed(() => {
  if (route.path.startsWith('/mobile-groups')) return 'groups'
  if (route.path.startsWith('/games')) return 'games'
  if (route.path.startsWith('/mobile-notifications')) return 'notifications'
  if (route.path.startsWith('/profile')) return 'profile'
  return 'home'
})

// 顶部栏标题
const pageTitle = computed(() => {
  const titles: Record<string, string> = {
    home: 'Game Group',
    groups: '我的群组',
    games: '游戏库',
    notifications: '通知',
    profile: '我的'
  }
  // 群组详情页显示群组名
  if (route.name === 'GroupView' && groupStore.currentGroup) {
    return groupStore.currentGroup.name
  }
  return titles[activeTab.value] || 'Game Group'
})

// 顶部栏是否显示返回按钮(非 Tab 根页面显示)
const showBack = computed(() => {
  return !['home', 'groups', 'games', 'notifications', 'profile'].includes(activeTab.value)
})

// 底部 Tab 切换
function onTabChange(name: string) {
  const routes: Record<string, string> = {
    home: '/',
    groups: '/mobile-groups',
    games: '/games',
    notifications: '/mobile-notifications',
    profile: '/profile'
  }
  const target = routes[name]
  if (target && route.path !== target) {
    router.push(target)
  }
}

// 返回上一页
function onBack() {
  if (window.history.length > 1) {
    router.back()
  } else {
    router.push('/')
  }
}

// 通知未读数
const unreadCount = computed(() => notificationStore.unreadCount)

function goNotifications() {
  router.push('/mobile-notifications')
}

// 退出登录
function handleLogout() {
  resetDeviceMode()
  userStore.logout()
  router.push({ name: 'Login' })
}
</script>

<template>
  <div class="mobile-app">
    <!-- 顶部栏 -->
    <van-nav-bar
      :title="pageTitle"
      :left-arrow="showBack"
      fixed
      placeholder
      @click-left="onBack"
    >
      <template #right>
        <van-badge :content="unreadCount > 0 ? unreadCount : ''" :show-zero="false">
          <van-icon name="bell" size="22" @click="goNotifications" />
        </van-badge>
      </template>
    </van-nav-bar>

    <!-- 内容区 -->
    <main class="mobile-content">
      <router-view />
    </main>

    <!-- 底部 Tab -->
    <van-tabbar v-model="activeTab" @change="onTabChange" placeholder fixed>
      <van-tabbar-item name="home" icon="wap-home-o">首页</van-tabbar-item>
      <van-tabbar-item name="groups" icon="friends-o">群组</van-tabbar-item>
      <van-tabbar-item name="games" icon="game-o">游戏</van-tabbar-item>
      <van-tabbar-item name="notifications" icon="bell">通知</van-tabbar-item>
      <van-tabbar-item name="profile" icon="user-o">我的</van-tabbar-item>
    </van-tabbar>
  </div>
</template>

<style scoped>
.mobile-app {
  min-height: 100vh;
  background: var(--gg-bg);
  display: flex;
  flex-direction: column;
}

.mobile-content {
  flex: 1;
  padding-bottom: 8px;
}
</style>
  • Step 2: 提交
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 完整替换为:

// 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<any>, mobile: () => Promise<any>) {
  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: 提交
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<meta name="viewport">

  • Step 1: 查看现有 index.html

Run:

type frontend\index.html
  • Step 2: 确认/修改 viewport meta

如果 <head> 内没有 viewport meta,或不是以下内容,将其改为:

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />

viewport-fit=cover 配合 Task 3 CSS 里的 env(safe-area-inset-bottom) 处理刘海屏。保留现有 <title> 等其他内容不变。

注意:如果 index.html 已有正确 viewport,此 Task 跳过修改直接进入 Step 3。

  • Step 3: 提交(如有修改)
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 下):

npx vue-tsc --noEmit

Expected: 无类型错误。如果报错,常见原因:

  • Vant 组件类型未识别 → 确认 vant 已安装且 main.ts 引入

  • @/mobile/useDevice 路径找不到 → 确认 tsconfig.json 的 paths 配置包含 @/*(现有项目已有)

  • 修复后重新运行直到通过

  • Step 2: 运行生产构建

Run:

npm run build

Expected: 构建成功,dist/ 目录生成。如果失败,根据报错修复(通常是 import 路径或类型问题)。

  • Step 3: 提交(如有修复)

如果构建过程中修复了任何问题:

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 下):

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 执行:

localStorage.setItem('device_mode', 'desktop'); location.reload()
  • 刷新后切换到桌面版
  • 执行 localStorage.removeItem('device_mode'); location.reload() 恢复自动检测

验收标准(本阶段完成标志)

  • 桌面端完全不受影响(回归测试通过)
  • 手机端自动识别设备并加载 MobileLayout
  • 底部 5 Tab 导航可切换,路由跳转正常
  • 顶部栏标题、返回箭头、通知铃铛显示正常
  • 绿色主题统一(Vant 变量映射成功)
  • 未实现的页面显示 Placeholder 占位
  • TypeScript 类型检查通过,生产构建成功
  • 已部署到 Dev7033)并可访问

后续阶段预告(本计划不实现)

本阶段完成后,后续按以下顺序逐步将各 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

每个阶段一份独立实施计划,每阶段产出可测试的手机页面。