Files
device-voice-h5/pages/package-order/package-order.vue
sexygoat 3f997063f4
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m37s
fix: 优化体验
2026-05-09 16:14:06 +08:00

563 lines
15 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">📦</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 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-data">{{ formatData(item.data_allowance, item.data_unit) }}</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 === '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 { assetApi, orderApi, walletApi } from '@/api/index.js';
import { useUserStore } from '@/store/index.js';
import { wechatH5Pay, handlePaymentError, showPaymentToast } from '@/utils/payment.js';
const userStore = useUserStore();
let showModal = ref(false);
let currentPackage = ref(null);
let packageList = reactive([]);
let loading = ref(false);
let paymentMethod = ref('wechat');
let walletBalance = ref(0);
let 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 (e) {
console.error('加载套餐列表失败', e);
}
loading.value = false;
};
const loadWalletBalance = async () => {
try {
const data = await walletApi.getDetail(userStore.state.identifier);
walletBalance.value = data.balance || 0;
} catch (e) {
console.error('加载钱包余额失败', e);
}
};
const buyPackage = (item) => {
currentPackage.value = item;
paymentMethod.value = 'wechat';
showModal.value = true;
};
const selectPaymentMethod = (method) => {
paymentMethod.value = method;
};
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]
);
// 判断是否为强充订单
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 && (!orderResult.pay_config || !orderResult.pay_config.package)) {
// 待支付订单已存在,引导用户跳转到订单页面
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 (!orderResult.pay_config || !orderResult.pay_config.package) {
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 || [];
uni.hideLoading();
showModal.value = false;
// 提示用户需要强充
const packageNameStr = packageNames.length > 0 ? packageNames.join('、') : '套餐';
const message = `购买${packageNameStr}需要先充值 ¥${formatMoney(forceRechargeAmount)},充值成功后将自动购买套餐`;
releaseInModalCallback = true;
uni.showModal({
title: '需要充值',
content: message,
confirmText: '去充值',
cancelText: '取消',
success: async (res) => {
try {
if (res.confirm) {
// 调起微信支付
await handleWechatPay(orderResult.pay_config, true);
}
} finally {
paySubmitting.value = false;
}
},
fail: () => {
paySubmitting.value = false;
}
});
return;
}
// 检查是否有订单信息(非强充订单)
if (!orderResult || !orderResult.order) {
uni.hideLoading();
showModal.value = false;
uni.showToast({ title: '订单创建失败', icon: 'none' });
return;
}
const orderId = orderResult.order.order_id;
// 第二步:获取支付参数
uni.showLoading({ title: '准备支付...', mask: true });
const payResult = await orderApi.pay(orderId, paymentMethod.value);
uni.hideLoading();
showModal.value = false;
// 处理不同支付方式
if (paymentMethod.value === 'wechat') {
// 微信支付
if (!payResult.pay_config || !payResult.pay_config.package) {
uni.showToast({ title: '支付参数获取失败', icon: 'none' });
return;
}
await handleWechatPay(payResult.pay_config);
} else if (paymentMethod.value === 'wallet') {
// 钱包支付成功
uni.showToast({ title: '支付成功', icon: 'success' });
setTimeout(() => {
loadWalletBalance();
}, 1500);
}
} catch (e) {
uni.hideLoading();
showModal.value = false;
console.error('创建订单或支付失败', e);
// 特殊错误处理:订单正在创建中
if (e.code === 1008) {
uni.showModal({
title: '提示',
content: '订单正在创建中,是否跳转到我的订单页面进行支付?',
confirmText: '去支付',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/order-list/order-list' });
}
}
});
return;
}
// 其他错误提示
const errorMsg = e.msg || e.message || '操作失败,请稍后重试';
uni.showToast({ title: errorMsg, icon: 'none' });
} finally {
if (!releaseInModalCallback) {
paySubmitting.value = false;
}
}
};
const handleWechatPay = async (payConfig, isForceRecharge = false) => {
try {
await wechatH5Pay(payConfig);
// 支付成功
const successMsg = isForceRecharge ? '充值成功,套餐将自动购买' : '支付成功';
showPaymentToast(true, successMsg);
setTimeout(() => {
loadWalletBalance();
}, 1500);
} catch (error) {
// 支付失败或取消
handlePaymentError(error);
}
};
onMounted(() => {
loadPackages();
loadWalletBalance();
});
</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: 120rpx; margin-bottom: 30rpx; opacity: 0.6; }
.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-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>