补充提交

This commit is contained in:
Yu 2026-03-24 16:01:24 +08:00
parent fddeeb0bc7
commit a3e1bc98f4
1 changed files with 845 additions and 0 deletions

View File

@ -0,0 +1,845 @@
<template>
<view class="payment">
<u-navbar
title="缴费"
:border-bottom="false"
back-icon-color="#333"
title-color="#333"
:background="{ background: '#FFFFFF' }"
/>
<view class="payment-content">
<!-- 缴费金额输入 -->
<view class="amount-section">
<view class="section-title">缴费金额</view>
<view class="amount-input-wrapper">
<text class="currency">¥</text>
<input
class="amount-input"
type="digit"
v-model="amount"
placeholder="请输入缴费金额"
placeholder-class="placeholder"
/>
</view>
</view>
<!-- 缴费说明 -->
<view class="remark-section">
<view class="section-title">缴费说明选填</view>
<textarea
class="remark-input"
v-model="remark"
placeholder="请输入缴费说明"
placeholder-class="placeholder"
maxlength="100"
/>
<text class="remark-count">{{ remark.length }}/100</text>
</view>
<!-- 支付方式 -->
<view class="payway-section">
<view class="section-title">支付方式</view>
<view class="payway-list">
<view
class="payway-item"
v-for="item in payWayList"
:key="item.pay_way"
:class="{ active: selectedPayWay === item.pay_way }"
@click="selectPayWay(item.pay_way)"
>
<view class="payway-info">
<u-icon
:name="getPayWayIcon(item.pay_way)"
size="48"
:color="getPayWayColor(item.pay_way)"
/>
<text class="payway-name">{{ item.name }}</text>
</view>
<view class="payway-check">
<u-icon
v-if="selectedPayWay === item.pay_way"
name="checkmark-circle-fill"
size="40"
color="#3B82F6"
/>
<view v-else class="check-circle"></view>
</view>
</view>
</view>
</view>
</view>
<!-- 底部提交按钮 -->
<view class="footer">
<view class="total-amount">
<text class="label">合计</text>
<text class="currency">¥</text>
<text class="amount">{{ amount || '0.00' }}</text>
</view>
<view
class="submit-btn"
:class="{ disabled: !canSubmit }"
@click="handleSubmit"
>
立即缴费
</view>
</view>
<!-- 支付密码弹窗 -->
<u-popup v-model="showPasswordPopup" mode="center" border-radius="20">
<view class="password-popup">
<view class="popup-title">请输入支付密码</view>
<u-message-input
mode="box"
:maxlength="6"
:focus="true"
v-model="password"
@finish="onPasswordFinish"
/>
<view class="popup-cancel" @click="showPasswordPopup = false">
取消
</view>
</view>
</u-popup>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { getPayWay, prepay, getPayResult, alipayJspay, wechatJspay, unifiedTradeQuery } from '@/api/pay'
import { useUserStore } from '@/stores/user'
import config from '@/config'
const userStore = useUserStore()
//
const paymentConfig = config.payment
//
const amount = ref('')
const remark = ref('')
const payWayList = ref<any[]>([])
const selectedPayWay = ref('')
const showPasswordPopup = ref(false)
const password = ref('')
const orderId = ref('')
//
const canSubmit = computed(() => {
const numAmount = parseFloat(amount.value)
return numAmount > 0 && selectedPayWay.value
})
// - 使
const getPayWayData = async () => {
//
payWayList.value = [
{
pay_way: 'wechat',
name: '微信支付'
},
{
pay_way: 'alipay',
name: '支付宝支付'
}
]
selectedPayWay.value = 'wechat'
}
//
const selectPayWay = (payWay: string) => {
selectedPayWay.value = payWay
}
//
const getPayWayIcon = (payWay: string) => {
const iconMap: Record<string, string> = {
'wechat': 'weixin-fill',
'alipay': 'zhifubao-circle-fill',
'balance': 'rmb-circle-fill'
}
return iconMap[payWay] || 'rmb-circle-fill'
}
//
const getPayWayColor = (payWay: string) => {
const colorMap: Record<string, string> = {
'wechat': '#07C160',
'alipay': '#1677FF',
'balance': '#FF6B00'
}
return colorMap[payWay] || '#999'
}
//
const handleSubmit = () => {
if (!canSubmit.value) return
const numAmount = parseFloat(amount.value)
if (numAmount <= 0) {
uni.showToast({ title: '请输入有效的缴费金额', icon: 'none' })
return
}
//
if (selectedPayWay.value === 'balance') {
showPasswordPopup.value = true
return
}
doPay()
}
//
const onPasswordFinish = () => {
showPasswordPopup.value = false
doPay()
}
//
const doPay = async () => {
try {
uni.showLoading({ title: '支付中...' })
const params: any = {
pay_way: selectedPayWay.value,
amount: parseFloat(amount.value),
remark: remark.value
}
if (selectedPayWay.value === 'balance') {
params.password = password.value
}
const res = await prepay(params)
if (res && res.order_id) {
orderId.value = res.order_id
// -
if (selectedPayWay.value === 'wechat' && res.pay_params) {
await wechatJspayPay(res.pay_params)
}
// -
else if (selectedPayWay.value === 'alipay' && res.pay_params) {
await alipayJspayPay(res.pay_params)
}
//
else {
uni.hideLoading()
goToResult(true)
}
} else {
throw new Error('支付参数错误')
}
} catch (error: any) {
uni.hideLoading()
uni.showToast({ title: error.message || '支付失败', icon: 'none' })
password.value = ''
}
}
//
const wechatPay = (payParams: any) => {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
uni.requestPayment({
provider: 'wxpay',
...payParams,
success: () => {
resolve(true)
checkPayResult()
},
fail: (err: any) => {
reject(err)
goToResult(false)
}
})
// #endif
// #ifdef H5
if (payParams.mweb_url) {
window.location.href = payParams.mweb_url
} else if (payParams.jsapi_params) {
// JSAPI
const { appId, timeStamp, nonceStr, package: packageStr, signType, paySign } = payParams.jsapi_params
if (typeof WeixinJSBridge !== 'undefined') {
WeixinJSBridge.invoke(
'getBrandWCPayRequest',
{ appId, timeStamp, nonceStr, package: packageStr, signType, paySign },
(res: any) => {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
resolve(true)
checkPayResult()
} else {
reject(res)
goToResult(false)
}
}
)
}
}
// #endif
// #ifdef APP-PLUS
uni.requestPayment({
provider: 'wxpay',
orderInfo: payParams,
success: () => {
resolve(true)
checkPayResult()
},
fail: (err: any) => {
reject(err)
goToResult(false)
}
})
// #endif
})
}
//
const alipay = (payParams: any) => {
return new Promise((resolve, reject) => {
// #ifdef H5
if (payParams.form) {
const div = document.createElement('div')
div.innerHTML = payParams.form
document.body.appendChild(div)
div.querySelector('form')?.submit()
}
// #endif
// #ifdef APP-PLUS
uni.requestPayment({
provider: 'alipay',
orderInfo: payParams.order_str,
success: () => {
resolve(true)
checkPayResult()
},
fail: (err: any) => {
reject(err)
goToResult(false)
}
})
// #endif
})
}
//
const alipayJspayPay = async (payParams: any) => {
try {
//
const params = {
version: '3.0',
charset: 'UTF-8',
service: 'pay.alipay.jspay',
mch_id: paymentConfig.mch_id, //
out_trade_no: orderId.value,
total_fee: String(Math.round(parseFloat(amount.value) * 100)), //
body: remark.value || '缴费支付',
mch_create_ip: '127.0.0.1', // IP
notify_url: paymentConfig.alipay.notify_url,
buyer_logon_id: '', //
'terminal_info.terminal_type': '11', //
'terminal_info.terminal_id': paymentConfig.terminal_id,
'terminal_info.app_version': '1.000000'
}
const res = await alipayJspay(params)
//
if (res && res.status === '0' && res.result_code === '0') {
// pay_info
const payInfo = JSON.parse(res.pay_info || '{}')
if (payInfo.tradeNO) {
// #ifdef APP-PLUS
// APP 使 tradeNO
uni.requestPayment({
provider: 'alipay',
orderInfo: {
tradeNO: payInfo.tradeNO
},
success: () => {
checkPayResult()
},
fail: (err: any) => {
console.error('支付宝支付失败:', err)
goToResult(false)
}
})
// #endif
// #ifdef H5
// H5
// 1使 pay_url
if (res.pay_url) {
window.location.href = res.pay_url
return
}
// 2使 tradeNO
if (payInfo.tradeNO && typeof AlipayJSBridge !== 'undefined') {
AlipayJSBridge.call('tradePay', {
tradeNO: payInfo.tradeNO
}, (result: any) => {
if (result.resultCode === '9000') {
checkPayResult()
} else {
goToResult(false)
}
})
}
// #endif
} else {
throw new Error('获取支付参数失败')
}
} else {
throw new Error(res.err_msg || res.message || '支付请求失败')
}
} catch (error: any) {
console.error('开放银行支付错误:', error)
uni.showToast({ title: error.message || '支付请求失败', icon: 'none' })
goToResult(false)
}
}
// JSPay
const wechatJspayPay = async (payParams: any) => {
try {
//
const params: any = {
version: '3.0',
charset: 'UTF-8',
service: 'pay.weixin.jspay',
mch_id: paymentConfig.mch_id, //
out_trade_no: orderId.value,
total_fee: String(Math.round(parseFloat(amount.value) * 100)), //
body: remark.value || '缴费支付',
mch_create_ip: '127.0.0.1', // IP
notify_url: paymentConfig.wechat.notify_url,
'terminal_info.terminal_type': '11', //
'terminal_info.terminal_id': paymentConfig.terminal_id,
'terminal_info.app_version': '1.000000'
}
// / sub_appid sub_openid
// #ifdef MP-WEIXIN
params.is_minipg = '1' //
params.sub_appid = paymentConfig.wechat.sub_appid // appid
// openid
const openid = userStore.userInfo?.openid || ''
if (openid) {
params.sub_openid = openid
}
// #endif
// #ifdef H5
// H5 -
params.is_raw = '1' // JS
params.sub_appid = paymentConfig.wechat.sub_appid // appid
// openid
const openid = userStore.userInfo?.openid || ''
if (openid) {
params.sub_openid = openid
}
// #endif
const res = await wechatJspay(params)
//
if (res && res.status === '0' && res.result_code === '0') {
const payInfo = res.pay_info
if (payInfo) {
// #ifdef MP-WEIXIN
// - XML pay_info
const payParams = parseXmlPayInfo(payInfo)
if (payParams) {
uni.requestPayment({
provider: 'wxpay',
...payParams,
success: () => {
checkPayResult()
},
fail: (err: any) => {
console.error('微信支付失败:', err)
goToResult(false)
}
})
} else {
throw new Error('解析支付参数失败')
}
// #endif
// #ifdef H5
// H5 - XML pay_info
const payParams = parseXmlPayInfo(payInfo)
if (payParams && typeof WeixinJSBridge !== 'undefined') {
WeixinJSBridge.invoke(
'getBrandWCPayRequest',
payParams,
(res: any) => {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
checkPayResult()
} else {
goToResult(false)
}
}
)
} else if (payParams) {
// 使 uni.requestPayment
uni.requestPayment({
provider: 'wxpay',
...payParams,
success: () => {
checkPayResult()
},
fail: (err: any) => {
console.error('微信支付失败:', err)
goToResult(false)
}
})
} else {
throw new Error('解析支付参数失败')
}
// #endif
// #ifdef APP-PLUS
// APP
uni.requestPayment({
provider: 'wxpay',
orderInfo: payInfo,
success: () => {
checkPayResult()
},
fail: (err: any) => {
console.error('微信支付失败:', err)
goToResult(false)
}
})
// #endif
} else {
throw new Error('获取支付参数失败')
}
} else {
throw new Error(res.err_msg || res.message || '支付请求失败')
}
} catch (error: any) {
console.error('开放银行微信支付错误:', error)
uni.showToast({ title: error.message || '支付请求失败', icon: 'none' })
goToResult(false)
}
}
// XML
const parseXmlPayInfo = (xmlStr: string) => {
try {
// XML
const getValue = (xml: string, tag: string) => {
const regex = new RegExp(`<${tag}><!\[CDATA\[(.*?)\]\]></${tag}>|<${tag}>(.*?)</${tag}>`, 'i')
const match = xml.match(regex)
return match ? (match[1] || match[2]) : ''
}
return {
appId: getValue(xmlStr, 'appId'),
timeStamp: getValue(xmlStr, 'timeStamp'),
nonceStr: getValue(xmlStr, 'nonceStr'),
package: getValue(xmlStr, 'package'),
signType: getValue(xmlStr, 'signType') || 'RSA',
paySign: getValue(xmlStr, 'paySign')
}
} catch (error) {
console.error('解析 XML 失败:', error)
return null
}
}
// - 使
const checkPayResult = async () => {
try {
uni.showLoading({ title: '查询中...' })
//
const params = {
version: '3.0',
service: 'unified.trade.query',
mch_id: paymentConfig.mch_id, //
out_trade_no: orderId.value
}
const res = await unifiedTradeQuery(params)
uni.hideLoading()
//
if (res && res.status === '0' && res.result_code === '0') {
// trade_state: SUCCESS-, REFUND-退, NOTPAY-,
// CLOSED-, REVOKED-, USERPAYING-, PAYERROR-
const tradeState = res.trade_state
if (tradeState === 'SUCCESS') {
//
goToResult(true)
} else if (tradeState === 'NOTPAY' || tradeState === 'USERPAYING') {
//
uni.showToast({ title: '支付处理中,请稍后...', icon: 'none' })
// 3
setTimeout(() => {
checkPayResult()
}, 3000)
} else {
//
goToResult(false)
}
} else {
// 使
console.error('开放银行订单查询失败:', res)
await fallbackCheckPayResult()
}
} catch (error) {
uni.hideLoading()
console.error('查询支付结果错误:', error)
// 使
await fallbackCheckPayResult()
}
}
// - 使
const fallbackCheckPayResult = async () => {
try {
const res = await getPayResult({ order_id: orderId.value })
if (res && res.pay_status === 1) {
goToResult(true)
} else {
goToResult(false)
}
} catch (error) {
goToResult(false)
}
}
//
const goToResult = (success: boolean) => {
uni.redirectTo({
url: `/pages/payment_result/payment_result?status=${success ? 'success' : 'fail'}&order_id=${orderId.value}`
})
}
onMounted(() => {
getPayWayData()
})
</script>
<style lang="scss" scoped>
.payment {
min-height: 100vh;
background-color: #F5F7FA;
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
}
.payment-content {
padding: 20rpx;
}
.section-title {
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
}
//
.amount-section {
background: #FFFFFF;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
.amount-input-wrapper {
display: flex;
align-items: center;
border-bottom: 2rpx solid #E5E7EB;
padding: 20rpx 0;
.currency {
font-size: 60rpx;
font-weight: 600;
color: #333;
margin-right: 20rpx;
}
.amount-input {
flex: 1;
font-size: 60rpx;
font-weight: 600;
color: #333;
height: 80rpx;
}
.placeholder {
font-size: 40rpx;
color: #999;
font-weight: normal;
}
}
}
//
.remark-section {
background: #FFFFFF;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
position: relative;
.remark-input {
width: 100%;
height: 160rpx;
font-size: 28rpx;
color: #333;
line-height: 1.6;
}
.placeholder {
font-size: 28rpx;
color: #999;
}
.remark-count {
position: absolute;
right: 30rpx;
bottom: 20rpx;
font-size: 24rpx;
color: #999;
}
}
//
.payway-section {
background: #FFFFFF;
border-radius: 20rpx;
padding: 30rpx;
.payway-list {
.payway-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 0;
border-bottom: 2rpx solid #F3F4F6;
&:last-child {
border-bottom: none;
}
&.active {
.payway-name {
color: #333;
font-weight: 500;
}
}
.payway-info {
display: flex;
align-items: center;
.payway-name {
font-size: 30rpx;
color: #666;
margin-left: 20rpx;
}
}
.payway-check {
.check-circle {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #D1D5DB;
border-radius: 50%;
}
}
}
}
}
//
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #FFFFFF;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
.total-amount {
display: flex;
align-items: baseline;
.label {
font-size: 28rpx;
color: #666;
}
.currency {
font-size: 32rpx;
color: #FF6B00;
font-weight: 600;
}
.amount {
font-size: 48rpx;
color: #FF6B00;
font-weight: 600;
}
}
.submit-btn {
background: linear-gradient(135deg, #3B82F6 0%, #2563EB 100%);
color: #FFFFFF;
font-size: 32rpx;
font-weight: 500;
padding: 24rpx 60rpx;
border-radius: 40rpx;
&:active {
opacity: 0.9;
}
&.disabled {
background: #D1D5DB;
color: #9CA3AF;
}
}
}
//
.password-popup {
width: 600rpx;
padding: 40rpx;
background: #FFFFFF;
border-radius: 20rpx;
.popup-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
text-align: center;
margin-bottom: 40rpx;
}
.popup-cancel {
font-size: 28rpx;
color: #999;
text-align: center;
margin-top: 40rpx;
padding: 20rpx;
}
}
</style>