feat(h5): 建立双端路由三桶约定与设备独占提示机制

- 新增 src/shared/router/paths.ts:RoutePath 常量,两端路由共用,杜绝 path 漂移
- 新增 src/shared/router/exclusive.ts:设备独占注册表 + otherDeviceExclusives 派生助手
- 两端路由分三桶:业务路由 → 对端独占提示存根(注册表派生)→ catch-all 404
- 新增 mobile/pc device-only 提示页:错设备打开独占页时提示而非 404
- 首页 path 改用 RoutePath.HOME;注册表初始为空,零副作用
This commit is contained in:
bootx
2026-06-24 12:27:05 +08:00
parent eb99deb803
commit c0c06222be
7 changed files with 185 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
import type { App } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
import { otherDeviceExclusives } from '@/shared/router/exclusive'
import { useRouteStoreWithOut } from '@/shared/store/modules/route'
import { ErrorPageRoute } from './base'
import routeModuleList from './modules'
@@ -14,9 +15,17 @@ const routeStore = useRouteStoreWithOut()
routeStore.setMenus(routeModuleList)
routeStore.setRouters(constantRouter.concat(routeModuleList))
// PC 独占路径:在移动端渲染"仅限电脑端"提示存根(由注册表派生,勿手写)
const pcOnlyStubs: RouteRecordRaw[] = otherDeviceExclusives('mobile').map(r => ({
path: r.path,
name: `${r.name}__stub`,
component: () => import('@/mobile/views/device-only.vue'),
}))
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: constantRouter.concat(...routeModuleList),
// 顺序:业务路由 → PC 独占提示存根 → catch-all 404
routes: [...routeModuleList, ...pcOnlyStubs, ErrorPageRoute],
strict: true,
scrollBehavior: () => ({ left: 0, top: 0 }),
})

View File

@@ -1,4 +1,5 @@
import type { RouteRecordRaw } from 'vue-router'
import { RoutePath } from '@/shared/router/paths'
const Layout = () => import('@/mobile/layout/index.vue')
@@ -6,7 +7,7 @@ const Layout = () => import('@/mobile/layout/index.vue')
const routeModuleList: Array<RouteRecordRaw> = [
// 首页(根路径 /,与 PC 端首页地址统一)
{
path: '/',
path: RoutePath.HOME,
name: 'Home',
component: Layout,
meta: {

View File

@@ -0,0 +1,47 @@
<script lang="ts" setup>
defineOptions({ name: 'MobileDeviceOnly' })
const router = useRouter()
function goHome() {
router.push('/')
}
</script>
<template>
<div class="device-only">
<p class="device-only__title">
此页面仅限电脑端访问
</p>
<p class="device-only__desc">
请在电脑浏览器中打开本页面
</p>
<van-button type="primary" @click="goHome">
返回首页
</van-button>
</div>
</template>
<style scoped lang="less">
.device-only {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 24px;
text-align: center;
}
.device-only__title {
margin: 0 0 8px;
font-size: 18px;
font-weight: 600;
}
.device-only__desc {
margin: 0 0 24px;
font-size: 14px;
opacity: 0.6;
}
</style>

View File

@@ -1,15 +1,25 @@
import type { App } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
import { otherDeviceExclusives } from '@/shared/router/exclusive'
import { RoutePath } from '@/shared/router/paths'
// 移动独占路径:在 PC 端渲染"仅限手机端"提示存根(由注册表派生,勿手写)
const mobileOnlyStubs: RouteRecordRaw[] = otherDeviceExclusives('pc').map(r => ({
path: r.path,
name: `${r.name}__stub`,
component: () => import('@/pc/views/device-only.vue'),
}))
// PC 端路由树(独立于移动端,按需在此扩展业务路由,例如收银台 PC 版)
// 内页(收银台等)建议套 PcLayout首页为全屏独立页贴合移动端首页视觉
// 顺序:真实路由 → 移动独占提示存根 → catch-all 404
const routes: RouteRecordRaw[] = [
{
path: '/',
path: RoutePath.HOME,
name: 'PcHome',
component: () => import('@/pc/views/Home.vue'),
},
...mobileOnlyStubs,
// 兜底:移动端专属路径(如 /home/index或任何未匹配路径渲染 PC 404 页
// 用 component 而非 redirect——vue-router 5 下 catch-all + redirect 在初始导航不触发
{

View File

@@ -0,0 +1,63 @@
<script lang="ts" setup>
defineOptions({ name: 'PcDeviceOnly' })
const router = useRouter()
function goHome() {
router.push('/')
}
</script>
<template>
<div class="pc-device-only">
<p class="pc-device-only__title">
此页面仅限手机端访问
</p>
<p class="pc-device-only__desc">
请用手机扫描二维码或在手机浏览器中打开本页面
</p>
<button class="pc-device-only__btn" type="button" @click="goHome">
返回首页
</button>
</div>
</template>
<style scoped>
/* PC 端scoped 原生 px + 媒体查询,不使用 UnoCSS 的 px 原子类 */
.pc-device-only {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f7f8fa;
}
.pc-device-only__title {
margin: 0 0 8px;
font-size: 20px;
font-weight: 600;
color: #1d2129;
}
.pc-device-only__desc {
margin: 0 0 32px;
font-size: 14px;
color: #4e5969;
}
.pc-device-only__btn {
padding: 10px 28px;
font-size: 14px;
color: #ffffff;
background: #5d9dfe;
border: none;
border-radius: 6px;
cursor: pointer;
transition: opacity 0.2s ease;
}
.pc-device-only__btn:hover {
opacity: 0.85;
}
</style>

View File

@@ -0,0 +1,33 @@
/**
* 设备独占路由注册表
*
* 仅"一端"实现的业务页登记在此。两端路由器据此自动派生"对端提示存根"
* 当用户在错误设备打开独占页时(如手机打开 PC-only 页),渲染 device-only
* 提示页而非 404。
*
* 约束:每项需保证其所属端路由器里有对应的"真实路由",否则该路径在本端
* (所属端)反而会落 catch-all 变 404。
*
* 新增时参照示例(取消注释,并确保所属端已注册真实路由):
* { path: RoutePath.MERCHANT_DASHBOARD, name: 'MerchantDashboard', device: 'pc' },
* { path: RoutePath.WECHAT_BIND, name: 'WechatBind', device: 'mobile' },
*/
export interface ExclusiveRoute {
/** 路由 path引用 RoutePath 常量) */
path: string
/** 路由 name全端唯一对端存根会用 `${name}__stub` */
name: string
/** 仅哪端实现 */
device: 'pc' | 'mobile'
}
// 设备独占业务页登记(暂无;新增时按上方示例添加)
export const EXCLUSIVE_ROUTES: ExclusiveRoute[] = []
/**
* 返回"非指定端"的独占路由,供该端生成 device-only 提示存根。
* 例forDevice='mobile' → 返回所有 device='pc' 的独占项(供移动端生成存根)
*/
export function otherDeviceExclusives(forDevice: 'pc' | 'mobile'): ExclusiveRoute[] {
return EXCLUSIVE_ROUTES.filter(r => r.device !== forDevice)
}

View File

@@ -0,0 +1,18 @@
/**
* 业务路由 path 常量
*
* 两端路由mobile/pc注册路由时必须从此引用杜绝 path 字面量漂移。
* 跨端页(两端都实现)尤其关键:必须用同一常量保证 path 形态完全一致。
*
* 注意本常量仅用于路由注册route record 的 path 字段)。
* 导航时请用 router.push({ name, params }),不要手工拼接 path 字符串。
*
* 新增业务页示例(按需取消注释并配合对应 view
* 跨端页(如收银台): CASHIER: '/cashier/:id',
* PC 独占如商户后台MERCHANT_DASHBOARD: '/merchant/dashboard',
* 移动独占如微信绑定WECHAT_BIND: '/wechat/bind',
*/
export const RoutePath = {
/** 首页(跨端) */
HOME: '/',
} as const