Compare commits
20
Commits
feature/refund
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30dcf6460a | ||
|
|
f98a1cca4f | ||
|
|
adfd2426b4 | ||
|
|
f62bc444ce | ||
|
|
b9ef0e4e3f | ||
|
|
d2890a25b6 | ||
|
|
39c2e05121 | ||
|
|
9c2d91ba96 | ||
|
|
ace15a79c6 | ||
|
|
6c3499b470 | ||
|
|
7c77b10c8c | ||
|
|
191ecd4c83 | ||
|
|
4d4727b17e | ||
|
|
b72bd2121a | ||
|
|
ac4e201f98 | ||
|
|
3cc72b946d | ||
|
|
621932f514 | ||
|
|
3cbaebb5d3 | ||
|
|
092864e661 | ||
|
|
d4c901d1c6 |
No files matched your search
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Generated
+15
@@ -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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const enUS = {
|
||||
system: {
|
||||
title: 'MacroHub'
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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:'支付成功',
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
Vendored
+14
-7
@@ -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 */
|
||||
|
||||
Vendored
+7
@@ -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']
|
||||
|
||||
Vendored
+1
-2
@@ -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']
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
Reference in new issue
Block a user