first commit
This commit is contained in:
235
src/composables/uni-router.ts
Normal file
235
src/composables/uni-router.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { pages } from 'virtual:uni-pages'
|
||||
|
||||
/**
|
||||
* 将 webview 的 vue-router 用法映射到 uni 路由(迁移页面使用)
|
||||
*/
|
||||
const pathToPage: Record<string, string> = {
|
||||
'/': '/pages/index',
|
||||
'/login': '/pages/login',
|
||||
'/historyQuery': '/pages/history-query',
|
||||
'/help': '/pages/help',
|
||||
'/help/detail': '/pages/help-detail',
|
||||
'/help/guide': '/pages/help-guide',
|
||||
'/withdraw': '/pages/withdraw',
|
||||
'/report': '/pages/report-result-webview',
|
||||
'/example': '/pages/report-example-webview',
|
||||
'/app/report': '/pages/report-result-webview',
|
||||
'/app/example': '/pages/report-example-webview',
|
||||
'/privacyPolicy': '/pages/privacy-policy',
|
||||
'/userAgreement': '/pages/user-agreement',
|
||||
'/authorization': '/pages/authorization',
|
||||
'/agentManageAgreement': '/pages/agent-manage-agreement',
|
||||
'/agentSerivceAgreement': '/pages/agent-service-agreement',
|
||||
'/agentServiceAgreement': '/pages/agent-service-agreement',
|
||||
'/payment/result': '/pages/payment-result',
|
||||
'/agent': '/pages/agent',
|
||||
'/agent/promote': '/pages/promote',
|
||||
'/me': '/pages/me',
|
||||
'/cancelAccount': '/pages/cancel-account',
|
||||
}
|
||||
|
||||
const nameToPage: Record<string, string> = {
|
||||
index: '/pages/index',
|
||||
login: '/pages/login',
|
||||
invite: '/pages/invitation',
|
||||
invitation: '/pages/invitation',
|
||||
promote: '/pages/promote',
|
||||
agent: '/pages/agent',
|
||||
history: '/pages/history-query',
|
||||
help: '/pages/help',
|
||||
helpDetail: '/pages/help-detail',
|
||||
helpGuide: '/pages/help-guide',
|
||||
withdraw: '/pages/withdraw',
|
||||
report: '/pages/report-result-webview',
|
||||
example: '/pages/report-example-webview',
|
||||
paymentResult: '/pages/payment-result',
|
||||
privacyPolicy: '/pages/privacy-policy',
|
||||
userAgreement: '/pages/user-agreement',
|
||||
authorization: '/pages/authorization',
|
||||
agentManageAgreement: '/pages/agent-manage-agreement',
|
||||
agentSerivceAgreement: '/pages/agent-service-agreement',
|
||||
agentServiceAgreement: '/pages/agent-service-agreement',
|
||||
me: '/pages/me',
|
||||
cancelAccount: '/pages/cancel-account',
|
||||
}
|
||||
|
||||
function withQuery(url: string, query?: Record<string, string>) {
|
||||
if (!query || !Object.keys(query).length)
|
||||
return url
|
||||
const q = Object.entries(query)
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
||||
.join('&')
|
||||
return `${url}${url.includes('?') ? '&' : '?'}${q}`
|
||||
}
|
||||
|
||||
export function resolveWebToUni(to: string | { name?: string, path?: string, query?: Record<string, string> }): string {
|
||||
if (typeof to === 'string') {
|
||||
const qIdx = to.indexOf('?')
|
||||
const pathOnly = qIdx === -1 ? to : to.slice(0, qIdx)
|
||||
const queryPart = qIdx === -1 ? '' : to.slice(qIdx + 1)
|
||||
|
||||
if (pathOnly.startsWith('/inquire/')) {
|
||||
const feature = pathOnly.replace(/^\/inquire\//, '')
|
||||
const base = `/pages/inquire?feature=${encodeURIComponent(feature)}`
|
||||
return queryPart ? `${base}&${queryPart}` : base
|
||||
}
|
||||
if (pathOnly.startsWith('/agent/invitationAgentApply/')) {
|
||||
const id = pathOnly.replace(/^\/agent\/invitationAgentApply\//, '')
|
||||
const base = `/pages/invitation-agent-apply?linkIdentifier=${encodeURIComponent(id)}`
|
||||
return queryPart ? `${base}&${queryPart}` : base
|
||||
}
|
||||
if (pathOnly.startsWith('/agent/promotionInquire/')) {
|
||||
const id = pathOnly.replace(/^\/agent\/promotionInquire\//, '')
|
||||
const base = `/pages/promotion-inquire?linkIdentifier=${encodeURIComponent(id)}`
|
||||
return queryPart ? `${base}&${queryPart}` : base
|
||||
}
|
||||
if (pathOnly.startsWith('/report/share/')) {
|
||||
const id = pathOnly.replace(/^\/report\/share\//, '')
|
||||
const base = `/pages/report-share?linkIdentifier=${encodeURIComponent(id)}`
|
||||
return queryPart ? `${base}&${queryPart}` : base
|
||||
}
|
||||
if (pathToPage[pathOnly]) {
|
||||
const mapped = pathToPage[pathOnly]
|
||||
return queryPart ? `${mapped}?${queryPart}` : mapped
|
||||
}
|
||||
if (to.startsWith('/pages/'))
|
||||
return to
|
||||
return `/pages/${pathOnly.replace(/^\//, '')}${queryPart ? `?${queryPart}` : ''}`
|
||||
}
|
||||
if (to.path) {
|
||||
const base = pathToPage[to.path] || to.path
|
||||
return withQuery(base, to.query)
|
||||
}
|
||||
if (to.name && nameToPage[to.name])
|
||||
return withQuery(nameToPage[to.name], to.query)
|
||||
return '/pages/index'
|
||||
}
|
||||
|
||||
export function useRouter() {
|
||||
return {
|
||||
push(to: string | { name?: string, path?: string, query?: Record<string, string> }) {
|
||||
const url = resolveWebToUni(to as any)
|
||||
uni.navigateTo({ url })
|
||||
},
|
||||
replace(to: string | { name?: string, path?: string, query?: Record<string, string> }) {
|
||||
const url = resolveWebToUni(to as any)
|
||||
uni.redirectTo({ url })
|
||||
},
|
||||
go() {
|
||||
uni.navigateBack({})
|
||||
},
|
||||
back() {
|
||||
uni.navigateBack({})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** 当前页 uni route,如 `pages/history-query`(无首尾多余斜杠) */
|
||||
export function getCurrentUniRoute(): string {
|
||||
const pages = getCurrentPages()
|
||||
const page = pages[pages.length - 1] as any
|
||||
return (page?.route || 'pages/index').replace(/^\//, '')
|
||||
}
|
||||
|
||||
type UniPageMeta = (typeof pages)[number] & {
|
||||
path?: string
|
||||
auth?: boolean
|
||||
style?: {
|
||||
navigationBarTitleText?: string
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeUniRoute(route?: string) {
|
||||
return (route || 'pages/index').replace(/^\//, '')
|
||||
}
|
||||
|
||||
const pageMetaByRoute = new Map(
|
||||
(pages as UniPageMeta[]).map(page => [normalizeUniRoute(page.path), page]),
|
||||
)
|
||||
|
||||
export function getCurrentPageMeta(): UniPageMeta | undefined {
|
||||
return pageMetaByRoute.get(getCurrentUniRoute())
|
||||
}
|
||||
|
||||
export function getPageTitleByRoute(route = getCurrentUniRoute()): string {
|
||||
return pageMetaByRoute.get(normalizeUniRoute(route))?.style?.navigationBarTitleText || 'BDRP'
|
||||
}
|
||||
|
||||
export function getLayoutPageTitle(): string {
|
||||
return getPageTitleByRoute()
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 webview vue-router path 对齐,用于全局通知 notificationPage 匹配
|
||||
*/
|
||||
const UNI_TO_WEB_NOTIFY_PATH: Record<string, string> = {
|
||||
'pages/index': '/',
|
||||
'pages/agent': '/agent',
|
||||
'pages/me': '/me',
|
||||
'pages/promote': '/agent/promote',
|
||||
'pages/history-query': '/historyQuery',
|
||||
'pages/help': '/help',
|
||||
'pages/help-detail': '/help/detail',
|
||||
'pages/help-guide': '/help/guide',
|
||||
'pages/withdraw': '/withdraw',
|
||||
'pages/report-result-webview': '/app/report',
|
||||
'pages/report-example-webview': '/app/example',
|
||||
'pages/privacy-policy': '/privacyPolicy',
|
||||
'pages/user-agreement': '/userAgreement',
|
||||
'pages/agent-manage-agreement': '/agentManageAgreement',
|
||||
'pages/agent-service-agreement': '/agentSerivceAgreement',
|
||||
'pages/authorization': '/authorization',
|
||||
'pages/payment-result': '/payment/result',
|
||||
'pages/inquire': '/inquire',
|
||||
'pages/login': '/login',
|
||||
'pages/invitation': '/agent/invitation',
|
||||
'pages/agent-promote-details': '/agent/promoteDetails',
|
||||
'pages/agent-rewards-details': '/agent/rewardsDetails',
|
||||
'pages/agent-vip': '/agent/agentVip',
|
||||
'pages/agent-vip-apply': '/agent/vipApply',
|
||||
'pages/agent-vip-config': '/agent/vipConfig',
|
||||
'pages/withdraw-details': '/agent/withdrawDetails',
|
||||
'pages/subordinate-list': '/agent/subordinateList',
|
||||
}
|
||||
|
||||
export function getWebPathForNotification(): string {
|
||||
const r = getCurrentUniRoute()
|
||||
const pages = getCurrentPages()
|
||||
const page = pages[pages.length - 1] as any
|
||||
const q: Record<string, string> = { ...(page?.options || {}) }
|
||||
if (r === 'pages/inquire' && q.feature)
|
||||
return `/inquire/${q.feature}`
|
||||
if (r === 'pages/subordinate-detail' && q.id)
|
||||
return `/agent/subordinateDetail/${q.id}`
|
||||
if (r === 'pages/invitation-agent-apply' && q.linkIdentifier)
|
||||
return `/agent/invitationAgentApply/${q.linkIdentifier}`
|
||||
if (r === 'pages/promotion-inquire' && q.linkIdentifier)
|
||||
return `/agent/promotionInquire/${q.linkIdentifier}`
|
||||
if (r === 'pages/report-share' && q.linkIdentifier)
|
||||
return `/report/share/${q.linkIdentifier}`
|
||||
return UNI_TO_WEB_NOTIFY_PATH[r] || '/'
|
||||
}
|
||||
|
||||
const uniRouteToName: Record<string, string> = {
|
||||
'pages/index': 'index',
|
||||
'pages/agent': 'agent',
|
||||
'pages/me': 'me',
|
||||
'pages/promote': 'promote',
|
||||
}
|
||||
|
||||
export function useRoute() {
|
||||
const pages = getCurrentPages()
|
||||
const page = pages[pages.length - 1] as any
|
||||
const query: Record<string, string> = { ...(page?.options || {}) }
|
||||
const params: Record<string, string> = { ...query }
|
||||
const routeKey = normalizeUniRoute(page?.route)
|
||||
return {
|
||||
query,
|
||||
path: page?.route ? `/${page.route}` : '/',
|
||||
params,
|
||||
name: uniRouteToName[routeKey] || routeKey,
|
||||
meta: {
|
||||
title: getPageTitleByRoute(routeKey),
|
||||
},
|
||||
}
|
||||
}
|
||||
196
src/composables/useApiFetch.ts
Normal file
196
src/composables/useApiFetch.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 与 bdrp-webview `useApiFetch.js` 行为对齐:链式 `.get().json()` / `.post().json()`,
|
||||
* 返回 `{ data, error }` 的 Ref(与迁移过来的页面兼容)。
|
||||
*/
|
||||
import type { Ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { envConfig } from '@/constants/env'
|
||||
import { useAgentStore } from '@/stores/agentStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { navigateLogin } from '@/utils/navigate'
|
||||
import { clearAuthStorage, getToken } from '@/utils/storage'
|
||||
|
||||
export interface ApiEnvelope<T = unknown> {
|
||||
code: number
|
||||
msg: string
|
||||
data: T
|
||||
}
|
||||
|
||||
let loadingCount = 0
|
||||
function showLoading() {
|
||||
loadingCount++
|
||||
if (loadingCount === 1)
|
||||
uni.showLoading({ title: '加载中...', mask: true })
|
||||
}
|
||||
function hideLoading() {
|
||||
loadingCount = Math.max(0, loadingCount - 1)
|
||||
if (loadingCount === 0)
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
function appendTimestamp(url: string) {
|
||||
const sep = url.includes('?') ? '&' : '?'
|
||||
return `${url}${sep}t=${Date.now()}`
|
||||
}
|
||||
|
||||
function joinApiUrl(path: string) {
|
||||
const base = envConfig.apiBaseUrl.replace(/\/$/, '')
|
||||
const p = path.startsWith('/') ? path : `/${path}`
|
||||
return appendTimestamp(`${base}${p}`)
|
||||
}
|
||||
|
||||
/** 本仓库仅面向 APP;与后端 `model.PlatformApp` / ctx `platform` 一致,须为小写 `app` */
|
||||
const REQUEST_PLATFORM_APP = 'app'
|
||||
|
||||
async function handleErrorCode<T>(payload: ApiEnvelope<T>) {
|
||||
if (payload.code === 100009) {
|
||||
clearAuthStorage()
|
||||
const userStore = useUserStore()
|
||||
const agentStore = useAgentStore()
|
||||
userStore.resetUser()
|
||||
agentStore.resetAgent()
|
||||
uni.reLaunch({ url: '/pages/index' })
|
||||
return
|
||||
}
|
||||
if (payload.code === 100011) {
|
||||
uni.showToast({ title: payload.msg || '账号已被封禁', icon: 'none' })
|
||||
clearAuthStorage()
|
||||
const userStore = useUserStore()
|
||||
const agentStore = useAgentStore()
|
||||
userStore.resetUser()
|
||||
agentStore.resetAgent()
|
||||
navigateLogin()
|
||||
return
|
||||
}
|
||||
if (payload.code === 100013) {
|
||||
uni.showToast({ title: payload.msg || '账号已注销', icon: 'none' })
|
||||
clearAuthStorage()
|
||||
const userStore = useUserStore()
|
||||
const agentStore = useAgentStore()
|
||||
userStore.resetUser()
|
||||
agentStore.resetAgent()
|
||||
navigateLogin()
|
||||
return
|
||||
}
|
||||
if (
|
||||
payload.code !== 200002
|
||||
&& payload.code !== 200003
|
||||
&& payload.code !== 200004
|
||||
&& payload.code !== 100009
|
||||
&& payload.code !== 100011
|
||||
&& payload.code !== 100013
|
||||
) {
|
||||
uni.showToast({ title: payload.msg || '请求失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function uniRequest<T>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
url: string,
|
||||
data?: unknown,
|
||||
): Promise<{ statusCode: number, data: ApiEnvelope<T> }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = getToken()
|
||||
uni.request({
|
||||
url: joinApiUrl(url),
|
||||
method,
|
||||
data: data as Record<string, unknown> | undefined,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Platform': REQUEST_PLATFORM_APP,
|
||||
...(token ? { Authorization: token } : {}),
|
||||
},
|
||||
success: (res) => {
|
||||
resolve({
|
||||
statusCode: res.statusCode || 0,
|
||||
data: res.data as ApiEnvelope<T>,
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function executeJson<T>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
url: string,
|
||||
data?: unknown,
|
||||
): Promise<{ data: Ref<ApiEnvelope<T> | null>, error: Ref<Error | null> }> {
|
||||
const dataRef = ref<ApiEnvelope<T> | null>(null) as Ref<ApiEnvelope<T> | null>
|
||||
const errorRef = ref<Error | null>(null)
|
||||
showLoading()
|
||||
try {
|
||||
const { statusCode, data: body } = await uniRequest<T>(method, url, data)
|
||||
hideLoading()
|
||||
|
||||
if (statusCode === 401) {
|
||||
clearAuthStorage()
|
||||
navigateLogin()
|
||||
errorRef.value = new Error('401')
|
||||
return { data: dataRef, error: errorRef }
|
||||
}
|
||||
if (statusCode === 403) {
|
||||
const b = body as ApiEnvelope<unknown> | null | undefined
|
||||
const toastTitle = b?.code === 100013
|
||||
? (b.msg || '账号已注销')
|
||||
: (b?.msg || '账号已被封禁')
|
||||
uni.showToast({ title: toastTitle, icon: 'none' })
|
||||
clearAuthStorage()
|
||||
useUserStore().resetUser()
|
||||
useAgentStore().resetAgent()
|
||||
navigateLogin()
|
||||
errorRef.value = new Error('403')
|
||||
return { data: dataRef, error: errorRef }
|
||||
}
|
||||
|
||||
if (!body || typeof body.code !== 'number') {
|
||||
errorRef.value = new Error('响应格式不正确')
|
||||
return { data: dataRef, error: errorRef }
|
||||
}
|
||||
|
||||
dataRef.value = body
|
||||
|
||||
if (body.code !== 200)
|
||||
await handleErrorCode(body)
|
||||
|
||||
return { data: dataRef, error: errorRef }
|
||||
}
|
||||
catch (e) {
|
||||
hideLoading()
|
||||
const err = e instanceof Error ? e : new Error(String(e))
|
||||
errorRef.value = err
|
||||
uni.showToast({ title: '网络异常,请稍后再试', icon: 'none' })
|
||||
return { data: dataRef, error: errorRef }
|
||||
}
|
||||
}
|
||||
|
||||
function chain(url: string) {
|
||||
return {
|
||||
get() {
|
||||
return {
|
||||
json: <T>() => executeJson<T>('GET', url),
|
||||
}
|
||||
},
|
||||
post(data?: unknown) {
|
||||
return {
|
||||
json: <T>() => executeJson<T>('POST', url, data),
|
||||
}
|
||||
},
|
||||
put(data?: unknown) {
|
||||
return {
|
||||
json: <T>() => executeJson<T>('PUT', url, data),
|
||||
}
|
||||
},
|
||||
delete() {
|
||||
return {
|
||||
json: <T>() => executeJson<T>('DELETE', url),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function useApiFetch(url: string) {
|
||||
return chain(url)
|
||||
}
|
||||
29
src/composables/useAppBootstrap.ts
Normal file
29
src/composables/useAppBootstrap.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { installNavigationAuthGuard } from '@/composables/useNavigationAuthGuard'
|
||||
// #ifdef APP-PLUS
|
||||
import { useHotUpdate } from '@/composables/useHotUpdate'
|
||||
// #endif
|
||||
import { useAgentStore } from '@/stores/agentStore'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
|
||||
export function useAppBootstrap() {
|
||||
const authStore = useAuthStore()
|
||||
const userStore = useUserStore()
|
||||
const agentStore = useAgentStore()
|
||||
|
||||
const bootstrap = async () => {
|
||||
installNavigationAuthGuard()
|
||||
userStore.restoreFromStorage()
|
||||
agentStore.restoreFromStorage()
|
||||
if (authStore.hasToken) {
|
||||
await Promise.allSettled([userStore.fetchUserInfo(), agentStore.fetchAgentStatus()])
|
||||
}
|
||||
authStore.markReady()
|
||||
// #ifdef APP-PLUS
|
||||
const { checkUpdate } = useHotUpdate()
|
||||
void checkUpdate()
|
||||
// #endif
|
||||
}
|
||||
|
||||
return { bootstrap }
|
||||
}
|
||||
11
src/composables/useAuthGuard.ts
Normal file
11
src/composables/useAuthGuard.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { getToken } from '@/utils/storage'
|
||||
|
||||
export function useAuthGuard() {
|
||||
const ensureLogin = () => {
|
||||
if (getToken())
|
||||
return true
|
||||
uni.navigateTo({ url: '/pages/login' })
|
||||
return false
|
||||
}
|
||||
return { ensureLogin }
|
||||
}
|
||||
16
src/composables/useCount.ts
Normal file
16
src/composables/useCount.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export function useCount() {
|
||||
const count = ref(Math.round(Math.random() * 20))
|
||||
|
||||
function inc() {
|
||||
count.value += 1
|
||||
}
|
||||
function dec() {
|
||||
count.value -= 1
|
||||
}
|
||||
|
||||
return {
|
||||
count,
|
||||
inc,
|
||||
dec,
|
||||
}
|
||||
}
|
||||
153
src/composables/useCustomerService.ts
Normal file
153
src/composables/useCustomerService.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { envConfig } from '@/constants/env'
|
||||
|
||||
function formatErr(e: unknown): string {
|
||||
if (e == null)
|
||||
return '未知错误'
|
||||
if (typeof e === 'string')
|
||||
return e
|
||||
if (e instanceof Error)
|
||||
return e.message
|
||||
try {
|
||||
const o = e as Record<string, unknown>
|
||||
if (typeof o.message === 'string')
|
||||
return o.message
|
||||
return JSON.stringify(e)
|
||||
}
|
||||
catch {
|
||||
return String(e)
|
||||
}
|
||||
}
|
||||
|
||||
function getErrCode(err: unknown): number | undefined {
|
||||
if (err && typeof err === 'object' && 'code' in err) {
|
||||
const c = (err as { code: unknown }).code
|
||||
return typeof c === 'number' ? c : undefined
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 微信 SDK 返回「接口不支持」类错误(常见于自定义基座未集成客服能力,仍为 -3) */
|
||||
function isWeixinSdkCustomerServiceUnsupported(err: unknown): boolean {
|
||||
if (getErrCode(err) === -3)
|
||||
return true
|
||||
const m = formatErr(err)
|
||||
return m.includes('不支持') || m.includes('此功能')
|
||||
}
|
||||
|
||||
/** 用系统能力打开客服链接(唤起微信或系统浏览器处理 work.weixin.qq.com) */
|
||||
function tryOpenUrlWithRuntime(url: string): boolean {
|
||||
try {
|
||||
if (typeof plus !== 'undefined' && plus.runtime?.openURL) {
|
||||
plus.runtime.openURL(
|
||||
url,
|
||||
(err: unknown) => {
|
||||
console.error('[客服] plus.runtime.openURL 失败', err, 'url=', url)
|
||||
uni.showToast({ title: `无法打开链接: ${formatErr(err)}`, icon: 'none' })
|
||||
},
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('[客服] plus.runtime.openURL 异常', e)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 投诉 / 联系客服(App)
|
||||
* - 默认先走微信 `openCustomerServiceChat`;若返回 -3「此功能不支持」则改用 `plus.runtime.openURL` 打开客服链接(企业微信 H5 客服页仍可进线)。
|
||||
* - 设置 `VITE_CUSTOMER_SERVICE_SKIP_SDK=1` 可跳过 SDK,始终用系统打开链接(推荐在一直报 -3 的环境使用)。
|
||||
*/
|
||||
export function openCustomerService() {
|
||||
const url = (envConfig.customerServiceUrl || '').trim()
|
||||
const corpId = (envConfig.wxworkCorpId || '').trim()
|
||||
|
||||
if (!url) {
|
||||
console.error('[客服] 未配置 VITE_CUSTOMER_SERVICE_URL')
|
||||
uni.showToast({ title: '未配置客服地址', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof plus === 'undefined') {
|
||||
console.error('[客服] 非 App 环境,无 5+ Runtime')
|
||||
uni.showToast({ title: '仅 App 内支持', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (envConfig.customerServiceSkipSdk) {
|
||||
console.warn('[客服] 已配置 SKIP_SDK,直接使用 plus.runtime.openURL')
|
||||
uni.showToast({ title: '正在打开客服…', icon: 'none' })
|
||||
if (!tryOpenUrlWithRuntime(url))
|
||||
uni.showToast({ title: '无法打开客服链接', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!plus.share?.getServices) {
|
||||
console.error('[客服] plus.share.getServices 不存在')
|
||||
uni.showToast({ title: '正在打开客服…', icon: 'none' })
|
||||
if (!tryOpenUrlWithRuntime(url))
|
||||
uni.showToast({ title: '当前环境无法打开客服', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
plus.share.getServices(
|
||||
(services) => {
|
||||
console.warn('[客服] getServices 成功', services?.map(s => ({ id: s.id, description: s.description, nativeClient: s.nativeClient })))
|
||||
|
||||
const weixin = services?.find(s => String(s.id) === 'weixin')
|
||||
if (!weixin) {
|
||||
console.error('[客服] 分享服务列表中无微信(weixin),完整列表:', services)
|
||||
uni.showToast({ title: '正在打开客服…', icon: 'none' })
|
||||
tryOpenUrlWithRuntime(url)
|
||||
return
|
||||
}
|
||||
|
||||
const openChat = (weixin as { openCustomerServiceChat?: (opts: { corpid: string, url: string }, ok?: () => void, fail?: (e: unknown) => void) => void }).openCustomerServiceChat
|
||||
if (typeof openChat !== 'function') {
|
||||
console.error('[客服] 无 openCustomerServiceChat 方法', weixin)
|
||||
uni.showToast({ title: '正在打开客服…', icon: 'none' })
|
||||
tryOpenUrlWithRuntime(url)
|
||||
return
|
||||
}
|
||||
|
||||
if (!corpId) {
|
||||
console.error('[客服] 未配置 VITE_WXWORK_CORP_ID,无法调用 openCustomerServiceChat,改为 openURL')
|
||||
uni.showToast({ title: '正在打开客服…', icon: 'none' })
|
||||
tryOpenUrlWithRuntime(url)
|
||||
return
|
||||
}
|
||||
|
||||
const opts = { corpid: corpId, url }
|
||||
console.warn('[客服] 调用 openCustomerServiceChat', opts)
|
||||
|
||||
openChat(
|
||||
opts,
|
||||
() => {
|
||||
console.warn('[客服] openCustomerServiceChat 成功')
|
||||
},
|
||||
(err: unknown) => {
|
||||
console.error('[客服] openCustomerServiceChat 失败', err)
|
||||
|
||||
if (isWeixinSdkCustomerServiceUnsupported(err)) {
|
||||
console.warn(
|
||||
'[客服] 当前运行环境微信 SDK 不支持原生客服接口(常见:自定义基座未集成/需勾选微信客服模块)。已改为用系统打开客服链接。',
|
||||
err,
|
||||
)
|
||||
uni.showToast({ title: '正在打开客服…', icon: 'none' })
|
||||
tryOpenUrlWithRuntime(url)
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: `拉起客服失败: ${formatErr(err)}`, icon: 'none', duration: 3000 })
|
||||
tryOpenUrlWithRuntime(url)
|
||||
},
|
||||
)
|
||||
},
|
||||
(err: unknown) => {
|
||||
console.error('[客服] getServices 失败', err)
|
||||
uni.showToast({ title: '正在打开客服…', icon: 'none' })
|
||||
tryOpenUrlWithRuntime(url)
|
||||
},
|
||||
)
|
||||
}
|
||||
10
src/composables/useEnv.js
Normal file
10
src/composables/useEnv.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
/** App 端固定非微信 H5;保留 isWeChat 以兼容迁移代码分支 */
|
||||
const isWeChat = ref(false)
|
||||
|
||||
export function useEnv() {
|
||||
return {
|
||||
isWeChat,
|
||||
}
|
||||
}
|
||||
261
src/composables/useHotUpdate.ts
Normal file
261
src/composables/useHotUpdate.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { ref } from 'vue'
|
||||
import { envConfig } from '@/constants/env'
|
||||
|
||||
export interface AppVersionPayload {
|
||||
version: string
|
||||
wgtUrl: string
|
||||
}
|
||||
|
||||
function joinApiPath(path: string): string {
|
||||
const base = envConfig.apiBaseUrl.replace(/\/$/, '')
|
||||
const p = path.startsWith('/') ? path : `/${path}`
|
||||
return `${base}${p}`
|
||||
}
|
||||
|
||||
function compareVersion(v1: string, v2: string): number {
|
||||
const v1Parts = v1.split('.').map(Number)
|
||||
const v2Parts = v2.split('.').map(Number)
|
||||
const len = Math.max(v1Parts.length, v2Parts.length)
|
||||
for (let i = 0; i < len; i++) {
|
||||
const a = v1Parts[i] ?? 0
|
||||
const b = v2Parts[i] ?? 0
|
||||
if (a > b)
|
||||
return 1
|
||||
if (a < b)
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/** APP 端 WGT 热更新:请求 `/app/version`,比对版本后静默下载安装 */
|
||||
export function useHotUpdate() {
|
||||
const updating = ref(false)
|
||||
const hasNewVersion = ref(false)
|
||||
const currentVersion = ref('')
|
||||
const latestVersion = ref('')
|
||||
const downloadProgress = ref(0)
|
||||
const serverWgtUrl = ref('')
|
||||
|
||||
const getCurrentVersion = (): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
// #ifdef APP-PLUS
|
||||
const appid = plus.runtime.appid
|
||||
if (!appid) {
|
||||
currentVersion.value = '0.0.0'
|
||||
resolve('0.0.0')
|
||||
return
|
||||
}
|
||||
plus.runtime.getProperty(appid, (inf) => {
|
||||
const wgtVer = inf.version ?? '0.0.0'
|
||||
currentVersion.value = wgtVer
|
||||
resolve(wgtVer)
|
||||
})
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
const defaultVersion = '0.0.0'
|
||||
currentVersion.value = defaultVersion
|
||||
resolve(defaultVersion)
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
/** 不弹业务 toast:版本检查失败时静默 */
|
||||
function fetchAppVersion(): Promise<AppVersionPayload | null> {
|
||||
return new Promise((resolve) => {
|
||||
uni.request({
|
||||
url: joinApiPath('/app/version'),
|
||||
method: 'GET',
|
||||
success: (res) => {
|
||||
const body = res.data as { code?: number, data?: AppVersionPayload }
|
||||
if (res.statusCode !== 200 || !body || body.code !== 200 || !body.data) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
resolve(body.data)
|
||||
},
|
||||
fail: () => resolve(null),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const checkVersionOnly = async (): Promise<boolean> => {
|
||||
try {
|
||||
await getCurrentVersion()
|
||||
const serverInfo = await fetchAppVersion()
|
||||
if (!serverInfo) {
|
||||
hasNewVersion.value = false
|
||||
return false
|
||||
}
|
||||
latestVersion.value = serverInfo.version
|
||||
if (serverInfo.wgtUrl)
|
||||
serverWgtUrl.value = serverInfo.wgtUrl
|
||||
hasNewVersion.value = compareVersion(serverInfo.version, currentVersion.value) > 0
|
||||
return hasNewVersion.value
|
||||
}
|
||||
catch (e) {
|
||||
console.error('[wgt] 检查版本失败', e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const checkUpdate = async () => {
|
||||
try {
|
||||
await getCurrentVersion()
|
||||
const serverInfo = await fetchAppVersion()
|
||||
if (!serverInfo)
|
||||
return
|
||||
latestVersion.value = serverInfo.version
|
||||
if (compareVersion(serverInfo.version, currentVersion.value) <= 0) {
|
||||
hasNewVersion.value = false
|
||||
return
|
||||
}
|
||||
hasNewVersion.value = true
|
||||
if (serverInfo.wgtUrl) {
|
||||
serverWgtUrl.value = serverInfo.wgtUrl
|
||||
silentUpdate(serverInfo.wgtUrl).catch((err) => {
|
||||
console.error('[wgt] 静默更新失败', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('[wgt] 检查更新失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
const silentUpdate = (wgtUrl: string): Promise<void> => {
|
||||
if (updating.value)
|
||||
return Promise.reject(new Error('更新已在进行中'))
|
||||
updating.value = true
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef APP-PLUS
|
||||
const dtask = plus.downloader.createDownload(
|
||||
wgtUrl,
|
||||
{ filename: '_doc/update/' },
|
||||
(download, status) => {
|
||||
if (status === 200) {
|
||||
const fp = download.filename
|
||||
if (!fp) {
|
||||
updating.value = false
|
||||
reject(new Error('下载路径无效'))
|
||||
return
|
||||
}
|
||||
installing(fp)
|
||||
.then(() => {
|
||||
updating.value = false
|
||||
resolve()
|
||||
})
|
||||
.catch((err) => {
|
||||
updating.value = false
|
||||
reject(err)
|
||||
})
|
||||
}
|
||||
else {
|
||||
updating.value = false
|
||||
reject(new Error('下载更新包失败'))
|
||||
}
|
||||
},
|
||||
)
|
||||
dtask.start()
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
updating.value = false
|
||||
resolve()
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
const installing = (filePath: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.install(
|
||||
filePath,
|
||||
{ force: false },
|
||||
() => {
|
||||
resolve()
|
||||
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
|
||||
try {
|
||||
(entry as { remove?: (cb?: () => void) => void }).remove?.()
|
||||
}
|
||||
catch {
|
||||
/* 忽略删除失败 */
|
||||
}
|
||||
})
|
||||
},
|
||||
(error: unknown) => {
|
||||
console.error('[wgt] 安装失败', error)
|
||||
reject(error)
|
||||
},
|
||||
)
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
resolve()
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
const manualUpdate = (wgtUrl: string): Promise<void> => {
|
||||
if (updating.value)
|
||||
return Promise.reject(new Error('更新已在进行中'))
|
||||
updating.value = true
|
||||
downloadProgress.value = 0
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef APP-PLUS
|
||||
const dtask = plus.downloader.createDownload(
|
||||
wgtUrl,
|
||||
{ filename: '_doc/update/' },
|
||||
(download, status) => {
|
||||
if (status === 200) {
|
||||
const fp = download.filename
|
||||
if (!fp) {
|
||||
updating.value = false
|
||||
reject(new Error('下载路径无效'))
|
||||
return
|
||||
}
|
||||
installing(fp)
|
||||
.then(() => {
|
||||
updating.value = false
|
||||
resolve()
|
||||
})
|
||||
.catch((err) => {
|
||||
updating.value = false
|
||||
reject(err)
|
||||
})
|
||||
}
|
||||
else {
|
||||
updating.value = false
|
||||
reject(new Error('下载更新包失败'))
|
||||
}
|
||||
},
|
||||
)
|
||||
dtask.addEventListener('statechanged', (task) => {
|
||||
if (task.state === 3) {
|
||||
const total = Number(task.totalSize ?? 0)
|
||||
const downloaded = Number(task.downloadedSize ?? 0)
|
||||
downloadProgress.value
|
||||
= total > 0 ? Math.round((downloaded / total) * 100) : 0
|
||||
}
|
||||
else if (task.state === 4) {
|
||||
downloadProgress.value = 100
|
||||
}
|
||||
})
|
||||
dtask.start()
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
updating.value = false
|
||||
resolve()
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
updating,
|
||||
hasNewVersion,
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
downloadProgress,
|
||||
serverWgtUrl,
|
||||
checkUpdate,
|
||||
checkVersionOnly,
|
||||
manualUpdate,
|
||||
}
|
||||
}
|
||||
27
src/composables/useHttp.js
Normal file
27
src/composables/useHttp.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createFetch, useFetch } from '@vueuse/core'
|
||||
|
||||
export function useHttp(url, options = {}, token) {
|
||||
const fetch = createFetch(url, {
|
||||
baseUrl: '/api/v1',
|
||||
options: {
|
||||
async beforeFetch({ url, options, cancel }) {
|
||||
console.log('asdasd', options)
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
Authorization: `${token}`,
|
||||
}
|
||||
return {
|
||||
options,
|
||||
}
|
||||
},
|
||||
async afterFetch(ctx) {
|
||||
console.log('ctx', ctx)
|
||||
// if (ctx.data.code !== 200) {
|
||||
// throw new Error(ctx.data.message || '请求失败');
|
||||
// }
|
||||
return ctx
|
||||
},
|
||||
},
|
||||
})
|
||||
return fetch(url)
|
||||
}
|
||||
163
src/composables/useNavigationAuthGuard.ts
Normal file
163
src/composables/useNavigationAuthGuard.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { pages } from 'virtual:uni-pages'
|
||||
import { useAgentStore } from '@/stores/agentStore'
|
||||
import { getAgentInfo, getToken } from '@/utils/storage'
|
||||
|
||||
interface PagePermission {
|
||||
requiresAuth?: boolean
|
||||
requiresAgent?: boolean
|
||||
}
|
||||
|
||||
const LOGIN_ROUTE = 'pages/login'
|
||||
const AGENT_APPLY_ROUTE = 'pages/invitation-agent-apply'
|
||||
const AGENT_APPLY_URL = '/pages/invitation-agent-apply'
|
||||
const HOME_URL = '/pages/index'
|
||||
|
||||
/**
|
||||
* 对齐 bdrp-webview/src/router/index.js 的鉴权配置
|
||||
* requiresAgent 一律隐含 requiresAuth
|
||||
*/
|
||||
const ROUTE_PERMISSION_MAP: Record<string, PagePermission> = {
|
||||
'pages/promote': { requiresAuth: true, requiresAgent: true },
|
||||
'pages/history-query': { requiresAuth: true },
|
||||
'pages/withdraw': { requiresAuth: true, requiresAgent: true },
|
||||
'pages/payment-result': { requiresAuth: true },
|
||||
'pages/report-result-webview': { requiresAuth: true },
|
||||
'pages/agent-promote-details': { requiresAuth: true, requiresAgent: true },
|
||||
'pages/agent-rewards-details': { requiresAuth: true, requiresAgent: true },
|
||||
'pages/invitation': { requiresAuth: true, requiresAgent: true },
|
||||
'pages/agent-vip': { requiresAuth: true, requiresAgent: true },
|
||||
'pages/agent-vip-apply': { requiresAuth: true, requiresAgent: true },
|
||||
'pages/agent-vip-config': { requiresAuth: true, requiresAgent: true },
|
||||
'pages/withdraw-details': { requiresAuth: true, requiresAgent: true },
|
||||
'pages/invitation-agent-apply': { requiresAuth: true },
|
||||
'pages/subordinate-list': { requiresAuth: true, requiresAgent: true },
|
||||
'pages/subordinate-detail': { requiresAuth: true, requiresAgent: true },
|
||||
}
|
||||
|
||||
const metaAuthMap = new Map(
|
||||
(pages as Array<{ path?: string, auth?: boolean }>).map(page => [(page.path || '').replace(/^\//, ''), Boolean(page.auth)]),
|
||||
)
|
||||
|
||||
function normalizePath(path = '') {
|
||||
return path.replace(/^\//, '').split('?')[0]
|
||||
}
|
||||
|
||||
function parseRouteFromUrl(url = '') {
|
||||
return normalizePath(url)
|
||||
}
|
||||
|
||||
function getPermission(route: string): PagePermission {
|
||||
const conf = ROUTE_PERMISSION_MAP[route] || {}
|
||||
const metaAuth = Boolean(metaAuthMap.get(route))
|
||||
const requiresAgent = Boolean(conf.requiresAgent)
|
||||
const requiresAuth = Boolean(conf.requiresAuth || metaAuth || requiresAgent)
|
||||
return { requiresAuth, requiresAgent }
|
||||
}
|
||||
|
||||
function buildLoginUrl(url: string) {
|
||||
const redirect = encodeURIComponent(url.startsWith('/') ? url : `/${url}`)
|
||||
return `/pages/login?redirect=${redirect}`
|
||||
}
|
||||
|
||||
function isAgentUser() {
|
||||
const agentStore = useAgentStore()
|
||||
if (agentStore.isLoaded)
|
||||
return Boolean(agentStore.isAgent)
|
||||
const agentInfo = getAgentInfo()
|
||||
if (agentInfo && typeof agentInfo === 'object')
|
||||
return Boolean((agentInfo as any).isAgent)
|
||||
return false
|
||||
}
|
||||
|
||||
type GuardMode = 'navigate' | 'redirect'
|
||||
|
||||
function goLogin(url: string, mode: GuardMode) {
|
||||
const targetUrl = buildLoginUrl(url)
|
||||
if (mode === 'redirect')
|
||||
uni.redirectTo({ url: targetUrl })
|
||||
else
|
||||
uni.navigateTo({ url: targetUrl })
|
||||
}
|
||||
|
||||
function goAgentApply(mode: GuardMode) {
|
||||
if (mode === 'redirect')
|
||||
uni.redirectTo({ url: AGENT_APPLY_URL })
|
||||
else
|
||||
uni.navigateTo({ url: AGENT_APPLY_URL })
|
||||
}
|
||||
|
||||
export function ensurePageAccessByUrl(url = '', mode: GuardMode = 'navigate') {
|
||||
const route = parseRouteFromUrl(url)
|
||||
if (!route || route === LOGIN_ROUTE)
|
||||
return false
|
||||
|
||||
const permission = getPermission(route)
|
||||
if (!permission.requiresAuth)
|
||||
return false
|
||||
|
||||
if (!getToken()) {
|
||||
goLogin(url, mode)
|
||||
return true
|
||||
}
|
||||
|
||||
if (permission.requiresAgent && route !== AGENT_APPLY_ROUTE && !isAgentUser()) {
|
||||
goAgentApply(mode)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/** 未套 layout 的页面在 onShow 中与 default 布局一致地做鉴权(如 webview 页使用原生导航栏时) */
|
||||
export function ensureCurrentPageAccess(mode: GuardMode = 'redirect') {
|
||||
const stack = getCurrentPages()
|
||||
const page = stack[stack.length - 1] as { route?: string, options?: Record<string, string> }
|
||||
if (!page?.route)
|
||||
return false
|
||||
const routePath = `/${String(page.route).replace(/^\//, '')}`
|
||||
const query = page.options || {}
|
||||
const qs = Object.keys(query).length
|
||||
? `?${Object.entries(query).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v ?? ''))}`).join('&')}`
|
||||
: ''
|
||||
return ensurePageAccessByUrl(`${routePath}${qs}`, mode)
|
||||
}
|
||||
|
||||
let installed = false
|
||||
|
||||
export function installNavigationAuthGuard() {
|
||||
if (installed)
|
||||
return
|
||||
installed = true
|
||||
|
||||
uni.addInterceptor('navigateTo', {
|
||||
invoke(args) {
|
||||
if (ensurePageAccessByUrl(args?.url, 'navigate'))
|
||||
return false
|
||||
return args
|
||||
},
|
||||
})
|
||||
|
||||
uni.addInterceptor('redirectTo', {
|
||||
invoke(args) {
|
||||
if (ensurePageAccessByUrl(args?.url, 'redirect'))
|
||||
return false
|
||||
return args
|
||||
},
|
||||
})
|
||||
|
||||
uni.addInterceptor('reLaunch', {
|
||||
invoke(args) {
|
||||
if (ensurePageAccessByUrl(args?.url, 'redirect'))
|
||||
return false
|
||||
return args
|
||||
},
|
||||
})
|
||||
|
||||
uni.addInterceptor('switchTab', {
|
||||
invoke(args) {
|
||||
if (ensurePageAccessByUrl(args?.url || HOME_URL, 'redirect'))
|
||||
return false
|
||||
return args
|
||||
},
|
||||
})
|
||||
}
|
||||
8
src/composables/useQuery.ts
Normal file
8
src/composables/useQuery.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function useQuery(key?: MaybeRefOrGetter<string>) {
|
||||
const query = ref<AnyObject>({})
|
||||
onLoad((q) => {
|
||||
query.value = q || {}
|
||||
})
|
||||
const value = computed(() => (key ? query.value[toValue(key)] : null))
|
||||
return { query, value }
|
||||
}
|
||||
78
src/composables/useReportWebview.ts
Normal file
78
src/composables/useReportWebview.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { envConfig } from '@/constants/env'
|
||||
import { getToken } from '@/utils/storage'
|
||||
|
||||
declare function getCurrentPages(): any[]
|
||||
|
||||
/** App 端部分 WebView 无 URLSearchParams,用手动编码保证兼容 */
|
||||
function buildQueryString(params: Record<string, string>): string {
|
||||
const parts: string[] = []
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value === undefined || value === '')
|
||||
continue
|
||||
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
||||
}
|
||||
return parts.join('&')
|
||||
}
|
||||
|
||||
export function useReportWebview() {
|
||||
const resolveBase = () => {
|
||||
return (envConfig.siteOrigin || envConfig.reportBaseUrl || '').replace(/\/$/, '')
|
||||
}
|
||||
|
||||
const buildReportUrl = (page: 'example' | 'report', params: Record<string, string> = {}) => {
|
||||
const token = getToken() || ''
|
||||
const base = resolveBase()
|
||||
if (!base) {
|
||||
console.warn('[report webview] 请配置 VITE_SITE_ORIGIN 或 VITE_REPORT_BASE_URL')
|
||||
return ''
|
||||
}
|
||||
const merged: Record<string, string> = { source: 'app', ...params }
|
||||
if (token)
|
||||
merged.token = token
|
||||
const search = buildQueryString(merged)
|
||||
/** 使用 H5 无顶栏的 App 专用路由,避免 PageLayout 与 App 原生导航栏重复 */
|
||||
return `${base}/app/${page === 'example' ? 'example' : 'report'}?${search}`
|
||||
}
|
||||
|
||||
const buildReportShareUrl = (linkIdentifier: string, params: Record<string, string> = {}) => {
|
||||
const token = getToken() || ''
|
||||
const base = resolveBase()
|
||||
if (!base) {
|
||||
console.warn('[report webview] 请配置 VITE_SITE_ORIGIN 或 VITE_REPORT_BASE_URL')
|
||||
return ''
|
||||
}
|
||||
const merged: Record<string, string> = { source: 'app', ...params }
|
||||
if (token)
|
||||
merged.token = token
|
||||
const search = buildQueryString(merged)
|
||||
const encodedLink = encodeURIComponent(linkIdentifier)
|
||||
return `${base}/report/share/${encodedLink}${search ? `?${search}` : ''}`
|
||||
}
|
||||
|
||||
const buildSitePathUrl = (path: string, params: Record<string, string> = {}) => {
|
||||
const token = getToken() || ''
|
||||
const base = resolveBase()
|
||||
if (!base) {
|
||||
console.warn('[site webview] 请配置 VITE_SITE_ORIGIN 或 VITE_REPORT_BASE_URL')
|
||||
return ''
|
||||
}
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
||||
const merged: Record<string, string> = { source: 'app', ...params }
|
||||
if (token)
|
||||
merged.token = token
|
||||
const search = buildQueryString(merged)
|
||||
return `${base}${normalizedPath}${search ? `?${search}` : ''}`
|
||||
}
|
||||
|
||||
const postMessageToWebview = (payload: Record<string, unknown>) => {
|
||||
// #ifdef APP-PLUS
|
||||
const current = getCurrentPages().at(-1) as any
|
||||
const webview = current?.$getAppWebview?.()?.children?.()[0]
|
||||
if (webview) {
|
||||
webview.evalJS(`window.postMessage(${JSON.stringify(payload)}, '*')`)
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
return { buildReportUrl, buildReportShareUrl, buildSitePathUrl, postMessageToWebview }
|
||||
}
|
||||
18
src/composables/useRiskNotifier.js
Normal file
18
src/composables/useRiskNotifier.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { watch } from 'vue'
|
||||
|
||||
/**
|
||||
* 风险评分通知 composable
|
||||
* 用于组件向父组件通知自己的风险评分(0-100分,分数越高越安全)
|
||||
*/
|
||||
export function useRiskNotifier(props, riskScore) {
|
||||
// 监听 riskScore 变化,通知父组件
|
||||
watch(
|
||||
riskScore,
|
||||
(newValue) => {
|
||||
if (props.apiId && props.notifyRiskStatus) {
|
||||
props.notifyRiskStatus(props.apiId, props.index, newValue)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
}
|
||||
6
src/composables/useSEO.js
Normal file
6
src/composables/useSEO.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/** App 端不做 H5 SEO,与 webview 接口兼容 */
|
||||
export function useSEO() {
|
||||
return {
|
||||
updateSEO() {},
|
||||
}
|
||||
}
|
||||
138
src/composables/useWebView.js
Normal file
138
src/composables/useWebView.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import { onMounted, ref } from "vue";
|
||||
import "@/static/uni-webview.js";
|
||||
import { getToken, setToken } from '@/utils/storage'
|
||||
|
||||
const WEBVIEW_PLATFORM_KEY = 'webview_platform'
|
||||
|
||||
export function useWebView() {
|
||||
const platform = ref("");
|
||||
const token = ref("");
|
||||
// 检测环境并通知父窗口加载完毕
|
||||
const handleBridgeReady = () => {
|
||||
if (platform.value) {
|
||||
h5PostMessage("loaded", true);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取 Token(从 URL 中解析)
|
||||
const getTokenFromUrl = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const tokenFromUrl = urlParams.get("token");
|
||||
token.value = tokenFromUrl || ""; // 如果 URL 没有 token,返回空字符串
|
||||
if (token.value) {
|
||||
setToken(token.value);
|
||||
} else {
|
||||
token.value = getToken() || "";
|
||||
}
|
||||
return tokenFromUrl;
|
||||
};
|
||||
|
||||
// 封装 postMessage 方法
|
||||
const postMessage = (data) => {
|
||||
if (platform.value === "h5") {
|
||||
h5PostMessage("postMessage", data);
|
||||
} else if (uni && uni.webView.postMessage) {
|
||||
uni.webView.postMessage(data);
|
||||
} else {
|
||||
console.error("uni.webView.postMessage is not available.");
|
||||
}
|
||||
};
|
||||
|
||||
const redirectTo = (data) => {
|
||||
if (platform.value === "h5") {
|
||||
h5PostMessage("redirectTo", data);
|
||||
} else if (uni && uni.webView.redirectTo) {
|
||||
// 非 H5 环境,调用 uni.webView.redirectTo
|
||||
uni.webView.redirectTo(data);
|
||||
} else {
|
||||
console.error("uni.webView.redirectTo is not available.");
|
||||
}
|
||||
};
|
||||
|
||||
// 封装 navigateBack 方法
|
||||
const navigateBack = (data) => {
|
||||
if (platform.value === "h5") {
|
||||
window.top.history.back();
|
||||
// h5PostMessage("navigateBack", data)
|
||||
} else if (uni && uni.webView.navigateBack) {
|
||||
// 非 H5 环境,调用 uni.webView.navigateBack
|
||||
uni.webView.navigateBack(data);
|
||||
} else {
|
||||
console.error("uni.webView.navigateBack is not available.");
|
||||
}
|
||||
};
|
||||
|
||||
// 封装 navigateTo 方法
|
||||
const navigateTo = (data) => {
|
||||
if (platform.value === "h5") {
|
||||
// h5PostMessage("navigateTo", data)
|
||||
window.top.location.href = `/app${data.url}`;
|
||||
} else if (uni && uni.webView.navigateTo) {
|
||||
uni.webView.navigateTo(data);
|
||||
} else {
|
||||
console.error("uni.webView.navigateTo is not available.");
|
||||
}
|
||||
};
|
||||
const payment = (data) => {
|
||||
if (platform.value === "h5") {
|
||||
h5PostMessage("payment", data);
|
||||
} else if (uni && uni.webView.navigateTo) {
|
||||
// 非 H5 环境,调用 uni.webView.navigateTo
|
||||
uni.webView.navigateTo(data);
|
||||
} else {
|
||||
console.error("uni.webView.navigateTo is not available.");
|
||||
}
|
||||
};
|
||||
const getEnv = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const env = uni.getStorageSync(WEBVIEW_PLATFORM_KEY);
|
||||
if (env) {
|
||||
platform.value = env;
|
||||
resolve(env);
|
||||
} else {
|
||||
uni.webView.getEnv((env) => {
|
||||
// 遍历 env 对象,找到值为 true 的键
|
||||
const platformKey = Object.keys(env).find((key) => env[key] === true);
|
||||
platform.value = platformKey;
|
||||
if (platformKey) {
|
||||
uni.setStorageSync(WEBVIEW_PLATFORM_KEY, platformKey);
|
||||
resolve(platformKey); // 返回键名(如 'h5', 'mp-weixin' 等)
|
||||
} else {
|
||||
reject("未知平台");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const envValue = await getEnv();
|
||||
console.log("当前环境", envValue);
|
||||
// 将返回的键名(如 'h5', 'mp-weixin')存储到 platform
|
||||
handleBridgeReady();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
// 获取 Token
|
||||
getTokenFromUrl();
|
||||
});
|
||||
|
||||
return {
|
||||
platform,
|
||||
token,
|
||||
getEnv,
|
||||
redirectTo,
|
||||
postMessage,
|
||||
navigateTo,
|
||||
navigateBack,
|
||||
payment,
|
||||
};
|
||||
}
|
||||
function h5PostMessage(action, data) {
|
||||
window.parent.postMessage(
|
||||
{ action, data, messageId: generateUniqueId(action) },
|
||||
"*",
|
||||
);
|
||||
}
|
||||
const generateUniqueId = (action) => `msg_${action}_${new Date().getTime()}`;
|
||||
8
src/composables/useWeixinShare.js
Normal file
8
src/composables/useWeixinShare.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* App 端不启用微信 JSSDK 分享;占位以兼容 QRcode 等迁移组件。
|
||||
*/
|
||||
export function useWeixinShare() {
|
||||
return {
|
||||
configWeixinShare() {},
|
||||
}
|
||||
}
|
||||
34
src/composables/useZoomAdapter.js
Normal file
34
src/composables/useZoomAdapter.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
import zoomAdapter from '../utils/zoomAdapter.js'
|
||||
|
||||
/**
|
||||
* 简化版缩放适配组合式函数
|
||||
*/
|
||||
export function useZoomAdapter() {
|
||||
const currentZoom = ref(1)
|
||||
const isTooHighZoom = ref(false)
|
||||
|
||||
const handleZoomChange = (event) => {
|
||||
const { zoom } = event.detail
|
||||
currentZoom.value = zoom
|
||||
isTooHighZoom.value = zoom > 3
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof window === 'undefined')
|
||||
return
|
||||
if (!zoomAdapter.isInitialized) {
|
||||
zoomAdapter.init()
|
||||
}
|
||||
window.addEventListener('zoomChanged', handleZoomChange)
|
||||
})
|
||||
|
||||
return {
|
||||
currentZoom,
|
||||
isTooHighZoom,
|
||||
getZoomAdaptiveClass: () => ({
|
||||
'zoom-adaptive': true,
|
||||
'too-high-zoom': isTooHighZoom.value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user