feat(mobile): stage 1 - infrastructure (vant, useDevice, MobileLayout, router, mobile.css)
- add vant@^4.9.0 dependency - migrate useDevice.ts (device detection + localStorage) - migrate MobileLayout.vue (tabbar + navbar + store init) - migrate mobile.css (vant theme -> --gg-* mapping) - migrate Placeholder.vue for unimplemented mobile views - index.html: viewport for mobile (no-zoom, safe-area) - main.ts: register Vant + import mobile.css - vite.config.ts: manualChunks code splitting (vue/element/vant/pocketbase/livekit) - router/index.ts: device-based routing via isMobile() + view() helper, preserve uat's JoinGroup/JoinTeam routes, mobile views use Placeholder pending stages 2-11 build verified: vue-tsc + vite build pass
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
<title>Game Group V2</title>
|
<title>Game Group V2</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎮</text></svg>">
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎮</text></svg>">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"livekit-client": "^2.18.3",
|
"livekit-client": "^2.18.3",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"pocketbase": "^0.21.1",
|
"pocketbase": "^0.21.1",
|
||||||
|
"vant": "^4.9.0",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-router": "^4.3.0"
|
"vue-router": "^4.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
@@ -3,7 +3,10 @@ import { createApp } from 'vue'
|
|||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
|
import Vant from 'vant'
|
||||||
|
import 'vant/lib/index.css'
|
||||||
import './assets/design.css'
|
import './assets/design.css'
|
||||||
|
import './assets/mobile.css'
|
||||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
@@ -19,5 +22,6 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
|||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(ElementPlus)
|
app.use(ElementPlus)
|
||||||
|
app.use(Vant)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<!-- src/mobile/MobileLayout.vue -->
|
||||||
|
<!-- 手机端主布局:顶部栏 + 底部 Tab + 内容区 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { 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'
|
||||||
|
|
||||||
|
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 | number) {
|
||||||
|
const routes: Record<string, string> = {
|
||||||
|
home: '/',
|
||||||
|
groups: '/mobile-groups',
|
||||||
|
games: '/games',
|
||||||
|
notifications: '/mobile-notifications',
|
||||||
|
profile: '/profile'
|
||||||
|
}
|
||||||
|
const target = routes[String(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')
|
||||||
|
}
|
||||||
|
</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>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
||||||
@@ -2,97 +2,179 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
import { isAuthenticated } from '@/api/pocketbase'
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 阶段 1 占位:尚未实现的 mobile 视图统一指向 Placeholder,后续阶段逐个替换为真实组件
|
||||||
|
// 阶段 2: LoginMobile / RegisterMobile
|
||||||
|
// 阶段 3: HomeMobile / GroupsMobile / NotificationsMobile
|
||||||
|
// 阶段 4: GroupViewMobile
|
||||||
|
// 阶段 5: VoiceRoomMobile
|
||||||
|
// 阶段 7: GamesLibraryMobile
|
||||||
|
// 阶段 8: LedgerMobile / AssetMobile / BlacklistMobile
|
||||||
|
// 阶段 9: ProfileMobile / SettingsMobile / ChangelogMobile
|
||||||
|
// 阶段 11: JoinGroupPageMobile / JoinTeamPageMobile
|
||||||
|
const mobilePlaceholder = () => import('@/views-mobile/Placeholder.vue')
|
||||||
|
|
||||||
// 路由配置
|
// 路由配置
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
component: () => import('@/views/Login.vue'),
|
component: view(
|
||||||
|
() => import('@/views/Login.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
),
|
||||||
meta: { requiresGuest: true }
|
meta: { requiresGuest: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/register',
|
path: '/register',
|
||||||
name: 'Register',
|
name: 'Register',
|
||||||
component: () => import('@/views/Register.vue'),
|
component: view(
|
||||||
|
() => import('@/views/Register.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
),
|
||||||
meta: { requiresGuest: true }
|
meta: { requiresGuest: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: () => import('@/views/Layout.vue'),
|
component: LayoutComponent,
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
component: () => import('@/views/Home.vue')
|
component: view(
|
||||||
|
() => import('@/views/Home.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'mobile-groups',
|
||||||
|
name: 'MobileGroups',
|
||||||
|
component: view(
|
||||||
|
() => import('@/views/Home.vue'), // 桌面端无此路由,回退首页
|
||||||
|
mobilePlaceholder
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'mobile-notifications',
|
||||||
|
name: 'MobileNotifications',
|
||||||
|
component: view(
|
||||||
|
() => import('@/views/Home.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'group/:id',
|
path: 'group/:id',
|
||||||
name: 'GroupView',
|
name: 'GroupView',
|
||||||
component: () => import('@/views/GroupView.vue'),
|
component: view(
|
||||||
|
() => import('@/views/GroupView.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
),
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'group/:groupId/ledger',
|
path: 'group/:groupId/ledger',
|
||||||
name: 'LedgerView',
|
name: 'LedgerView',
|
||||||
component: () => import('@/views/LedgerView.vue'),
|
component: view(
|
||||||
|
() => import('@/views/LedgerView.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'group/:groupId/assets',
|
path: 'group/:groupId/assets',
|
||||||
name: 'AssetView',
|
name: 'AssetView',
|
||||||
component: () => import('@/views/AssetView.vue'),
|
component: view(
|
||||||
|
() => import('@/views/AssetView.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'group/:groupId/blacklist',
|
path: 'group/:groupId/blacklist',
|
||||||
name: 'BlacklistView',
|
name: 'BlacklistView',
|
||||||
component: () => import('@/views/BlacklistView.vue'),
|
component: view(
|
||||||
|
() => import('@/views/BlacklistView.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'group/:groupId/voice/:sessionId',
|
path: 'group/:groupId/voice/:sessionId',
|
||||||
name: 'VoiceRoom',
|
name: 'VoiceRoom',
|
||||||
component: () => import('@/views/VoiceRoom.vue'),
|
component: view(
|
||||||
|
() => import('@/views/VoiceRoom.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'games',
|
path: 'games',
|
||||||
name: 'GamesLibrary',
|
name: 'GamesLibrary',
|
||||||
component: () => import('@/views/GamesLibrary.vue')
|
component: view(
|
||||||
|
() => import('@/views/GamesLibrary.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'profile',
|
path: 'profile',
|
||||||
name: 'Profile',
|
name: 'Profile',
|
||||||
component: () => import('@/views/Profile.vue')
|
component: view(
|
||||||
|
() => import('@/views/Profile.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
component: () => import('@/views/Settings.vue')
|
component: view(
|
||||||
|
() => import('@/views/Settings.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'changelog',
|
path: 'changelog',
|
||||||
name: 'Changelog',
|
name: 'Changelog',
|
||||||
component: () => import('@/views/Changelog.vue')
|
component: view(
|
||||||
|
() => import('@/views/Changelog.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/join/group/:groupId',
|
path: '/join/group/:groupId',
|
||||||
name: 'JoinGroup',
|
name: 'JoinGroup',
|
||||||
component: () => import('@/views/JoinGroupPage.vue'),
|
// 邀请落地页:手机端组件阶段 11 填充,现先用 Placeholder 让分流跑通
|
||||||
|
component: view(
|
||||||
|
() => import('@/views/JoinGroupPage.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
),
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/join/team/:sessionId',
|
path: '/join/team/:sessionId',
|
||||||
name: 'JoinTeam',
|
name: 'JoinTeam',
|
||||||
component: () => import('@/views/JoinTeamPage.vue'),
|
component: view(
|
||||||
|
() => import('@/views/JoinTeamPage.vue'),
|
||||||
|
mobilePlaceholder
|
||||||
|
),
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<!-- 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>
|
||||||
@@ -9,6 +9,25 @@ export default defineConfig({
|
|||||||
'@': path.resolve(__dirname, 'src')
|
'@': path.resolve(__dirname, 'src')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
// 代码分割:vendor 按依赖分组,避免单个超大 chunk
|
||||||
|
manualChunks: {
|
||||||
|
// Vue 核心运行时(vue + vue-router + pinia)
|
||||||
|
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||||
|
// 桌面端 UI 库
|
||||||
|
'element-plus': ['element-plus', '@element-plus/icons-vue'],
|
||||||
|
// 手机端 UI 库
|
||||||
|
'vant': ['vant'],
|
||||||
|
// 后端 SDK
|
||||||
|
'pocketbase': ['pocketbase'],
|
||||||
|
// 语音房依赖(仅 VoiceRoom 用到,体积大,单独拆分)
|
||||||
|
'livekit': ['livekit-client'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: Number(process.env.VITE_PORT) || 5173,
|
port: Number(process.env.VITE_PORT) || 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
Reference in New Issue
Block a user