feat: add avatar payments and finance management

This commit is contained in:
stefanfeng
2026-09-08 18:47:29 +08:00
parent 62eb9578fd
commit 8585d101d5
36 changed files with 10228 additions and 43 deletions
+10
View File
@@ -0,0 +1,10 @@
<script setup>
import { onLaunch } from '@dcloudio/uni-app'
import { useUserStore } from '@/store/user'
onLaunch(() => useUserStore().init())
</script>
<style>
page { background:#f5f6f8; }
</style>
+9
View File
@@ -0,0 +1,9 @@
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
app.use(createPinia())
return { app }
}
+21
View File
@@ -0,0 +1,21 @@
{
"name": "会会数字分身",
"appid": "",
"description": "会会数字分身 uni-app 混合壳",
"versionName": "1.1.0",
"versionCode": "110",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"compilerVersion": 3,
"modules": { "Payment": {} },
"distribute": {
"android": { "permissions": ["<uses-permission android:name=\"android.permission.INTERNET\"/>"] },
"ios": {},
"sdkConfigs": {}
}
},
"mp-weixin": { "appid": "", "setting": { "urlCheck": true }, "usingComponents": true },
"h5": { "publicPath": "./", "router": { "base": "./" } },
"vueVersion": "3"
}
+16
View File
@@ -0,0 +1,16 @@
{
"pages": [{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "会会数字分身",
"navigationBarBackgroundColor": "#0F2B4C",
"navigationBarTextStyle": "white"
}
}],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "会会数字分身",
"navigationBarBackgroundColor": "#0F2B4C",
"backgroundColor": "#F5F6F8"
}
}
+55
View File
@@ -0,0 +1,55 @@
<template>
<view class="shell">
<web-view ref="webview" :src="h5Url" @message="onH5Message" />
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useUserStore } from '@/store/user'
import { buildH5Url, parseH5Message, postToH5 } from '@/utils/bridge'
import { requestAppPayment } from '@/utils/payment'
const userStore = useUserStore()
const webview = ref(null)
const H5_BASE_URL = import.meta.env.VITE_AVATAR_H5_URL || 'https://digital.99hui.com/'
const h5Url = computed(() => buildH5Url(H5_BASE_URL, userStore.$state))
function runtimeSurface() {
let value = 'h5'
// #ifdef APP-PLUS
value = 'app'
// #endif
// #ifdef MP-WEIXIN
value = 'mp-weixin'
// #endif
return value
}
async function handlePayment(payment) {
try {
await requestAppPayment(payment)
postToH5(webview.value, { type: 'paymentResult', orderId: payment.orderId, status: 'success' })
} catch (error) {
const message = error?.message || '支付未完成'
const status = /cancel/i.test(message) || /取消/.test(message) ? 'cancelled' : 'failed'
postToH5(webview.value, { type: 'paymentResult', orderId: payment.orderId, status, message })
}
}
function onH5Message(event) {
const message = parseH5Message(event)
if (!message?.type) return
if (message.type === 'ready') {
postToH5(webview.value, { type: 'context', surface: runtimeSurface(), version: '1.1.0' })
} else if (message.type === 'payment') {
void handlePayment(message.payment)
} else if (message.type === 'setTitle' && message.title) {
uni.setNavigationBarTitle({ title: message.title })
} else if (message.type === 'back') {
uni.navigateBack({ delta: 1 })
}
}
</script>
<style>.shell{width:100%;height:100vh}</style>
+24
View File
@@ -0,0 +1,24 @@
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({ token: '', userId: '', nickname: '', avatarUrl: '', initialized: false }),
getters: { isLogin: state => Boolean(state.token) },
actions: {
init() {
this.$patch({
token: uni.getStorageSync('hh_token') || '',
userId: uni.getStorageSync('hh_userId') || '',
nickname: uni.getStorageSync('hh_nickname') || '',
avatarUrl: uni.getStorageSync('hh_avatarUrl') || '',
initialized: true
})
},
applySession(session) {
this.$patch(session)
uni.setStorageSync('hh_token', this.token)
uni.setStorageSync('hh_userId', this.userId)
uni.setStorageSync('hh_nickname', this.nickname)
uni.setStorageSync('hh_avatarUrl', this.avatarUrl)
}
}
})
+3
View File
@@ -0,0 +1,3 @@
$brand-navy: #0f2b4c;
$brand-orange: #f97316;
$bg-page: #f5f6f8;
+33
View File
@@ -0,0 +1,33 @@
export function buildH5Url(base, session) {
const url = new URL(base)
if (session.token) url.searchParams.set('token', session.token)
if (session.userId) url.searchParams.set('userId', session.userId)
if (session.nickname) url.searchParams.set('nickname', session.nickname)
if (session.avatarUrl) url.searchParams.set('avatar', session.avatarUrl)
url.searchParams.set('ts', String(Date.now()))
return url.toString()
}
export function postToH5(webviewRef, message) {
if (!webviewRef) return false
const js = `window.__uniBridgeHandle__&&window.__uniBridgeHandle__(${JSON.stringify(message)})`
try {
if (typeof webviewRef.evalJS === 'function') {
webviewRef.evalJS(js)
return true
}
// #ifdef APP-PLUS
const child = plus.webview.currentWebview().children()[0]
if (child) { child.evalJS(js); return true }
// #endif
} catch (error) {
console.error('[bridge] postToH5 failed', error)
}
return false
}
export function parseH5Message(event) {
const items = event?.detail?.data
if (Array.isArray(items) && items.length) return items[items.length - 1]
return event?.detail || null
}
+22
View File
@@ -0,0 +1,22 @@
function parseOrderInfo(payment) {
if (payment.paymentParams && typeof payment.paymentParams === 'object') return payment.paymentParams
if (typeof payment.payMessage !== 'string') return payment.payMessage
try { return JSON.parse(payment.payMessage) } catch { return payment.payMessage }
}
export function requestAppPayment(payment) {
if (!payment || payment.payWay !== 'APP') {
return Promise.reject(new Error('当前订单不是 App 支付订单'))
}
const provider = payment.paymentMethod === 'alipay' ? 'alipay' : 'wxpay'
const orderInfo = parseOrderInfo(payment)
if (!orderInfo) return Promise.reject(new Error('支付参数为空'))
return new Promise((resolve, reject) => {
uni.requestPayment({
provider,
orderInfo,
success: resolve,
fail: error => reject(new Error(error?.errMsg || '支付未完成'))
})
})
}