Compare commits

..
7 changed files with 328 additions and 861 deletions
+117 -2
View File
@@ -1,10 +1,75 @@
<template>
<div id="app">
<div id="app" :class="{ 'embedded-shell': isInUniAppWebView }">
<router-view />
<!-- 底部菜单暂不展示,多分身能力统一从分身卡片进入。 -->
<!-- 底部导航栏 -->
<nav class="bottom-nav" v-if="showNav">
<button
class="nav-item"
:class="{ active: currentRoute === '/' || currentRoute === '/avatar/manage' }"
@click="navigateTo('/avatar/manage')"
>
<span class="nav-icon">🤖</span>
<span class="nav-label">我的分身</span>
</button>
<button
class="nav-item"
:class="{ active: currentRoute === '/authorization' }"
@click="navigateTo('/authorization')"
>
<span class="nav-icon">🔑</span>
<span class="nav-label">授权管理</span>
</button>
<button
v-if="isInUniAppWebView"
class="nav-item"
:class="{ active: currentRoute === '/token/charge' }"
@click="navigateTo('/token/charge')"
>
<span class="nav-icon">💰</span>
<span class="nav-label">充值购买</span>
</button>
</nav>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { isInUniWebView } from '@/utils/uniapp-bridge'
const router = useRouter()
const route = useRoute()
const isInUniAppWebView = isInUniWebView()
const currentRoute = ref<string>(route.path)
const showNav = ref<boolean>(shouldShowNav(route.path))
function shouldShowNav(path: string) {
return path !== '/'
&& path !== '/avatar/create'
&& path !== '/login/sms'
&& !path.startsWith('/avatar/edit')
&& !path.startsWith('/avatar/chat')
&& !path.startsWith('/share/')
}
// 监听路由变化
watch(() => route.path, (newPath) => {
currentRoute.value = newPath
showNav.value = shouldShowNav(newPath)
})
// 导航
const navigateTo = (path: string) => {
router.push(path)
}
onMounted(() => {
currentRoute.value = route.path
showNav.value = shouldShowNav(route.path)
})
</script>
<style>
* {
margin: 0;
@@ -18,4 +83,54 @@
background: #F8F9FA;
padding-bottom: env(safe-area-inset-bottom);
}
/* 原生 App / 微信小程序容器已经提供了自己的导航栏,H5 不再重复显示页头。 */
.embedded-shell .page-header,
.embedded-shell .chat-header {
display: none;
}
/* 底部导航栏 */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
background: white;
border-top: 1px solid #EDEEF1;
padding-bottom: env(safe-area-inset-bottom);
z-index: 100;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px 0;
background: none;
border: none;
cursor: pointer;
transition: color 0.2s;
}
.nav-icon {
font-size: 20px;
}
.nav-label {
font-size: 11px;
color: #9398AE;
font-weight: 500;
}
.nav-item.active .nav-label {
color: #F97316;
}
.nav-item.active .nav-icon {
filter: none;
}
</style>
+6 -1
View File
@@ -1,6 +1,7 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { getAuthToken } from '@/api'
import { isInUniWebView } from '@/utils/uniapp-bridge'
const routes: RouteRecordRaw[] = [
{
@@ -55,7 +56,7 @@ const routes: RouteRecordRaw[] = [
path: '/token/charge',
name: 'TokenCharge',
component: () => import('@/views/TokenCharge.vue'),
meta: { title: '积分充值', requiresAuth: true }
meta: { title: '积分充值', requiresAuth: true, requiresUniWebView: true }
},
{
path: '/avatar/card',
@@ -133,6 +134,10 @@ const router = createRouter({
router.beforeEach((to, from, next) => {
document.title = to.meta.title as string || '会会数字分身'
if (to.meta.requiresUniWebView && !isInUniWebView()) {
next({ path: '/avatar/manage' })
return
}
const hasLocalSession = Boolean(localStorage.getItem('hh_app_token'))
const hasInjectedSession = Boolean(getAuthToken())
if (to.meta.requiresAuth && !hasLocalSession && !hasInjectedSession) {
+16 -2
View File
@@ -12,9 +12,10 @@ export interface UniLaunchParams {
nickname?: string
avatar?: string
ts?: string
nativeShell?: string
}
const PARAM_KEYS: (keyof UniLaunchParams)[] = ['token', 'userId', 'nickname', 'avatar', 'ts']
const PARAM_KEYS: (keyof UniLaunchParams)[] = ['token', 'userId', 'nickname', 'avatar', 'ts', 'nativeShell']
function readParams(search: string, target: UniLaunchParams): void {
const sp = new URLSearchParams(search)
@@ -24,6 +25,16 @@ function readParams(search: string, target: UniLaunchParams): void {
}
}
function hasNativeShellMarker(): boolean {
const params = getLaunchParams()
if (params.nativeShell === 'uniapp') return true
// Compatibility for already-installed shells. They have always appended a
// timestamp together with the native SSO context, even before the explicit
// nativeShell marker existed.
return Boolean(params.ts && (params.token || params.userId))
}
// 是否运行在 uniapp web-view 环境中
export function isInUniWebView(): boolean {
const runtime = window as any
@@ -40,7 +51,10 @@ export function isInUniWebView(): boolean {
runtime.swan?.webView ||
runtime.tt?.miniProgram
)
return Boolean(runtime.uni?.webView && (isDCloudApp || isMiniProgram))
// `plus` can be injected after the H5 entry point runs. The native shell
// therefore adds a URL marker while creating its web-view URL, so the
// payment entry does not disappear during that startup window.
return Boolean(runtime.uni?.webView && (isDCloudApp || isMiniProgram || hasNativeShellMarker()))
}
// 解析 web-view 加载 URL 时原生注入的参数(token / 会会用户)
File diff suppressed because it is too large Load Diff
@@ -20,7 +20,7 @@
</div>
</section>
<!-- 积分余额条:暂时隐藏,保留完整实现便于后续恢复。 -->
<!-- 积分余额条:仅在 uni-app 原生壳内开放充值购买。 -->
<section v-if="SHOW_POINTS_BALANCE_CARD" class="token-section">
<div class="token-card">
<div class="token-info">
@@ -28,7 +28,7 @@
<span class="token-amount">{{ tokenBalance.toLocaleString() }}</span>
<span class="token-used">累计使用 {{ tokenConsumed.toLocaleString() }}</span>
</div>
<button class="recharge-btn" @click="goToRecharge">充值</button>
<button class="recharge-btn" @click="goToRecharge">充值购买</button>
</div>
</section>
@@ -89,14 +89,15 @@ import { useAvatarStore } from '@/store/avatar'
import { useUserStore } from '@/store/user'
import { createAvatarShareLink } from '@/api'
import { isHuihuiEmbeddedMode } from '@/utils/embed-mode'
import { isInUniWebView } from '@/utils/uniapp-bridge'
const router = useRouter()
const avatarStore = useAvatarStore()
const userStore = useUserStore()
const isEmbedded = isHuihuiEmbeddedMode()
// 临时产品开关:余额卡片代码保留,后续改为 true 即可恢复展示。
const SHOW_POINTS_BALANCE_CARD = false
// 充值购买只在 uni-app 原生壳内提供,避免普通 H5 进入支付链路。
const SHOW_POINTS_BALANCE_CARD = isInUniWebView()
// 当前登录会会用户的资料(头像 / 昵称)
const me = computed(() => userStore.user)
+6
View File
@@ -13,6 +13,12 @@ import { requestAppPayment } from '@/utils/payment'
const userStore = useUserStore()
const webview = ref(null)
const H5_BASE_URL = import.meta.env.VITE_AVATAR_H5_URL || 'https://digital.99hui.com/'
// Restore the native shell session before constructing the H5 URL. Without
// this, the web-view opens as an anonymous H5 page and cannot enter the
// authenticated avatar-management route.
userStore.init()
const h5Url = computed(() => buildH5Url(H5_BASE_URL, userStore.$state))
function runtimeSurface() {
+3
View File
@@ -1,5 +1,8 @@
export function buildH5Url(base, session) {
const url = new URL(base)
// This is deliberately explicit instead of relying on the timing of the
// H5+ `plus` injection inside the embedded page.
url.searchParams.set('nativeShell', 'uniapp')
if (session.token) url.searchParams.set('token', session.token)
if (session.userId) url.searchParams.set('userId', session.userId)
if (session.nickname) url.searchParams.set('nickname', session.nickname)