Files
device-voice-h5/components/RenewalPaymentPopup.vue
luo dfa68a6a79
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m2s
fix: ui
2026-07-30 12:30:50 +08:00

484 lines
14 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>
<up-popup :show="show" mode="center" @close="close">
<view class="renewal-popup">
<view class="popup-header">
<view class="popup-title">{{ popupTitle }}</view>
<view class="popup-close" @tap="close">×</view>
</view>
<view class="package-summary">
<view class="summary-label">{{ summaryLabel }}</view>
<view class="summary-name">{{ summaryName }}</view>
</view>
<view class="payment-methods">
<view
v-for="method in paymentMethodOptions"
:key="method.value"
class="method-item"
:class="{ active: paymentMethod === method.value }"
@tap="selectPaymentMethod(method.value)"
>
<view class="method-left">
<image v-if="method.value === 'alipay'" class="method-icon" src="/static/支付宝支付.png" mode="aspectFit"></image>
<image v-else-if="method.value === 'wallet'" class="method-icon" src="/static/wallet.png" mode="aspectFit"></image>
<image v-else-if="method.value === 'wechat'" class="method-icon" src="/static/微信支付.png" mode="aspectFit"></image>
<text class="method-name">{{ method.label }}</text>
</view>
<view class="method-radio" :class="{ checked: paymentMethod === method.value }"></view>
</view>
<view v-if="paymentMethodOptions.length === 0" class="method-empty">暂无可用支付方式</view>
</view>
<view class="popup-footer">
<button class="btn-apple btn-secondary" @tap="close">取消</button>
<button class="btn-apple btn-primary" :disabled="submitting" @tap="confirmPay">
{{ submitting ? '处理中...' : confirmText }}
</button>
</view>
</view>
</up-popup>
</template>
<script setup>
import { ref, computed } from 'vue';
import { assetApi, isAssetRealNameCompleted, orderApi } from '@/api/index.js';
import {
handlePaymentError,
isValidAlipayPaymentLink,
isValidWechatPayConfig,
openAlipayPayment,
PAYMENT_REFRESH_TARGETS,
showPaymentToast,
wechatH5Pay
} from '@/utils/payment.js';
import { formatMoney } from '@/utils/display.js';
import { getDefaultPaymentMethod, getPaymentMethodOptions, normalizePaymentMethods } from '@/utils/payment-methods.js';
const props = defineProps({
identifier: { type: String, default: '' },
paymentRefreshTarget: { type: String, default: PAYMENT_REFRESH_TARGETS.ORDER_LIST }
});
const emit = defineEmits(['completed']);
const show = ref(false);
const submitting = ref(false);
const packageIds = ref([]);
const packageNames = ref([]);
const paymentMethod = ref('alipay');
const allowedPaymentMethods = ref([]);
const assetInfo = ref({});
const operationMode = ref('renewal');
const orderPaymentId = ref(null);
const paymentMethodOptions = computed(() => getPaymentMethodOptions(allowedPaymentMethods.value));
const packageNamesText = computed(() => packageNames.value.filter(Boolean).join('、') || '当前套餐');
const popupTitle = computed(() => operationMode.value === 'order-payment' ? '立即支付' : '立即续费');
const summaryLabel = computed(() => operationMode.value === 'order-payment' ? '支付订单' : '续费套餐');
const summaryName = computed(() => operationMode.value === 'order-payment'
? packageNamesText.value.replace(/^当前套餐$/, '当前订单')
: packageNamesText.value);
const confirmText = computed(() => operationMode.value === 'order-payment' ? '确认支付' : '确认续费');
const hasPreparedPaymentData = (paymentData) => {
return isValidWechatPayConfig(paymentData?.pay_config) ||
isValidAlipayPaymentLink(paymentData?.payment_link);
};
const close = () => {
if (!submitting.value) show.value = false;
};
const loadAssetInfo = async () => {
assetInfo.value = await assetApi.getInfo(props.identifier);
allowedPaymentMethods.value = normalizePaymentMethods(assetInfo.value.allowed_payment_methods);
paymentMethod.value = getDefaultPaymentMethod(allowedPaymentMethods.value);
};
const open = async (ids, names = []) => {
operationMode.value = 'renewal';
orderPaymentId.value = null;
const validIds = [...new Set((Array.isArray(ids) ? ids : [ids])
.map((value) => Number(value))
.filter((value) => Number.isInteger(value) && value > 0))];
if (!validIds.length) {
uni.showToast({ title: '当前暂无可续费套餐', icon: 'none' });
return;
}
if (!props.identifier) {
uni.showToast({ title: '未找到当前资产', icon: 'none' });
return;
}
packageIds.value = validIds;
packageNames.value = Array.isArray(names) ? names.filter(Boolean) : [];
try {
await loadAssetInfo();
} catch (error) {
console.error('加载续费支付方式失败', error);
uni.showToast({ title: error.msg || error.message || '加载支付方式失败', icon: 'none' });
return;
}
if (assetInfo.value.effective_realname_policy === 'before_order' &&
assetInfo.value.realname_required && !isAssetRealNameCompleted(assetInfo.value)) {
uni.showModal({
title: '需要实名认证',
content: '当前资产下单前需要完成实名认证',
confirmText: '去认证',
cancelText: '取消',
success: ({ confirm }) => {
if (confirm) uni.navigateTo({ url: '/pages/auth/auth' });
}
});
return;
}
if (!paymentMethodOptions.value.length) {
uni.showToast({ title: '暂无可用支付方式', icon: 'none' });
return;
}
show.value = true;
};
const openOrderPayment = async (orderId, names = []) => {
if (!orderId) return;
if (!props.identifier) {
uni.showToast({ title: '未找到当前资产', icon: 'none' });
return;
}
operationMode.value = 'order-payment';
orderPaymentId.value = orderId;
packageIds.value = [];
packageNames.value = Array.isArray(names) ? names.filter(Boolean) : [];
try {
await loadAssetInfo();
} catch (error) {
console.error('加载订单支付方式失败', error);
uni.showToast({ title: error.msg || error.message || '加载支付方式失败', icon: 'none' });
return;
}
if (!paymentMethodOptions.value.length) {
uni.showToast({ title: '暂无可用支付方式', icon: 'none' });
return;
}
show.value = true;
};
const selectPaymentMethod = (method) => {
if (paymentMethodOptions.value.some((item) => item.value === method)) {
paymentMethod.value = method;
}
};
const confirmOrderStatus = async (orderId) => {
try {
const detail = await orderApi.getDetail(orderId);
const status = detail?.payment_status ?? detail?.order?.payment_status;
showPaymentToast(status === 2, status === 2 ? '支付成功' : '支付结果确认中');
if (status === 2) emit('completed');
} catch (error) {
console.error('确认订单支付状态失败', error);
showPaymentToast(false, '支付结果确认失败,请稍后查看订单');
}
};
const handlePreparedPayment = async (paymentData, orderId = null, isRecharge = false) => {
if (isValidWechatPayConfig(paymentData?.pay_config)) {
try {
await wechatH5Pay(paymentData.pay_config);
if (orderId) {
await confirmOrderStatus(orderId);
} else {
showPaymentToast(true, isRecharge ? '充值成功,套餐将自动购买' : '支付成功');
emit('completed');
}
} catch (error) {
handlePaymentError(error);
}
return;
}
if (isValidAlipayPaymentLink(paymentData?.payment_link)) {
await openAlipayPayment(paymentData.payment_link, props.paymentRefreshTarget);
return;
}
uni.showToast({ title: '支付参数获取失败', icon: 'none' });
};
const handleRechargeOrder = async (orderResult) => {
const recharge = orderResult.recharge;
if (recharge.status === 2 || recharge.status === 3) {
uni.showToast({ title: `充值订单${recharge.status_name || '不可支付'},请重新下单`, icon: 'none' });
return;
}
if (recharge.status === 1 && !hasPreparedPaymentData(orderResult)) {
uni.showModal({
title: '提示',
content: '检测到您有待支付的充值订单,是否前往我的钱包查看?',
confirmText: '去查看',
cancelText: '取消',
success: ({ confirm }) => {
if (confirm) uni.navigateTo({ url: '/pages/my-wallet/my-wallet' });
}
});
return;
}
if (!hasPreparedPaymentData(orderResult)) {
uni.showToast({ title: '支付参数获取失败', icon: 'none' });
return;
}
const packageName = packageNamesText.value;
const rechargeAmount = orderResult.linked_package_info?.force_recharge_amount || recharge.amount;
uni.showModal({
title: '需要充值',
content: `续费${packageName}需要先充值¥${formatMoney(rechargeAmount)},充值成功后将自动购买套餐`,
confirmText: '去充值',
cancelText: '取消',
success: async ({ confirm }) => {
if (confirm) await handlePreparedPayment(orderResult, null, true);
}
});
};
const confirmPay = async () => {
if (submitting.value) return;
if (operationMode.value === 'order-payment') {
await confirmOrderPayment();
return;
}
if (!packageIds.value.length) return;
submitting.value = true;
uni.showLoading({ title: '创建订单...', mask: true });
try {
const orderResult = await orderApi.create(props.identifier, packageIds.value, paymentMethod.value);
if (orderResult?.order_type === 'recharge' && orderResult.recharge) {
uni.hideLoading();
show.value = false;
await handleRechargeOrder(orderResult);
return;
}
const orderId = orderResult?.order?.order_id;
if (!orderId) {
uni.hideLoading();
show.value = false;
uni.showToast({ title: '订单创建失败', icon: 'none' });
return;
}
uni.showLoading({ title: '准备支付...', mask: true });
const payResult = await orderApi.pay(orderId, paymentMethod.value);
uni.hideLoading();
show.value = false;
if (paymentMethod.value === 'wallet') {
await confirmOrderStatus(orderId);
return;
}
await handlePreparedPayment(payResult, orderId);
} catch (error) {
uni.hideLoading();
show.value = false;
console.error('创建续费订单或支付失败', error);
uni.showToast({ title: error.msg || error.message || '操作失败,请稍后重试', icon: 'none' });
} finally {
submitting.value = false;
}
};
const confirmOrderPayment = async () => {
if (!orderPaymentId.value) return;
submitting.value = true;
uni.showLoading({ title: '准备支付...', mask: true });
try {
const payData = await orderApi.pay(orderPaymentId.value, paymentMethod.value);
uni.hideLoading();
show.value = false;
if (paymentMethod.value === 'wallet') {
await confirmOrderStatus(orderPaymentId.value);
return;
}
if (paymentMethod.value === 'wechat') {
if (!isValidWechatPayConfig(payData?.pay_config)) {
uni.showToast({ title: '支付参数获取失败', icon: 'none' });
return;
}
try {
await wechatH5Pay(payData.pay_config);
await confirmOrderStatus(orderPaymentId.value);
} catch (error) {
handlePaymentError(error);
}
return;
}
if (!isValidAlipayPaymentLink(payData?.payment_link)) {
uni.showToast({ title: '支付链接获取失败', icon: 'none' });
return;
}
await openAlipayPayment(payData.payment_link, props.paymentRefreshTarget);
} catch (error) {
uni.hideLoading();
console.error('订单支付失败', error);
uni.showToast({ title: error.msg || error.message || '支付失败,请稍后重试', icon: 'none' });
} finally {
submitting.value = false;
}
};
defineExpose({ open, openOrderPayment });
</script>
<style scoped lang="scss">
.renewal-popup {
width: 600rpx;
max-width: calc(100vw - 80rpx);
padding: 0;
box-sizing: border-box;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
}
.popup-header,
.popup-footer,
.method-left {
display: flex;
align-items: center;
}
.popup-header {
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid var(--border-light);
}
.popup-title {
color: var(--text-primary);
font-size: 32rpx;
font-weight: 600;
}
.popup-close {
width: 48rpx;
height: 48rpx;
color: var(--text-tertiary);
font-size: 40rpx;
line-height: 48rpx;
text-align: center;
}
.package-summary {
padding: 40rpx 30rpx 30rpx;
background: linear-gradient(135deg, rgba(85, 171, 92, 0.05) 0%, rgba(85, 171, 92, 0.02) 100%);
border-bottom: 1rpx solid var(--border-light);
text-align: center;
}
.summary-label {
color: var(--text-tertiary);
font-size: 22rpx;
}
.summary-name {
margin: 20rpx 0 0;
color: var(--text-primary);
font-size: 32rpx;
font-weight: 700;
line-height: 1.4;
}
.payment-methods {
padding: 30rpx;
}
.method-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
margin-bottom: 20rpx;
border: 2rpx solid var(--border-light);
border-radius: var(--radius-medium);
transition: all 0.3s;
&:last-child { margin-bottom: 0; }
&.active {
border-color: var(--primary);
background: rgba(85, 171, 92, 0.05);
}
}
.method-left { gap: 20rpx; }
.method-icon {
width: 64rpx;
height: 64rpx;
}
.method-badge {
display: flex;
align-items: center;
justify-content: center;
background: #07c160;
color: #fff;
font-size: 24rpx;
font-weight: 600;
}
.method-badge-alipay { background: #1677ff; }
.method-name { color: var(--text-primary); font-size: 28rpx; }
.method-radio {
width: 36rpx;
height: 36rpx;
border: 2rpx solid var(--gray-400);
border-radius: 50%;
position: relative;
&.checked {
border-color: var(--primary);
background: var(--primary);
&::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 20rpx;
font-weight: 700;
}
}
}
.method-empty {
padding: 28rpx;
color: var(--text-tertiary);
font-size: 26rpx;
text-align: center;
}
.popup-footer {
display: flex;
padding: 30rpx;
gap: 20rpx;
border-top: 1rpx solid var(--border-light);
button {
flex: 1;
margin: 0;
}
}
</style>