2
0
Files
fe.wfc.user/src/views/recharge/package/index.vue
2025-02-27 18:34:01 +08:00

648 lines
17 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.
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref, onMounted, computed } from 'vue';
import { fetchPackageList, submitOrder, fetchDashboardData } from '@/service/api/auth';
import { message } from 'ant-design-vue';
import OrderConfirmModal from '@/components/order-confirm/orderConfirmModal.vue';
import { aliPayPcPay,aliPayWapPay, wxPayScanCode, payBalance } from '@/service/api/payment';
import { useRouterPush } from '@/hooks/common/router';
import { useAppStore } from '@/store/modules/app';
import { useAuthStore} from '@/store/modules/auth';
defineOptions({
name: 'PackageSubscription'
});
const { t } = useI18n();
const authStore = useAuthStore();
interface RateLimit {
upLimitEnable: boolean;
downLimitEnable: boolean;
upLimit: number;
downLimit: number;
}
interface PackageOption {
id: string;
packageName: string;
price: number;
clientNum: number;
clientNumEnable: boolean;
traffic: number;
trafficEnable: boolean;
trafficDisplay: string;
durationEnable: boolean;
isRecommended?: boolean;
promotion?: string;
periodNum: number;
periodType: number;
validityPeriod: string;
rateLimitEnable: boolean;
rateLimits: RateLimit;
}
// 添加有效期类型枚举
const PERIOD_TYPE = {
HOUR: 0,
DAY: 1,
MONTH: 2,
YEAR: 3
} as const;
// 添加有效期单位映射
const PERIOD_UNIT = {
0: t('page.setmeal.hour'),
1: t('page.setmeal.day'),
2: t('page.setmeal.month'),
3: t('page.setmeal.year')
} as const;
// 格式化有效期显示
const formatValidityPeriod = (num: number, type: number): string => {
const unit = PERIOD_UNIT[type as keyof typeof PERIOD_UNIT] || t('page.setmeal.unknow');
return `${num}${unit}`;
};
// 流量单位转换函数
const formatTraffic = (bytes: number): string => {
// 处理 0 值、undefined 或 null 的情况
if (!bytes || bytes === 0) {
return '0B';
}
const B_TO_KB = 1024;
const B_TO_MB = 1024 * 1024;
const B_TO_GB = 1024 * 1024 * 1024;
const B_TO_TB = 1024 * 1024 * 1024 * 1024;
if (bytes >= B_TO_TB) {
// B -> TB
return `${(bytes / B_TO_TB).toFixed(2)}TB`;
}
if (bytes >= B_TO_GB) {
// B -> GB
return `${(bytes / B_TO_GB).toFixed(2)}GB`;
}
if (bytes >= B_TO_MB) {
// B -> MB
return `${(bytes / B_TO_MB).toFixed(2)}MB`;
}
if (bytes >= B_TO_KB) {
// B -> KB
return `${(bytes / B_TO_KB).toFixed(2)}KB`;
}
// 小于1KB的情况保持B单位
return `${bytes.toFixed(2)}B`;
};
// 添加速率格式化函数
const formatSpeed = (speed: number): string => {
if (speed >= 1048576) {
return `${(speed / 1048576).toFixed(1)}Gbps`;
} else if (speed >= 1024) {
return `${(speed / 1024).toFixed(1)}Mbps`;
}
return `${speed}Kbps`;
};
const packageOptions = ref<PackageOption[]>([]);
const selectedPackage = ref<PackageOption>({
id: '1',
packageName: '',
price: 0,
clientNum: 0,
clientNumEnable: false,
traffic: 0,
trafficEnable: false,
trafficDisplay: '0GB',
durationEnable: false,
isRecommended: false,
promotion: '',
periodNum: 0,
periodType: PERIOD_TYPE.MONTH,
validityPeriod: '0月',
rateLimitEnable: false,
rateLimits: {
upLimitEnable: false,
downLimitEnable: false,
upLimit: 0,
downLimit: 0
}
});
// 添加路由跳转方法
const { routerPushByKey } = useRouterPush();
// 添加订单确认弹窗相关状态
const showOrderModal = ref(false);
const currentOrderInfo = ref<{
orderId: string;
orderType: number;
orderAmount: number;
}>({
orderId: '',
orderType: 0,
orderAmount: 0
});
// 添加 appStore 实例
const appStore = useAppStore();
// 添加加载状态和数据状态的响应式引用
const isLoading = ref(false);
const hasPackages = computed(() => packageOptions.value.length > 0);
// 添加用户余额状态
const userBalance = ref(0);
// 添加检查余额是否足够的计算属性
const canUseBalancePay = computed(() => {
// 添加日志来调试
// console.log('Current balance:', userBalance.value);
// console.log('Package price:', selectedPackage.value?.price);
// console.log('Can use balance pay:', userBalance.value >= (selectedPackage.value?.price || 0));
return userBalance.value >= (selectedPackage.value?.price || 0);
});
// 添加 loading 状态
const paymentLoading = ref(false);
// 添加用户状态
const userPackageStatus = ref<number | null>(null); // 用于存储套餐状态
const fetchPackages = async () => {
isLoading.value = true;
try {
const response = await fetchPackageList();
if (response.data && Array.isArray(response.data)) {
packageOptions.value = response.data.map(pkg => ({
id: pkg.id,
packageName: pkg.packageName,
price: parseFloat(pkg.price),
clientNum: Number(pkg.clientNum),
clientNumEnable: pkg.clientNumEnable,
traffic: Number(pkg.traffic),
trafficEnable: pkg.trafficEnable,
trafficDisplay: pkg.trafficEnable ? formatTraffic(Number(pkg.traffic)) : t('page.setmeal.unlimit'),
durationEnable: Boolean(pkg.periodNum && pkg.periodType !== undefined),
isRecommended: pkg.isRecommended || false,
promotion: pkg.promotion || '',
periodNum: Number(pkg.periodNum),
periodType: Number(pkg.periodType),
validityPeriod: pkg.periodNum && pkg.periodType !== undefined
? formatValidityPeriod(Number(pkg.periodNum), Number(pkg.periodType))
: t('page.setmeal.unlimit'),
rateLimitEnable: pkg.rateLimitEnable,
rateLimits: {
upLimitEnable: pkg.rateLimits?.upLimitEnable || false,
downLimitEnable: pkg.rateLimits?.downLimitEnable || false,
upLimit: Number(pkg.rateLimits?.upLimit) || 0,
downLimit: Number(pkg.rateLimits?.downLimit) || 0
}
}));
if (packageOptions.value.length > 0) {
selectedPackage.value = packageOptions.value[0];
}
}
} catch (error) {
// console.error('Failed to fetch packages:', error);
// message.error(t('page.setmeal.fetchFailed'));
} finally {
isLoading.value = false;
}
};
const selectPackage = (option: PackageOption) => {
selectedPackage.value = option;
// console.log('Selected package price:', option.price);
// console.log('Can use balance pay after selection:', canUseBalancePay.value);
};
// 修改套餐办理方法添加日志跟踪订单ID
const handleSubmitOrder = async () => {
try {
const orderRes = await submitOrder({
type: 0,
packageId: selectedPackage.value.id
});
// 添加日志
// console.log('Created order ID:', orderRes.data);
// 重新获取最新余额
await fetchUserBalance();
// 更新订单信息并显示弹窗
currentOrderInfo.value = {
orderId: String(orderRes.data), // 确保转换为字符串
orderType: 0,
orderAmount: selectedPackage.value.price
};
// 添加日志
// console.log('Current order info after creation:', currentOrderInfo.value);
showOrderModal.value = true;
} catch (error) {
// message.error(t('page.order.createOrderFailed'));
// console.error('Failed to create order:', error);
}
};
// 修改支付处理方法
const handlePaymentConfirm = async (paymentMethod: 'alipay' | 'wxpay' | 'balance') => {
try {
// console.log('Payment confirmation for order:', currentOrderInfo.value);
if (!currentOrderInfo.value.orderId) {
throw new Error('Order ID is missing');
}
if (paymentMethod === 'balance') {
// 设置 loading 状态
paymentLoading.value = true;
// console.log('Attempting balance payment for order:', currentOrderInfo.value.orderId);
const result = await payBalance({
orderId: currentOrderInfo.value.orderId
});
// console.log('Balance payment response:', result);
if (result.error) {
throw new Error(result.error.message || 'Payment failed');
}
message.success(t('page.order.paymentSuccess'));
// 延迟一下再关闭弹窗和刷新页面
setTimeout(() => {
showOrderModal.value = false;
window.location.reload();
}, 1000);
return;
} else if (paymentMethod === 'alipay') {
// 区分手机端和pc端支付
let res;
if (appStore.isMobile) {
res = await aliPayWapPay({ orderId: currentOrderInfo.value.orderId });
} else {
res = await aliPayPcPay({ orderId: currentOrderInfo.value.orderId });
}
const div = document.createElement('div');
div.innerHTML = res;
document.body.appendChild(div);
document.forms['punchout_form'].submit();
div.remove();
} else {
const res = await wxPayScanCode({ orderId: currentOrderInfo.value.orderId });
routerPushByKey('billing_wxpay', {
query: {
url: res.data,
orderId: currentOrderInfo.value.orderId
}
});
}
} catch (error) {
// console.error('Payment failed with error:', error);
// message.error(error instanceof Error ? error.message : t('page.order.paymentFailed'));
} finally {
// 清除 loading 状态
paymentLoading.value = false;
}
};
// 修改获取用户余额的方法,添加日志
const fetchUserBalance = async () => {
try {
const response = await authStore.getDashboardData();
if (response && !response.error) {
userBalance.value = Number(response.balance) || 0;
// console.log('Fetched balance:', userBalance.value);
}
} catch (error) {
// console.error('Failed to fetch user balance:', error);
}
};
// 获取仪表盘数据
const fetchDashboardData = async () => {
try {
const response = await authStore.getDashboardData(); // 获取仪表盘数据
if (response && typeof response === 'object' && !response.error) {
userPackageStatus.value = response.status; // 获取套餐状态
}
} catch (error) {
console.error('Failed to fetch dashboard data:', error);
}
};
// 计算属性,检查套餐状态
const isPackageActive = computed(() => {
return userPackageStatus.value === 1; // 如果状态为 1则套餐有效
});
onMounted(async () => {
fetchDashboardData();
await fetchPackages();
await fetchUserBalance();
});
</script>
<template>
<div class="package-container">
<!-- 顶部价格展示 -->
<div class="price-header">
<div class="price">
<span class="currency">¥</span>
<span class="amount">{{ selectedPackage.price }}</span>
</div>
<div class="subtitle">{{ selectedPackage.packageName }}</div>
</div>
<!-- 套餐选择 -->
<div class="package-options">
<h3 class="section-title">{{ t('page.setmeal.changablelevel') }}</h3>
<div class="options-grid">
<div
v-for="option in packageOptions"
:key="option.id"
:class="[
'option-card',
{
recommended: option.isRecommended,
selected: selectedPackage.id === option.id
}
]"
@click="selectPackage(option)"
>
<div v-if="option.isRecommended" class="recommended-tag">
{{ t('page.setmeal.highlyrecommended') }}
</div>
<div class="package-name">{{ option.packageName }}</div>
<div class="price">¥{{ option.price }}</div>
<div class="traffic">{{ option.trafficEnable ? option.trafficDisplay : t('page.setmeal.unlimit') }}</div>
<div class="device-count">
{{ option.clientNumEnable ? `${option.clientNum} ${t('page.setmeal.device')}` : t('page.setmeal.unlimit') }}
</div>
</div>
</div>
</div>
<!-- 套餐详情 -->
<a-card>
<h3 class="section-title">{{ t('page.setmeal.mealdetail') }}</h3>
<div class="details-list">
<div class="detail-item">
<div class="label">{{ t('page.setmeal.packagename') }}</div>
<div class="value">{{ selectedPackage.packageName }}</div>
</div>
<div class="detail-item">
<div class="label">{{ t('page.setmeal.GeneralPurposeTraffic') }}</div>
<div class="value">
{{ selectedPackage.trafficEnable ? selectedPackage.trafficDisplay : t('page.setmeal.unlimit') }}
</div>
</div>
<div class="detail-item">
<div class="label">{{ t('page.setmeal.client') }}</div>
<div class="value">
{{ selectedPackage.clientNumEnable ? `${t('page.setmeal.upto')} ${selectedPackage.clientNum} ${t('page.setmeal.canbe')}` : t('page.setmeal.unlimit') }}
</div>
</div>
<div class="detail-item">
<div class="label">{{ t('page.setmeal.Expirationdate') }}</div>
<div class="value">
{{ selectedPackage.durationEnable ? selectedPackage.validityPeriod+t('page.setmeal.useful') : t('page.setmeal.unlimit') }}
</div>
</div>
<div class="detail-item">
<div class="label">{{ t('page.setmeal.rate') }}</div>
<div class="value">
<template v-if="!selectedPackage.rateLimitEnable">
{{ t('page.setmeal.unlimit') }}
</template>
<template v-else>
<div>
{{ t('page.setmeal.uplimit') }}{{ selectedPackage.rateLimits.upLimitEnable ? formatSpeed(selectedPackage.rateLimits.upLimit) : '-' }}
</div>
<div>
{{ t('page.setmeal.downlimit') }}{{ selectedPackage.rateLimits.downLimitEnable ? formatSpeed(selectedPackage.rateLimits.downLimit) : '-' }}
</div>
</template>
</div>
</div>
<div class="bottom-bar">
<button
class="btn-primary"
@click="handleSubmitOrder"
:disabled="isLoading || !hasPackages || isPackageActive"
>
{{ isLoading ? t('page.common.loading') :
!hasPackages ? t('page.setmeal.noPackages') :
t('page.setmeal.Applynow')
}}
</button>
</div>
</div>
</a-card>
<!-- 订单确认弹窗 -->
<OrderConfirmModal
v-model:visible="showOrderModal"
:order-info="currentOrderInfo"
:enable-balance-pay="canUseBalancePay"
:user-balance="userBalance"
:loading="paymentLoading"
@confirm="handlePaymentConfirm"
/>
</div>
</template>
<style scoped>
.package-container {
min-height: 100vh;
padding: 16px 16px 80px;
}
.price-header {
border-color: #ff4d4f;
background: rgba(218, 7, 7, 0.1);
padding: 20px;
border-radius: 12px;
margin-bottom: 16px;
}
.price {
color: #ff4d4f;
margin-bottom: 8px;
}
.currency {
font-size: 20px;
}
.amount {
font-size: 32px;
font-weight: bold;
}
.period {
font-size: 16px;
}
.subtitle {
font-size: 14px;
color: var(--text-color, var(--ant-text-color)); /* 使用主题变量 */
}
.section-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
color: var(--text-color, var(--ant-text-color)); /* 使用主题变量 */
position: relative;
padding-left: 12px;
}
.section-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background: #1890ff;
border-radius: 2px;
}
.options-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.option-card {
background: var(--primary-color, var(--ant-primary-color));
padding: 16px;
border-radius: 8px;
text-align: center;
position: relative;
border: 1px solid #e8e8e8;
cursor: pointer;
transition: all 0.3s;
}
.option-card:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.option-card.selected {
border-color: #ff4d4f;
background: rgba(218, 7, 7, 0.1);
}
.recommended-tag {
position: absolute;
top: 0;
left: 0;
background: #ff4d4f;
color: white;
font-size: 12px;
padding: 2px 8px;
border-radius: 0 0 8px 0;
}
.package-details {
background: white;
padding: 16px;
border-radius: 12px;
}
.detail-item {
display: flex;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.detail-item:last-child {
border-bottom: none;
}
.label {
width: 80px;
color: var(--primary-color, var(--ant-primary-color));
}
.value {
flex: 1;
color: var(--primary-color, var(--ant-primary-color));
}
.value.highlight {
color: #ff4d4f;
}
.bottom-bar {
padding: 12px 16px;
display: flex;
justify-content: center;
align-items: center;
height: 60px;
}
.btn-primary {
background: #ff4d4f;
color: white;
border: none;
padding: 12px 32px;
border-radius: 24px;
font-size: 16px;
width: 90%;
max-width: 400px;
transition: all 0.3s;
}
.btn-primary:disabled {
background: #ccc;
cursor: not-allowed;
opacity: 0.7;
}
.package-name {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: var(--primary-color, var(--ant-primary-color));
}
.traffic {
font-size: 14px;
color: var(--primary-color, var(--ant-primary-color));
margin-top: 8px;
}
.device-count {
font-size: 14px;
color: var(--primary-color, var(--ant-primary-color));
margin-top: 4px;
}
.speed-limit {
font-size: 14px;
color: #666;
margin-top: 4px;
}
.value > div {
margin-bottom: 4px;
}
.value > div:last-child {
margin-bottom: 0;
}
</style>