Files
device-voice-h5/pages/package-order/package-order.vue
luo 42d94ce290
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 2m14s
fix: ui
2026-08-13 14:22:41 +08:00

1187 lines
29 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 class="tab-section">
<view class="tab-header">
<view class="tab-indicator" :style="{ left: packageTypeIndex === 0 ? '8rpx' : '50%' }"></view>
<view
v-for="tab in packageTypeTabs"
:key="tab.value"
class="tab-item"
:class="{ active: activePackageType === tab.value }"
@tap="switchPackageType(tab.value)"
>
{{ tab.label }}
</view>
</view>
<swiper
class="tab-swiper package-swiper"
:current="packageTypeIndex"
:circular="false"
:acceleration="true"
@change="onPackageSwiperChange"
>
<swiper-item v-for="tab in packageTypeTabs" :key="tab.value" class="package-swiper-item">
<scroll-view v-if="activePackageType === tab.value" scroll-y class="package-scroll">
<view v-if="!packagesLoaded || (loading && packageList.length === 0)" class="loading-state">
<up-loading-icon text="加载中" size="30"></up-loading-icon>
</view>
<view v-else-if="packageList.length === 0" class="empty-state">
<image class="empty-icon" src="/static/shop.png" mode="aspectFit" alt="套餐订购"></image>
<view class="empty-title">暂无{{ activePackageType === 'formal' ? '正式套餐' : '加油包' }}</view>
<view class="empty-desc">当前资产暂无适用的套餐</view>
</view>
<view v-else class="package-list">
<view class="package-card" v-for="item in packageList" :key="item.package_id">
<view class="package-info">
<view class="package-name">{{ item.package_name }}</view>
<view class="package-meta">
<view class="package-meta-item">
<image class="package-meta-icon" src="/static/有效期.png" mode="aspectFit" />
<text>{{ formatValidity(item.validity_days) }}</text>
</view>
<view class="package-meta-item">
<image class="package-meta-icon" src="/static/流量.png" mode="aspectFit" />
<text>{{ formatData(item.data_allowance, item.data_unit) }}</text>
</view>
</view>
<view class="package-description">高速流量 · 全国通用 · 不限速</view>
</view>
<view class="package-side">
<view class="package-price"><text class="price-symbol">¥</text>{{ formatPackagePrice(item.retail_price) }}</view>
<button class="btn-apple btn-primary package-buy-button" @tap="buyPackage(item)">立即订购</button>
</view>
</view>
</view>
</scroll-view>
</swiper-item>
</swiper>
</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">¥{{ formatPackagePrice(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">{{ formatValidity(currentPackage?.validity_days) }}</text>
</view>
</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 }}<template v-if="method.value === 'wallet'">¥{{ formatMoney(walletBalance) }}</template></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="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, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { assetApi, isAssetRealNameCompleted, 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';
import { formatMoney } from '@/utils/display.js';
import { getDefaultPaymentMethod, getPaymentMethodOptions, normalizePaymentMethods } from '@/utils/payment-methods.js';
const userStore = useUserStore();
const showModal = ref(false);
const currentPackage = ref(null);
const packageList = reactive([]);
const loading = ref(false);
const packagesLoaded = ref(false);
const paymentMethod = ref('alipay');
const walletBalance = ref(0);
const paySubmitting = ref(false);
const assetInfo = ref({});
const allowedPaymentMethods = ref([]);
const renewalPackageIds = ref([]);
const renewalPackageNames = ref([]);
const activePackageType = ref('formal');
const packageTypeIndex = ref(0);
const packageTypeTabs = [
{ label: '正式套餐', value: 'formal' },
{ label: '加油包', value: 'addon' }
];
let assetTypePromise = null;
let packageRequestId = 0;
const paymentMethodOptions = computed(() => getPaymentMethodOptions(allowedPaymentMethods.value));
const formatData = (allowance, unit) => {
if (unit === 'MB') {
return allowance >= 1024 ? `${(allowance / 1024).toFixed(0)}GB` : `${allowance}MB`;
}
return `${allowance}${unit}`;
};
const formatPackagePrice = (amount) => amount === null || amount === undefined ? '-' : formatMoney(amount);
const formatValidity = (days) => {
const value = String(days ?? '').trim();
return value && value !== '-' ? `${value}天有效` : '有效期未知';
};
const isPaymentMethodAvailable = (method) => {
return paymentMethodOptions.value.some((item) => item.value === method);
};
const normalizePaymentMethod = () => {
if (!isPaymentMethodAvailable(paymentMethod.value)) {
paymentMethod.value = getDefaultPaymentMethod(allowedPaymentMethods.value);
}
};
const loadAssetType = async () => {
if (!userStore.state.identifier) return;
if (assetTypePromise) return assetTypePromise;
assetTypePromise = assetApi.getInfo(userStore.state.identifier)
.then((data) => {
assetInfo.value = data;
allowedPaymentMethods.value = normalizePaymentMethods(data.allowed_payment_methods);
normalizePaymentMethod();
})
.catch((error) => {
console.error('加载资产类型失败', error);
})
.finally(() => {
assetTypePromise = null;
});
return assetTypePromise;
};
const loadPackages = async (packageType = activePackageType.value) => {
const requestId = ++packageRequestId;
packagesLoaded.value = false;
loading.value = true;
packageList.splice(0, packageList.length);
try {
const data = await assetApi.getPackages(userStore.state.identifier, packageType);
if (requestId !== packageRequestId) return;
const currentPackageId = Number(assetInfo.value.current_package_id || 0);
const packages = (data.packages || []).filter((item) => {
const status = String(item.status || '').toLowerCase();
const discontinued = item.is_on_sale === false || item.on_sale === false ||
['off_shelf', 'offline', 'discontinued', '下架'].includes(status);
return !discontinued || Number(item.package_id) === currentPackageId;
}).map((item) => ({
...item,
is_renewal: Number(item.package_id) === currentPackageId &&
(item.is_on_sale === false || item.on_sale === false ||
['off_shelf', 'offline', 'discontinued', '下架'].includes(String(item.status || '').toLowerCase()))
}));
const packageIdsInList = new Set(packages.map((item) => Number(item.package_id)));
const renewalOnlyPackages = renewalPackageIds.value
.filter(() => packageType === 'formal')
.filter((packageId) => !packageIdsInList.has(Number(packageId)))
.map((packageId, index) => ({
package_id: Number(packageId),
package_name: renewalPackageNames.value[index] || `历史套餐 ${packageId}`,
retail_price: null,
data_allowance: 0,
data_unit: 'MB',
validity_days: '-',
package_type: 'formal',
is_renewal: true
}));
packageList.splice(0, packageList.length, ...packages, ...renewalOnlyPackages);
} catch (error) {
if (requestId !== packageRequestId) return;
console.error('加载套餐列表失败', error);
} finally {
if (requestId === packageRequestId) {
loading.value = false;
packagesLoaded.value = true;
}
}
};
const switchPackageType = (packageType) => {
const nextIndex = packageTypeTabs.findIndex((item) => item.value === packageType);
if (nextIndex < 0) return;
packageTypeIndex.value = nextIndex;
if (packageType === activePackageType.value) return;
activePackageType.value = packageType;
loadPackages(packageType);
};
const onPackageSwiperChange = (event) => {
const nextIndex = Number(event?.detail?.current);
const nextTab = packageTypeTabs[nextIndex];
if (nextTab) switchPackageType(nextTab.value);
};
const loadWalletBalance = async () => {
try {
const data = await walletApi.getDetail(userStore.state.identifier);
walletBalance.value = data.balance || 0;
} catch (error) {
console.error('加载钱包余额失败', error);
}
};
const syncPackagePageState = async () => {
await loadAssetType();
await Promise.all([loadPackages(), loadWalletBalance()]);
};
const buyPackage = async (item) => {
await loadAssetType();
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;
}
currentPackage.value = item;
paymentMethod.value = getDefaultPaymentMethod(allowedPaymentMethods.value);
showModal.value = true;
};
const selectPaymentMethod = (method) => {
if (!isPaymentMethodAvailable(method)) return;
paymentMethod.value = method;
};
const hasPreparedPaymentData = (paymentData) => {
return isValidWechatPayConfig(paymentData?.pay_config) ||
isValidAlipayPaymentLink(paymentData?.payment_link);
};
const handleWechatPay = async (payConfig, isForceRecharge = false, orderId = null) => {
try {
await wechatH5Pay(payConfig);
if (orderId) {
const detail = await orderApi.getDetail(orderId);
const status = detail?.payment_status ?? detail?.order?.payment_status;
showPaymentToast(status === 2, status === 2 ? '支付成功' : '支付结果确认中');
} else {
showPaymentToast(true, isForceRecharge ? '充值成功,套餐将自动购买' : '支付成功');
}
setTimeout(() => {
loadWalletBalance();
}, 1500);
} catch (error) {
handlePaymentError(error);
}
};
const handlePreparedPayment = async (paymentData, isForceRecharge = false, orderId = null) => {
if (isValidWechatPayConfig(paymentData?.pay_config)) {
await handleWechatPay(paymentData.pay_config, isForceRecharge, orderId);
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;
};
const getCreateOrderPackageIds = () => renewalPackageIds.value.length
? renewalPackageIds.value
: [currentPackage.value.package_id];
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,
getCreateOrderPackageIds(),
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') {
try {
const detail = await orderApi.getDetail(orderResult.order.order_id);
const status = detail?.payment_status ?? detail?.order?.payment_status;
showPaymentToast(status === 2, status === 2 ? '支付成功' : '支付结果确认中');
} catch (error) {
showPaymentToast(false, '支付结果确认失败,请稍后查看订单');
}
setTimeout(() => {
loadWalletBalance();
}, 1500);
return;
}
await handlePreparedPayment(payResult, false, orderResult.order.order_id);
} 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' });
}
}
});
});
onLoad((query = {}) => {
const ids = String(query.renewal_package_ids || '')
.split(',')
.map((value) => Number(value))
.filter((value) => Number.isInteger(value) && value > 0);
renewalPackageIds.value = [...new Set(ids)];
try {
renewalPackageNames.value = decodeURIComponent(String(query.renewal_package_names || ''))
.split('|')
.filter(Boolean);
} catch (error) {
renewalPackageNames.value = [];
}
});
onMounted(() => {
syncPackagePageState();
});
</script>
<style lang="scss" scoped>
.container {
gap: 16rpx;
padding: 24rpx 20rpx 40rpx;
.package-tabs {
display: flex;
gap: 6rpx;
padding: 6rpx;
background: var(--gray-100);
border: 1rpx solid var(--gray-200);
border-radius: 16rpx;
.package-tab {
flex: 1;
padding: 16rpx 0;
border-radius: 12rpx;
color: var(--text-tertiary);
font-size: 26rpx;
text-align: center;
transition: all 0.2s ease;
&.active {
background: var(--bg-primary);
box-shadow: var(--shadow-small);
color: var(--primary);
font-weight: 600;
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
min-height: 400rpx;
.empty-icon {
width: 120rpx;
height: 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: 0;
padding: 28rpx;
border: 1rpx solid var(--gray-200);
border-radius: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.03);
.package-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 24rpx;
.package-name {
font-size: 30rpx;
font-weight: 600;
color: var(--text-primary);
line-height: 1.4;
flex: 1;
}
.package-header-meta {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 8rpx;
}
.package-tag {
padding: 6rpx 12rpx;
background: var(--gray-100);
color: var(--gray-700);
border-radius: var(--radius-small);
font-size: 22rpx;
font-weight: 600;
border: none;
}
.package-validity {
padding: 8rpx 12rpx;
border-radius: var(--radius-small);
background: rgba(85, 171, 92, 0.1);
color: var(--primary-dark);
font-size: 22rpx;
font-weight: 600;
white-space: nowrap;
}
}
.package-main {
display: flex;
align-items: stretch;
justify-content: space-between;
gap: 24rpx;
padding: 24rpx;
margin-bottom: 24rpx;
background: linear-gradient(135deg, rgba(85, 171, 92, 0.08), rgba(85, 171, 92, 0.02));
border: 1rpx solid rgba(85, 171, 92, 0.1);
border-radius: 18rpx;
.data-block {
flex: 1;
padding-right: 20rpx;
.data-label {
font-size: 23rpx;
color: var(--text-tertiary);
margin-bottom: 10rpx;
}
}
.package-data {
font-size: 48rpx;
font-weight: 700;
color: var(--primary);
line-height: 1.1;
letter-spacing: -1rpx;
}
.price-block {
min-width: 150rpx;
display: flex;
align-items: center;
justify-content: center;
padding-left: 24rpx;
border-left: 1rpx solid rgba(85, 171, 92, 0.18);
color: var(--primary-dark);
white-space: nowrap;
.price-symbol {
font-size: 26rpx;
font-weight: 700;
margin-right: 4rpx;
}
.package-price {
font-size: 40rpx;
font-weight: 700;
letter-spacing: -1rpx;
}
}
}
.package-footer {
display: block;
padding-top: 22rpx;
border-top: 1rpx solid var(--gray-100);
.btn {
width: 100%;
}
}
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 400rpx;
}
}
/* Screenshot-aligned package list skin */
.container {
position: fixed;
top: 88rpx;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
gap: 18rpx;
padding: 20rpx 24rpx 48rpx;
height: auto;
min-height: 0;
background: #f8f9fb;
box-sizing: border-box;
overflow: hidden;
}
.tab-section {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.tab-header {
flex-shrink: 0;
display: flex;
position: relative;
background: #fff;
border-radius: 16rpx;
padding: 8rpx;
}
.tab-indicator {
position: absolute;
top: 8rpx;
width: calc(50% - 16rpx);
height: calc(100% - 16rpx);
background: var(--primary);
border-radius: 12rpx;
transition: left 0.3s ease;
z-index: 0;
}
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 0;
font-size: 28rpx;
font-weight: 500;
color: var(--text-tertiary);
position: relative;
z-index: 1;
transition: color 0.3s;
&.active {
color: #fff;
font-weight: 600;
}
}
.tab-swiper {
margin-top: 20rpx;
flex: 1 1 0;
min-height: 0;
height: auto;
}
.container .package-tabs {
display: flex;
gap: 0;
padding: 0;
background: #f3f5f8;
border: 0;
border-radius: 22rpx;
box-shadow: none;
overflow: hidden;
}
.container .package-tab {
flex: 1;
padding: 26rpx 0 24rpx;
border-radius: 22rpx;
color: #737985;
font-size: 30rpx;
font-weight: 500;
line-height: 1.2;
}
.container .package-tab.active {
background: transparent;
box-shadow: none;
color: #fff;
font-weight: 600;
}
.container .package-tabs {
position: relative;
flex-shrink: 0;
display: flex;
padding: 8rpx;
background: #fff;
border-radius: 16rpx;
}
.container .package-tab {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
padding: 20rpx 0;
color: var(--text-tertiary);
font-size: 28rpx;
font-weight: 500;
transition: color 0.3s;
}
.package-tab-indicator {
position: absolute;
top: 8rpx;
width: calc(50% - 16rpx);
height: calc(100% - 16rpx);
background: var(--primary);
border-radius: 12rpx;
transition: left 0.3s ease;
z-index: 0;
}
.package-swiper,
.package-swiper-item,
.package-scroll {
width: 100%;
min-height: 0;
}
.package-swiper {
/* 固定页面提供确定高度,轮播填充 Tab 下方的全部可用空间。 */
flex: 1 1 0;
height: auto;
overflow: hidden;
}
.package-swiper-item,
.package-scroll {
height: 100%;
}
.package-list {
display: flex;
flex-direction: column;
gap: 18rpx;
}
.container .package-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24rpx;
min-height: 210rpx;
margin: 0;
padding: 30rpx 32rpx;
background: #fff;
border: 1rpx solid #f0f1f3;
border-radius: 20rpx;
box-shadow: 0 6rpx 18rpx rgba(31, 35, 41, 0.04);
box-sizing: border-box;
}
.package-info {
min-width: 0;
flex: 1;
}
.package-info .package-name {
color: #14161a;
font-size: 36rpx;
font-weight: 600;
line-height: 1.25;
word-break: break-all;
}
.package-meta {
display: flex;
align-items: center;
gap: 16rpx;
margin-top: 22rpx;
}
.package-meta-item {
display: inline-flex;
align-items: center;
gap: 10rpx;
min-width: 0;
padding: 10rpx 16rpx;
border-radius: 28rpx;
background: rgba(85, 171, 92, 0.08);
color: var(--primary-dark);
font-size: 24rpx;
line-height: 1.2;
white-space: nowrap;
}
.package-meta-icon {
width: 32rpx;
height: 32rpx;
flex-shrink: 0;
}
.package-description {
margin-top: 18rpx;
color: #7c838e;
font-size: 25rpx;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.package-side {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: space-between;
align-self: stretch;
min-width: 190rpx;
padding: 4rpx 0;
box-sizing: border-box;
}
.package-side .package-price {
color: #ed2524;
font-size: 44rpx;
font-weight: 600;
line-height: 1;
white-space: nowrap;
}
.package-side .price-symbol {
margin-right: 4rpx;
font-size: 26rpx;
font-weight: 600;
vertical-align: 10rpx;
}
.package-buy-button {
width: 184rpx;
height: 64rpx;
min-height: 64rpx;
padding: 0;
border-radius: 14rpx;
font-size: 28rpx;
line-height: 64rpx;
box-sizing: border-box;
}
.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(85, 171, 92, 0.05) 0%, rgba(85, 171, 92, 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(85, 171, 92, 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(85, 171, 92, 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>