Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4389612d1 | ||
|
|
ac5a332bce | ||
|
|
9ba89ef2d1 | ||
|
|
b1023eb783 | ||
|
|
2f287ef538 | ||
|
|
f5cbbe9eef | ||
|
|
5fc56143ee | ||
|
|
d521585bf2 | ||
|
|
35e47bf0a1 | ||
|
|
f52e42d9c0 | ||
|
|
1ab56ad0b1 |
@@ -1,10 +1,75 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app" :class="{ 'embedded-shell': isInUniAppWebView }">
|
||||||
<router-view />
|
<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>
|
</div>
|
||||||
</template>
|
</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>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -18,4 +83,54 @@
|
|||||||
background: #F8F9FA;
|
background: #F8F9FA;
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
import { getAuthToken } from '@/api'
|
import { getAuthToken } from '@/api'
|
||||||
|
import { isInUniWebView } from '@/utils/uniapp-bridge'
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
@@ -55,7 +56,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/token/charge',
|
path: '/token/charge',
|
||||||
name: 'TokenCharge',
|
name: 'TokenCharge',
|
||||||
component: () => import('@/views/TokenCharge.vue'),
|
component: () => import('@/views/TokenCharge.vue'),
|
||||||
meta: { title: '积分充值', requiresAuth: true }
|
meta: { title: '积分充值', requiresAuth: true, requiresUniWebView: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/avatar/card',
|
path: '/avatar/card',
|
||||||
@@ -133,6 +134,10 @@ const router = createRouter({
|
|||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
document.title = to.meta.title as string || '会会数字分身'
|
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 hasLocalSession = Boolean(localStorage.getItem('hh_app_token'))
|
||||||
const hasInjectedSession = Boolean(getAuthToken())
|
const hasInjectedSession = Boolean(getAuthToken())
|
||||||
if (to.meta.requiresAuth && !hasLocalSession && !hasInjectedSession) {
|
if (to.meta.requiresAuth && !hasLocalSession && !hasInjectedSession) {
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ export interface UniLaunchParams {
|
|||||||
nickname?: string
|
nickname?: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
ts?: 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 {
|
function readParams(search: string, target: UniLaunchParams): void {
|
||||||
const sp = new URLSearchParams(search)
|
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 环境中
|
// 是否运行在 uniapp web-view 环境中
|
||||||
export function isInUniWebView(): boolean {
|
export function isInUniWebView(): boolean {
|
||||||
const runtime = window as any
|
const runtime = window as any
|
||||||
@@ -40,7 +51,10 @@ export function isInUniWebView(): boolean {
|
|||||||
runtime.swan?.webView ||
|
runtime.swan?.webView ||
|
||||||
runtime.tt?.miniProgram
|
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 / 会会用户)
|
// 解析 web-view 加载 URL 时原生注入的参数(token / 会会用户)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 积分余额条:暂时隐藏,保留完整实现便于后续恢复。 -->
|
<!-- 积分余额条:仅在 uni-app 原生壳内开放充值购买。 -->
|
||||||
<section v-if="SHOW_POINTS_BALANCE_CARD" class="token-section">
|
<section v-if="SHOW_POINTS_BALANCE_CARD" class="token-section">
|
||||||
<div class="token-card">
|
<div class="token-card">
|
||||||
<div class="token-info">
|
<div class="token-info">
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<span class="token-amount">{{ tokenBalance.toLocaleString() }}</span>
|
<span class="token-amount">{{ tokenBalance.toLocaleString() }}</span>
|
||||||
<span class="token-used">累计使用 {{ tokenConsumed.toLocaleString() }}</span>
|
<span class="token-used">累计使用 {{ tokenConsumed.toLocaleString() }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="recharge-btn" @click="goToRecharge">充值</button>
|
<button class="recharge-btn" @click="goToRecharge">充值购买</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -89,14 +89,15 @@ import { useAvatarStore } from '@/store/avatar'
|
|||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { createAvatarShareLink } from '@/api'
|
import { createAvatarShareLink } from '@/api'
|
||||||
import { isHuihuiEmbeddedMode } from '@/utils/embed-mode'
|
import { isHuihuiEmbeddedMode } from '@/utils/embed-mode'
|
||||||
|
import { isInUniWebView } from '@/utils/uniapp-bridge'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const avatarStore = useAvatarStore()
|
const avatarStore = useAvatarStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const isEmbedded = isHuihuiEmbeddedMode()
|
const isEmbedded = isHuihuiEmbeddedMode()
|
||||||
|
|
||||||
// 临时产品开关:余额卡片代码保留,后续改为 true 即可恢复展示。
|
// 充值购买只在 uni-app 原生壳内提供,避免普通 H5 进入支付链路。
|
||||||
const SHOW_POINTS_BALANCE_CARD = false
|
const SHOW_POINTS_BALANCE_CARD = isInUniWebView()
|
||||||
|
|
||||||
// 当前登录会会用户的资料(头像 / 昵称)
|
// 当前登录会会用户的资料(头像 / 昵称)
|
||||||
const me = computed(() => userStore.user)
|
const me = computed(() => userStore.user)
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ import { requestAppPayment } from '@/utils/payment'
|
|||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const webview = ref(null)
|
const webview = ref(null)
|
||||||
const H5_BASE_URL = import.meta.env.VITE_AVATAR_H5_URL || 'https://digital.99hui.com/'
|
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))
|
const h5Url = computed(() => buildH5Url(H5_BASE_URL, userStore.$state))
|
||||||
|
|
||||||
function runtimeSurface() {
|
function runtimeSurface() {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
export function buildH5Url(base, session) {
|
export function buildH5Url(base, session) {
|
||||||
const url = new URL(base)
|
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.token) url.searchParams.set('token', session.token)
|
||||||
if (session.userId) url.searchParams.set('userId', session.userId)
|
if (session.userId) url.searchParams.set('userId', session.userId)
|
||||||
if (session.nickname) url.searchParams.set('nickname', session.nickname)
|
if (session.nickname) url.searchParams.set('nickname', session.nickname)
|
||||||
|
|||||||
Reference in New Issue
Block a user