2
0

Merge branch 'feature/multi-packages' of http://192.168.2.166:3180/WFC/fe.wfc.user into feature/multi-packages

This commit is contained in:
caiyuchao
2025-03-20 10:04:30 +08:00
10 changed files with 410 additions and 35 deletions

View File

@@ -559,6 +559,7 @@ const local: any = {
pay:'pay now'
},
headerbanner:{
packageCount:'My packages',
nopackage:'No package',
packageinfo:'Package information',
packagename:'Package name',
@@ -690,6 +691,7 @@ const local: any = {
refresh:"Refresh",
},
usercard:{
package:"My packages",
changeInfo:"Change Information",
resetpwd:"Reset password",
kyc:"KYC Certification",
@@ -783,6 +785,22 @@ const local: any = {
submitcer:'Submit for certification.',
previewimage:'Preview image',
},
package:{
myPackages:'My Packages',
active:'Active',
expired:'Waiting',
price:'Price',
period: 'Period',
traffic: 'Traffic',
devices: 'Device',
unlimited: 'Unlimited',
upLimit: 'Up rate',
downLimit: 'Down rate',
day: 'Day',
month: 'Month',
year: 'Year',
total:'Total',
},
},
form: {
required: 'Cannot be empty',

View File

@@ -22,6 +22,7 @@ const viewZh: any = {
"view.userInfo_accsee":"当前设备",
"view.userInfo_records":"历史设备",
"view.userInfo_cdrlrecords":"上网记录",
"view.userInfo_package":"我的套餐",
};
const local:any = {
@@ -560,6 +561,7 @@ const local:any = {
pay:'立即支付',
},
headerbanner:{
packageCount:'我的套餐',
packageinfo:'套餐信息',
packagename:'套餐名称',
price:'套餐费用',
@@ -783,6 +785,22 @@ const local:any = {
submitcer:'提交认证',
previewimage:'预览图片',
},
package:{
myPackages:'我的套餐',
active:'启用',
expired:'待启用',
price:'价格',
period: '有效期',
traffic: '流量',
devices: '设备数',
unlimited: '无限制',
upLimit: '上行限速',
downLimit: '下行限速',
day: '天',
month: '月',
year: '年',
total:'共',
},
},
form: {
required: '不能为空',

View File

@@ -538,6 +538,17 @@ export const customRoutes: GeneratedRoute[] = [
order:22,
hideInMenu: true
}
},
{
name:'user-info_package',
path:'/userInfo/package',
component:'view.userInfo_package',
meta:{
title:'已办理套餐',
i18nKey:'view.userInfo_package',
order:23,
hideInMenu:true
}
}
]
},

View File

@@ -206,6 +206,15 @@ export function resetPasswordByEmail(data: { email: string; code: string; passwo
data
});
}
/** Get user's purchased packages */
export function fetchUserPackages(params: Api.Package.UserPackageQueryParams) {
return request<Api.Package.UserPackageListResponse>({
url: '/u/accountPackage/page',
method: 'get',
params
});
}

26
src/typings/api.d.ts vendored
View File

@@ -666,6 +666,32 @@ declare namespace Api {
pageNum: number;
pageSize: number;
}
/** User package information */
interface UserPackage {
id: number;
packageName: string;
price: number;
startTime: number;
endTime: number;
status: number; // 1: active, 0: expired
}
/** User package list response */
interface UserPackageListResponse {
code: number;
msg: string;
data: {
rows: UserPackage[];
total: number;
};
}
/** User package query parameters */
interface UserPackageQueryParams {
pageNum: number;
pageSize: number;
}
}
/**

View File

@@ -112,6 +112,7 @@ declare global {
const fetchRechargeHistory: typeof import('../service/api/auth')['fetchRechargeHistory']
const fetchRefreshToken: typeof import('../service/api/auth')['fetchRefreshToken']
const fetchRegister: typeof import('../service/api/auth')['fetchRegister']
const fetchUserPackages: typeof import('../service/api/auth')['fetchUserPackages']
const filterAuthRoutesByRoles: typeof import('../store/modules/route/shared')['filterAuthRoutesByRoles']
const filterTabsById: typeof import('../store/modules/tab/shared')['filterTabsById']
const filterTabsByIds: typeof import('../store/modules/tab/shared')['filterTabsByIds']

View File

@@ -281,11 +281,10 @@ const speedLimits = ref({
// 添加套餐信息的响应式引用
const packageInfo = ref({
// packageName: '',
// price: '0.00'
packageName: t('page.headerbanner.nopackage'),
price: '-',
status:3,
status: 3,
packageNum: 0 // 添加套餐数量字段
});
// 修改数据更新函数,添加套餐信息的更新
@@ -296,8 +295,9 @@ async function mockDataUpdate() {
// 更新套餐信息
packageInfo.value = {
packageName: response.packageName ? response.packageName : t('page.headerbanner.nopackage'),
price: response.packageName ? formatBalance(response.price) : '-',// 有套餐时才格式化价格
status: response.status !== undefined ? Number(response.status) : 3,//有套餐时才判断状态
price: response.packageName ? formatBalance(response.price) : '-',
status: response.status !== undefined ? Number(response.status) : 3,
packageNum: response.packageNum || 0 // 添加套餐数量
};
// 更新余额和设备数据
@@ -310,9 +310,9 @@ async function mockDataUpdate() {
unit: t('page.headerbanner.money'),
subTitle: t('page.headerbanner.deviceCount') + `: ${
response.packageName ? (
!response.clientNumEnable
? t('page.headerbanner.nolimit')
: (response.clientNum || 0) + t('page.headerbanner.device')
!response.clientNumEnable
? t('page.headerbanner.nolimit')
: (response.clientNum || 0) + t('page.headerbanner.device')
):'-'
}`
};
@@ -334,35 +334,35 @@ async function mockDataUpdate() {
};
} else {
// 有套餐时的正常显示逻辑
const totalTraffic = response.trafficEnable ? (response.traffic || 0) : 0;
const usedTraffic = response.trafficEnable ? (response.trafficUsed || 0) : 0;
const remainingTraffic = Math.max(0, totalTraffic - usedTraffic);
const totalTraffic = response.trafficEnable ? (response.traffic || 0) : 0;
const usedTraffic = response.trafficEnable ? (response.trafficUsed || 0) : 0;
const remainingTraffic = Math.max(0, totalTraffic - usedTraffic);
// 格式化流量显示
const formattedTotal = formatTraffic(totalTraffic);
const formattedUsed = formatTraffic(usedTraffic);
// 格式化流量显示
const formattedTotal = formatTraffic(totalTraffic);
const formattedUsed = formatTraffic(usedTraffic);
const formattedRemaining = formatTraffic(remainingTraffic);
const formattedRemaining = formatTraffic(remainingTraffic);
// 更新流量数据显示
// baseData.value[1] = {
// ...baseData.value[1],
// value: remainingTraffic,
// max: totalTraffic,
// displayValue: !response.trafficEnable ? t('page.headerbanner.nolimit') : `${formattedRemaining.value}${formattedRemaining.unit}`,
// unit: '',
// description: !response.trafficEnable
// ? t('page.headerbanner.nolimit')
// : `${formattedTotal.value}${formattedTotal.unit}`,
// subTitle: !response.trafficEnable
// ? t('page.headerbanner.nolimit')
// : formattedUsed.value+formattedUsed.unit,
// speedLimits: {
// upLimit: speedLimits.value.upLimit,
// downLimit: speedLimits.value.downLimit
// }
// };
// console.log(baseData.value[1].description)
// 更新流量数据显示
// baseData.value[1] = {
// ...baseData.value[1],
// value: remainingTraffic,
// max: totalTraffic,
// displayValue: !response.trafficEnable ? t('page.headerbanner.nolimit') : `${formattedRemaining.value}${formattedRemaining.unit}`,
// unit: '',
// description: !response.trafficEnable
// ? t('page.headerbanner.nolimit')
// : `${formattedTotal.value}${formattedTotal.unit}`,
// subTitle: !response.trafficEnable
// ? t('page.headerbanner.nolimit')
// : formattedUsed.value+formattedUsed.unit,
// speedLimits: {
// upLimit: speedLimits.value.upLimit,
// downLimit: speedLimits.value.downLimit
// }
// };
// console.log(baseData.value[1].description)
baseData.value[1] = {
...baseData.value[1],
value: remainingTraffic,
@@ -537,6 +537,12 @@ const getDeviceCount = (subTitle?: string, clientNumEnable?: boolean): string =>
<span class="info-label">{{ t('page.headerbanner.client') }}</span>
<span class="info-value">{{ getDeviceCount(baseData[0].subTitle, baseData[0].speedLimits?.upLimit !== t('page.headerbanner.nolimit')) }}</span>
</div>
<div class="info-item">
<router-link to="/userInfo/package" class="info-item clickable-row">
<span class="info-label">{{ t('page.headerbanner.packageCount') }}</span>
<span class="info-value link-text">{{ packageInfo.packageNum }}</span>
</router-link>
</div>
</div>
</div>
</div>
@@ -799,4 +805,33 @@ const getDeviceCount = (subTitle?: string, clientNumEnable?: boolean): string =>
margin-bottom: 6px;
}
}
.clickable-row {
text-decoration: none;
color: inherit;
width: 100%;
cursor: pointer;
transition: background-color 0.3s;
}
.clickable-row:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* 移除之前的链接样式 */
.package-link {
text-decoration: none;
color: inherit;
}
.link-text {
cursor: pointer;
color: #fff;
position: relative;
display: inline-block;
}
.link-text:hover {
opacity: 0.9;
}
</style>

View File

@@ -435,7 +435,7 @@ onMounted(async () => {
<button
class="btn-primary"
@click="handleSubmitOrder"
:disabled="isLoading || !hasPackages"
:disabled="isLoading"
>
{{ isLoading ? t('page.common.loading') :
!hasPackages ? t('page.setmeal.noPackages') :

View File

@@ -0,0 +1,252 @@
<script setup lang="ts">
import { ref, onMounted, h } from 'vue';
import { useI18n } from 'vue-i18n';
import { fetchUserPackages } from '@/service/api/auth';
import dayjs from 'dayjs';
import { Tag } from 'ant-design-vue';
const { t } = useI18n();
// 表格数据
const packageData = ref([]);
const pagination = ref({
current: 1,
pageSize: 8,
total: 0,
showSizeChanger: true,
showTotal: (total: number) => `${t('page.package.total')} ${total} `
});
const loading = ref(false);
// 格式化流量
const formatTrafficValue = (bytes: number): string => {
if (!bytes) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let value = bytes;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return `${value.toFixed(2)} ${units[unitIndex]}`;
};
// 格式化带宽
const formatBandwidthValue = (kbps: number): string => {
if (!kbps) return '0 Kbps';
if (kbps < 1024) return `${kbps} Kbps`;
if (kbps < 1024 * 1024) return `${(kbps / 1024).toFixed(2)} Mbps`;
return `${(kbps / (1024 * 1024)).toFixed(2)} Gbps`;
};
// 格式化时间周期
const formatPeriod = (num: number, type: number): string => {
const typeMap = {
1: t('page.package.day'),
2: t('page.package.month'),
3: t('page.package.year')
};
return `${num}${typeMap[type] || ''}`;
};
// 获取套餐列表
const fetchPackageList = async () => {
try {
loading.value = true;
const params = {
pageNum: pagination.value.current,
pageSize: pagination.value.pageSize
};
const res = await fetchUserPackages(params);
if (res.data) {
packageData.value = res.data.rows;
pagination.value.total = res.data.total;
}
} catch (error) {
console.error('Failed to fetch package list:', error);
} finally {
loading.value = false;
}
};
// 处理分页变化
const handlePageChange = (page: number, pageSize: number) => {
pagination.value.current = page;
pagination.value.pageSize = pageSize;
fetchPackageList();
};
onMounted(() => {
fetchPackageList();
});
</script>
<template>
<div class="package-list-container">
<a-card :bordered="false">
<template #title>
<div class="card-title">
<span>{{ t('page.package.myPackages') }}</span>
</div>
</template>
<a-spin :spinning="loading">
<div class="package-grid">
<a-card
v-for="item in packageData"
:key="item.id"
class="package-card"
:bordered="true"
>
<div class="package-header">
<h3 class="package-name">{{ item.packageName }}</h3>
<a-tag :color="item.status === 1 ? 'success' : 'error'" class="package-status">
{{ item.status === 1 ? t('page.package.active') : t('page.package.expired') }}
</a-tag>
</div>
<div class="package-info">
<div class="info-item">
<span class="label">{{ t('page.package.price') }}:</span>
<span class="value price">¥{{ Number(item.price).toFixed(2) }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('page.package.period') }}:</span>
<span class="value">{{ formatPeriod(item.periodNum, item.periodType) }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('page.package.traffic') }}:</span>
<span class="value">{{ item.trafficEnable ? formatTrafficValue(item.traffic) : t('page.package.unlimited') }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('page.package.devices') }}:</span>
<span class="value">{{ item.clientNumEnable ? item.clientNum : t('page.package.unlimited') }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('page.package.upLimit') }}:</span>
<span class="value">{{ !item.rateLimitEnable ? t('page.package.unlimited') : (item.upLimitEnable ? formatBandwidthValue(item.upLimit) : t('page.package.unlimited')) }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('page.package.downLimit') }}:</span>
<span class="value">{{ !item.rateLimitEnable ? t('page.package.unlimited') : (item.downLimitEnable ? formatBandwidthValue(item.downLimit) : t('page.package.unlimited')) }}</span>
</div>
</div>
</a-card>
</div>
<div class="pagination-container">
<a-pagination
v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize"
:total="pagination.total"
:show-size-changer="pagination.showSizeChanger"
:show-total="pagination.showTotal"
@change="handlePageChange"
/>
</div>
</a-spin>
</a-card>
</div>
</template>
<style scoped>
.package-list-container {
padding: 16px;
}
.card-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 500;
}
.package-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.package-card {
transition: all 0.3s;
}
.package-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.package-name {
margin: 0;
font-size: 16px;
font-weight: 500;
}
.package-status {
margin: 0;
}
.package-info {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.value {
font-size: 14px;
}
.value.price {
font-size: 16px;
font-weight: 500;
color: #ff4d4f;
}
.pagination-container {
margin-top: 24px;
display: flex;
justify-content: center;
}
:deep(.ant-card-head) {
padding: 0 12px;
}
:deep(.ant-card-body) {
padding: 12px;
}
/* 移动端适配 */
@media screen and (max-width: 768px) {
.package-list-container {
padding: 8px;
}
.package-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.package-card {
margin-bottom: 0;
}
.info-item {
font-size: 13px;
}
}
</style>

View File

@@ -53,6 +53,11 @@ const menuItems: MenuItem[] = [
title: t('page.usercard.kyc'),
path: '/userInfo/kyc'
},
{
icon:SafetyCertificateOutlined,
title:t('page.usercard.package'),
path:'/userInfo/package'
},
// {
// icon: MobileOutlined,
// title: t('page.usercard.deviceconsole'),