first commit

This commit is contained in:
2026-04-20 16:42:28 +08:00
commit c77780fa0e
365 changed files with 41599 additions and 0 deletions

View 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),
},
}
}

View 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)
}

View 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 }
}

View 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 }
}

View 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,
}
}

View 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
View File

@@ -0,0 +1,10 @@
import { ref } from 'vue'
/** App 端固定非微信 H5保留 isWeChat 以兼容迁移代码分支 */
const isWeChat = ref(false)
export function useEnv() {
return {
isWeChat,
}
}

View 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,
}
}

View 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)
}

View 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
},
})
}

View 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 }
}

View 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 }
}

View 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 },
)
}

View File

@@ -0,0 +1,6 @@
/** App 端不做 H5 SEO与 webview 接口兼容 */
export function useSEO() {
return {
updateSEO() {},
}
}

View 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()}`;

View File

@@ -0,0 +1,8 @@
/**
* App 端不启用微信 JSSDK 分享;占位以兼容 QRcode 等迁移组件。
*/
export function useWeixinShare() {
return {
configWeixinShare() {},
}
}

View 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,
}),
}
}