2
0

fix:历史记录界面后端对接

This commit is contained in:
zhongzm
2024-12-18 17:24:26 +08:00
parent 7cf6249fd8
commit a6342d7036
8 changed files with 327 additions and 216 deletions

View File

@@ -562,6 +562,8 @@ const local: any = {
uptime:"Up Time",
lasttime:"Last seen Time",
cdr:"CDR Records",
refresh:"Refresh",
mac:"MAC"
},
records:{
clientID:"Client ID",
@@ -572,6 +574,7 @@ const local: any = {
endtime:"End Time",
clienthistory:"Client history",
refresh:"Refresh",
dataUsage:"Data Usage",
},
access:{
devicename:"Device Name",

View File

@@ -562,6 +562,8 @@ const local:any = {
uptime:"接入时间",
lasttime:"断开时间",
cdr:"CDR记录",
refresh:"刷新",
mac:"mac地址"
},
records:{
clientID:"设备ID",
@@ -572,6 +574,7 @@ const local:any = {
endtime:"结束时间",
clienthistory:"接入历史记录",
refresh:"刷新",
dataUsage:"流量使用量",
},
access:{
devicename:"设备名称",

View File

@@ -91,3 +91,34 @@ export function doGetCheckCode() {
url: '/code'
});
}
//首页仪表盘
/** Get dashboard gauge data */
export function fetchDashboardData() {
return request<Api.Dashboard.GaugeData>({
url: '/u/cdr/getOne',
method: 'get'
});
}
/** Get current connected devices */
export function fetchCurrentDevices() {
return request<Api.Device.DeviceListResponse>({
url: '/u/client/pageCurrentClient',
method: 'get'
});
}
/** Get historical devices */
export function fetchHistoricalDevices() {
return request<Api.Device.HistoricalDeviceListResponse>({
url: '/u/client/pageHistoryClient',
method: 'get'
});
}
/** Get CDR history records */
export function fetchCDRHistory(params: Api.CDR.CDRQueryParams) {
return request<Api.CDR.CDRListResponse>({
url: '/u/cdr/pageHistory',
method: 'get',
params
});
}

View File

@@ -7,7 +7,7 @@ import { localStg } from '@/utils/storage';
import { $t } from '@/locales';
import { useRouteStore } from '../route';
import { clearAuthStorage, emptyInfo, getToken } from './shared';
import { doCheckUserRepeat, sendCaptcha } from '@/service/api/auth';
import { doCheckUserRepeat, sendCaptcha, fetchDashboardData } from '@/service/api/auth';
export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const routeStore = useRouteStore();
@@ -18,6 +18,8 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const userInfo: Api.Auth.UserInfo = reactive(emptyInfo);
const permissions = computed(() => userInfo.permissions);
/** Dashboard data */
const dashboardData = ref<Api.Dashboard.GaugeData | null>(null);
watch(
() => token.value,
@@ -159,6 +161,16 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const { data, error } = await sendCaptcha({ email }); // 这里调用后端接口发送验证码
return { data, error };
}
/** Fetch dashboard data */
async function getDashboardData() {
const { data, error } = await fetchDashboardData();
if (!error) {
dashboardData.value = data;
return data;
}
return null;
}
return {
token,
@@ -172,6 +184,8 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
register,
captcha,
checkUserRepeat,
updateUserProfile
updateUserProfile,
dashboardData,
getDashboardData
};
});

View File

@@ -162,6 +162,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
if (!error) {
const {authRoutes: staticAuthRoutes} = createStaticRoutes();
const filteredAuthRoutes = filterAuthRoutesByRoles(staticAuthRoutes, authStore.userInfo.roles ?? []);
console.log (filteredAuthRoutes.concat(routes))
addAuthRoutes(filteredAuthRoutes.concat(routes));
handleAuthRoutes();
@@ -184,7 +185,6 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
const vueRoutes = getAuthVueRoutes(sortRoutes);
resetVueRoutes();
addRoutesToVueRouter(vueRoutes);
getGlobalMenus(sortRoutes);

150
src/typings/api.d.ts vendored
View File

@@ -435,4 +435,154 @@ declare namespace Api {
type DictList = Common.PaginatingQueryRecord<Dict>;
}
/**
* Namespace Dashboard
*
* Backend api module: "dashboard"
*/
namespace Dashboard {
/** Dashboard gauge data */
interface GaugeData {
/** Remaining credit amount */
remainingCredit: number;
/** Used credit amount */
usedCredit: number;
/** Remaining flow amount (MB) */
remainingFlow: number;
/** Used flow amount (MB) */
usedFlow: number;
/** Current traffic rate (MB/s) */
trafficRate: number;
/** Peak traffic rate (MB/s) */
peakTrafficRate: number;
}
}
/**
* Namespace Device
*
* Backend api module: "device"
*/
namespace Device {
/** Device information */
interface DeviceInfo {
/** Device unique key */
key: string;
/** Device name */
deviceName: string;
/** MAC address */
macAddress: string;
/** Current speed */
speed: string;
}
/** Device list response */
interface DeviceListResponse {
/** List of devices */
rows: DeviceInfo[];
/** Total count */
total: number;
}
/** Historical device information */
interface HistoricalDeviceInfo {
/** Device unique key */
key: string;
/** Device name */
deviceName: string;
/** MAC address */
macAddress: string;
/** Connection time */
connectionTime: string;
/** Disconnection time */
disconnectionTime: string;
/** Data usage */
dataUsage: string;
}
/** Historical device list response */
interface HistoricalDeviceListResponse {
/** List of historical devices */
rows: HistoricalDeviceInfo[];
/** Total count */
total: number;
}
interface DeviceInfo {
id: number;
clientName: string;
clientMac: string;
clientDeviceType: string;
activity: number;
}
interface DeviceResponse {
total: number;
rows: DeviceInfo[];
}
interface HistoricalDeviceInfo {
id: number;
clientName: string;
clientMac: string;
startTime: number; // 时间戳
endTime: number; // 时间戳
duration: number; // 流量使用量(bytes)
}
interface HistoricalDeviceResponse {
total: number;
rows: HistoricalDeviceInfo[];
}
}
namespace CDR {
/** CDR record information */
interface CDRRecord {
/** Record ID */
id: number;
/** AP name */
ap_name: string;
/** Upload traffic */
traffic_up: number;
/** Download traffic */
traffic_down: number;
/** Up time */
up_time: string;
/** Last seen time */
last_seen_time: string;
}
interface CDRRecord {
/** Record ID */
id: number;
/** Client name */
clientName: string;
/** Client MAC address */
clientMac: string;
/** Upload traffic in bytes */
trafficUp: number;
/** Download traffic in bytes */
trafficDown: number;
/** Start time timestamp */
startTime: number;
/** End time timestamp */
endTime: number;
}
/** CDR record list response */
interface CDRListResponse {
/** List of CDR records */
rows: CDRRecord[];
/** Total count */
total: number;
}
/** CDR query params */
interface CDRQueryParams {
/** Page number */
pageNum: number;
/** Page size */
pageSize: number;
}
}
}

View File

@@ -92,10 +92,14 @@ declare global {
const expect: typeof import('vitest')['expect']
const extendRef: typeof import('@vueuse/core')['extendRef']
const extractTabsByAllRoutes: typeof import('../store/modules/tab/shared')['extractTabsByAllRoutes']
const fetchCDRHistory: typeof import('../service/api/auth')['fetchCDRHistory']
const fetchCurrentDevices: typeof import('../service/api/auth')['fetchCurrentDevices']
const fetchCustomBackendError: typeof import('../service/api/auth')['fetchCustomBackendError']
const fetchDashboardData: typeof import('../service/api/auth')['fetchDashboardData']
const fetchGetAllPages: typeof import('../service/api/menu')['fetchGetAllPages']
const fetchGetConstantRoutes: typeof import('../service/api/route')['fetchGetConstantRoutes']
const fetchGetMenuTree: typeof import('../service/api/menu')['fetchGetMenuTree']
const fetchHistoricalDevices: typeof import('../service/api/auth')['fetchHistoricalDevices']
const fetchIsRouteExist: typeof import('../service/api/route')['fetchIsRouteExist']
const fetchLogin: typeof import('../service/api/auth')['fetchLogin']
const fetchRefreshToken: typeof import('../service/api/auth')['fetchRefreshToken']

View File

@@ -1,133 +1,122 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import type { TableColumnsType } from 'ant-design-vue'
import { HistoryOutlined } from '@ant-design/icons-vue'
import type { Dayjs } from 'dayjs'
import {useI18n} from "vue-i18n";
import { useI18n } from "vue-i18n";
import { fetchHistoricalDevices } from '@/service/api/auth';
const {t} = useI18n();
const { t } = useI18n();
interface AccessRecord {
key: string
clientId: string
clientName: string
clientDeviceType: string
clientMac: string
startTime: string
endTime: string
// 格式化时间戳
function formatTimestamp(timestamp: number): string {
const date = new Date(timestamp);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
// 格式化流量
function formatDataUsage(bytes: number): string {
if (bytes < 1024) {
return `${bytes}B`;
} else if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(2)}KB`;
} else if (bytes < 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
} else {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
}
}
// 定义记录类型
interface DeviceRecord {
id: number;
deviceName: string;
macAddress: string;
dataUsage: string;
connectionTime: string;
disconnectionTime: string;
}
// 设备列表数据
const deviceList = ref<DeviceRecord[]>([]);
const loading = ref(false);
const columns: TableColumnsType = [
{
title: t('page.records.clientID'),
dataIndex: 'clientId',
key: 'clientId',
width: 100,
responsive: ['md']
},
{
title: t('page.records.clientname'),
dataIndex: 'clientName',
key: 'clientName',
width: 90,
className: 'wrap-cell'
},
{
title: t('page.records.clienttype'),
dataIndex: 'clientDeviceType',
key: 'clientDeviceType',
width: 90,
responsive: ['lg']
dataIndex: 'deviceName',
key: 'deviceName',
width: '30%'
},
{
title: t('page.records.clientmac'),
dataIndex: 'clientMac',
key: 'clientMac',
width: 140,
responsive: ['md']
dataIndex: 'macAddress',
key: 'macAddress',
width: '40%'
},
{
title: t('page.records.starttime'),
dataIndex: 'startTime',
key: 'startTime',
width: 110,
ellipsis: true
},
{
title: t('page.records.endtime'),
dataIndex: 'endTime',
key: 'endTime',
width: 110,
ellipsis: true
title: t('page.records.dataUsage'),
dataIndex: 'dataUsage',
key: 'dataUsage',
width: '30%',
align: 'right'
}
]
];
// 添加一个存储所有记录的数组和当前显示记录的数组
const allRecords = ref<AccessRecord[]>([
{
key: '1',
clientId: 'DEV001',
clientName: 'iPhone 13',
clientDeviceType: '手机',
clientMac: '00:11:22:33:44:55',
startTime: '2024-03-20 10:00:00',
endTime: '2024-03-20 12:30:00'
},
{
key: '2',
clientId: 'DEV002',
clientName: 'MacBook Pro',
clientDeviceType: '笔记本',
clientMac: '66:77:88:99:AA:BB',
startTime: '2024-03-20 14:00:00',
endTime: '2024-03-20 18:00:00'
// 添加展开行控制
const expandedRowKeys = ref<number[]>([]);
// 处理展开行变化
function handleExpandChange(expanded: boolean, record: DeviceRecord) {
if (expanded) {
expandedRowKeys.value = [record.id];
} else {
expandedRowKeys.value = [];
}
])
const accessRecords = ref<AccessRecord[]>(allRecords.value)
// 日期范围变更处理函数
const onRangeChange = (
value: [Dayjs, Dayjs] | [string, string] | null,
dateStrings: [string, string]
) => {
if (!value) {
// 如果清空日期范围,显示所有记录
accessRecords.value = allRecords.value
return
}
// 使用字符串数组进行过滤
const [startStr, endStr] = dateStrings
// 筛选在选定时间范围内的记录
accessRecords.value = allRecords.value.filter((record: AccessRecord) => {
const recordStart = new Date(record.startTime).getTime()
const recordEnd = new Date(record.endTime).getTime()
const filterStart = new Date(startStr).getTime()
const filterEnd = new Date(endStr).getTime()
// 记录的时间范围与选择的时间范围有重叠
return !(recordEnd < filterStart || recordStart > filterEnd)
})
}
// 刷新数据的方法
const refreshData = () => {
// 模拟刷新操作
accessRecords.value = allRecords.value
console.log('刷新数据')
// 获取历史设备列表数据
async function getHistoricalDeviceList() {
loading.value = true;
try {
const { data, error } = await fetchHistoricalDevices();
if (!error && data) {
deviceList.value = data.rows.map(item => ({
id: item.id,
deviceName: item.clientName,
macAddress: item.clientMac,
dataUsage: formatDataUsage(item.duration),
connectionTime: formatTimestamp(item.startTime),
disconnectionTime: formatTimestamp(item.endTime)
}));
}
} catch (err) {
console.error('Failed to fetch historical device list:', err);
} finally {
loading.value = false;
}
}
// 确认选择时间范围
const onRangeOk = (dates: [Dayjs, Dayjs] | [string, string]) => {
console.log('确认选择时间范围: ', dates)
// 刷新设备列表
async function handleRefresh() {
await getHistoricalDeviceList();
}
// 组件挂载时获取数据
onMounted(() => {
getHistoricalDeviceList();
});
</script>
<template>
<div class="records-container">
<div class="device-list-container">
<a-card :bordered="false">
<template #title>
<div class="card-title">
@@ -137,42 +126,33 @@ const onRangeOk = (dates: [Dayjs, Dayjs] | [string, string]) => {
</template>
<template #extra>
<a-space>
<a-range-picker
style="margin-right: 8px"
:show-time="{ format: 'HH:mm' }"
format="YYYY-MM-DD HH:mm"
:placeholder="['开始时间', '结束时间']"
@change="onRangeChange"
@ok="onRangeOk"
/>
<a-button type="primary" @click="refreshData">
<template #icon>
<span class="i-carbon:refresh"></span>
</template>
{{ t('page.records.refresh') }}
</a-button>
</a-space>
<a-button type="primary" :loading="loading" @click="handleRefresh">
<template #icon>
<span class="i-carbon:refresh"></span>
</template>
{{ t('page.records.refresh') }}
</a-button>
</template>
<a-table
:loading="loading"
:columns="columns"
:data-source="accessRecords"
:scroll="{ x: 'max-content' }"
size="small"
:data-source="deviceList"
:row-key="(record: DeviceRecord) => record.id"
:expanded-row-keys="expandedRowKeys"
@expand="handleExpandChange"
:pagination="{
total: accessRecords.length,
total: deviceList.length,
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
size: 'small',
showTotal: (total) => `共 ${total} 条记录`
showSizeChanger: false,
showTotal: (total) => `${total}`
}"
>
<template #bodyCell="{ column, text }">
<template v-if="column.key === 'clientDeviceType'">
<a-tag :color="text === '手机' ? 'blue' : 'green'">{{ text }}</a-tag>
</template>
<template #expandedRowRender="{ record }">
<div class="pl-4">
<div>{{ t('page.records.starttime') }}: {{ record.connectionTime }}</div>
<div>{{ t('page.records.endtime') }}: {{ record.disconnectionTime }}</div>
</div>
</template>
</a-table>
</a-card>
@@ -180,7 +160,7 @@ const onRangeOk = (dates: [Dayjs, Dayjs] | [string, string]) => {
</template>
<style scoped>
.records-container {
.device-list-container {
padding: 8px;
background-color: #f5f5f7;
min-height: 100vh;
@@ -194,6 +174,10 @@ const onRangeOk = (dates: [Dayjs, Dayjs] | [string, string]) => {
font-weight: 500;
}
.pl-4 {
padding-left: 1rem;
}
:deep(.ant-card-head) {
border-bottom: 1px solid #f0f0f0;
padding: 0 12px;
@@ -208,89 +192,11 @@ const onRangeOk = (dates: [Dayjs, Dayjs] | [string, string]) => {
}
:deep(.ant-table) {
font-size: 14px;
}
:deep(.wrap-cell) {
white-space: normal;
word-break: break-word;
line-height: 1.2;
}
@media screen and (max-width: 768px) {
.records-container {
padding: 4px;
}
:deep(.ant-card-head-wrapper) {
flex-direction: column;
align-items: flex-start;
}
:deep(.ant-card-extra) {
margin-left: 0;
margin-top: 8px;
width: 100%;
}
:deep(.ant-picker) {
width: 100%;
}
:deep(.ant-space) {
width: 100%;
flex-direction: column;
}
:deep(.ant-space-item) {
width: 100%;
margin-right: 0 !important;
}
:deep(.ant-btn) {
width: 100%;
}
:deep(.ant-table) {
font-size: 12px;
}
:deep(.ant-table th),
:deep(.ant-table td:not(.wrap-cell)) {
padding: 8px 4px !important;
white-space: nowrap;
}
:deep(.wrap-cell) {
padding: 4px !important;
}
:deep(.ant-tag) {
margin-right: 0;
padding: 0 4px;
}
:deep(.ant-picker-panels) {
flex-direction: column;
}
:deep(.ant-picker-panel) {
width: auto !important;
}
:deep(.ant-picker-datetime-panel) {
display: flex;
flex-direction: column;
}
:deep(.ant-picker-time-panel) {
width: auto !important;
border-left: none !important;
}
width: 100%;
}
@media screen and (min-width: 768px) {
.records-container {
.device-list-container {
padding: 16px;
}
}