2
0

fix:上网记录界面添加条件查询,排序,添加时长字段

This commit is contained in:
zhongzm
2025-03-07 17:38:48 +08:00
parent 9e1d12db30
commit 46fc4d33ec
4 changed files with 371 additions and 37 deletions

View File

@@ -650,6 +650,7 @@ const local: any = {
cdrlrecords:"Internet records" cdrlrecords:"Internet records"
}, },
cdrlrecords:{ cdrlrecords:{
total:'Total',
devicename:"Device name", devicename:"Device name",
uptraffic:"Traffic Up", uptraffic:"Traffic Up",
downtraffic:"Traffic Down", downtraffic:"Traffic Down",
@@ -657,7 +658,12 @@ const local: any = {
lasttime:"Last seen Time", lasttime:"Last seen Time",
cdr:"CDR Records", cdr:"CDR Records",
refresh:"Refresh", refresh:"Refresh",
mac:"MAC" mac:"MAC",
duration:'Duration',
sec:'s',
min:'min',
hour:'hour',
day:'day'
}, },
records:{ records:{
clientID:"Client ID", clientID:"Client ID",

View File

@@ -650,6 +650,13 @@ const local:any = {
cdrlrecords:"上网记录" cdrlrecords:"上网记录"
}, },
cdrlrecords:{ cdrlrecords:{
total:'共',
searchdevice:'输入设备名称',
timerange:'开始时间范围',
starttime:'开始时间',
endtime:'结束时间',
search:'查询',
reset:'重置',
devicename:"设备名称", devicename:"设备名称",
uptraffic:"上传流量", uptraffic:"上传流量",
downtraffic:"下载流量", downtraffic:"下载流量",
@@ -657,7 +664,12 @@ const local:any = {
lasttime:"断开时间", lasttime:"断开时间",
cdr:"上网记录", cdr:"上网记录",
refresh:"刷新", refresh:"刷新",
mac:"mac地址" mac:"mac地址",
duration:'时长',
sec:'秒',
min:'分',
hour:'时',
day:'天'
}, },
records:{ records:{
clientID:"设备ID", clientID:"设备ID",

View File

@@ -40,6 +40,7 @@ declare module 'vue' {
AppProvider: typeof import('./../components/common/app-provider.vue')['default'] AppProvider: typeof import('./../components/common/app-provider.vue')['default']
ARadio: typeof import('ant-design-vue/es')['Radio'] ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
ARow: typeof import('ant-design-vue/es')['Row'] ARow: typeof import('ant-design-vue/es')['Row']
ASegmented: typeof import('ant-design-vue/es')['Segmented'] ASegmented: typeof import('ant-design-vue/es')['Segmented']
ASelect: typeof import('ant-design-vue/es')['Select'] ASelect: typeof import('ant-design-vue/es')['Select']

View File

@@ -1,10 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted, watch } from 'vue'
import type { TableColumnsType } from 'ant-design-vue' import type { TableColumnsType } from 'ant-design-vue'
import { HistoryOutlined } from '@ant-design/icons-vue' import { HistoryOutlined } from '@ant-design/icons-vue'
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { fetchCDRHistory } from '@/service/api/auth'; import { fetchCDRHistory } from '@/service/api/auth';
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import dayjs from 'dayjs';
defineOptions({ defineOptions({
name: 'cdrlrecords' name: 'cdrlrecords'
}); });
@@ -17,11 +19,11 @@ const handleBack = () => {
interface CDRRecord { interface CDRRecord {
id: number; id: number;
clientName: string; clientName: string;
clientName: string;
trafficUp: string; trafficUp: string;
trafficDown: string; trafficDown: string;
startTime: string; startTime: string;
endTime: string; endTime: string;
duration:number;
} }
// 定义API返回的原始数据类型 // 定义API返回的原始数据类型
@@ -48,7 +50,6 @@ const expandedRowKeys = ref<number[]>([]);
// 处理展开行变化 // 处理展开行变化
function handleExpandChange(expanded: boolean, record: CDRRecord) { function handleExpandChange(expanded: boolean, record: CDRRecord) {
console.log('Expanded: ', expanded, 'Record: ', record);
if (expanded) { if (expanded) {
expandedRowKeys.value = [record.id]; expandedRowKeys.value = [record.id];
} else { } else {
@@ -63,7 +64,22 @@ const formatTraffic = (bytes: number): string => {
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB` if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB` return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`
} }
//格式化时间
const formatTime = (totalSeconds: number): string => {
const days = Math.floor(totalSeconds / (24 * 60 * 60));
const hours = Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60));
const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
const seconds = Math.floor(totalSeconds % 60);
const parts: string[] = [];
if (days > 0) parts.push(`${days}${t('page.cdrlrecords.day')}`);
if (hours > 0) parts.push(`${hours}${t('page.cdrlrecords.hour')}`);
if (minutes > 0) parts.push(`${minutes}${t('page.cdrlrecords.min')}`);
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}${t('page.cdrlrecords.sec')}`);
return parts.join('');
};
// 格式化时间戳 // 格式化时间戳
function formatTimestamp(timestamp: number): string { function formatTimestamp(timestamp: number): string {
const date = new Date(timestamp); const date = new Date(timestamp);
@@ -77,52 +93,215 @@ function formatTimestamp(timestamp: number): string {
}); });
} }
const searchForm = ref({
clientName: '',
timeRange: [undefined, undefined]
});
const { columns, data, loading, getData, mobilePagination, searchParams } = useTable({ // 定义查询参数接口
apiFn: fetchCDRHistory, interface SearchParams {
apiParams: { pageNum: number;
pageNum: 1, pageSize: number;
pageSize: 5, clientName?: string;
}, startTimeS?: number;
rowKey: 'id', startTimeE?: number;
columns: () => [ orderBy?: string;
orderType?: 'asc' | 'desc';
}
// 表格数据和分页
const tableData = ref([]);
const tablePagination = ref({
current: 1,
pageSize: 5,
total: 0,
showSizeChanger: true,
showTotal: (total: number) => `${t('page.cdrlrecords.total')} ${total} `
});
const tableLoading = ref(false);
// 添加排序状态
const sortState = ref({
field: '',
order: undefined as 'ascend' | 'descend' | undefined
});
// 添加排序字段映射
const sortFieldMapping = {
duration: 'duration', // 根据后端API的实际字段名进行映射
clientName: 'clientName',
trafficUp: 'trafficUp',
trafficDown: 'trafficDown'
};
// 处理搜索
const handleSearch = async () => {
const params: SearchParams = {
pageNum: tablePagination.value.current,
pageSize: tablePagination.value.pageSize
};
if (searchForm.value.clientName) {
params.clientName = searchForm.value.clientName;
}
if (searchForm.value.timeRange[0]) {
params.startTimeS = dayjs(searchForm.value.timeRange[0]).startOf('day').valueOf();
}
if (searchForm.value.timeRange[1]) {
params.startTimeE = dayjs(searchForm.value.timeRange[1]).endOf('day').valueOf();
}
// 添加排序参数,使用映射后的字段名
if (sortState.value.field && sortState.value.order) {
const mappedField = sortFieldMapping[sortState.value.field];
if (mappedField) {
params.orderBy = mappedField;
params.orderType = sortState.value.order === 'ascend' ? 'asc' : 'desc';
}
}
try {
tableLoading.value = true;
const res = await fetchCDRHistory(params);
// 检查响应数据的具体结构
if (res.data) {
let records = [];
let total = 0;
// 检查数据结构
if (res.data.rows && Array.isArray(res.data.rows)) {
records = res.data.rows;
total = res.data.total || records.length;
}
if (records.length > 0) {
// 处理数据,确保所有必要的字段都存在
const processedData = records.map((record: any) => ({
id: record.id || Math.random(),
clientName: record.clientName || '',
clientMac: record.clientMac || '',
trafficUp: Number(record.trafficUp || 0),
trafficDown: Number(record.trafficDown || 0),
startTime: Number(record.startTime || 0),
endTime: Number(record.endTime || 0),
duration: Number(record.duration || 0),
key: record.id || Math.random()
}));
tableData.value = processedData;
tablePagination.value = {
...tablePagination.value,
total: total,
current: params.pageNum,
pageSize: params.pageSize
};
} else {
tableData.value = [];
tablePagination.value = {
...tablePagination.value,
total: 0,
current: 1
};
}
} else {
tableData.value = [];
tablePagination.value = {
...tablePagination.value,
total: 0,
current: 1
};
}
} catch (error) {
tableData.value = [];
tablePagination.value = {
...tablePagination.value,
total: 0,
current: 1
};
} finally {
tableLoading.value = false;
}
};
// 监听分页和排序变化
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
// 只有分页变化时才重新请求数据
if (pagination.current !== tablePagination.value.current ||
pagination.pageSize !== tablePagination.value.pageSize) {
tablePagination.value = {
...tablePagination.value,
current: pagination.current,
pageSize: pagination.pageSize
};
// 只在分页变化时重新获取数据
handleSearch();
}
};
// 重置搜索
const handleReset = () => {
searchForm.value = {
clientName: '',
timeRange: [undefined, undefined]
};
// 重置排序状态
sortState.value = {
field: '',
order: undefined
};
tablePagination.value.current = 1;
handleSearch();
};
// 初始化数据
onMounted(() => {
handleSearch();
});
// 定义表格列
const tableColumns = [
{ {
title: t('page.cdrlrecords.devicename'), title: t('page.cdrlrecords.devicename'),
dataIndex: 'clientName', dataIndex: 'clientName',
key: 'clientName', key: 'clientName',
width: '40%' width: '25%'
},
{
title: t('page.cdrlrecords.duration'),
dataIndex: 'duration',
key: 'duration',
width: '25%',
align: 'right',
sorter: (a, b) => a.duration - b.duration,
defaultSortOrder: undefined,
customRender: ({ text }: { text: number }) => formatTime(text)
}, },
{ {
title: t('page.cdrlrecords.uptraffic'), title: t('page.cdrlrecords.uptraffic'),
dataIndex: 'trafficUp', dataIndex: 'trafficUp',
key: 'trafficUp', key: 'trafficUp',
width: '30%', width: '25%',
align: 'right', align: 'right',
customRender(opt) { customRender: ({ text }: { text: number }) => formatTraffic(text)
const record = opt.value;
return formatTraffic(record);
},
}, },
{ {
title: t('page.cdrlrecords.downtraffic'), title: t('page.cdrlrecords.downtraffic'),
dataIndex: 'trafficDown', dataIndex: 'trafficDown',
key: 'trafficDown', key: 'trafficDown',
width: '30%', width: '25%',
align: 'right', align: 'right',
customRender(opt) { customRender: ({ text }: { text: number }) => formatTraffic(text)
const record = opt.value;
return formatTraffic(record);
},
} }
] ];
});
// 监听数据变化
watch(tableData, (newData) => {
}, { deep: true });
</script> </script>
@@ -137,25 +316,67 @@ const { columns, data, loading, getData, mobilePagination, searchParams } = useT
</template> </template>
<template #extra> <template #extra>
<a-space size="middle">
<a-button @click="handleBack"> <a-button @click="handleBack">
{{ t('page.login.common.back') }} {{ t('page.login.common.back') }}
</a-button> </a-button>
<a-button type="primary" :loading="loading" @click="getData"> <a-button type="primary" :loading="tableLoading" @click="handleSearch">
<template #icon> <template #icon>
<span class="i-carbon:refresh"></span> <span class="i-carbon:refresh"></span>
</template> </template>
{{ t('page.cdrlrecords.refresh') }} {{ t('page.cdrlrecords.refresh') }}
</a-button> </a-button>
</a-space>
</template> </template>
<!-- 搜索表单 -->
<div class="search-form">
<a-form layout="inline" class="search-form-container">
<a-form-item :label="t('page.cdrlrecords.devicename')" class="form-item">
<a-input
v-model:value="searchForm.clientName"
:placeholder="t('page.cdrlrecords.searchdevice')"
allow-clear
class="device-input"
/>
</a-form-item>
<a-form-item :label="t('page.cdrlrecords.timerange')" class="form-item date-picker-group">
<a-date-picker
v-model:value="searchForm.timeRange[0]"
:placeholder="t('page.cdrlrecords.starttime')"
class="date-picker"
format="YYYY-MM-DD"
/>
<span class="date-separator">-</span>
<a-date-picker
v-model:value="searchForm.timeRange[1]"
:placeholder="t('page.cdrlrecords.endtime')"
class="date-picker"
format="YYYY-MM-DD"
/>
</a-form-item>
<a-form-item class="form-item button-group">
<a-space size="middle">
<a-button type="primary" @click="handleSearch">
{{ t('page.cdrlrecords.search') }}
</a-button>
<a-button @click="handleReset">
{{ t('page.cdrlrecords.reset') }}
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<a-table <a-table
:loading="loading" :loading="tableLoading"
:columns="columns" :columns="tableColumns"
:data-source="data" :data-source="tableData"
row-key="id" row-key="id"
:expanded-row-keys="expandedRowKeys" :expanded-row-keys="expandedRowKeys"
@expand="handleExpandChange" @expand="handleExpandChange"
:pagination="mobilePagination" :pagination="tablePagination"
@change="handleTableChange"
> >
<template #expandedRowRender="{ record }"> <template #expandedRowRender="{ record }">
<div class="pl-4"> <div class="pl-4">
@@ -237,4 +458,98 @@ const { columns, data, loading, getData, mobilePagination, searchParams } = useT
.pl-4 { .pl-4 {
padding-left: 1rem; padding-left: 1rem;
} }
.search-form {
margin-bottom: 16px;
padding: 16px;
background: #fafafa;
border-radius: 4px;
}
.search-form-container {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.form-item {
margin-bottom: 0;
margin-right: 0;
}
.device-input {
width: 200px;
}
.date-picker-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-picker {
width: 130px;
}
.date-separator {
color: #999;
padding: 0 4px;
}
.button-group {
margin-left: auto;
}
/* 移动端适配搜索表单 */
@media screen and (max-width: 768px) {
.search-form-container {
flex-direction: column;
gap: 16px;
}
.form-item {
width: 100%;
margin-bottom: 0;
}
.device-input,
.date-picker-group {
width: 100%;
}
.button-group {
display: flex;
justify-content: flex-end;
}
.date-picker-group {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.date-picker {
width: 100%;
}
.date-separator {
text-align: center;
}
:deep(.ant-picker-panels) {
flex-direction: column !important;
}
:deep(.ant-picker-panel) {
width: auto !important;
}
:deep(.ant-picker-dropdown) {
max-width: calc(100vw - 32px) !important;
}
:deep(.ant-picker-panel-container) {
max-width: 100%;
}
}
</style> </style>