Files
device-voice-h5/pages/package-order/package-order.vue
sexygoat 169d2bdaad
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 53s
fix: 修改套餐位置
2026-05-28 14:16:07 +08:00

667 lines
16 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="container">
<view v-if="packageList.length === 0 && !loading" class="empty-state">
<view class="empty-icon">PKG</view>
<view class="empty-title">暂无可购套餐请联系客服</view>
<view class="empty-desc">当前资产暂无适用的套餐</view>
</view>
<view class="card package-card" v-for="item in packageList" :key="item.package_id">
<view class="package-header flex-row-sb">
<view class="package-name">{{ item.package_name }}</view>
</view>
<view>
<view class="package-data">{{ formatData(item.data_allowance, item.data_unit) }}</view>
<view class="header-right">
<view class="validity-tag">有效期: {{ item.validity_days }} </view>
<view class="tag-apple" :class="item.is_addon ? 'tag-warning' : 'tag-primary'">
{{ item.is_addon ? '加油包' : '正式套餐' }}
</view>
</view>
</view>
<view class="package-desc">{{ item.description }}</view>
<view class="package-footer flex-row-sb">
<view class="package-price">¥{{ formatMoney(item.retail_price) }}</view>
<view class="btn">
<up-button type="primary" @click="buyPackage(item)">立即订购</up-button>
</view>
</view>
</view>
<up-popup :show="showModal" mode="center" @close="showModal = false">
<view class="payment-popup">
<view class="popup-header">
<view class="popup-title">选择支付方式</view>
<view class="popup-close" @tap="showModal = false">×</view>
</view>
<view class="package-summary">
<view class="summary-name">{{ currentPackage?.package_name }}</view>
<view class="summary-price">¥{{ formatMoney(currentPackage?.retail_price) }}</view>
<view class="summary-details">
<view class="detail-item">
<text class="detail-label">套餐流量</text>
<text class="detail-value">{{ formatData(currentPackage?.data_allowance, currentPackage?.data_unit) }}</text>
</view>
<view class="detail-divider"></view>
<view class="detail-item">
<text class="detail-label">有效期</text>
<text class="detail-value">{{ currentPackage?.validity_days }} </text>
</view>
</view>
</view>
<view class="payment-methods">
<view class="method-item" :class="{ active: paymentMethod === 'wechat' }" @tap="selectPaymentMethod('wechat')">
<view class="method-left">
<image class="method-icon" src="/static/wechat.png" mode="aspectFit"></image>
<text class="method-name">微信支付</text>
</view>
<view class="method-radio" :class="{ checked: paymentMethod === 'wechat' }"></view>
</view>
<view class="method-item" :class="{ active: paymentMethod === 'alipay' }" @tap="selectPaymentMethod('alipay')">
<view class="method-left">
<view class="method-icon method-badge method-badge-alipay"></view>
<text class="method-name">支付宝支付</text>
</view>
<view class="method-radio" :class="{ checked: paymentMethod === 'alipay' }"></view>
</view>
<view class="method-item" :class="{ active: paymentMethod === 'wallet' }" @tap="selectPaymentMethod('wallet')">
<view class="method-left">
<image class="method-icon" src="/static/wallet.png" mode="aspectFit"></image>
<text class="method-name">账户余额¥{{ formatMoney(walletBalance) }}</text>
</view>
<view class="method-radio" :class="{ checked: paymentMethod === 'wallet' }"></view>
</view>
</view>
<view class="popup-footer">
<button class="btn-apple btn-secondary" @tap="showModal = false">取消</button>
<button class="btn-apple btn-primary" :disabled="paySubmitting" @tap="confirmPay">
{{ paySubmitting ? '处理中...' : '确认支付' }}
</button>
</view>
</view>
</up-popup>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { assetApi, orderApi, walletApi } from '@/api/index.js';
import { useUserStore } from '@/store/index.js';
import {
consumePendingPaymentRefresh,
handlePaymentError,
isValidAlipayPaymentLink,
isValidWechatPayConfig,
openAlipayPayment,
PAYMENT_REFRESH_TARGETS,
showPaymentToast,
wechatH5Pay
} from '@/utils/payment.js';
const userStore = useUserStore();
const showModal = ref(false);
const currentPackage = ref(null);
const packageList = reactive([]);
const loading = ref(false);
const paymentMethod = ref('wechat');
const walletBalance = ref(0);
const paySubmitting = ref(false);
const formatMoney = (amount) => {
if (!amount && amount !== 0) return '0.00';
return (amount / 100).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
const formatData = (allowance, unit) => {
if (unit === 'MB') {
return allowance >= 1024 ? `${(allowance / 1024).toFixed(0)} GB` : `${allowance} MB`;
}
return `${allowance} ${unit}`;
};
const loadPackages = async () => {
loading.value = true;
try {
const data = await assetApi.getPackages(userStore.state.identifier);
packageList.splice(0, packageList.length, ...(data.packages || []));
} catch (error) {
console.error('加载套餐列表失败', error);
}
loading.value = false;
};
const loadWalletBalance = async () => {
try {
const data = await walletApi.getDetail(userStore.state.identifier);
walletBalance.value = data.balance || 0;
} catch (error) {
console.error('加载钱包余额失败', error);
}
};
const syncPackagePageState = () => {
loadPackages();
loadWalletBalance();
};
const buyPackage = (item) => {
currentPackage.value = item;
paymentMethod.value = 'wechat';
showModal.value = true;
};
const selectPaymentMethod = (method) => {
paymentMethod.value = method;
};
const hasPreparedPaymentData = (paymentData) => {
return isValidWechatPayConfig(paymentData?.pay_config) ||
isValidAlipayPaymentLink(paymentData?.payment_link);
};
const handleWechatPay = async (payConfig, isForceRecharge = false) => {
try {
await wechatH5Pay(payConfig);
showPaymentToast(true, isForceRecharge ? '充值成功,套餐将自动购买' : '支付成功');
setTimeout(() => {
loadWalletBalance();
}, 1500);
} catch (error) {
handlePaymentError(error);
}
};
const handlePreparedPayment = async (paymentData, isForceRecharge = false) => {
if (isValidWechatPayConfig(paymentData?.pay_config)) {
await handleWechatPay(paymentData.pay_config, isForceRecharge);
return;
}
if (isValidAlipayPaymentLink(paymentData?.payment_link)) {
await openAlipayPayment(paymentData.payment_link, PAYMENT_REFRESH_TARGETS.PACKAGE_ORDER);
return;
}
uni.showToast({
title: '支付参数获取失败',
icon: 'none'
});
};
const getCreateOrderPaymentMethod = () => {
return paymentMethod.value === 'alipay' ? 'alipay' : undefined;
};
const confirmPay = async () => {
if (paySubmitting.value || !currentPackage.value) return;
paySubmitting.value = true;
let releaseInModalCallback = false;
uni.showLoading({
title: '创建订单...',
mask: true
});
try {
const orderResult = await orderApi.create(
userStore.state.identifier,
[currentPackage.value.package_id],
getCreateOrderPaymentMethod()
);
if (orderResult.order_type === 'recharge' && orderResult.recharge) {
const recharge = orderResult.recharge;
if (recharge.status === 2 || recharge.status === 3) {
uni.hideLoading();
showModal.value = false;
uni.showToast({
title: `充值订单${recharge.status_name},请重新下单`,
icon: 'none',
duration: 2000
});
return;
}
if (recharge.status === 1 && !hasPreparedPaymentData(orderResult)) {
uni.hideLoading();
showModal.value = false;
uni.showModal({
title: '提示',
content: '检测到您有待支付的充值订单,是否前往我的钱包查看?',
confirmText: '去查看',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/my-wallet/my-wallet' });
}
}
});
return;
}
if (!hasPreparedPaymentData(orderResult)) {
uni.hideLoading();
showModal.value = false;
uni.showToast({
title: '支付参数获取失败',
icon: 'none'
});
return;
}
const forceRechargeAmount = orderResult.linked_package_info?.force_recharge_amount || recharge.amount;
const packageNames = orderResult.linked_package_info?.package_names || [];
const packageNameStr = packageNames.length > 0 ? packageNames.join('、') : '套餐';
uni.hideLoading();
showModal.value = false;
releaseInModalCallback = true;
uni.showModal({
title: '需要充值',
content: `购买${packageNameStr}需要先充值 ¥${formatMoney(forceRechargeAmount)},充值成功后将自动购买套餐`,
confirmText: '去充值',
cancelText: '取消',
success: async (res) => {
try {
if (res.confirm) {
await handlePreparedPayment(orderResult, true);
}
} finally {
paySubmitting.value = false;
}
},
fail: () => {
paySubmitting.value = false;
}
});
return;
}
if (!orderResult?.order?.order_id) {
uni.hideLoading();
showModal.value = false;
uni.showToast({
title: '订单创建失败',
icon: 'none'
});
return;
}
uni.showLoading({
title: '准备支付...',
mask: true
});
const payResult = await orderApi.pay(orderResult.order.order_id, paymentMethod.value);
uni.hideLoading();
showModal.value = false;
if (paymentMethod.value === 'wallet') {
uni.showToast({
title: '支付成功',
icon: 'success'
});
setTimeout(() => {
loadWalletBalance();
}, 1500);
return;
}
await handlePreparedPayment(payResult);
} catch (error) {
uni.hideLoading();
showModal.value = false;
console.error('创建订单或支付失败', error);
if (error.code === 1008) {
uni.showModal({
title: '提示',
content: '订单正在创建中,是否跳转到我的订单页面进行支付?',
confirmText: '去支付',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/order-list/order-list' });
}
}
});
return;
}
uni.showToast({
title: error.msg || error.message || '操作失败,请稍后重试',
icon: 'none'
});
} finally {
if (!releaseInModalCallback) {
paySubmitting.value = false;
}
}
};
onShow(() => {
if (!consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.PACKAGE_ORDER)) {
return;
}
syncPackagePageState();
uni.showModal({
title: '支付状态确认',
content: '已返回页面,支付结果以订单状态为准。是否前往我的订单查看最新状态?',
confirmText: '查看订单',
cancelText: '稍后',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/order-list/order-list' });
}
}
});
});
onMounted(() => {
syncPackagePageState();
});
</script>
<style lang="scss" scoped>
.container {
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
min-height: 400rpx;
.empty-icon {
font-size: 60rpx;
margin-bottom: 30rpx;
opacity: 0.6;
letter-spacing: 4rpx;
}
.empty-title {
font-size: 32rpx;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 16rpx;
}
.empty-desc {
font-size: 26rpx;
color: var(--text-tertiary);
text-align: center;
}
}
.package-card {
margin-bottom: var(--space-md);
.package-header {
margin-bottom: var(--space-sm);
.package-name {
font-size: 32rpx;
font-weight: 600;
color: var(--text-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 12rpx;
.validity-tag {
font-size: 24rpx;
color: var(--text-secondary);
padding: 4rpx 12rpx;
background: var(--gray-100);
border-radius: 8rpx;
}
}
}
.package-data {
font-size: 48rpx;
font-weight: 700;
color: var(--primary);
margin-bottom: var(--space-sm);
letter-spacing: -1rpx;
}
.package-desc {
font-size: 26rpx;
color: var(--text-secondary);
margin-bottom: var(--space-md);
}
.package-footer {
align-items: center;
.package-price {
font-size: 40rpx;
font-weight: 700;
color: var(--warning);
}
.btn {
flex-shrink: 0;
}
}
}
}
.payment-popup {
width: 600rpx;
background: var(--bg-primary);
border-radius: var(--radius-large);
overflow: hidden;
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid var(--border-light);
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: var(--text-primary);
}
.popup-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
color: var(--text-tertiary);
cursor: pointer;
}
}
.package-summary {
padding: 40rpx 30rpx 30rpx;
background: linear-gradient(135deg, rgba(0, 122, 255, 0.05) 0%, rgba(0, 122, 255, 0.02) 100%);
border-bottom: 1rpx solid var(--border-light);
text-align: center;
.summary-name {
font-size: 32rpx;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 20rpx;
line-height: 1.4;
}
.summary-price {
font-size: 56rpx;
font-weight: 700;
color: var(--primary);
margin-bottom: 24rpx;
letter-spacing: -1rpx;
}
.summary-details {
display: flex;
align-items: center;
justify-content: center;
gap: 24rpx;
padding-top: 20rpx;
border-top: 1rpx solid rgba(0, 122, 255, 0.1);
.detail-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
.detail-label {
font-size: 22rpx;
color: var(--text-tertiary);
}
.detail-value {
font-size: 26rpx;
font-weight: 600;
color: var(--text-primary);
}
}
.detail-divider {
width: 1rpx;
height: 40rpx;
background: var(--border-light);
}
}
}
.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);
cursor: pointer;
transition: all 0.3s;
&:last-child {
margin-bottom: 0;
}
&.active {
border-color: var(--primary);
background: rgba(0, 122, 255, 0.05);
}
.method-left {
display: flex;
align-items: center;
gap: 20rpx;
.method-icon {
width: 64rpx;
height: 64rpx;
}
.method-badge {
display: flex;
align-items: center;
justify-content: center;
border-radius: 18rpx;
font-size: 32rpx;
font-weight: 700;
color: #fff;
}
.method-badge-alipay {
background: linear-gradient(135deg, #1677ff 0%, #45a5ff 100%);
box-shadow: 0 8rpx 20rpx rgba(22, 119, 255, 0.2);
}
.method-name {
font-size: 28rpx;
font-weight: 500;
color: var(--text-primary);
}
}
.method-radio {
width: 36rpx;
height: 36rpx;
border: 2rpx solid var(--gray-400);
border-radius: 50%;
position: relative;
transition: all 0.3s;
&.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;
}
}
}
}
}
.popup-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 1rpx solid var(--border-light);
.btn-apple {
flex: 1;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-medium);
font-size: 28rpx;
font-weight: 500;
border: none;
&[disabled] {
opacity: 0.7;
}
&.btn-secondary {
background: var(--gray-200);
color: var(--text-primary);
}
&.btn-primary {
background: var(--primary);
color: #fff;
}
}
}
}
</style>