Author SHA1 Message Date
caiyuchao 30dcf6460a fix: paypal按钮刷新渲染 2025-06-23 18:55:44 +08:00
zhongzm f98a1cca4f feat:部分按钮的双端隐藏和显示配置 2025-06-20 17:25:11 +08:00
zhongzm adfd2426b4 feat:修复首页控制台报错 2025-06-20 15:53:08 +08:00
zhongzm f62bc444ce feat:提交补充 2025-06-19 16:04:47 +08:00
zhongzm b9ef0e4e3f feat:菜单顺序调整 2025-06-19 15:39:18 +08:00
zhongzm d2890a25b6 Merge remote-tracking branch 'origin/main' 2025-06-18 19:55:01 +08:00
zhongzm 39c2e05121 feat:账单记录界面移动端适配 2025-06-18 19:54:49 +08:00
caiyuchao 9c2d91ba96 feat: 统一修改系统名补充 2025-06-18 17:44:17 +08:00
caiyuchao ace15a79c6 feat: 统一修改系统名 2025-06-18 17:15:02 +08:00
zhongzm 6c3499b470 feat:账单系统 2025-06-17 18:40:51 +08:00
zhongzm 7c77b10c8c Merge remote-tracking branch 'origin/main' 2025-04-30 11:36:35 +08:00
zhongzm 191ecd4c83 feat:图标更改 2025-04-30 11:36:24 +08:00
caiyuchao 4d4727b17e feat: paypal配置 2025-04-29 18:07:23 +08:00
caiyuchao b72bd2121a feat: palpay支付成功刷新 2025-04-28 23:50:59 +08:00
caiyuchao ac4e201f98 feat: paypal中英文适配 2025-04-28 19:00:31 +08:00
zhongzm 3cc72b946d feat:多支付以及货币符号显示 2025-04-27 20:17:35 +08:00
caiyuchao 621932f514 feat: stripe支付 2025-04-25 11:35:41 +08:00
caiyuchao 3cbaebb5d3 feat: paypal支付 2025-04-23 16:01:56 +08:00
zhongzm 092864e661 fix:登录界面校验删除以及显示修改 2025-04-15 20:03:23 +08:00
zhongzm d4c901d1c6 fix:登录界面增加中英文切换功能 2025-03-20 11:46:09 +08:00
34 changed files with 1371 additions and 92 deletions

No files matched your search

+2 -1
View File
@@ -1,6 +1,6 @@
VITE_BASE_URL=/
VITE_APP_TITLE="WANFi Platform"
VITE_APP_TITLE="Platform"
# the prefix of the icon name
VITE_ICON_PREFIX=icon
@@ -35,3 +35,4 @@ VITE_SERVICE_EXPIRED_TOKEN_CODES=403
VITE_SERVICE_SERVER_ERROR_CODE=500
VITE_PAYPAL_CLIENT_ID=AfPgwFAmo9K7KCqiiGpNRCyQMSxI6V33eH-nEMnVndJNVEYOEOEn5wSPkHUybfzcjDLnBejt-RKnIfqX
+2
View File
@@ -4,6 +4,8 @@
<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,6 +14,7 @@
"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)) {
if (opts.isBackendSuccess(response) || response.config?.responseType === "blob") {
return Promise.resolve(response);
}
+15
View File
@@ -14,6 +14,9 @@ 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
@@ -730,6 +733,9 @@ 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'}
@@ -3263,6 +3269,9 @@ 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'}
@@ -4633,6 +4642,10 @@ 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
@@ -7553,6 +7566,8 @@ snapshots:
progress@2.0.3: {}
promise-polyfill@8.3.0: {}
prompts@2.4.2:
dependencies:
kleur: 3.0.3
+5
View File
@@ -0,0 +1,5 @@
const enUS = {
system: {
title: 'MacroHub'
}
};
+5
View File
@@ -0,0 +1,5 @@
const zhCN = {
system: {
title: 'MacroHub'
}
};
@@ -1,8 +1,10 @@
<script setup lang="ts">
import { defineProps, defineEmits, computed } from 'vue';
import { defineProps, defineEmits, computed, ref, onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { AlipayOutlined, WechatOutlined, WalletOutlined } from '@ant-design/icons-vue';
import { AlipayOutlined, WechatOutlined, WalletOutlined,MoneyCollectOutlined } 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();
@@ -21,6 +23,27 @@ 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')
@@ -37,7 +60,7 @@ const isBalancePayDisabled = computed(() => {
return props.loading || false;
});
const handleConfirm = (paymentMethod: 'alipay' | 'wxpay' | 'balance') => {
const handleConfirm = async (paymentMethod: 'alipay' | 'wxpay' | 'balance') => {
if (paymentMethod === 'balance') {
Modal.confirm({
title: t('page.order.confirmPayment'),
@@ -48,6 +71,9 @@ const handleConfirm = (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);
}
@@ -80,7 +106,7 @@ const handleCancel = () => {
</div>
<div class="info-item">
<span class="label">{{ t('page.order.orderAmount') }}:</span>
<span class="value highlight">¥{{ orderInfo.orderAmount.toFixed(2) }}</span>
<span class="value highlight">{{ currencySymbol }}{{ orderInfo.orderAmount.toFixed(2) }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('page.order.orderId') }}:</span>
@@ -99,20 +125,19 @@ 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') }}: ¥{{ userBalance.toFixed(2) }}
{{ t('page.order.availableBalance') }}: {{ currencySymbol }}{{ 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')"
@@ -120,7 +145,9 @@ 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')"
@@ -128,6 +155,21 @@ 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>
@@ -183,10 +225,13 @@ 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
@@ -0,0 +1,58 @@
<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,MoneyCollectOutlined,ClusterOutlined} from "@ant-design/icons-vue";
import {UserOutlined,HomeOutlined,WalletOutlined,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>
<MoneyCollectOutlined class="text-icon-large" @click="routerPushByKey('billing_billservice')"/>
<WalletOutlined class="text-icon-large" @click="routerPushByKey('billing_billservice')"/>
</ButtonIcon>
</div>
<div class="flex-item">
+31 -3
View File
@@ -7,6 +7,7 @@ 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",
@@ -196,7 +197,7 @@ const local: any = {
login: {
common: {
loginOrRegister: 'Login / Register',
userNamePlaceholder: 'Please enter user name',
userNamePlaceholder: 'Please enter user name or email',
phonePlaceholder: 'Please enter phone number',
codePlaceholder: 'Please enter verification code',
passwordPlaceholder: 'Please enter password',
@@ -213,7 +214,7 @@ const local: any = {
getCodeFailed: 'Failed to get verification code, please try again'
},
pwdLogin: {
title: 'Password Login',
title: 'Login',
rememberMe: 'Remember me',
forgetPassword: 'Forget password?',
register: 'Register',
@@ -647,7 +648,8 @@ const local: any = {
endpoint:{
access:"current client",
records:"Historical client",
cdrlrecords:"Internet records"
cdrlrecords:"Internet records",
bill:'Bill records',
},
cdrlrecords:{
total:'Total',
@@ -728,6 +730,8 @@ 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',
@@ -738,6 +742,30 @@ 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',
+37 -3
View File
@@ -7,6 +7,7 @@ const viewZh: any = {
"view.endpoint_access": "当前设备",
"view.endpoint_records": "历史设备",
"view.endpoint_cdrlrecords":"上网记录",
"view.endpoint_bill":"账单记录",
"view.billing": "账单",
"view.billing_billservice":"账单服务",
"view.billing_histories": "历史查询",
@@ -196,7 +197,7 @@ const local:any = {
login: {
common: {
loginOrRegister: '登录 / 注册',
userNamePlaceholder: '请输入用户名',
userNamePlaceholder: '请输入用户名或邮箱',
phonePlaceholder: '请输入手机号',
codePlaceholder: '请输入验证码',
passwordPlaceholder: '请输入密码',
@@ -213,7 +214,7 @@ const local:any = {
getCodeFailed: '获取验证码失败,请重试'
},
pwdLogin: {
title: '密码登录',
title: '登录',
rememberMe: '记住我',
forgetPassword: '忘记密码?',
register: '注册账号',
@@ -602,6 +603,36 @@ 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:"月",
@@ -647,7 +678,8 @@ const local:any = {
endpoint:{
access:"当前设备",
records:"历史设备",
cdrlrecords:"上网记录"
cdrlrecords:"上网记录",
bill:'账单记录',
},
cdrlrecords:{
total:'共',
@@ -728,6 +760,8 @@ const local:any = {
selectPayment:'支付方式',
alipay:'支付宝支付',
wxpay:'微信支付',
wxpayScan:'使用微信扫码支付',
stripe:'Stripe',
balancePay:'余额支付',
availableBalance:'可用余额',
paymentSuccess:'支付成功',
+7 -4
View File
@@ -1,9 +1,12 @@
import zhCN from './langs/zh-cn';
import enUS from './langs/en-us';
import localeZhCN from './langs/zh-cn';
import localeEnUS from './langs/en-us';
const fullZhCN = { ...localeZhCN, ...zhCN };
const fullEnUS = { ...localeEnUS, ...enUS };
const locales: Record<App.I18n.LangType, App.I18n.Schema> = {
'zh-CN': zhCN,
'en-US': enUS
'zh-CN': fullZhCN,
'en-US': fullEnUS
};
export default locales;
+29 -18
View File
@@ -330,7 +330,7 @@ export const customRoutes: GeneratedRoute[] = [
meta: {
title: '账单',
i18nKey: 'view.billing',
icon: 'ant-design:account-book-outlined',
icon: 'ant-design:container-outlined',
order: 13,
//hideInMenu: true
},
@@ -354,7 +354,7 @@ export const customRoutes: GeneratedRoute[] = [
meta: {
title: '充值服务',
i18nKey: 'view.recharge_rechargeservice',
icon: 'ant-design:money-collect-outlined',
icon: 'ant-design:wallet-outlined',
order: 2,
// hideInMenu: true
},
@@ -378,7 +378,7 @@ export const customRoutes: GeneratedRoute[] = [
title: '充值记录',
i18nKey: 'view.billing_Rechargehistory',
icon: 'ant-design:calendar-outlined',
order: 4
order: 3
},
},
{
@@ -389,21 +389,21 @@ export const customRoutes: GeneratedRoute[] = [
title: '套餐记录',
i18nKey: 'view.billing_packagehistories',
icon: 'ant-design:shopping-outlined',
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
order: 4
},
},
// {
// 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,7 +412,18 @@ export const customRoutes: GeneratedRoute[] = [
title: '上网记录',
i18nKey: 'view.endpoint_cdrlrecords',
icon: 'ant-design:book-outlined',
order: 4
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
},
},
{
@@ -425,7 +436,7 @@ export const customRoutes: GeneratedRoute[] = [
hideInMenu: true,
order: 10,
},
}
},
]
},
{
+11 -2
View File
@@ -1,5 +1,5 @@
import { request } from '../request';
import { request, rawRequest } from '../request';
/**
* Login
*
@@ -206,6 +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}`,
method: 'get',
responseType: 'blob'
});
}
+40
View File
@@ -37,3 +37,43 @@ 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'
});
}
+14 -7
View File
@@ -676,13 +676,20 @@ declare namespace Api {
namespace Bill {
/** Bill record information */
interface BillRecord {
id: string;
startTime: string | null;
endTime: string | null;
traffic: string | null;
amount: string;
status: number;
createTime: string;
/** 账单编号 */
invoiceNumber: string;
/** 账单金额 */
amount: number;
/** 账单类型:0-购买套餐,1-余额充值 */
type: 0 | 1;
/** 账单时间 */
invoiceTime: string;
/** 账单状态:0-待支付,1-已支付,2-支付失败 */
status: 0 | 1 | 2;
/** 账单文件URL */
fileUrl?: string;
/** 发票文件URL */
invoiceFile?: string;
}
/** Bill list response */
+7
View File
@@ -30,6 +30,7 @@ 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']
@@ -89,6 +90,7 @@ 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']
@@ -127,6 +129,8 @@ 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']
@@ -180,6 +184,8 @@ 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']
@@ -211,6 +217,7 @@ 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']
+1 -2
View File
@@ -34,14 +34,12 @@ 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']
@@ -80,6 +78,7 @@ 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
@@ -0,0 +1,105 @@
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;
}
+10 -2
View File
@@ -11,6 +11,8 @@ 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;
@@ -53,12 +55,18 @@ const bgColor = computed(() => {
<WaveBg :theme-color="bgThemeColor" />
<ACard class="relative z-4">
<div class="w-400px lt-sm:w-300px">
<header class="flex justify-center items-center">
<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">{{ "WANFi" }}</h3>
<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>
<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" :rules="rules">
<AForm ref="formRef" :model="model">
<AFormItem name="username">
<AInput v-model:value="model.username" size="large" :placeholder="$t('page.login.common.userNamePlaceholder')" autocomplete="off"/>
</AFormItem>
+410
View File
@@ -0,0 +1,410 @@
<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>
+8 -2
View File
@@ -7,7 +7,8 @@ import {
FileTextOutlined,
CreditCardOutlined,
ContainerOutlined,
MoneyCollectOutlined,
WalletOutlined,
AuditOutlined
} from '@ant-design/icons-vue';
@@ -16,7 +17,7 @@ const router = useRouter();
const menuItems = [
{
icon: MoneyCollectOutlined,
icon: WalletOutlined,
title: t('page.recharge.recharge'),
path: '/billing/recharge'
@@ -45,6 +46,11 @@ const menuItems = [
icon: FileTextOutlined,
title: t('page.endpoint.cdrlrecords'),
path: '/billing/cdrlrecords'
},
{
icon: AuditOutlined,
title:t('page.endpoint.bill'),
path: '/billing/bill'
}
];
+5 -2
View File
@@ -1,17 +1,20 @@
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import {ref, onMounted, watch, computed} 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');
};
@@ -317,7 +320,7 @@ watch(tableData, (newData) => {
<template #extra>
<a-space size="middle">
<a-button @click="handleBack">
<a-button v-if="isMobile" @click="handleBack">
{{ t('page.login.common.back') }}
</a-button>
<a-button type="primary" :loading="tableLoading" @click="handleSearch">
+31 -4
View File
@@ -7,6 +7,7 @@ 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 = () => {
@@ -14,7 +15,18 @@ 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);
@@ -36,6 +48,7 @@ const formatDateTime = (dateTimeString: string) => {
};
const getPackageData = async () => {
tableLoading.value=true;
try {
const response = await fetchPackageHistory({
pageNum: 1,
@@ -48,9 +61,11 @@ const getPackageData = async () => {
} catch (error) {
// console.error('Failed to fetch package history:', error);
}
tableLoading.value=false;
};
onMounted(() => {
fetchCurrencySymbol();
getPackageData();
});
@@ -75,7 +90,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' }, `¥${Number(record.upackage?.price || 0).toFixed(2)}`)
h('span', { class: 'detail-value' }, `${currencySymbol.value}${Number(record.upackage?.price || 0).toFixed(2)}`)
]),
h('div', { class: 'detail-item' }, [
h('span', { class: 'detail-label' }, t('page.packagehistories.traffic') + ':'),
@@ -168,7 +183,7 @@ const { columns, data, loading, getData, mobilePagination, searchParams } = useT
key: 'orderAmount',
customRender: ({ text }: { text: string }) => {
const formattedAmount = Number(text).toFixed(2);
return `¥${formattedAmount}`;
return `${currencySymbol.value}${formattedAmount}`;
}
},
{
@@ -223,9 +238,15 @@ 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 @click="handleBack">
<a-button v-if="isMobile" @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"
@@ -304,4 +325,10 @@ 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
@@ -0,0 +1,360 @@
<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>
+31 -4
View File
@@ -6,14 +6,26 @@ 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 rechargeData = ref<Api.Recharge.RechargeRecord[]>([]);
const total = ref(0);
@@ -34,7 +46,6 @@ const formatDateTime = (dateTimeString: string) => {
});
};
const {columns, data, loading, getData, mobilePagination, searchParams} = useTable({
apiFn: fetchRechargeHistory,
apiParams: {
@@ -56,7 +67,7 @@ const { columns, data, loading, getData, mobilePagination, searchParams } = useT
key: 'orderAmount',
customRender: ({text}: { text: string }) => {
const formattedAmount = Number(text).toFixed(2);
return `¥${formattedAmount}`;
return `${currencySymbol.value}${formattedAmount}`;
}
},
{
@@ -86,15 +97,25 @@ const { columns, data, loading, getData, mobilePagination, searchParams } = useT
]
});
onMounted(()=>{
fetchCurrencySymbol();
})
</script>
<template>
<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 @click="handleBack">
<a-button v-if="isMobile" @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
@@ -132,4 +153,10 @@ const { columns, data, loading, getData, mobilePagination, searchParams } = useT
.pl-4 {
padding-left: 1rem;
}
/* 在PC端隐藏展开按钮图标 */
@media screen and (min-width: 768px) {
:deep(.ant-table-row-expand-icon) {
display: none !important;
}
}
</style>
+6 -4
View File
@@ -3,9 +3,11 @@
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);
@@ -34,15 +36,15 @@
<div class="weChat__card--div">
<a-card class="weChat__card" :bordered="false">
<template #title>
<span class="title__span">微信支付</span>
<span class="title__span">{{ t('page.order.wxpay') }}</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>
+4 -2
View File
@@ -9,7 +9,9 @@ defineOptions({
});
const { t } = useI18n();
const router = useRouter();
// 响应式布局
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const handleBack = () => {
router.push('/endpoint/clientservice');
};
@@ -108,7 +110,7 @@ onMounted(() => {
<template #extra>
<a-space size="middle">
<a-button @click="handleBack">
<a-button v-if="isMobile" @click="handleBack">
{{ t('page.login.common.back') }}
</a-button>
<a-button type="primary" :loading="loading" @click="handleRefresh">
+4 -1
View File
@@ -10,6 +10,9 @@ defineOptions({
});
const router = useRouter();
const { t } = useI18n();
// 响应式布局
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const handleBack = () => {
router.push('/endpoint/clientservice');
};
@@ -134,7 +137,7 @@ onMounted(() => {
<template #extra>
<a-space size="middle">
<a-button @click="handleBack">
<a-button v-if="isMobile" @click="handleBack">
{{ t('page.login.common.back') }}
</a-button>
<a-button type="primary" :loading="loading" @click="handleRefresh">
+34 -6
View File
@@ -6,9 +6,26 @@ 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'
});
@@ -307,7 +324,7 @@ async function mockDataUpdate() {
value: numBalance,
max: Math.max(numBalance, 100),
displayValue:formatBalance(response.balance),
unit: t('page.headerbanner.money'),
unit: currencySymbol.value,
subTitle: t('page.headerbanner.deviceCount') + `: ${
response.packageName ? (
!response.clientNumEnable
@@ -316,6 +333,10 @@ async function mockDataUpdate() {
):'-'
}`
};
// 初始化流量变量,在外层定义
let totalTraffic = 0;
let usedTraffic = 0;
let remainingTraffic = 0;
// 更新流量数据
if (!response.packageName) {
@@ -334,9 +355,13 @@ 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);
//内层调用,则不会存在控制台报错
totalTraffic = response.trafficEnable ? (response.traffic || 0) : 0;
usedTraffic = response.trafficEnable ? (response.trafficUsed || 0) : 0;
remainingTraffic = Math.max(0, totalTraffic - usedTraffic);
// 格式化流量显示
const formattedTotal = formatTraffic(totalTraffic);
@@ -373,6 +398,7 @@ async function mockDataUpdate() {
};
}
// 更新速率限制显示
if (!response.packageName) {
speedLimits.value = {
@@ -422,6 +448,8 @@ let timer: ReturnType<typeof setInterval> | null = null;
// 初始化
async function init() {
// 立即执行一次数据更新
// 先获取货币符号
await fetchCurrencySymbol();
await mockDataUpdate();
// 设置定期执行的定时器
timer = setInterval(mockDataUpdate, 30000);
@@ -500,7 +528,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 === '-' ? '-' : '¥' + packageInfo.price }}</span>
<span class="info-value">{{ packageInfo.price === '-' ? '-' : currencySymbol + packageInfo.price }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ t('page.headerbanner.monthflowr') }}</span>
@@ -531,7 +559,7 @@ 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 === '-' ? '-' : '¥' + baseData[0].displayValue }}</span>
<span class="info-value">{{ baseData[0].displayValue === '-' ? '-' : currencySymbol + baseData[0].displayValue }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ t('page.headerbanner.client') }}</span>
+22 -8
View File
@@ -8,6 +8,7 @@ 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'
@@ -16,7 +17,18 @@ 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;
@@ -27,13 +39,13 @@ const customAmount = ref<string | number | undefined>(undefined);
const selectedAmount = ref<number | null>(null);
const isCustomMode = ref<boolean>(false);
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 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 paymentAmount = computed(() => {
@@ -76,10 +88,12 @@ const handleBlur = () => {
};
onMounted(() => {
fetchCurrencySymbol();
document.addEventListener('click', handleClickOutside);
});
onUnmounted(() => {
fetchCurrencySymbol();
document.removeEventListener('click', handleClickOutside);
});
@@ -202,7 +216,7 @@ const handlePaymentConfirm = async (paymentMethod: 'alipay' | 'wxpay') => {
:disabled="!paymentAmount || paymentAmount <= 0"
@click="handleRecharge"
>
<span style="color: var(--text-color, var(--ant-text-color))"> ¥{{ paymentAmount.toFixed(2) }} {{ t('page.carddata.pay') }} </span>
<span style="color: var(--text-color, var(--ant-text-color))"> {{ currencySymbol }}{{ paymentAmount.toFixed(2) }} {{ t('page.carddata.pay') }} </span>
</AButton>
</div>
+16 -2
View File
@@ -8,10 +8,23 @@ 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 {
@@ -343,6 +356,7 @@ const isPackageActive = computed(() => {
});
onMounted(async () => {
await fetchCurrencySymbol();
fetchDashboardData();
await fetchPackages();
await fetchUserBalance();
@@ -354,7 +368,7 @@ onMounted(async () => {
<!-- 顶部价格展示 -->
<div class="price-header">
<div class="price">
<span class="currency">¥</span>
<span class="currency">{{ currencySymbol }}</span>
<span class="amount">{{ selectedPackage.price }}</span>
</div>
<div class="subtitle">{{ selectedPackage.packageName }}</div>
@@ -380,7 +394,7 @@ onMounted(async () => {
{{ t('page.setmeal.highlyrecommended') }}
</div>
<div class="package-name">{{ option.packageName }}</div>
<div class="price">¥{{ option.price }}</div>
<div class="price">{{ currencySymbol }}{{ 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') }}