2
0

feat:套餐与充值界面修改

This commit is contained in:
zhongzm
2025-01-03 18:03:41 +08:00
parent 9ba3922f6d
commit 671498b216
8 changed files with 817 additions and 9 deletions

View File

@@ -0,0 +1,403 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref, onMounted, inject } from 'vue';
import { fetchPackageList, submitOrder } from '@/service/api/auth';
import { message } from 'ant-design-vue';
defineOptions({
name: 'PackageSubscription'
});
const { t } = useI18n();
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;
}
// 添加有效期类型枚举
const PERIOD_TYPE = {
HOUR: 0,
DAY: 1,
MONTH: 2,
YEAR: 3
} as const;
// 添加有效期单位映射
const PERIOD_UNIT = {
[PERIOD_TYPE.HOUR]: '小时',
[PERIOD_TYPE.DAY]: '天',
[PERIOD_TYPE.MONTH]: '月',
[PERIOD_TYPE.YEAR]: '年'
} as const;
// 格式化有效期显示
const formatValidityPeriod = (num: number, type: number): string => {
const unit = PERIOD_UNIT[type as keyof typeof PERIOD_UNIT] || '未知';
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 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月'
});
const fetchPackages = async () => {
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)) : '无限制',
durationEnable: pkg.durationEnable,
isRecommended: pkg.isRecommended || false,
promotion: pkg.promotion || '',
periodNum: Number(pkg.periodNum),
periodType: Number(pkg.periodType),
validityPeriod: pkg.durationEnable ? formatValidityPeriod(Number(pkg.periodNum), Number(pkg.periodType)) : '无限制'
}));
if (packageOptions.value.length > 0) {
selectedPackage.value = packageOptions.value[0];
}
}
} catch (error) {
console.error('Failed to fetch packages:', error);
}
};
const selectPackage = (option: PackageOption) => {
selectedPackage.value = option;
};
// 注入更新仪表盘的方法
const updateDashboard = inject('updateDashboard') as () => Promise<void>;
// 修改套餐办理方法
const handleSubmitOrder = async () => {
try {
await submitOrder({
type: 0,
packageId: selectedPackage.value.id
});
message.success('套餐办理成功!');
// 更新<E69BB4><E696B0>表盘数据
await updateDashboard();
} catch (error) {
message.error('套餐办理失败,请重试!');
console.error('Failed to submit order:', error);
}
};
onMounted(async () => {
await fetchPackages();
});
</script>
<template>
<div class="package-container">
<!-- 顶部价格展示 -->
<div class="price-header">
<div class="price">
<span class="currency">¥</span>
<span class="amount">{{ selectedPackage.price }}</span>
<span class="period">/</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 : '无限制' }}</div>
<div class="device-count">
{{ option.clientNumEnable ? `${option.clientNum}台设备` : '无限制' }}
</div>
</div>
</div>
</div>
<!-- 套餐详情 -->
<div class="package-details">
<h3 class="section-title">{{ t('page.setmeal.mealdetail') }}</h3>
<div class="details-list">
<div class="detail-item">
<div class="label">套餐名称</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},当月有效` : '无限制' }}
</div>
</div>
<div class="detail-item">
<div class="label">设备数量</div>
<div class="value">
{{ selectedPackage.clientNumEnable ? `最多${selectedPackage.clientNum}台设备同时在线` : '无限制' }}
</div>
</div>
<div class="detail-item">
<div class="label">有效期限</div>
<div class="value">
{{ selectedPackage.durationEnable ? selectedPackage.validityPeriod : '无限制' }}
</div>
</div>
<div class="bottom-bar">
<button
class="btn-primary"
@click="handleSubmitOrder"
:disabled="!selectedPackage.id"
>
{{ t('page.setmeal.Applynow') }}
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.package-container {
min-height: 100vh;
padding: 16px 16px 80px;
}
.price-header {
background: #fff1f0;
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: #666;
}
.section-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
color: #333;
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: white;
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: #fff1f0;
}
.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: #666;
}
.value {
flex: 1;
color: #333;
}
.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;
}
.package-name {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
.traffic {
font-size: 14px;
color: #666;
margin-top: 8px;
}
.device-count {
font-size: 14px;
color: #666;
margin-top: 4px;
}
</style>