Author SHA1 Message Date
caiyuchao bdbaf63cc7 Merge branch 'feature/multi-packages' of http://192.168.2.166:3180/WFC/fe.wfc.user into feature/multi-packages 2025-03-20 10:04:30 +08:00
caiyuchao ba2ea8e129 Merge branch 'main' into feature/multi-packages
merge main to feature/multi-packages
2025-03-20 09:59:15 +08:00
zhongzm 6b453ad687 fix:多套餐 2025-03-19 17:36:45 +08:00
caiyuchao 8a9cfe986d feat: 购买多套餐开启 2025-03-13 12:02:42 +08:00
36 changed files with 546 additions and 1450 deletions

No files matched your search

+1 -2
View File
@@ -1,6 +1,6 @@
VITE_BASE_URL=/
VITE_APP_TITLE="Platform"
VITE_APP_TITLE="WANFi Platform"
# the prefix of the icon name
VITE_ICON_PREFIX=icon
@@ -35,4 +35,3 @@ VITE_SERVICE_EXPIRED_TOKEN_CODES=403
VITE_SERVICE_SERVER_ERROR_CODE=500
VITE_PAYPAL_CLIENT_ID=AfPgwFAmo9K7KCqiiGpNRCyQMSxI6V33eH-nEMnVndJNVEYOEOEn5wSPkHUybfzcjDLnBejt-RKnIfqX
-2
View File
@@ -4,8 +4,6 @@
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="/langs/zh-cn.js"></script>
<script src="/langs/en-us.js"></script>
<title>%VITE_APP_TITLE%</title>
</head>
<body>
-1
View File
@@ -14,7 +14,6 @@
"dependencies": {
"@better-scroll/core": "2.5.1",
"@iconify/vue": "4.1.2",
"@paypal/paypal-js": "^8.2.0",
"@sa/axios": "workspace:*",
"@sa/color-palette": "workspace:*",
"@sa/hooks": "workspace:*",
+1 -1
View File
@@ -61,7 +61,7 @@ function createCommonRequest<ResponseData = any>(
return Promise.resolve(response);
}
if (opts.isBackendSuccess(response) || response.config?.responseType === "blob") {
if (opts.isBackendSuccess(response)) {
return Promise.resolve(response);
}
-15
View File
@@ -14,9 +14,6 @@ importers:
'@iconify/vue':
specifier: 4.1.2
version: 4.1.2(vue@3.4.27(typescript@5.4.5))
'@paypal/paypal-js':
specifier: ^8.2.0
version: 8.2.0
'@sa/axios':
specifier: workspace:*
version: link:packages/axios
@@ -733,9 +730,6 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@paypal/paypal-js@8.2.0':
resolution: {integrity: sha512-hLg5wNORW3WiyMiRNJOm6cN2IqjPlClpxd971bEdm0LNpbbejQZYtesb0/0arTnySSbGcxg7MxjkZ/N5Z5qBNQ==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -3269,9 +3263,6 @@ packages:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'}
promise-polyfill@8.3.0:
resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==}
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
@@ -4642,10 +4633,6 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
'@paypal/paypal-js@8.2.0':
dependencies:
promise-polyfill: 8.3.0
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -7566,8 +7553,6 @@ snapshots:
progress@2.0.3: {}
promise-polyfill@8.3.0: {}
prompts@2.4.2:
dependencies:
kleur: 3.0.3
-5
View File
@@ -1,5 +0,0 @@
const enUS = {
system: {
title: 'MacroHub'
}
};
-5
View File
@@ -1,5 +0,0 @@
const zhCN = {
system: {
title: 'MacroHub'
}
};
@@ -1,10 +1,8 @@
<script setup lang="ts">
import { defineProps, defineEmits, computed, ref, onMounted, watch } from 'vue';
import { defineProps, defineEmits, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { AlipayOutlined, WechatOutlined, WalletOutlined,MoneyCollectOutlined } from '@ant-design/icons-vue';
import { AlipayOutlined, WechatOutlined, WalletOutlined } from '@ant-design/icons-vue';
import { Modal } from 'ant-design-vue';
import PaypalButton from '@/components/payment/paypal-button.vue';
import { stripePay, getPaymentConfig } from '@/service/api/payment';
const { t } = useI18n();
@@ -23,27 +21,6 @@ interface Props {
const props = defineProps<Props>();
const emit = defineEmits(['update:visible', 'confirm', 'cancel']);
const paymentMethods = ref<string[]>([]);
const currencySymbol = ref('¥');
// 获取支付配置
const fetchPaymentConfig = async () => {
try {
const response = await getPaymentConfig();
paymentMethods.value = response.data.paymentMethods || [];
currencySymbol.value = response.data.currencySymbol || '¥';
} catch (error) {
console.error('Failed to fetch payment config:', error);
}
};
// 监听visible属性变化
watch(() => props.visible, (newVisible) => {
if (newVisible) {
fetchPaymentConfig();
}
}, { immediate: true });
const orderTypeMap = {
0: t('page.order.packagePurchase'),
1: t('page.order.balanceRecharge')
@@ -60,7 +37,7 @@ const isBalancePayDisabled = computed(() => {
return props.loading || false;
});
const handleConfirm = async (paymentMethod: 'alipay' | 'wxpay' | 'balance') => {
const handleConfirm = (paymentMethod: 'alipay' | 'wxpay' | 'balance') => {
if (paymentMethod === 'balance') {
Modal.confirm({
title: t('page.order.confirmPayment'),
@@ -71,9 +48,6 @@ const handleConfirm = async (paymentMethod: 'alipay' | 'wxpay' | 'balance') => {
emit('confirm', paymentMethod);
}
});
} else if(paymentMethod === 'stripe') {
const response = await stripePay(props.orderInfo.orderId);
window.location.href = response.data
} else {
emit('confirm', paymentMethod);
}
@@ -106,7 +80,7 @@ const handleCancel = () => {
</div>
<div class="info-item">
<span class="label">{{ t('page.order.orderAmount') }}:</span>
<span class="value highlight">{{ currencySymbol }}{{ orderInfo.orderAmount.toFixed(2) }}</span>
<span class="value highlight">¥{{ orderInfo.orderAmount.toFixed(2) }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('page.order.orderId') }}:</span>
@@ -125,19 +99,20 @@ const handleCancel = () => {
}"
@click="!isBalancePayDisabled && handleConfirm('balance')"
>
<div class="debug-info" style="display: none;">
Enable balance pay: {{ enableBalancePay }}
</div>
<WalletOutlined class="payment-icon balance-icon" />
<span>{{ t('page.order.balancePay') }}</span>
<div class="balance-info" v-if="userBalance !== undefined">
{{ t('page.order.availableBalance') }}: {{ currencySymbol }}{{ userBalance.toFixed(2) }}
{{ t('page.order.availableBalance') }}: ¥{{ userBalance.toFixed(2) }}
<template v-if="userBalance < orderInfo.orderAmount">
<br>
<span class="balance-insufficient">{{ t('page.order.insufficientBalance') }}</span>
</template>
</div>
</div>
<div
v-if="paymentMethods.includes('alipay')"
class="method-item"
:class="{ disabled: loading }"
@click="!loading && handleConfirm('alipay')"
@@ -145,9 +120,7 @@ const handleCancel = () => {
<AlipayOutlined class="payment-icon alipay-icon" />
<span>{{ t('page.order.alipay') }}</span>
</div>
<div
v-if="paymentMethods.includes('wxpay')"
class="method-item"
:class="{ disabled: loading }"
@click="!loading && handleConfirm('wxpay')"
@@ -155,21 +128,6 @@ const handleCancel = () => {
<WechatOutlined class="payment-icon wxpay-icon" />
<span>{{ t('page.order.wxpay') }}</span>
</div>
<div
v-if="paymentMethods.includes('stripe')"
class="method-item"
:class="{ disabled: loading }"
@click="!loading && handleConfirm('stripe')"
>
<MoneyCollectOutlined class="payment-icon wxpay-icon" />
<span>{{ t('page.order.stripe') }}</span>
</div>
<PaypalButton
v-if="visible && paymentMethods.includes('paypal')"
:order-info="props.orderInfo"
/>
</div>
</div>
</a-spin>
@@ -225,13 +183,10 @@ const handleCancel = () => {
.methods-container {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.method-item {
flex: 1;
min-width: 120px;
max-width: calc(50% - 6px);
display: flex;
flex-direction: column;
align-items: center;
-58
View File
@@ -1,58 +0,0 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { constructPaypalBtn } from '@/utils/paypal';
import { getPaymentConfig, getPayPalConfig } from '@/service/api/payment';
import CryptoJS from 'crypto-js';
/**
* AES解密处理(CBC模式)
*/
function decryptCBC(word: string) {
word = (word + '').replace(/\n*$/g, '').replace(/\n/g, ''); // 这一行,将换行符替换为空
// 密钥
const keyStr = "EolSdjfd89v2PubN";
// 向量
const ivStr = "EjlnujOBvlv2PubN";
const key = CryptoJS.enc.Utf8.parse(keyStr);
let iv = CryptoJS.enc.Utf8.parse(ivStr);
const decrypt = CryptoJS.AES.decrypt(word, key,
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
)
return decrypt.toString(CryptoJS.enc.Utf8)
}
interface Props {
orderInfo: {
orderId: string;
orderType: number; // 0: 购买套餐, 1: 余额充值
orderAmount: number;
};
}
const props = defineProps<Props>();
onMounted(async()=>{
const response = await getPaymentConfig();
let currency = 'USD';
if (response && response.data) {
currency = response.data.currency || 'USD';
}
const configRes = await getPayPalConfig();
const clientId = decryptCBC(configRes.data);
await constructPaypalBtn(clientId, props.orderInfo.orderId, currency);
})
</script>
<template>
<div id="paypal-buttons"></div>
</template>
<style scoped>
</style>
+2 -2
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import {UserOutlined,HomeOutlined,WalletOutlined,ClusterOutlined} from "@ant-design/icons-vue";
import {UserOutlined,HomeOutlined,MoneyCollectOutlined,ClusterOutlined} from "@ant-design/icons-vue";
import {useRouterPush} from "@/hooks/common";
const { routerPushByKey } = useRouterPush();
@@ -22,7 +22,7 @@ defineOptions({
</div>
<div class="flex-item">
<ButtonIcon>
<WalletOutlined class="text-icon-large" @click="routerPushByKey('billing_billservice')"/>
<MoneyCollectOutlined class="text-icon-large" @click="routerPushByKey('billing_billservice')"/>
</ButtonIcon>
</div>
<div class="flex-item">
+21 -31
View File
@@ -7,7 +7,6 @@ const viewEn: any = {
"view.endpoint_access": "Current Connection",
"view.endpoint_records": "Past Connection",
"view.endpoint_cdrlrecords":"Internet Records",
"view.endpoint_bill":"Bill Records",
"view.billing": "Billing",
"view.billing_histories": "Historical",
"view.billing_Rechargehistory":"Recharge History",
@@ -197,7 +196,7 @@ const local: any = {
login: {
common: {
loginOrRegister: 'Login / Register',
userNamePlaceholder: 'Please enter user name or email',
userNamePlaceholder: 'Please enter user name',
phonePlaceholder: 'Please enter phone number',
codePlaceholder: 'Please enter verification code',
passwordPlaceholder: 'Please enter password',
@@ -214,7 +213,7 @@ const local: any = {
getCodeFailed: 'Failed to get verification code, please try again'
},
pwdLogin: {
title: 'Login',
title: 'Password Login',
rememberMe: 'Remember me',
forgetPassword: 'Forget password?',
register: 'Register',
@@ -560,6 +559,7 @@ const local: any = {
pay:'pay now'
},
headerbanner:{
packageCount:'My packages',
nopackage:'No package',
packageinfo:'Package information',
packagename:'Package name',
@@ -648,8 +648,7 @@ const local: any = {
endpoint:{
access:"current client",
records:"Historical client",
cdrlrecords:"Internet records",
bill:'Bill records',
cdrlrecords:"Internet records"
},
cdrlrecords:{
total:'Total',
@@ -692,6 +691,7 @@ const local: any = {
refresh:"Refresh",
},
usercard:{
package:"My packages",
changeInfo:"Change Information",
resetpwd:"Reset password",
kyc:"KYC Certification",
@@ -730,8 +730,6 @@ const local: any = {
selectPayment:'Payment',
alipay:'Alipay',
wxpay:'WeChat Pay',
wxpayScan:'Scan QR code on WeChat',
stripe:'Stripe',
balancePay:'Balance Pay',
availableBalance:'Balance',
paymentSuccess:'Payment successful',
@@ -742,30 +740,6 @@ const local: any = {
confirm:'Confirm',
insufficientBalance:'Insufficient balance'
},
bill:{
loading:'Loading',
tipTitle:'Bill DownLoad',
exportTip:'Sure to download?',
total:'Total',
invoiceNumber:'Bill Number',
title: 'Bill List',
billDate: 'Bill Date',
amount: 'Amount',
billType: 'Bill Type',
statu: 'Status',
actions: 'Action',
download: 'Download',
preview: 'Preview',
exportOk: 'Download Success',
exportFail: 'Download Fail',
previewTitle: 'Bill Preview',
previewFailed: 'Preview fail',
noFile: 'No billing documents available',
types: {
recharge:'Recharge',
package: 'Package',
},
},
kyc:{
rejectReason:'Reject Reason:',
drive:'Driving license',
@@ -811,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',
+21 -37
View File
@@ -7,7 +7,6 @@ const viewZh: any = {
"view.endpoint_access": "当前设备",
"view.endpoint_records": "历史设备",
"view.endpoint_cdrlrecords":"上网记录",
"view.endpoint_bill":"账单记录",
"view.billing": "账单",
"view.billing_billservice":"账单服务",
"view.billing_histories": "历史查询",
@@ -23,6 +22,7 @@ const viewZh: any = {
"view.userInfo_accsee":"当前设备",
"view.userInfo_records":"历史设备",
"view.userInfo_cdrlrecords":"上网记录",
"view.userInfo_package":"我的套餐",
};
const local:any = {
@@ -197,7 +197,7 @@ const local:any = {
login: {
common: {
loginOrRegister: '登录 / 注册',
userNamePlaceholder: '请输入用户名或邮箱',
userNamePlaceholder: '请输入用户名',
phonePlaceholder: '请输入手机号',
codePlaceholder: '请输入验证码',
passwordPlaceholder: '请输入密码',
@@ -214,7 +214,7 @@ const local:any = {
getCodeFailed: '获取验证码失败,请重试'
},
pwdLogin: {
title: '登录',
title: '密码登录',
rememberMe: '记住我',
forgetPassword: '忘记密码?',
register: '注册账号',
@@ -561,6 +561,7 @@ const local:any = {
pay:'立即支付',
},
headerbanner:{
packageCount:'我的套餐',
packageinfo:'套餐信息',
packagename:'套餐名称',
price:'套餐费用',
@@ -603,36 +604,6 @@ const local:any = {
Paid:'已支付',
Unpaid:'未支付',
},
bill:{
loading:'加载中',
tipTitle:'账单下载',
exportTip:'确定下载吗?',
total:'共',
invoiceNumber:'账单编号',
title: '账单列表',
billDate: '账单日期',
amount: '金额',
billType: '账单类型',
statu: '状态',
actions: '操作',
download: '下载',
preview: '预览',
status: {
pending: '待支付',
paid: '已支付',
failed: '支付失败',
unknown: '未知状态'
},
exportOk: '下载成功',
exportFail: '下载失败',
previewTitle: '账单预览',
previewFailed: '预览失败',
noFile: '暂无账单文件',
types: {
recharge:'余额充值',
package: '套餐费用',
},
},
Internetdetails:{
title:"上网详单",
month:"月",
@@ -678,8 +649,7 @@ const local:any = {
endpoint:{
access:"当前设备",
records:"历史设备",
cdrlrecords:"上网记录",
bill:'账单记录',
cdrlrecords:"上网记录"
},
cdrlrecords:{
total:'共',
@@ -760,8 +730,6 @@ const local:any = {
selectPayment:'支付方式',
alipay:'支付宝支付',
wxpay:'微信支付',
wxpayScan:'使用微信扫码支付',
stripe:'Stripe',
balancePay:'余额支付',
availableBalance:'可用余额',
paymentSuccess:'支付成功',
@@ -817,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: '不能为空',
+4 -7
View File
@@ -1,12 +1,9 @@
import localeZhCN from './langs/zh-cn';
import localeEnUS from './langs/en-us';
const fullZhCN = { ...localeZhCN, ...zhCN };
const fullEnUS = { ...localeEnUS, ...enUS };
import zhCN from './langs/zh-cn';
import enUS from './langs/en-us';
const locales: Record<App.I18n.LangType, App.I18n.Schema> = {
'zh-CN': fullZhCN,
'en-US': fullEnUS
'zh-CN': zhCN,
'en-US': enUS
};
export default locales;
+29 -29
View File
@@ -330,7 +330,7 @@ export const customRoutes: GeneratedRoute[] = [
meta: {
title: '账单',
i18nKey: 'view.billing',
icon: 'ant-design:container-outlined',
icon: 'ant-design:account-book-outlined',
order: 13,
//hideInMenu: true
},
@@ -354,7 +354,7 @@ export const customRoutes: GeneratedRoute[] = [
meta: {
title: '充值服务',
i18nKey: 'view.recharge_rechargeservice',
icon: 'ant-design:wallet-outlined',
icon: 'ant-design:money-collect-outlined',
order: 2,
// hideInMenu: true
},
@@ -378,7 +378,7 @@ export const customRoutes: GeneratedRoute[] = [
title: '充值记录',
i18nKey: 'view.billing_Rechargehistory',
icon: 'ant-design:calendar-outlined',
order: 3
order: 4
},
},
{
@@ -389,21 +389,21 @@ export const customRoutes: GeneratedRoute[] = [
title: '套餐记录',
i18nKey: 'view.billing_packagehistories',
icon: 'ant-design:shopping-outlined',
order: 4
order: 8
},
},
{
name: 'billing_Internetdetails',
path: '/billing/Internetdetails',
component: 'view.billing_Internetdetails',
meta: {
title: '上网详单',
i18nKey: 'view.billing_Internetdetails',
icon: 'material-symbols:filter-list-off',
order: 9,
hideInMenu: true
},
},
// {
// name: 'billing_Internetdetails',
// path: '/billing/Internetdetails',
// component: 'view.billing_Internetdetails',
// meta: {
// title: '上网详单',
// i18nKey: 'view.billing_Internetdetails',
// icon: 'material-symbols:filter-list-off',
// order: 5,
// hideInMenu: true
// },
// },
{
name: 'billing_cdrlrecords',
path: '/billing/cdrlrecords',
@@ -412,18 +412,7 @@ export const customRoutes: GeneratedRoute[] = [
title: '上网记录',
i18nKey: 'view.endpoint_cdrlrecords',
icon: 'ant-design:book-outlined',
order: 6
},
},
{
name: 'billing_bill',
path: '/billing/bill',
component: 'view.billing_bill',
meta: {
title: '账单记录',
i18nKey: 'view.endpoint_bill',
icon: 'ant-design:book-outlined',
order: 8
order: 4
},
},
{
@@ -436,7 +425,7 @@ export const customRoutes: GeneratedRoute[] = [
hideInMenu: true,
order: 10,
},
},
}
]
},
{
@@ -549,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
}
}
]
},
+8 -8
View File
@@ -1,5 +1,5 @@
import { request } from '../request';
import { request, rawRequest } from '../request';
/**
* Login
*
@@ -206,15 +206,15 @@ export function resetPasswordByEmail(data: { email: string; code: string; passwo
data
});
}
/** 下载账单 PDF 文件 */
/** 下载账单 PDF 文件(使用 axios 直连) */
/** 下载账单 PDF 文件(使用 rawRequest,避免 transformBackendResponse 破坏 blob) */
export function downloadBill(id: string) {
return request({
url: `/u/bill/download/${id}`,
/** Get user's purchased packages */
export function fetchUserPackages(params: Api.Package.UserPackageQueryParams) {
return request<Api.Package.UserPackageListResponse>({
url: '/u/accountPackage/page',
method: 'get',
responseType: 'blob'
params
});
}
-40
View File
@@ -37,43 +37,3 @@ export function payBalance(params: { orderId: string }) {
}
});
}
/** Paypal createOrder */
export function payPalOrders(params: {orderId: number}) {
return request({
url: '/payment/paypal/orders',
method: 'post',
params
});
}
/** Paypal captureOrder */
export function payPalCapture(paypalOrderId: string, orderId: number) {
return request({
url: `/payment/paypal/orders/${paypalOrderId}/capture/${orderId}`,
method: 'post'
});
}
/** Get paypal configuration */
export function getPayPalConfig() {
return request({
url: '/payment/paypal/result/one',
method: 'get'
});
}
/** Stripe pay with orderId */
export function stripePay(orderId: number) {
return request({
url: `/payment/stripe/pay/${orderId}`,
method: 'post'
});
}
/** Get payment configuration */
export function getPaymentConfig() {
return request({
url: '/u/config/pay',
method: 'get'
});
}
+33 -14
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;
}
}
/**
@@ -676,20 +702,13 @@ declare namespace Api {
namespace Bill {
/** Bill record information */
interface BillRecord {
/** 账单编号 */
invoiceNumber: string;
/** 账单金额 */
amount: number;
/** 账单类型:0-购买套餐,1-余额充值 */
type: 0 | 1;
/** 账单时间 */
invoiceTime: string;
/** 账单状态:0-待支付,1-已支付,2-支付失败 */
status: 0 | 1 | 2;
/** 账单文件URL */
fileUrl?: string;
/** 发票文件URL */
invoiceFile?: string;
id: string;
startTime: string | null;
endTime: string | null;
traffic: string | null;
amount: string;
status: number;
createTime: string;
}
/** Bill list response */
+1 -7
View File
@@ -30,7 +30,6 @@ declare global {
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const constructPaypalBtn: typeof import('../utils/paypal')['constructPaypalBtn']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
@@ -90,7 +89,6 @@ declare global {
const doPostUser: typeof import('../service/api/user')['doPostUser']
const doPutRole: typeof import('../service/api/role')['doPutRole']
const doPutUser: typeof import('../service/api/user')['doPutUser']
const downloadBill: typeof import('../service/api/auth')['downloadBill']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const emptyInfo: typeof import('../store/modules/auth/shared')['emptyInfo']
@@ -114,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']
@@ -129,8 +128,6 @@ declare global {
const getFixedTabIds: typeof import('../store/modules/tab/shared')['getFixedTabIds']
const getFixedTabs: typeof import('../store/modules/tab/shared')['getFixedTabs']
const getGlobalMenusByAuthRoutes: typeof import('../store/modules/route/shared')['getGlobalMenusByAuthRoutes']
const getPayPalConfig: typeof import('../service/api/payment')['getPayPalConfig']
const getPaymentConfig: typeof import('../service/api/payment')['getPaymentConfig']
const getQueryParams: typeof import('../utils/common')['getQueryParams']
const getRouteIcons: typeof import('../store/modules/tab/shared')['getRouteIcons']
const getSelectedMenuKeyPathByKey: typeof import('../store/modules/route/shared')['getSelectedMenuKeyPathByKey']
@@ -184,8 +181,6 @@ declare global {
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const payBalance: typeof import('../service/api/payment')['payBalance']
const payPalCapture: typeof import('../service/api/payment')['payPalCapture']
const payPalOrders: typeof import('../service/api/payment')['payPalOrders']
const pick: typeof import('lodash-es')['pick']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
@@ -217,7 +212,6 @@ declare global {
const shallowRef: typeof import('vue')['shallowRef']
const sortRoutesByOrder: typeof import('../store/modules/route/shared')['sortRoutesByOrder']
const storeToRefs: typeof import('pinia')['storeToRefs']
const stripePay: typeof import('../service/api/payment')['stripePay']
const submitKYCVerification: typeof import('../service/api/auth')['submitKYCVerification']
const submitOrder: typeof import('../service/api/auth')['submitOrder']
const submitPackageOrder: typeof import('../service/api/auth')['submitPackageOrder']
+2 -1
View File
@@ -34,12 +34,14 @@ declare module 'vue' {
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
APopover: typeof import('ant-design-vue/es')['Popover']
AppLoading: typeof import('./../components/common/app-loading.vue')['default']
AppProvider: typeof import('./../components/common/app-provider.vue')['default']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
ARow: typeof import('ant-design-vue/es')['Row']
ASegmented: typeof import('ant-design-vue/es')['Segmented']
ASelect: typeof import('ant-design-vue/es')['Select']
@@ -78,7 +80,6 @@ declare module 'vue' {
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
OrderConfirmModal: typeof import('./../components/order-confirm/orderConfirmModal.vue')['default']
PaypalButton: typeof import('./../components/payment/paypal-button.vue')['default']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
-105
View File
@@ -1,105 +0,0 @@
import { loadScript } from "@paypal/paypal-js";
import { payPalOrders, payPalCapture } from '@/service/api/payment';
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const locale = appStore.locale.includes("zh") ? "zh_CN" : "en_US"
export const constructPaypalBtn = async(clientId: string, orderId: number, currency: string) =>{
loadScript({ clientId, currency, locale: locale })
.then((paypal: any) => {
paypal
.Buttons({
style: {
shape: "rect",
layout: "vertical",
color: "white",
label: "paypal",
},
message: {
},
async createOrder() {
try {
const response = await payPalOrders({orderId: orderId});
const orderData = response.data;
if (orderData.id) {
return orderData.id;
}
const errorDetail = orderData?.details?.[0];
const errorMessage = errorDetail
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
: JSON.stringify(orderData);
throw new Error(errorMessage);
} catch (error) {
console.error(error);
// resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
}
},
async onApprove(data, actions) {
try {
const response = await payPalCapture(data.orderID, orderId)
const orderData = response.data
// Three cases to handle:
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// (2) Other non-recoverable errors -> Show a failure message
// (3) Successful transaction -> Show confirmation or thank you message
const errorDetail = orderData?.details?.[0];
if (errorDetail?.issue === "INSTRUMENT_DECLINED") {
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// recoverable state, per
// https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
return actions.restart();
} else if (errorDetail) {
// (2) Other non-recoverable errors -> Show a failure message
throw new Error(`${errorDetail.description} (${orderData.debug_id})`);
} else if (!orderData.purchase_units) {
throw new Error(JSON.stringify(orderData));
} else {
// (3) Successful transaction -> Show confirmation or thank you message
// Or go to another URL: actions.redirect('thank_you.html');
const transaction =
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
// resultMessage(
// `Transaction ${transaction.status}: ${transaction.id}<br>
//<br>See console for all available details`
//);
console.log(
"Capture result",
orderData,
JSON.stringify(orderData, null, 2)
);
location.reload();
}
} catch (error) {
console.error(error);
// resultMessage(
// `Sorry, your transaction could not be processed...<br><br>${error}`
//);
}
},
})
.render("#paypal-buttons")
.catch((error) => {
console.error("failed to render the PayPal Buttons", error);
});
})
.catch((error) => {
console.error("failed to load the PayPal JS SDK script", error);
});
}
// Example function to show a result to the user. Your site's UI library can be used instead.
function resultMessage(message: any) {
const container: any = document.querySelector("#result-message");
container.innerHTML = message;
}
+3 -11
View File
@@ -11,8 +11,6 @@ import ResetPwd from './modules/reset-pwd.vue';
import BindWechat from './modules/bind-wechat.vue';
import { WifiOutlined } from '@ant-design/icons-vue';
import { useI18n } from "vue-i18n";
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
interface Props {
/** The login module */
module?: UnionKey.LoginModule;
@@ -55,18 +53,12 @@ const bgColor = computed(() => {
<WaveBg :theme-color="bgThemeColor" />
<ACard class="relative z-4">
<div class="w-400px lt-sm:w-300px">
<header class="flex items-center justify-center relative w-full">
<div class="flex items-center">
<WifiOutlined class="text-64px text-primary lt-sm:text-48px" style="margin-left: 9px"/>
<h3 class="text-10px text-primary font-medium self-end">{{ $t('system.title') }}</h3>
</div>
<div class="absolute right-0">
<LangSwitch :lang="appStore.locale" :lang-options="appStore.localeOptions" @change-lang="appStore.changeLocale" />
</div>
<header class="flex justify-center items-center">
<WifiOutlined class="text-64px text-primary lt-sm:text-48px" style="margin-left: 9px"/>
<h3 class="text-10px text-primary font-medium self-end">{{ "WANFi" }}</h3>
</header>
<main >
<h3 class="text-28px text-primary font-500 lt-sm:text-22px ">{{ t(activeModule.label) }}</h3>
<div class="animation-slide-in-left ">
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
@@ -49,9 +49,9 @@ const model = reactive<LoginForm>({
wanfiRedirectParams: {}
});
// const rules = {
// username: patternRules.username,
// };
const rules = {
username: patternRules.username,
};
async function handleSubmit() {
await validate();
@@ -95,7 +95,7 @@ async function getCheckCode() {
</script>
<template>
<AForm ref="formRef" :model="model">
<AForm ref="formRef" :model="model" :rules="rules">
<AFormItem name="username">
<AInput v-model:value="model.username" size="large" :placeholder="$t('page.login.common.userNamePlaceholder')" autocomplete="off"/>
</AFormItem>
-410
View File
@@ -1,410 +0,0 @@
<script setup lang="ts">
import { ref, computed, onMounted, h } from 'vue'
import type { TableColumnsType } from 'ant-design-vue'
import { Table as ATable, Button as AButton, Modal, message } from 'ant-design-vue'
import { useI18n } from "vue-i18n"
import { useRouter } from 'vue-router'
import { useWindowSize } from '@vueuse/core'
import { useTable } from '@/hooks/common/table'
import { fetchBillHistory, downloadBill } from '@/service/api/auth'
import { getPaymentConfig } from '@/service/api/payment'
import type { Api } from '@/typings/api'
import { SimpleScrollbar } from '~/packages/materials/src'
import { Card as ACard, Tag as ATag } from 'ant-design-vue'
import { useElementSize } from '@vueuse/core'
import {
EyeOutlined,
DownloadOutlined
} from '@ant-design/icons-vue';
import axios from 'axios'
const router = useRouter()
const { t } = useI18n()
const currencySymbol = ref('¥')
// 获取货币符号
const fetchCurrencySymbol = async () => {
try {
const response = await getPaymentConfig()
if (response && response.data) {
currencySymbol.value = response.data.currencySymbol
}
} catch (error) {
// console.error('Failed to fetch currency symbol:', error)
}
}
// 格式化日期时间
const formatDateTime = (dateTimeString: string) => {
if (!dateTimeString) return ''
const date = new Date(dateTimeString)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
}
// 响应式布局
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
// 处理文件URL
const parseFile = (path: string) => {
let baseUrl = import.meta.env.VITE_SERVICE_BASE_URL;
// console.log(baseUrl)
// debugger
// 移除末尾的斜杠
baseUrl = baseUrl.replace(/\/+$/, '');
// 从完整 URL 中提取域名部分
if (baseUrl.includes('://')) {
try {
const url = new URL(baseUrl);
baseUrl = url.host; // 这将获取域名和端口(如果有的话)
} catch (error) {
// 如果解析失败,使用原始值的域名部分
baseUrl = baseUrl.replace(/^https?:\/\//, '').split('/')[0];
}
}
// 构造最终的 URL
return `${path.startsWith('/') ? path : '/' + path}`;
}
const wrapperEl = ref<HTMLElement | null>(null)
const { height: wrapperElHeight } = useElementSize(wrapperEl)
const scrollConfig = computed(() => ({
y: wrapperElHeight.value - 72,
x: 800
}))
const getStatusColor = (status: number) => {
switch (status) {
case 0:
return 'warning'
case 1:
return 'success'
case 2:
return 'error'
default:
return 'default'
}
}
const getStatusText = (status: number) => {
switch (status) {
case 0:
return t('page.bill.status.pending')
case 1:
return t('page.bill.status.paid')
case 2:
return t('page.bill.status.failed')
default:
return t('page.bill.status.unknown')
}
}
// 表格相关配置
const { columns, data, loading, getData, mobilePagination, searchParams } = useTable<Api.Bill.BillRecord>({
apiFn: fetchBillHistory,
apiParams: {
pageNum: 1,
pageSize: 10
},
rowKey: 'id',
pagination: true,
columns: (): AntDesign.TableColumn<Api.Bill.BillRecord>[] => [
{
key: 'invoiceNumber',
dataIndex: 'invoiceNumber',
title: t('page.bill.invoiceNumber'),
align: 'left',
width: 120
},
{
key: 'invoiceTime',
dataIndex: 'invoiceTime',
title: t('page.bill.billDate'),
align: 'center',
width: 150,
customRender: ({ text }: { text: string }) => text ? formatDateTime(text) : ''
},
{
key: 'amount',
dataIndex: 'amount',
title: t('page.bill.amount'),
align: 'center',
width: 120,
responsive: ['md'],
customRender: ({ text }: { text: number }) => {
const formattedAmount = Number(text).toFixed(2)
return `${currencySymbol.value}${formattedAmount}`
}
},
{
key: 'type',
dataIndex: 'type',
title: t('page.bill.billType'),
align: 'center',
width: 120,
responsive: ['md'],
customRender: ({ text }: { text: Api.Bill.BillRecord['type'] }) => {
const typeMap: Record<Api.Bill.BillRecord['type'],{ color: string; label: string }> = {
0: { color: 'blue', label: t('page.bill.types.package') },
1: { color: 'gold', label: t('page.bill.types.recharge') }
}
const typeInfo = typeMap[text] || { color: 'default', label: t('page.bill.types.unknown') }
return h(ATag, { color: typeInfo.color }, () => typeInfo.label)
}
},
{
key: 'invoiceFile',
title: t('page.bill.actions'),
align: 'center',
width: 60,
}
]
})
// 下载账单
const handleExport = (record: Api.Bill.BillRecord) => {
Modal.confirm({
title: t('page.bill.tipTitle'),
content: t('page.bill.exportTip'),
async onOk() {
const key = 'exportJob';
message.loading({ content: t('page.bill.loading'), key });
try {
const res = await downloadBill(record.id);
// console.log('downloadBill 返回:', res);
let blob;
if (res instanceof Blob) {
blob = res;
} else if (res.data instanceof Blob) {
blob = res.data;
} else {
blob = new Blob([res.data]);
}
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = `invoice-${record.invoiceNumber || record.id}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
message.success({ content: t('page.bill.exportOk'), key, duration: 2 });
} catch (e) {
// console.error('导出异常:', e);
message.error({ content: t('page.bill.exportFail'), key, duration: 2 });
}
}
});
}
// 预览账单
const handlePreview = async (record: Api.Bill.BillRecord) => {
// console.log('handlePreview被调用', record);
if (!record.invoiceFile) {
message.error(t('page.bill.noFile'))
return
}
try {
const url = parseFile(record.invoiceFile)
// console.log('预览PDF地址:', url, record.invoiceFile)
Modal.info({
title: t('page.bill.previewTitle'),
width: '80%',
content: h('iframe', {
src: url,
style: {
width: '100%',
height: '80vh',
border: 'none'
}
})
})
} catch (error) {
message.error(t('page.bill.previewFailed'))
}
}
const handleBack = () => {
router.push('/billing/billservice');
};
onMounted(() => {
fetchCurrencySymbol()
getData()
})
</script>
<template>
<SimpleScrollbar>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<ACard
:title="
isMobile
? h('div', { class: 'flex justify-between items-center w-full' }, [
h('span', t('page.bill.title')),
h(
AButton,
{
onClick: handleBack,
size: 'small',
style: { marginLeft: '8px' }
},
{ default: () => t('page.login.common.back') }
)
])
: t('page.bill.title')
"
:bordered="false"
:body-style="{ flex: 1, overflow: 'hidden' }"
class="flex-col-stretch sm:flex-1-hidden card-wrapper"
>
<ATable
ref="wrapperEl"
:columns="columns"
:data-source="data"
:loading="loading"
row-key="id"
size="small"
:pagination="{
...mobilePagination,
total: mobilePagination.total,
current: searchParams.pageNum,
pageSize: searchParams.pageSize,
showTotal: (total: number) => `${t('page.bill.total')} ${total} `
}"
:scroll="scrollConfig"
class="h-full"
@change="(pagination) => {
searchParams.pageNum = pagination.current;
searchParams.pageSize = pagination.pageSize;
getData();
}"
>
<template #expandedRowRender="{ record }">
<div v-if="isMobile" class="pl-4">
<div class="mb-2">
<strong>{{ t('page.bill.billDate') }}:</strong>
{{ formatDateTime(record.invoiceTime) }}
</div>
<div class="mb-2">
<strong>{{ t('page.bill.amount') }}:</strong>
{{ currencySymbol + Number(record.amount).toFixed(2) }}
</div>
<div class="mb-2">
<strong>{{ t('page.bill.billType') }}:</strong>
<ATag :color="record.type === 0 ? 'blue' : 'gold'">
{{ record.type === 0 ? t('page.bill.types.package') : t('page.bill.types.recharge') }}
</ATag>
</div>
<div class="mb-2">
<AButton
type="link"
size="small"
:disabled="!record.invoiceFile"
@click="handlePreview(record)"
>
<EyeOutlined />
</AButton>
<AButton
type="link"
size="small"
:disabled="!record.invoiceFile"
@click="handleExport(record)"
>
<DownloadOutlined />
</AButton>
</div>
</div>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<ATag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</ATag>
</template>
<template v-if="column.key === 'invoiceFile'">
<div class="table-file-wrapper">
<AButton
type="link"
size="small"
:disabled="!record.invoiceFile"
@click="handlePreview(record)"
>
<EyeOutlined />
</AButton>
<AButton
type="link"
size="small"
:disabled="!record.invoiceFile"
@click="handleExport(record)"
>
<DownloadOutlined />
</AButton>
</div>
</template>
</template>
</ATable>
</ACard>
</div>
</SimpleScrollbar>
</template>
<style scoped>
.h-full {
height: 100%;
}
.card-wrapper {
margin-top: 16px;
}
.table-file-wrapper {
display: flex;
justify-content: center;
align-items: center;
gap: 2px;
padding: 0;
}
:deep(.ant-btn-link) {
padding: 0;
height: auto;
line-height: inherit;
}
:deep(.ant-btn-link:hover) {
color: #1890ff;
text-decoration: underline;
}
:deep(.ant-modal-body) {
padding: 0;
}
:deep(.ant-modal-confirm-content) {
margin: 0;
}
:deep(.ant-table-row-expand-icon-cell) {
width: 24px !important;
min-width: 24px !important;
padding: 0 !important;
}
/* 在PC端隐藏展开按钮图标 */
@media screen and (min-width: 768px) {
:deep(.ant-table-row-expand-icon) {
display: none !important;
}
}
</style>
+2 -8
View File
@@ -7,8 +7,7 @@ import {
FileTextOutlined,
CreditCardOutlined,
ContainerOutlined,
WalletOutlined,
AuditOutlined
MoneyCollectOutlined,
} from '@ant-design/icons-vue';
@@ -17,7 +16,7 @@ const router = useRouter();
const menuItems = [
{
icon: WalletOutlined,
icon: MoneyCollectOutlined,
title: t('page.recharge.recharge'),
path: '/billing/recharge'
@@ -46,11 +45,6 @@ const menuItems = [
icon: FileTextOutlined,
title: t('page.endpoint.cdrlrecords'),
path: '/billing/cdrlrecords'
},
{
icon: AuditOutlined,
title:t('page.endpoint.bill'),
path: '/billing/bill'
}
];
+2 -5
View File
@@ -1,20 +1,17 @@
<script setup lang="ts">
import {ref, onMounted, watch, computed} from 'vue'
import { ref, onMounted, watch } from 'vue'
import type { TableColumnsType } from 'ant-design-vue'
import { HistoryOutlined } from '@ant-design/icons-vue'
import { useI18n } from "vue-i18n";
import { fetchCDRHistory } from '@/service/api/auth';
import {useRouter} from "vue-router";
import dayjs from 'dayjs';
import {useWindowSize} from "@vueuse/core";
defineOptions({
name: 'cdrlrecords'
});
const { t } = useI18n();
const router = useRouter();
const { width } = useWindowSize();
const isMobile = computed(() => width.value < 768);
const handleBack = () => {
router.push('/billing/billservice');
};
@@ -320,7 +317,7 @@ watch(tableData, (newData) => {
<template #extra>
<a-space size="middle">
<a-button v-if="isMobile" @click="handleBack">
<a-button @click="handleBack">
{{ t('page.login.common.back') }}
</a-button>
<a-button type="primary" :loading="tableLoading" @click="handleSearch">
+4 -31
View File
@@ -7,7 +7,6 @@ import { useRouter } from 'vue-router';
import { fetchPackageHistory } from '@/service/api/auth';
import { useWindowSize } from '@vueuse/core';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { getPaymentConfig } from '@/service/api/payment';
const router = useRouter();
const handleBack = () => {
@@ -15,18 +14,7 @@ const handleBack = () => {
};
const { t } = useI18n();
const currencySymbol = ref('¥');
const tableLoading = ref(false);
const fetchCurrencySymbol = async () => {
try {
const response = await getPaymentConfig();
if (response && response.data) {
currencySymbol.value = response.data.currencySymbol || '¥';
}
} catch (error) {
console.error('Failed to fetch currency symbol:', error);
}
};
const packageData = ref<Api.Package.PackageHistoryRecord[]>([]);
const total = ref(0);
@@ -48,7 +36,6 @@ const formatDateTime = (dateTimeString: string) => {
};
const getPackageData = async () => {
tableLoading.value=true;
try {
const response = await fetchPackageHistory({
pageNum: 1,
@@ -61,11 +48,9 @@ const getPackageData = async () => {
} catch (error) {
// console.error('Failed to fetch package history:', error);
}
tableLoading.value=false;
};
onMounted(() => {
fetchCurrencySymbol();
getPackageData();
});
@@ -90,7 +75,7 @@ const showPackageDetail = (record: Api.Package.PackageHistoryRecord) => {
]),
h('div', { class: 'detail-item' }, [
h('span', { class: 'detail-label' }, t('page.packagehistories.price') + ':'),
h('span', { class: 'detail-value' }, `${currencySymbol.value}${Number(record.upackage?.price || 0).toFixed(2)}`)
h('span', { class: 'detail-value' }, `¥${Number(record.upackage?.price || 0).toFixed(2)}`)
]),
h('div', { class: 'detail-item' }, [
h('span', { class: 'detail-label' }, t('page.packagehistories.traffic') + ':'),
@@ -183,7 +168,7 @@ const { columns, data, loading, getData, mobilePagination, searchParams } = useT
key: 'orderAmount',
customRender: ({ text }: { text: string }) => {
const formattedAmount = Number(text).toFixed(2);
return `${currencySymbol.value}${formattedAmount}`;
return `¥${formattedAmount}`;
}
},
{
@@ -238,15 +223,9 @@ const formatTraffic = (bytes: number): string => {
<div class="p-4">
<div class="text-lg font-bold mb-4 flex justify-between items-center">
<span>{{ t('page.packagehistories.title') }}</span>
<a-button v-if="isMobile" @click="handleBack" >
<a-button @click="handleBack">
{{ t('page.login.common.back') }}
</a-button>
<a-button type="primary" :loading="tableLoading" @click="getPackageData">
<template #icon>
<span class="i-carbon:refresh"></span>
</template>
{{ t('page.cdrlrecords.refresh') }}
</a-button>
</div>
<div>
<ATable :columns="columns" :data-source="data" row-key="id"
@@ -325,10 +304,4 @@ const formatTraffic = (bytes: number): string => {
color: #1890ff;
text-decoration: underline;
}
/* 在PC端隐藏展开按钮图标 */
@media screen and (min-width: 768px) {
:deep(.ant-table-row-expand-icon) {
display: none !important;
}
}
</style>
-360
View File
@@ -1,360 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, computed, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { AlipayOutlined, WechatOutlined, WalletOutlined, ArrowLeftOutlined } from '@ant-design/icons-vue';
import { Modal } from 'ant-design-vue';
import PaypalButton from '@/components/payment/paypal-button.vue';
import { useAuthStore } from '@/store/modules/auth';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const authStore = useAuthStore();
const emit = defineEmits(['update:visible', 'confirm', 'cancel']);
const loading = ref(false);
const orderInfo = ref({
orderId: '',
orderType: 0,
orderAmount: 0
});
const userBalance = ref(0);
// 监听余额变化
watchEffect(async () => {
try {
const response = await authStore.getDashboardData();
if (response && typeof response === 'object' && !response.error) {
const numBalance = Number(response.balance);
userBalance.value = isNaN(numBalance) ? 0 : numBalance;
}
} catch (error) {
console.error('Failed to fetch user balance:', error);
userBalance.value = 0;
}
});
// Debug: 添加 authStore 状态日志
console.log('Debug - Complete Auth Store:', {
userInfo: JSON.parse(JSON.stringify(authStore.userInfo)),
user: authStore.userInfo?.user ? JSON.parse(JSON.stringify(authStore.userInfo.user)) : null,
allKeys: authStore.userInfo ? Object.keys(authStore.userInfo) : [],
userKeys: authStore.userInfo?.user ? Object.keys(authStore.userInfo.user) : []
});
// 判断是否显示余额支付按钮
const showBalancePayment = computed(() => {
// 如果是余额充值,不显示余额支付
return orderInfo.value.orderType !== 1;
});
// 判断余额支付是否禁用
const isBalancePayDisabled = computed(() => {
const balanceNum = userBalance.value; // 已经确保是数字类型
const amountNum = Number(orderInfo.value.orderAmount);
console.log('Debug - Balance Check:', {
balance: balanceNum,
amount: amountNum,
isBalanceNaN: isNaN(balanceNum),
isAmountNaN: isNaN(amountNum),
loading: loading.value
});
// 如果转换后不是有效数字,禁用按钮
if (isNaN(balanceNum) || isNaN(amountNum)) {
console.log('Debug - Invalid number conversion');
return true;
}
// 如果余额为0或小于订单金额,禁用按钮
const isInsufficient = balanceNum === 0 || balanceNum < amountNum;
console.log('Debug - Balance sufficient check:', {
balance: balanceNum,
amount: amountNum,
isInsufficient
});
if (isInsufficient) {
return true;
}
// 如果正在加载,禁用按钮
return loading.value;
});
// 从路由参数中获取订单信息
onMounted(async () => {
const { orderId, orderType, orderAmount } = route.query;
// 添加参数验证和错误处理
if (!orderId) {
Modal.error({
title: t('page.common.error'),
content: t('page.pay.missingOrderId'),
onOk: () => router.back()
});
return;
}
try {
orderInfo.value = {
orderId: orderId as string,
orderType: orderType ? parseInt(orderType as string) : 0,
orderAmount: orderAmount ? parseFloat(orderAmount as string) : 0
};
// 验证转换后的值是否有效
if (isNaN(orderInfo.value.orderType) || isNaN(orderInfo.value.orderAmount)) {
throw new Error('Invalid parameter values');
}
// Debug: 打印订单信息
console.log('Debug - Order Info:', orderInfo.value);
} catch (error) {
Modal.error({
title: t('page.common.error'),
content: t('page.pay.invalidParameters'),
onOk: () => router.back()
});
}
});
const orderTypeMap = {
0: t('page.order.packagePurchase'),
1: t('page.order.balanceRecharge')
} as const;
const handleConfirm = (paymentMethod: 'alipay' | 'wxpay' | 'balance') => {
if (paymentMethod === 'balance') {
Modal.confirm({
title: t('page.order.confirmPayment'),
content: t('page.order.balancePayConfirm'),
okText: t('page.order.confirm'),
cancelText: t('page.order.cancel'),
onOk: () => {
// 处理余额支付
emit('confirm', paymentMethod);
}
});
} else {
// 处理其他支付方式
// emit('confirm', paymentMethod);
}
};
const handleBack = () => {
router.back();
};
</script>
<template>
<div class="pay-container">
<div class="header">
<a-button type="link" @click="handleBack" class="back-button">
<ArrowLeftOutlined />
{{ t('page.pay.back') }}
</a-button>
<h2>{{ t('page.pay.title') }}</h2>
</div>
<div class="content">
<div class="order-info">
<h3>{{ t('page.pay.orderDetails') }}</h3>
<div class="info-item">
<span class="label">{{ t('page.order.orderType') }}:</span>
<span class="value">{{ orderTypeMap[orderInfo.orderType] }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('page.order.orderAmount') }}:</span>
<span class="value highlight">¥{{ orderInfo.orderAmount.toFixed(2) }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('page.order.orderId') }}:</span>
<span class="value">{{ orderInfo.orderId }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('page.order.balance') }}:</span>
<span class="value">¥{{ userBalance.toFixed(2) }}</span>
</div>
</div>
<div class="payment-methods">
<h3>{{ t('page.order.selectPayment') }}</h3>
<div class="methods-container">
<!-- 余额支付按钮 -->
<div
v-if="showBalancePayment"
class="method-item"
:class="{ disabled: loading || isBalancePayDisabled }"
@click="!loading && !isBalancePayDisabled && handleConfirm('balance')"
>
<WalletOutlined class="payment-icon balance-icon" />
<span>{{ t('page.order.balancePay') }}</span>
<span v-if="isBalancePayDisabled" class="balance-insufficient">
{{ t('page.order.insufficientBalance') }}
</span>
</div>
<div
class="method-item"
:class="{ disabled: loading }"
@click="!loading && handleConfirm('alipay')"
>
<AlipayOutlined class="payment-icon alipay-icon" />
<span>{{ t('page.order.alipay') }}</span>
</div>
<div
class="method-item"
:class="{ disabled: loading }"
@click="!loading && handleConfirm('wxpay')"
>
<WechatOutlined class="payment-icon wxpay-icon" />
<span>{{ t('page.order.wxpay') }}</span>
</div>
<div class="method-item">
<PaypalButton
:amount="orderInfo.orderAmount"
:loading="loading"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.pay-container {
max-width: 800px;
margin: 0 auto;
padding: 24px;
}
.header {
display: flex;
align-items: center;
margin-bottom: 24px;
position: relative;
}
.back-button {
position: absolute;
left: 0;
}
.header h2 {
flex: 1;
text-align: center;
margin: 0;
}
.content {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.order-info {
padding: 24px;
border-bottom: 1px solid #f0f0f0;
}
.order-info h3,
.payment-methods h3 {
margin: 0 0 16px;
font-size: 16px;
color: #333;
}
.info-item {
display: flex;
margin-bottom: 16px;
line-height: 24px;
font-size: 14px;
}
.info-item:last-child {
margin-bottom: 0;
}
.label {
width: 84px;
color: #666;
}
.value {
flex: 1;
color: #333;
}
.value.highlight {
color: #ff4d4f;
font-size: 16px;
font-weight: 500;
}
.payment-methods {
padding: 24px;
}
.methods-container {
display: flex;
gap: 16px;
}
.method-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
border: 1px solid #e8e8e8;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.method-item:hover {
border-color: #1890ff;
background: #f0f5ff;
}
.payment-icon {
font-size: 28px;
margin-bottom: 12px;
}
.alipay-icon {
color: #1677ff;
}
.wxpay-icon {
color: #07c160;
}
.balance-icon {
color: #52c41a;
}
.method-item.disabled {
opacity: 0.5;
cursor: not-allowed;
background: #f5f5f5;
}
.method-item.disabled:hover {
border-color: #e8e8e8;
background: #f5f5f5;
}
.balance-insufficient {
font-size: 12px;
color: #ff4d4f;
margin-top: 4px;
text-align: center;
}
</style>
+53 -80
View File
@@ -6,26 +6,14 @@ import { useI18n } from "vue-i18n";
import { useRouter } from 'vue-router';
import { fetchRechargeHistory } from '@/service/api/auth';
import { useWindowSize } from '@vueuse/core';
import { getPaymentConfig } from '@/service/api/payment';
const router = useRouter();
const handleBack = () => {
router.push('/billing/billservice');
};
const tableLoading = ref(false);
const { t } = useI18n();
const currencySymbol = ref('¥');
const fetchCurrencySymbol = async () => {
try {
const response = await getPaymentConfig();
if (response && response.data) {
currencySymbol.value = response.data.currencySymbol || '¥';
}
} catch (error) {
console.error('Failed to fetch currency symbol:', error);
}
};
const { t } = useI18n();
const rechargeData = ref<Api.Recharge.RechargeRecord[]>([]);
const total = ref(0);
@@ -46,60 +34,57 @@ const formatDateTime = (dateTimeString: string) => {
});
};
const {columns, data, loading, getData, mobilePagination, searchParams} = useTable({
apiFn: fetchRechargeHistory,
apiParams: {
pageNum: 1,
pageSize: 5,
},
rowKey: 'id',
columns: () => [
{
title: t('page.Rechargehistory.topupdate'),
dataIndex: 'createTime',
key: 'createTime',
responsive: ['md'],
customRender: ({text}) => text ? formatDateTime(text) : ''
},
{
title: t('page.Rechargehistory.amount'),
dataIndex: 'orderAmount',
key: 'orderAmount',
customRender: ({text}: { text: string }) => {
const formattedAmount = Number(text).toFixed(2);
return `${currencySymbol.value}${formattedAmount}`;
}
},
{
title: t('page.Rechargehistory.type'),
dataIndex: 'type',
key: 'type',
customRender: ({text}: { text: number }) => {
return h(ATag, {
color: text === 1 ? 'blue' : 'green'
}, {
default: () => text === 1 ? t('page.Rechargehistory.rechargeType') : t('page.Rechargehistory.packageType')
})
}
},
{
title: t('page.Rechargehistory.status'),
dataIndex: 'status',
key: 'status',
customRender: ({text}: { text: number }) => {
return h(ATag, {
color: text === 1 ? 'success' : 'warning'
}, {
default: () => text === 1 ? t('page.Rechargehistory.paid') : t('page.Rechargehistory.unpaid')
})
}
}
]
});
onMounted(()=>{
fetchCurrencySymbol();
})
const { columns, data, loading, getData, mobilePagination, searchParams } = useTable({
apiFn: fetchRechargeHistory,
apiParams: {
pageNum: 1,
pageSize: 5,
},
rowKey: 'id',
columns: () => [
{
title: t('page.Rechargehistory.topupdate'),
dataIndex: 'createTime',
key: 'createTime',
responsive: ['md'],
customRender: ({ text }) => text ? formatDateTime(text) : ''
},
{
title: t('page.Rechargehistory.amount'),
dataIndex: 'orderAmount',
key: 'orderAmount',
customRender: ({ text }: { text: string }) => {
const formattedAmount = Number(text).toFixed(2);
return `¥${formattedAmount}`;
}
},
{
title: t('page.Rechargehistory.type'),
dataIndex: 'type',
key: 'type',
customRender: ({ text }: { text: number }) => {
return h(ATag, {
color: text === 1 ? 'blue' : 'green'
}, {
default: () => text === 1 ? t('page.Rechargehistory.rechargeType') : t('page.Rechargehistory.packageType')
})
}
},
{
title: t('page.Rechargehistory.status'),
dataIndex: 'status',
key: 'status',
customRender: ({ text }: { text: number }) => {
return h(ATag, {
color: text === 1 ? 'success' : 'warning'
}, {
default: () => text === 1 ? t('page.Rechargehistory.paid') : t('page.Rechargehistory.unpaid')
})
}
}
]
});
</script>
@@ -107,15 +92,9 @@ onMounted(()=>{
<div class="p-4">
<div class="text-lg font-bold mb-4 flex justify-between items-center">
<span>{{ t('page.Rechargehistory.recharge') }}</span>
<a-button v-if="isMobile" @click="handleBack">
<a-button @click="handleBack">
{{ t('page.login.common.back') }}
</a-button>
<!-- <a-button type="primary" :loading="tableLoading" @click="getPackageData">-->
<!-- <template #icon>-->
<!-- <span class="i-carbon:refresh"></span>-->
<!-- </template>-->
<!-- {{ t('page.cdrlrecords.refresh') }}-->
<!-- </a-button>-->
</div>
<div>
<ATable
@@ -153,10 +132,4 @@ onMounted(()=>{
.pl-4 {
padding-left: 1rem;
}
/* 在PC端隐藏展开按钮图标 */
@media screen and (min-width: 768px) {
:deep(.ant-table-row-expand-icon) {
display: none !important;
}
}
</style>
+4 -6
View File
@@ -3,11 +3,9 @@
import { useRoute } from 'vue-router';
import { useRouterPush } from '@/hooks/common/router';
import { doGetOrderInfo } from '@/service/api/order';
import { useI18n } from 'vue-i18n';
const route = useRoute();
const { routerPushByKey } = useRouterPush();
const { t } = useI18n();
const qrCodeImg = ref<string>(window.location.origin + route.query.url);
@@ -36,15 +34,15 @@
<div class="weChat__card--div">
<a-card class="weChat__card" :bordered="false">
<template #title>
<span class="title__span">{{ t('page.order.wxpay') }}</span>
<span class="title__span">微信支付</span>
</template>
<div class="weChat__card__content">
<div class="bottomContent">
<span class="weChat__card_span"> {{ t('page.order.wxpayScan') }}</span>
</div>
<p>
<img :src="qrCodeImg" class="weChat__card_qrCodeImg" />
</p>
<div class="bottomContent">
<span class="weChat__card_span">使用微信扫码支付</span>
</div>
</div>
</a-card>
</div>
+2 -4
View File
@@ -9,9 +9,7 @@ defineOptions({
});
const { t } = useI18n();
const router = useRouter();
// 响应式布局
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const handleBack = () => {
router.push('/endpoint/clientservice');
};
@@ -110,7 +108,7 @@ onMounted(() => {
<template #extra>
<a-space size="middle">
<a-button v-if="isMobile" @click="handleBack">
<a-button @click="handleBack">
{{ t('page.login.common.back') }}
</a-button>
<a-button type="primary" :loading="loading" @click="handleRefresh">
+1 -4
View File
@@ -10,9 +10,6 @@ defineOptions({
});
const router = useRouter();
const { t } = useI18n();
// 响应式布局
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const handleBack = () => {
router.push('/endpoint/clientservice');
};
@@ -137,7 +134,7 @@ onMounted(() => {
<template #extra>
<a-space size="middle">
<a-button v-if="isMobile" @click="handleBack">
<a-button @click="handleBack">
{{ t('page.login.common.back') }}
</a-button>
<a-button type="primary" :loading="loading" @click="handleRefresh">
+72 -65
View File
@@ -6,26 +6,9 @@ import type { ECOption } from '@/hooks/common/echarts';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '@/store/modules/auth';
import { clientAuth } from '@/service/ue/client';
import { getPaymentConfig } from '@/service/api/payment';
const { t } = useI18n();
const authStore = useAuthStore();
// 添加货币符号的响应式引用
const currencySymbol = ref('¥');
// 获取货币符号
const fetchCurrencySymbol = async () => {
try {
const response = await getPaymentConfig();
if (response && response.data) {
currencySymbol.value = response.data.currencySymbol || '¥';
}
} catch (error) {
console.error('Failed to fetch currency symbol:', error);
}
};
defineOptions({
name: 'HeaderBanner'
});
@@ -298,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 // 添加套餐数量字段
});
// 修改数据更新函数,添加套餐信息的更新
@@ -313,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 // 添加套餐数量
};
// 更新余额和设备数据
@@ -324,19 +307,15 @@ async function mockDataUpdate() {
value: numBalance,
max: Math.max(numBalance, 100),
displayValue:formatBalance(response.balance),
unit: currencySymbol.value,
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')
):'-'
}`
};
// 初始化流量变量,在外层定义
let totalTraffic = 0;
let usedTraffic = 0;
let remainingTraffic = 0;
// 更新流量数据
if (!response.packageName) {
@@ -355,39 +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);
//内层调用,则不会存在控制台报错
totalTraffic = response.trafficEnable ? (response.traffic || 0) : 0;
usedTraffic = response.trafficEnable ? (response.trafficUsed || 0) : 0;
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,
@@ -398,7 +373,6 @@ async function mockDataUpdate() {
};
}
// 更新速率限制显示
if (!response.packageName) {
speedLimits.value = {
@@ -448,8 +422,6 @@ let timer: ReturnType<typeof setInterval> | null = null;
// 初始化
async function init() {
// 立即执行一次数据更新
// 先获取货币符号
await fetchCurrencySymbol();
await mockDataUpdate();
// 设置定期执行的定时器
timer = setInterval(mockDataUpdate, 30000);
@@ -528,7 +500,7 @@ const getDeviceCount = (subTitle?: string, clientNumEnable?: boolean): string =>
</div>
<div class="info-item">
<span class="info-label">{{ t('page.headerbanner.price') }}</span>
<span class="info-value">{{ packageInfo.price === '-' ? '-' : currencySymbol + packageInfo.price }}</span>
<span class="info-value">{{ packageInfo.price === '-' ? '-' : '¥' + packageInfo.price }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ t('page.headerbanner.monthflowr') }}</span>
@@ -559,12 +531,18 @@ const getDeviceCount = (subTitle?: string, clientNumEnable?: boolean): string =>
<div class="info-group">
<div class="info-item">
<span class="info-label">{{ t('page.headerbanner.Remainingcredit') }}</span>
<span class="info-value">{{ baseData[0].displayValue === '-' ? '-' : currencySymbol + baseData[0].displayValue }}</span>
<span class="info-value">{{ baseData[0].displayValue === '-' ? '-' : '¥' + baseData[0].displayValue }}</span>
</div>
<div class="info-item">
<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>
@@ -827,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>
+8 -22
View File
@@ -8,7 +8,6 @@ import { useRouterPush } from '@/hooks/common/router';
import type { Ref } from 'vue';
import OrderConfirmModal from '@/components/order-confirm/orderConfirmModal.vue';
import { useAppStore } from '@/store/modules/app';
import { getPaymentConfig } from '@/service/api/payment';
defineOptions({
name: 'BalanceRecharge'
@@ -17,18 +16,7 @@ defineOptions({
const { t } = useI18n();
const { routerPushByKey } = useRouterPush();
const appStore = useAppStore();
const currencySymbol = ref('¥');
const fetchCurrencySymbol = async () => {
try {
const response = await getPaymentConfig();
if (response && response.data) {
currencySymbol.value = response.data.currencySymbol || '¥';
}
} catch (error) {
console.error('Failed to fetch currency symbol:', error);
}
};
interface RechargeOption {
amount: number;
displayAmount: string;
@@ -39,13 +27,13 @@ const customAmount = ref<string | number | undefined>(undefined);
const selectedAmount = ref<number | null>(null);
const isCustomMode = ref<boolean>(false);
const rechargeOptions = computed(() => [
{ amount: 10, displayAmount: `${currencySymbol.value}10`, price: 10.00 },
{ amount: 20, displayAmount: `${currencySymbol.value}20`, price: 20.00 },
{ amount: 30, displayAmount: `${currencySymbol.value}30`, price: 30.00 },
{ amount: 50, displayAmount: `${currencySymbol.value}50`, price: 50.00 },
{ amount: 100, displayAmount: `${currencySymbol.value}100`, price: 100.00 },
{ amount: 200, displayAmount: `${currencySymbol.value}200`, price: 200.00 },
const rechargeOptions: Ref<RechargeOption[]> = ref([
{ amount: 10, displayAmount: `${t('page.carddata.money')}10`, price: 10.00 },
{ amount: 20, displayAmount: `${t('page.carddata.money')}20`, price: 20.00 },
{ amount: 30, displayAmount: `${t('page.carddata.money')}30`, price: 30.00 },
{ amount: 50, displayAmount: `${t('page.carddata.money')}50`, price: 50.00 },
{ amount: 100, displayAmount: `${t('page.carddata.money')}100`, price: 100.00 },
{ amount: 200, displayAmount: `${t('page.carddata.money')}200`, price: 200.00 },
]);
const paymentAmount = computed(() => {
@@ -88,12 +76,10 @@ const handleBlur = () => {
};
onMounted(() => {
fetchCurrencySymbol();
document.addEventListener('click', handleClickOutside);
});
onUnmounted(() => {
fetchCurrencySymbol();
document.removeEventListener('click', handleClickOutside);
});
@@ -216,7 +202,7 @@ const handlePaymentConfirm = async (paymentMethod: 'alipay' | 'wxpay') => {
:disabled="!paymentAmount || paymentAmount <= 0"
@click="handleRecharge"
>
<span style="color: var(--text-color, var(--ant-text-color))"> {{ currencySymbol }}{{ paymentAmount.toFixed(2) }} {{ t('page.carddata.pay') }} </span>
<span style="color: var(--text-color, var(--ant-text-color))"> ¥{{ paymentAmount.toFixed(2) }} {{ t('page.carddata.pay') }} </span>
</AButton>
</div>
+3 -17
View File
@@ -8,23 +8,10 @@ import { aliPayPcPay,aliPayWapPay, wxPayScanCode, payBalance } from '@/service/a
import { useRouterPush } from '@/hooks/common/router';
import { useAppStore } from '@/store/modules/app';
import { useAuthStore} from '@/store/modules/auth';
import { getPaymentConfig } from '@/service/api/payment';
defineOptions({
name: 'PackageSubscription'
});
const currencySymbol = ref('¥');
const fetchCurrencySymbol = async () => {
try {
const response = await getPaymentConfig();
if (response && response.data) {
currencySymbol.value = response.data.currencySymbol || '¥';
}
} catch (error) {
console.error('Failed to fetch currency symbol:', error);
}
};
const { t } = useI18n();
const authStore = useAuthStore();
interface RateLimit {
@@ -356,7 +343,6 @@ const isPackageActive = computed(() => {
});
onMounted(async () => {
await fetchCurrencySymbol();
fetchDashboardData();
await fetchPackages();
await fetchUserBalance();
@@ -368,7 +354,7 @@ onMounted(async () => {
<!-- 顶部价格展示 -->
<div class="price-header">
<div class="price">
<span class="currency">{{ currencySymbol }}</span>
<span class="currency">¥</span>
<span class="amount">{{ selectedPackage.price }}</span>
</div>
<div class="subtitle">{{ selectedPackage.packageName }}</div>
@@ -394,7 +380,7 @@ onMounted(async () => {
{{ t('page.setmeal.highlyrecommended') }}
</div>
<div class="package-name">{{ option.packageName }}</div>
<div class="price">{{ currencySymbol }}{{ option.price }}</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') }}
@@ -449,7 +435,7 @@ onMounted(async () => {
<button
class="btn-primary"
@click="handleSubmitOrder"
:disabled="isLoading || !hasPackages || isPackageActive"
:disabled="isLoading"
>
{{ isLoading ? t('page.common.loading') :
!hasPackages ? t('page.setmeal.noPackages') :
+252
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>
+5
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'),