2
0

fix:仪表盘界面(未接通)

This commit is contained in:
zhongzm
2025-01-10 14:28:59 +08:00
parent 4708df2a6f
commit 28200f4931
7 changed files with 506 additions and 122 deletions

View File

@@ -202,6 +202,14 @@ export function updateBillRule(data: Api.Billing.BillRuleUpdate) {
data data
}); });
} }
/** 获取仪表盘概览数据 */
export function getDashboardOverview() {
return request<Api.DashboardOverview>({
url: '/system/dashboard/overview',
method: 'get'
});
}

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

@@ -607,4 +607,45 @@ declare namespace Api {
enable: boolean; enable: boolean;
} }
} }
interface DashboardOverview {
cloud: {
connected: boolean;
};
sites: {
total: number;
countries: number;
connected: number;
disconnected: number;
};
devices: {
gateways: {
connected: number;
disconnected: number;
};
switches: {
connected: number;
disconnected: number;
};
olts: {
connected: number;
disconnected: number;
};
ap: {
connected: number;
disconnected: number;
isolated: number;
};
};
client: {
wiredUsers: number;
wirelessUsers: number;
wirelessGuests: number;
};
alerts: number;
users: {
registered: number;
online: number;
};
}
} }

View File

@@ -152,6 +152,7 @@ declare global {
const getCacheRouteNames: typeof import('../store/modules/route/shared')['getCacheRouteNames'] const getCacheRouteNames: typeof import('../store/modules/route/shared')['getCacheRouteNames']
const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope'] const getCurrentScope: typeof import('vue')['getCurrentScope']
const getDashboardOverview: typeof import('../service/api/auth')['getDashboardOverview']
const getData: typeof import('../service/api/dictData')['getData'] const getData: typeof import('../service/api/dictData')['getData']
const getDefaultHomeTab: typeof import('../store/modules/tab/shared')['getDefaultHomeTab'] const getDefaultHomeTab: typeof import('../store/modules/tab/shared')['getDefaultHomeTab']
const getDictDataType: typeof import('../service/api/dict')['getDictDataType'] const getDictDataType: typeof import('../service/api/dict')['getDictDataType']

View File

@@ -45,7 +45,6 @@ declare module 'vue' {
ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASpace: typeof import('ant-design-vue/es')['Space'] ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin'] ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic']
AStep: typeof import('ant-design-vue/es')['Step'] AStep: typeof import('ant-design-vue/es')['Step']
ASteps: typeof import('ant-design-vue/es')['Steps'] ASteps: typeof import('ant-design-vue/es')['Steps']
ASwitch: typeof import('ant-design-vue/es')['Switch'] ASwitch: typeof import('ant-design-vue/es')['Switch']
@@ -77,7 +76,11 @@ declare module 'vue' {
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default'] IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default'] IconLocalBanner: typeof import('~icons/local/banner')['default']
IconLocalLogo: typeof import('~icons/local/logo')['default'] IconLocalLogo: typeof import('~icons/local/logo')['default']
IconMdiAccessPoint: typeof import('~icons/mdi/access-point')['default']
IconMdiAlert: typeof import('~icons/mdi/alert')['default']
IconMdiCloud: typeof import('~icons/mdi/cloud')['default']
IconMdiDrag: typeof import('~icons/mdi/drag')['default'] IconMdiDrag: typeof import('~icons/mdi/drag')['default']
IconMdiLaptop: typeof import('~icons/mdi/laptop')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
IconMdiSearch: typeof import('~icons/mdi/search')['default'] IconMdiSearch: typeof import('~icons/mdi/search')['default']
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default'] LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']

View File

@@ -1,109 +1,231 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed, ref } from 'vue';
import { createReusableTemplate } from '@vueuse/core'; import {
import { $t } from '@/locales'; EnvironmentOutlined,
EditOutlined,
CopyOutlined,
DeleteOutlined,
HomeOutlined,
SearchOutlined,
PlusOutlined
} from '@ant-design/icons-vue';
defineOptions({ defineOptions({
name: 'CardData' name: 'CardData'
}); });
interface CardData { interface SiteData {
key: string; id: number;
title: string; name: string;
value: number; region: string;
unit: string; alerts: number;
color: { gateway: string;
start: string; switches: string;
end: string; olts: string;
}; eaps: string;
icon: string;
} }
const cardData = computed<CardData[]>(() => [ // 搜索和分页状态
const searchValue = ref('');
const currentPage = ref(1);
const pageSize = ref(10);
// 表格列定义
const columns = [
{ {
key: 'visitCount', title: 'NAME',
title: $t('page.home.visitCount'), key: 'name',
value: 9725, dataIndex: 'name'
unit: '',
color: {
start: '#ec4786',
end: '#b955a4'
},
icon: 'ant-design:bar-chart-outlined'
}, },
{ {
key: 'turnover', title: 'COUNTRY/REGION',
title: $t('page.home.turnover'), key: 'region',
value: 1026, dataIndex: 'region'
unit: '$',
color: {
start: '#865ec0',
end: '#5144b4'
},
icon: 'ant-design:money-collect-outlined'
}, },
{ {
key: 'downloadCount', title: 'ALERTS',
title: $t('page.home.downloadCount'), key: 'alerts',
value: 970925, dataIndex: 'alerts',
unit: '', width: 100
color: {
start: '#56cdf3',
end: '#719de3'
},
icon: 'carbon:document-download'
}, },
{ {
key: 'dealCount', title: 'GATEWAY',
title: $t('page.home.dealCount'), key: 'gateway',
value: 9527, dataIndex: 'gateway',
unit: '', width: 100
color: {
start: '#fcbc25',
end: '#f68057'
}, },
icon: 'ant-design:trademark-circle-outlined' {
title: 'SWITCHES',
key: 'switches',
dataIndex: 'switches',
width: 100
},
{
title: 'OLTS',
key: 'olts',
dataIndex: 'olts',
width: 100
},
{
title: 'EAPS',
key: 'eaps',
dataIndex: 'eaps',
width: 100
},
{
title: 'ACTION',
key: 'action',
width: 150,
fixed: 'right'
}
];
const siteData = computed<SiteData[]>(() => [
{
id: 1,
name: 'wfc-dev-omada1-site1',
region: 'China mainland',
alerts: 0,
gateway: '-',
switches: '0 / 0',
olts: '0 / 0',
eaps: '1 / 1'
},
{
id: 2,
name: 'wfc-dev-omada1-site2',
region: 'China mainland',
alerts: 0,
gateway: '-',
switches: '0 / 0',
olts: '0 / 0',
eaps: '0 / 1'
} }
]); ]);
interface GradientBgProps { // 按钮操作处理函数
gradientColor: string; const handleEdit = (record: SiteData) => {
} console.log('Edit:', record);
};
const [DefineGradientBg, GradientBg] = createReusableTemplate<GradientBgProps>(); const handleCopy = (record: SiteData) => {
console.log('Copy:', record);
};
function getGradientColor(color: CardData['color']) { const handleDelete = (record: SiteData) => {
return `linear-gradient(to bottom right, ${color.start}, ${color.end})`; console.log('Delete:', record);
} };
const handleHome = (record: SiteData) => {
console.log('Home:', record);
};
// 分页处理函数
// const handlePageChange = (page: number) => {
// currentPage.value = page;
// };
// const handlePageSizeChange = (size: number) => {
// pageSize.value = size;
// currentPage.value = 1;
// };
// const handleGoToPage = () => {
// // 处理跳转页面逻辑
// console.log('Go to page:', currentPage.value);
// };
</script> </script>
<template> <template>
<ACard :bordered="false" size="small" class="card-wrapper"> <ACard :bordered="false" size="small" class="card-wrapper">
<!-- define component start: GradientBg --> <div class="flex justify-between items-center mb-16px">
<DefineGradientBg v-slot="{ $slots, gradientColor }"> <div class="flex items-center gap-8px">
<div class="rd-8px px-16px pb-4px pt-8px text-white" :style="{ backgroundImage: gradientColor }"> <span class="text-16px font-medium">Site List</span>
<component :is="$slots.default" /> <!-- <span class="text-12px text-gray-500">1-2 of 2 records</span>-->
</div>
<div class="flex items-center gap-16px">
<AInput
v-model:value="searchValue"
placeholder="Search Site Name"
class="w-240px"
allow-clear
>
<template #prefix>
<search-outlined />
</template>
</AInput>
<AButton type="primary">
<template #icon>
<plus-outlined />
</template>
Import Site
</AButton>
<AButton type="primary">
<template #icon>
<plus-outlined />
</template>
Add New Site
</AButton>
</div>
</div> </div>
</DefineGradientBg>
<!-- define component end: GradientBg -->
<ARow :gutter="[16, 16]"> <ATable
<ACol v-for="item in cardData" :key="item.key" :span="24" :md="12" :lg="6"> :columns="columns"
<GradientBg :gradient-color="getGradientColor(item.color)" class="flex-1"> :data-source="siteData"
<h3 class="text-16px">{{ item.title }}</h3> :pagination="false"
<div class="flex justify-between pt-12px"> :scroll="{ x: 1200 }"
<SvgIcon :icon="item.icon" class="text-32px" /> row-key="id"
<CountTo >
:prefix="item.unit" <template #bodyCell="{ column, record }">
:start-value="1" <template v-if="column.key === 'name'">
:end-value="item.value" <div class="flex items-center gap-8px">
class="text-30px text-white dark:text-dark" <environment-outlined class="text-16px" />
/> <span>{{ record.name }}</span>
</div>
</template>
<template v-else-if="column.key === 'alerts'">
<ATag v-if="record.alerts > 0" color="error">{{ record.alerts }}</ATag>
<span v-else>{{ record.alerts }}</span>
</template>
<template v-else-if="column.key === 'action'">
<div class="flex items-center gap-8px">
<edit-outlined class="cursor-pointer text-primary" @click="handleEdit(record)" />
<copy-outlined class="cursor-pointer text-primary" @click="handleCopy(record)" />
<delete-outlined class="cursor-pointer text-red-500" @click="handleDelete(record)" />
<home-outlined class="cursor-pointer text-primary" @click="handleHome(record)" />
</div>
</template>
</template>
</ATable>
<div class="flex justify-between items-center mt-16px">
<div class="text-12px text-gray-500">
Showing 1-2 of 2 records
</div>
<div class="flex items-center gap-8px">
<ASelect v-model:value="pageSize" style="width: 100px">
<ASelectOption :value="10">10 / page</ASelectOption>
<ASelectOption :value="20">20 / page</ASelectOption>
<ASelectOption :value="50">50 / page</ASelectOption>
</ASelect>
<span class="text-12px text-gray-500">Go to page:</span>
<AInput v-model:value="currentPage" style="width: 50px" />
<AButton>Go</AButton>
</div>
</div> </div>
</GradientBg>
</ACol>
</ARow>
</ACard> </ACard>
</template> </template>
<style scoped></style> <style scoped>
.card-wrapper {
margin-bottom: 16px;
}
.cursor-pointer {
cursor: pointer;
}
.w-240px {
width: 240px;
}
</style>

View File

@@ -1,62 +1,263 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import {
import { $t } from '@/locales'; EnvironmentOutlined,
import { useAuthStore } from '@/store/modules/auth'; SafetyOutlined,
HddOutlined,
GroupOutlined,
UserAddOutlined,
UserSwitchOutlined
} from '@ant-design/icons-vue';
defineOptions({ defineOptions({
name: 'HeaderBanner' name: 'HeaderBanner'
}); });
const authStore = useAuthStore(); const overviewData = ref<Api.DashboardOverview>({
cloud: { connected: false },
interface StatisticData { sites: { total: 0, countries: 0, connected: 0, disconnected: 0 },
id: number; devices: {
title: string; gateways: { connected: 0, disconnected: 0 },
value: string; switches: { connected: 0, disconnected: 0 },
} olts: { connected: 0, disconnected: 0 },
ap: { connected: 0, disconnected: 0, isolated: 0 }
const statisticData = computed<StatisticData[]>(() => [
{
id: 0,
title: $t('page.home.projectCount'),
value: '25'
}, },
{ client: { wiredUsers: 0, wirelessUsers: 0, wirelessGuests: 0 },
id: 1, alerts: 0,
title: $t('page.home.todo'), users: { registered: 0, online: 0 }
value: '4/16' });
},
{ const fetchOverviewData = async () => {
id: 2, // try {
title: $t('page.home.message'), // const data = await authStore.getDashboardOverview();
value: '12' // overviewData.value = data;
} // } catch (error) {
]); // console.error('Failed to fetch overview data:', error);
// }
};
onMounted(() => {
fetchOverviewData();
});
const deviceStatus = computed(() => ({
ap: overviewData.value.devices.ap,
client: overviewData.value.client,
alerts: overviewData.value.alerts,
users: overviewData.value.users
}));
const siteInfo = computed(() => overviewData.value.sites);
const otherDevices = computed(() => ({
gateways: overviewData.value.devices.gateways,
switches: overviewData.value.devices.switches,
olts: overviewData.value.devices.olts
}));
</script> </script>
<template> <template>
<ACard :bordered="false" class="card-wrapper"> <ACard :bordered="false" class="card-wrapper">
<div class="text-16px font-bold mb-4px">Controller Overview</div>
<ARow :gutter="[16, 16]"> <ARow :gutter="[16, 16]">
<ACol :span="24" :md="18"> <ACol :span="24">
<div class="flex-y-center"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-8px">
<div class="size-72px shrink-0 overflow-hidden rd-1/2"> <!-- Connected Status -->
<img src="@/assets/imgs/soybean.jpg" class="size-full" /> <div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-blue-50 rounded-lg">
<icon-mdi-cloud class="text-primary text-24px" />
</div>
<div class="text-16px font-medium">Connected</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="text-12px text-gray-500">
Cloud Access
</div>
</div>
<!-- Sites -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-gray-100 rounded-lg relative">
<environment-outlined class="text-24px text-primary" />
<span class="text-20px font-semibold absolute -right-2 -top-2 bg-primary text-white rounded-full w-6 h-6 flex-center">{{ siteInfo.total }}</span>
</div>
<div class="text-16px font-medium">Sites</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>Total Sites: {{ siteInfo.total }}</span>
<span>Countries: {{ siteInfo.countries }}</span>
<span>Connected: {{ siteInfo.connected }}</span>
<span>Disconnected: {{ siteInfo.disconnected }}</span>
</div>
</div>
<!-- Gateways -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-gray-100 rounded-lg relative">
<safety-outlined class="text-24px text-primary" />
<span class="text-20px font-semibold absolute -right-2 -top-2 bg-primary text-white rounded-full w-6 h-6 flex-center">{{ otherDevices.gateways.connected + otherDevices.gateways.disconnected }}</span>
</div>
<div class="text-16px font-medium">Gateways</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>Connected: {{ otherDevices.gateways.connected }}</span>
<span>Disconnected: {{ otherDevices.gateways.disconnected }}</span>
</div>
</div>
<!-- Switches -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-gray-100 rounded-lg relative">
<hdd-outlined class="text-24px text-primary" />
<span class="text-20px font-semibold absolute -right-2 -top-2 bg-primary text-white rounded-full w-6 h-6 flex-center">{{ otherDevices.switches.connected + otherDevices.switches.disconnected }}</span>
</div>
<div class="text-16px font-medium">Switches</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>Connected: {{ otherDevices.switches.connected }}</span>
<span>Disconnected: {{ otherDevices.switches.disconnected }}</span>
</div>
</div>
<!-- OLTs -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-gray-100 rounded-lg relative">
<group-outlined class="text-24px text-primary" />
<span class="text-20px font-semibold absolute -right-2 -top-2 bg-primary text-white rounded-full w-6 h-6 flex-center">{{ otherDevices.olts.connected + otherDevices.olts.disconnected }}</span>
</div>
<div class="text-16px font-medium">OLTs</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>Connected: {{ otherDevices.olts.connected }}</span>
<span>Disconnected: {{ otherDevices.olts.disconnected }}</span>
</div>
</div>
<!-- Register Users -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-blue-50 rounded-lg">
<user-add-outlined class="text-primary text-24px" />
</div>
<div class="text-16px font-medium">Register Users</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>Total: {{ deviceStatus.users.registered }}</span>
<span>Online: {{ deviceStatus.users.online }}</span>
</div>
</div>
<!-- Online Users -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-green-50 rounded-lg">
<user-switch-outlined class="text-primary text-24px" />
</div>
<div class="text-16px font-medium">Online Users</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>Active: {{ deviceStatus.users.online }}</span>
<span>Total: {{ deviceStatus.users.registered }}</span>
</div>
</div>
<!-- AP Status -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-green-50 rounded-lg">
<icon-mdi-access-point class="text-primary text-24px" />
</div>
<div class="text-16px font-medium">AP</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>Connected: {{ deviceStatus.ap.connected }}</span>
<span>Disconnected: {{ deviceStatus.ap.disconnected }}</span>
<span>Isolated: {{ deviceStatus.ap.isolated }}</span>
</div>
</div>
<!-- Client Status -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-purple-50 rounded-lg">
<icon-mdi-laptop class="text-primary text-24px" />
</div>
<div class="text-16px font-medium">Client</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>Wired Users: {{ deviceStatus.client.wiredUsers }}</span>
<span>Wireless Users: {{ deviceStatus.client.wirelessUsers }}</span>
<span>Wireless Guests: {{ deviceStatus.client.wirelessGuests }}</span>
</div>
</div>
<!-- Alerts -->
<div class="flex flex-col p-6px">
<div class="flex items-center gap-6px mb-6px">
<div class="size-48px flex-center bg-yellow-50 rounded-lg">
<icon-mdi-alert class="text-warning text-24px" />
</div>
<div class="text-16px font-medium">Alerts</div>
</div>
<div class="border-t border-gray-100 my-4px"></div>
<div class="flex flex-col text-12px text-gray-500">
<span>Total: {{ deviceStatus.alerts }}</span>
<span>Active: {{ deviceStatus.alerts }}</span>
</div> </div>
<div class="pl-12px">
<h3 class="text-18px font-semibold">
{{ $t('page.home.greeting', { username: authStore.userInfo.username }) }}
</h3>
<p class="text-#999 leading-30px">{{ $t('page.home.weatherDesc') }}</p>
</div> </div>
</div> </div>
</ACol>
<ACol :span="24" :md="6">
<ASpace class="w-full justify-end" :size="24">
<AStatistic v-for="item in statisticData" :key="item.id" class="whitespace-nowrap" v-bind="item" />
</ASpace>
</ACol> </ACol>
</ARow> </ARow>
</ACard> </ACard>
</template> </template>
<style scoped></style> <style scoped>
.card-wrapper {
margin-bottom: 16px;
padding: 12px;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.size-48px {
width: 48px;
height: 48px;
position: relative;
}
.flex-col > span {
line-height: 1.5;
}
.bg-gray-50 {
background-color: #fafafa;
}
.p-6px {
padding: 6px;
}
.mt-8px {
margin-top: 8px;
}
.mb-6px {
margin-bottom: 6px;
}
</style>

View File

@@ -0,0 +1,8 @@
<script setup lang="ts">
</script>
<template>
<div>仪表盘面板</div>
</template>
<style scoped></style>