feat:账单记录界面移动端适配
This commit is contained in:
@@ -648,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',
|
||||
|
||||
@@ -678,7 +678,8 @@ const local:any = {
|
||||
endpoint:{
|
||||
access:"当前设备",
|
||||
records:"历史设备",
|
||||
cdrlrecords:"上网记录"
|
||||
cdrlrecords:"上网记录",
|
||||
bill:'账单记录',
|
||||
},
|
||||
cdrlrecords:{
|
||||
total:'共',
|
||||
|
||||
@@ -12,6 +12,10 @@ 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()
|
||||
@@ -121,8 +125,8 @@ const { columns, data, loading, getData, mobilePagination, searchParams } = useT
|
||||
key: 'invoiceNumber',
|
||||
dataIndex: 'invoiceNumber',
|
||||
title: t('page.bill.invoiceNumber'),
|
||||
align: 'center',
|
||||
width: 150
|
||||
align: 'left',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'invoiceTime',
|
||||
@@ -138,6 +142,7 @@ const { columns, data, loading, getData, mobilePagination, searchParams } = useT
|
||||
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}`
|
||||
@@ -149,6 +154,7 @@ const { columns, data, loading, getData, mobilePagination, searchParams } = useT
|
||||
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') },
|
||||
@@ -162,8 +168,7 @@ const { columns, data, loading, getData, mobilePagination, searchParams } = useT
|
||||
key: 'invoiceFile',
|
||||
title: t('page.bill.actions'),
|
||||
align: 'center',
|
||||
width: 150,
|
||||
fixed: 'right'
|
||||
width: 60,
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -229,6 +234,10 @@ const handlePreview = async (record: Api.Bill.BillRecord) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
router.push('/billing/billservice');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchCurrencySymbol()
|
||||
getData()
|
||||
@@ -239,7 +248,22 @@ onMounted(() => {
|
||||
<SimpleScrollbar>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<ACard
|
||||
:title="t('page.bill.title')"
|
||||
: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"
|
||||
@@ -251,6 +275,7 @@ onMounted(() => {
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
size="small"
|
||||
:expandIconColumnWidth="24"
|
||||
:pagination="{
|
||||
...mobilePagination,
|
||||
total: mobilePagination.total,
|
||||
@@ -266,6 +291,42 @@ onMounted(() => {
|
||||
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)">
|
||||
@@ -280,7 +341,7 @@ onMounted(() => {
|
||||
:disabled="!record.invoiceFile"
|
||||
@click="handlePreview(record)"
|
||||
>
|
||||
{{ t('page.bill.preview') }}
|
||||
<EyeOutlined />
|
||||
</AButton>
|
||||
<AButton
|
||||
type="link"
|
||||
@@ -288,7 +349,7 @@ onMounted(() => {
|
||||
:disabled="!record.invoiceFile"
|
||||
@click="handleExport(record)"
|
||||
>
|
||||
{{ t('page.bill.download') }}
|
||||
<DownloadOutlined />
|
||||
</AButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -312,8 +373,8 @@ onMounted(() => {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px;
|
||||
gap: 2px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
@@ -334,4 +395,10 @@ onMounted(() => {
|
||||
:deep(.ant-modal-confirm-content) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-table-row-expand-icon-cell) {
|
||||
width: 24px !important;
|
||||
min-width: 24px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CreditCardOutlined,
|
||||
ContainerOutlined,
|
||||
WalletOutlined,
|
||||
AuditOutlined
|
||||
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
360
src/views/billing/pay/index.vue
Normal file
360
src/views/billing/pay/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user