Files
device-voice-h5/pages/package-order/package-order.vue
sexygoat f634af1d65
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 44s
fix: 套餐列表支付, 非设备显示phone
2026-04-14 11:48:03 +08:00

454 lines
12 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="tag-apple" :class="item.is_addon ? 'tag-warning' : 'tag-primary'">
{{ item.is_addon ? '加油包' : '正式套餐' }}
</view>
</view>
<view class="package-price">¥{{ formatMoney(item.retail_price) }}</view>
<view class="package-desc">{{ item.description }}</view>
<view class="package-info">
<view class="info-item">流量: {{ formatData(item.data_allowance, item.data_unit) }}</view>
<view class="info-item">有效: {{ item.validity_days }}</view>
</view>
<view class="btn">
<up-button type="primary" @click="buyPackage(item)">立即订购</up-button>
</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-item">
<text class="label">套餐名称</text>
<text class="value">{{ currentPackage?.package_name }}</text>
</view>
<view class="summary-item">
<text class="label">支付金额</text>
<text class="value price">¥{{ formatMoney(currentPackage?.retail_price) }}</text>
</view>
</view>
<view class="payment-methods">
<view class="method-item" :class="{ active: paymentMethod === 'wechat' }" @tap="selectPaymentMethod('wechat')">
<view class="method-left">
<view class="method-icon wechat-icon">💚</view>
<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">
<view class="method-icon wallet-icon">💰</view>
<view class="method-info">
<text class="method-name">账户余额</text>
<text class="wallet-balance">余额: ¥{{ formatMoney(walletBalance) }}</text>
</view>
</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" @tap="confirmPay">确认支付</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';
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);
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 (!currentPackage.value) return;
uni.showLoading({ title: '创建订单...', mask: true });
try {
// 第一步:创建订单
const orderResult = await orderApi.create(
userStore.state.identifier,
[currentPackage.value.package_id]
);
const orderId = orderResult.order.order_id;
// 判断是否为强充订单
if (orderResult.order_type === 'recharge' && orderResult.pay_config) {
// 强充订单:直接使用返回的 pay_config 支付
if (!orderResult.pay_config.package) {
uni.hideLoading();
uni.showToast({ title: '支付参数获取失败', icon: 'none' });
return;
}
uni.hideLoading();
showModal.value = false;
// 调起微信支付
await handleWechatPay(orderResult.pay_config);
return;
}
// 第二步:获取支付参数
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);
const errorMsg = e.msg || e.message || '操作失败,请稍后重试';
uni.showToast({ title: errorMsg, icon: 'none' });
}
};
const handleWechatPay = async (payConfig) => {
// 验证参数
if (!payConfig.package || !payConfig.package.startsWith('prepay_id=')) {
uni.showToast({ title: '支付参数无效', icon: 'none' });
return;
}
// H5 和小程序都使用 uni.requestPayment
uni.requestPayment({
timeStamp: payConfig.timestamp,
nonceStr: payConfig.nonce_str,
package: payConfig.package,
signType: payConfig.sign_type,
paySign: payConfig.pay_sign,
success: (res) => {
console.log('支付成功', res);
uni.showToast({ title: '支付成功', icon: 'success' });
setTimeout(() => {
loadWalletBalance();
}, 1500);
},
fail: (err) => {
console.error('支付失败', err);
// 判断是否用户取消
if (err.errMsg && (err.errMsg.includes('cancel') || err.errMsg.includes('用户取消'))) {
uni.showToast({ title: '已取消支付', icon: 'none' });
} else {
uni.showToast({ title: '支付失败', icon: 'none' });
}
}
});
};
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); }
}
.package-price {
font-size: 40rpx;
font-weight: bold;
color: var(--warning);
margin-bottom: var(--space-sm);
}
.package-desc {
font-size: 26rpx;
color: var(--text-secondary);
margin-bottom: var(--space-sm);
}
.package-info {
display: flex;
gap: var(--space-lg);
margin-bottom: var(--space-md);
.info-item { font-size: 24rpx; color: var(--text-tertiary); }
}
.btn { margin-top: var(--space-sm); }
}
}
.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: 30rpx;
background: var(--gray-100);
border-bottom: 1rpx solid var(--border-light);
.summary-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.label {
font-size: 26rpx;
color: var(--text-secondary);
}
.value {
font-size: 26rpx;
color: var(--text-primary);
&.price {
font-size: 32rpx;
font-weight: 700;
color: var(--warning);
}
}
}
}
.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;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
border-radius: var(--radius-medium);
background: var(--gray-100);
}
.method-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.method-name {
font-size: 28rpx;
font-weight: 500;
color: var(--text-primary);
}
.wallet-balance {
font-size: 22rpx;
color: var(--text-tertiary);
}
}
.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;
&.btn-secondary {
background: var(--gray-200);
color: var(--text-primary);
}
&.btn-primary {
background: var(--primary);
color: #fff;
}
}
}
}
</style>