2
0

feat:账单记录界面移动端适配

This commit is contained in:
zhongzm
2025-06-18 19:54:49 +08:00
parent 6c3499b470
commit 39c2e05121
5 changed files with 446 additions and 11 deletions

View File

@@ -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',

View File

@@ -678,7 +678,8 @@ const local:any = {
endpoint:{
access:"当前设备",
records:"历史设备",
cdrlrecords:"上网记录"
cdrlrecords:"上网记录",
bill:'账单记录',
},
cdrlrecords:{
total:'共',

View File

@@ -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>

View File

@@ -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'
}
];

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>