edu/uniapp/src/pages/payment/payment.vue

846 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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