feat: 添加概览页并更新默认首页路径,隐藏无用页面和功能
- 新增概览页 (`/overview`) 相关组件和页面文件,包括 KPI 指标展示、设备流量图表、 设备类型占比饼图、设备接入步骤引导和运维管理步骤引导。 - 更新路由配置,将默认首页重定向路径从 `/analytics` 改为 `/overview`。 - 注释掉部分原有菜单项与路由配置(如文档、Gitee 地址、Vben 官方地址等)。 - 设置 `defaultHomePath` 为 `/overview`,确保应用启动后默认进入概览页。
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 1.3 KiB |
|
@ -1,17 +1,10 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, h, onMounted, watch } from 'vue';
|
import { computed, onMounted, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
|
||||||
import { useWatermark } from '@vben/hooks';
|
import { useWatermark } from '@vben/hooks';
|
||||||
import {
|
import { UserOutlined } from '@vben/icons';
|
||||||
BookOpenText,
|
|
||||||
CircleHelp,
|
|
||||||
GiteeIcon,
|
|
||||||
GitHubOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
} from '@vben/icons';
|
|
||||||
import {
|
import {
|
||||||
BasicLayout,
|
BasicLayout,
|
||||||
LockScreen,
|
LockScreen,
|
||||||
|
@ -20,7 +13,6 @@ import {
|
||||||
} from '@vben/layouts';
|
} from '@vben/layouts';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
import { openWindow } from '@vben/utils';
|
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
@ -42,15 +34,15 @@ const { destroyWatermark, updateWatermark } = useWatermark();
|
||||||
const tenantStore = useTenantStore();
|
const tenantStore = useTenantStore();
|
||||||
const menus = computed(() => {
|
const menus = computed(() => {
|
||||||
const defaultMenus = [
|
const defaultMenus = [
|
||||||
{
|
// {
|
||||||
handler: () => {
|
// handler: () => {
|
||||||
openWindow(VBEN_DOC_URL, {
|
// openWindow(VBEN_DOC_URL, {
|
||||||
target: '_blank',
|
// target: '_blank',
|
||||||
});
|
// });
|
||||||
},
|
// },
|
||||||
icon: BookOpenText,
|
// icon: BookOpenText,
|
||||||
text: $t('ui.widgets.document'),
|
// text: $t('ui.widgets.document'),
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
handler: () => {
|
handler: () => {
|
||||||
router.push('/profile');
|
router.push('/profile');
|
||||||
|
@ -58,33 +50,33 @@ const menus = computed(() => {
|
||||||
icon: UserOutlined,
|
icon: UserOutlined,
|
||||||
text: $t('ui.widgets.profile'),
|
text: $t('ui.widgets.profile'),
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
handler: () => {
|
// handler: () => {
|
||||||
openWindow('https://gitee.com/dapppp/ruoyi-plus-vben5', {
|
// openWindow('https://gitee.com/dapppp/ruoyi-plus-vben5', {
|
||||||
target: '_blank',
|
// target: '_blank',
|
||||||
});
|
// });
|
||||||
},
|
// },
|
||||||
icon: () => h(GiteeIcon, { class: 'text-red-800' }),
|
// icon: () => h(GiteeIcon, { class: 'text-red-800' }),
|
||||||
text: 'Gitee项目地址',
|
// text: 'Gitee项目地址',
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
handler: () => {
|
// handler: () => {
|
||||||
openWindow(VBEN_GITHUB_URL, {
|
// openWindow(VBEN_GITHUB_URL, {
|
||||||
target: '_blank',
|
// target: '_blank',
|
||||||
});
|
// });
|
||||||
},
|
// },
|
||||||
icon: GitHubOutlined,
|
// icon: GitHubOutlined,
|
||||||
text: 'Vben官方地址',
|
// text: 'Vben官方地址',
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
handler: () => {
|
// handler: () => {
|
||||||
openWindow(`${VBEN_GITHUB_URL}/issues`, {
|
// openWindow(`${VBEN_GITHUB_URL}/issues`, {
|
||||||
target: '_blank',
|
// target: '_blank',
|
||||||
});
|
// });
|
||||||
},
|
// },
|
||||||
icon: CircleHelp,
|
// icon: CircleHelp,
|
||||||
text: $t('ui.widgets.qa'),
|
// text: $t('ui.widgets.qa'),
|
||||||
},
|
// },
|
||||||
];
|
];
|
||||||
/**
|
/**
|
||||||
* 租户选中状态 不显示个人中心
|
* 租户选中状态 不显示个人中心
|
||||||
|
|
|
@ -34,6 +34,7 @@ export const overridesPreferences = defineOverridesPreferences({
|
||||||
enableCheckUpdates: false,
|
enableCheckUpdates: false,
|
||||||
// 检查更新的时间间隔,单位为分钟
|
// 检查更新的时间间隔,单位为分钟
|
||||||
checkUpdatesInterval: 1,
|
checkUpdatesInterval: 1,
|
||||||
|
defaultHomePath: '/overview',
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,10 +2,10 @@ import type { RouteRecordStringComponent } from '@vben/types';
|
||||||
|
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
const {
|
// const {
|
||||||
version,
|
// version,
|
||||||
// vite inject-metadata 插件注入的全局变量
|
// // vite inject-metadata 插件注入的全局变量
|
||||||
} = __VBEN_ADMIN_METADATA__ || {};
|
// } = __VBEN_ADMIN_METADATA__ || {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 该文件放非后台返回的路由 比如个人中心 等需要跳转显示的页面
|
* 该文件放非后台返回的路由 比如个人中心 等需要跳转显示的页面
|
||||||
|
@ -39,59 +39,68 @@ export const localMenuList: RouteRecordStringComponent[] = [
|
||||||
},
|
},
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/analytics',
|
redirect: '/overview',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'Analytics',
|
name: 'Overview',
|
||||||
path: '/analytics',
|
path: '/overview',
|
||||||
component: '/dashboard/analytics/index',
|
component: '/dashboard/overview/index',
|
||||||
meta: {
|
meta: {
|
||||||
affixTab: true,
|
affixTab: true,
|
||||||
title: 'page.dashboard.analytics',
|
title: '概览页',
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Workspace',
|
|
||||||
path: '/workspace',
|
|
||||||
component: '/dashboard/workspace/index',
|
|
||||||
meta: {
|
|
||||||
title: 'page.dashboard.workspace',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'VbenDocument',
|
|
||||||
path: '/vben-admin/document',
|
|
||||||
component: 'IFrameView',
|
|
||||||
meta: {
|
|
||||||
icon: 'lucide:book-open-text',
|
|
||||||
iframeSrc: 'https://dapdap.top',
|
|
||||||
keepAlive: true,
|
|
||||||
title: $t('demos.vben.document'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'V5UpdateLog',
|
|
||||||
path: '/changelog',
|
|
||||||
component: '/演示使用自行删除/changelog/index',
|
|
||||||
meta: {
|
|
||||||
icon: 'lucide:book-open-text',
|
|
||||||
keepAlive: true,
|
|
||||||
title: '更新记录',
|
|
||||||
badge: `当前: ${version}`,
|
|
||||||
badgeVariants: 'bg-primary',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// name: 'Analytics',
|
||||||
|
// path: '/analytics',
|
||||||
|
// component: '/dashboard/analytics/index',
|
||||||
|
// meta: {
|
||||||
|
// affixTab: true,
|
||||||
|
// title: 'page.dashboard.analytics',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Workspace',
|
||||||
|
// path: '/workspace',
|
||||||
|
// component: '/dashboard/workspace/index',
|
||||||
|
// meta: {
|
||||||
|
// title: 'page.dashboard.workspace',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'VbenDocument',
|
||||||
|
// path: '/vben-admin/document',
|
||||||
|
// component: 'IFrameView',
|
||||||
|
// meta: {
|
||||||
|
// icon: 'lucide:book-open-text',
|
||||||
|
// iframeSrc: 'https://dapdap.top',
|
||||||
|
// keepAlive: true,
|
||||||
|
// title: $t('demos.vben.document'),
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'V5UpdateLog',
|
||||||
|
// path: '/changelog',
|
||||||
|
// component: '/演示使用自行删除/changelog/index',
|
||||||
|
// meta: {
|
||||||
|
// icon: 'lucide:book-open-text',
|
||||||
|
// keepAlive: true,
|
||||||
|
// title: '更新记录',
|
||||||
|
// badge: `当前: ${version}`,
|
||||||
|
// badgeVariants: 'bg-primary',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
component: '/_core/about/index',
|
// component: '/_core/about/index',
|
||||||
meta: {
|
// meta: {
|
||||||
icon: 'lucide:copyright',
|
// icon: 'lucide:copyright',
|
||||||
order: 9999,
|
// order: 9999,
|
||||||
title: $t('demos.vben.about'),
|
// title: $t('demos.vben.about'),
|
||||||
},
|
// },
|
||||||
name: 'About',
|
// name: 'About',
|
||||||
path: '/vben-admin/about',
|
// path: '/vben-admin/about',
|
||||||
},
|
// },
|
||||||
...localRoutes,
|
...localRoutes,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { TabOption } from '@vben/types';
|
||||||
|
|
||||||
|
import { AnalysisChartCard, AnalysisChartsTabs } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import OverviewAccessSource from './overview-access-source.vue';
|
||||||
|
import OverviewDeviceSteps from './overview-device-steps.vue';
|
||||||
|
import OverviewHeader from './overview-header.vue';
|
||||||
|
import OverviewOpsSteps from './overview-ops-steps.vue';
|
||||||
|
import OverviewTrafficRevenue from './overview-traffic-revenue.vue';
|
||||||
|
|
||||||
|
const overviewItems: any[] = [
|
||||||
|
{
|
||||||
|
imgUrl: './images/dashboard/product.png',
|
||||||
|
title: '产品数',
|
||||||
|
totalTitle: '未启用数',
|
||||||
|
totalValue: 10,
|
||||||
|
suffix: '',
|
||||||
|
value: 125,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
imgUrl: './images/dashboard/device.png',
|
||||||
|
title: '设备数',
|
||||||
|
totalTitle: '未激活数',
|
||||||
|
totalValue: 12,
|
||||||
|
suffix: '',
|
||||||
|
value: 1024,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
imgUrl: './images/dashboard/online.png',
|
||||||
|
title: '在线数',
|
||||||
|
totalTitle: '在线率',
|
||||||
|
totalValue: 85,
|
||||||
|
suffix: '%',
|
||||||
|
value: 1020,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
imgUrl: './images/dashboard/alarm.png',
|
||||||
|
title: '告警总数',
|
||||||
|
totalTitle: '今日告警',
|
||||||
|
totalValue: 0,
|
||||||
|
value: 4,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const chartTabs: TabOption[] = [
|
||||||
|
{
|
||||||
|
label: '近7天',
|
||||||
|
value: 'week',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '近一个月',
|
||||||
|
value: 'month',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '半年',
|
||||||
|
value: 'halfyear',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-5">
|
||||||
|
<!-- 顶部 KPI 指标 -->
|
||||||
|
<OverviewHeader :items="overviewItems" />
|
||||||
|
|
||||||
|
<!-- 中间图表区域 -->
|
||||||
|
<div class="mt-4 w-full md:flex">
|
||||||
|
<!-- 设备流量 -->
|
||||||
|
<!-- <AnalysisChartCard class="mt-4 md:mr-4 md:mt-0 md:w-2/3" title="设备流量"> -->
|
||||||
|
<AnalysisChartsTabs
|
||||||
|
:tabs="chartTabs"
|
||||||
|
class="device-flow mt-4 md:mr-4 md:mt-0 md:w-2/3"
|
||||||
|
>
|
||||||
|
<template #week>
|
||||||
|
<OverviewTrafficRevenue type="week" />
|
||||||
|
</template>
|
||||||
|
<template #month>
|
||||||
|
<OverviewTrafficRevenue type="month" />
|
||||||
|
</template>
|
||||||
|
<template #halfyear>
|
||||||
|
<OverviewTrafficRevenue type="halfyear" />
|
||||||
|
</template>
|
||||||
|
</AnalysisChartsTabs>
|
||||||
|
<!-- </AnalysisChartCard> -->
|
||||||
|
|
||||||
|
<!-- 访问源情况 -->
|
||||||
|
<AnalysisChartCard class="mt-4 md:mt-0 md:w-1/3" title="设备类型占比">
|
||||||
|
<OverviewAccessSource />
|
||||||
|
</AnalysisChartCard>
|
||||||
|
</div>
|
||||||
|
<AnalysisChartCard class="mt-4" title="设备接入步骤">
|
||||||
|
<OverviewDeviceSteps />
|
||||||
|
</AnalysisChartCard>
|
||||||
|
<AnalysisChartCard class="mt-4" title="运维管理步骤">
|
||||||
|
<OverviewOpsSteps />
|
||||||
|
</AnalysisChartCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.device-flow) {
|
||||||
|
/* border: none !important; */
|
||||||
|
|
||||||
|
/* border-width: 0 !important; */
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.device-flow > div) {
|
||||||
|
position: relative;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.device-flow > div > div:first-child) {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.device-flow > div > div.ring-offset-background) {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
renderEcharts({
|
||||||
|
legend: {
|
||||||
|
bottom: '5%',
|
||||||
|
left: 'center',
|
||||||
|
itemGap: 20,
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
animationDelay() {
|
||||||
|
return Math.random() * 100;
|
||||||
|
},
|
||||||
|
animationEasing: 'exponentialInOut',
|
||||||
|
animationType: 'scale',
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9', '#ffb980'],
|
||||||
|
data: [
|
||||||
|
{ name: '直连设备', value: 35 },
|
||||||
|
{ name: '网关子设备', value: 225 },
|
||||||
|
{ name: '网关设备', value: 20 },
|
||||||
|
],
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
fontSize: '12',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 8,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#fff',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
position: 'center',
|
||||||
|
show: true,
|
||||||
|
formatter: '设备类型占比',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
name: '设备类型占比',
|
||||||
|
radius: ['40%', '65%'],
|
||||||
|
center: ['50%', '45%'],
|
||||||
|
type: 'pie',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: '{a}: {b} <br/>数量: {c} 台 (占比: {d}%)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EchartsUI ref="chartRef" style="height: 340px" />
|
||||||
|
</template>
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
path: '/device/product',
|
||||||
|
imgUrl: './images/dashboard/device-access1.png',
|
||||||
|
title: '创建产品',
|
||||||
|
description:
|
||||||
|
'产品是设备的集合,通常指一组具有相同功能的设备。物联设备必须通过产品进行接入方式配置。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/device/product',
|
||||||
|
imgUrl: './images/dashboard/device-access2.png',
|
||||||
|
title: '配置产品接入方式',
|
||||||
|
description:
|
||||||
|
'通过产品对同一类型的设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/device/device',
|
||||||
|
imgUrl: './images/dashboard/device-access3.png',
|
||||||
|
title: '添加测试设备',
|
||||||
|
description: '添加单个设备,用于验证产品模型是否配置正确。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/device/device',
|
||||||
|
imgUrl: './images/dashboard/device-access4.png',
|
||||||
|
title: '功能调试',
|
||||||
|
description:
|
||||||
|
'对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/device/device',
|
||||||
|
imgUrl: './images/dashboard/device-access5.png',
|
||||||
|
title: '批量添加设备',
|
||||||
|
description: '批量添加同一产品下的设备',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const goPath = (path) => {
|
||||||
|
if (path) {
|
||||||
|
router.push(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="device-steps-card">
|
||||||
|
<div class="steps-container">
|
||||||
|
<div
|
||||||
|
v-for="(step, index) in steps"
|
||||||
|
:key="index"
|
||||||
|
class="step-item"
|
||||||
|
:class="index < steps.length - 1 ? 'flex-[2]' : 'flex-1'"
|
||||||
|
>
|
||||||
|
<Tooltip :title="step.description" color="#0F46B2">
|
||||||
|
<div class="step-box" @click="goPath(step.path)">
|
||||||
|
<div class="step-img">
|
||||||
|
<img :src="step.imgUrl" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="step-title">{{ step.title }}</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div v-if="index < steps.length - 1" class="step-arrow">
|
||||||
|
<img src="/images/dashboard/arrow.png" alt="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.device-steps-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.steps-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.step-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
.step-box {
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,46 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { VbenCountToAnimator } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Card } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'OverviewHeader',
|
||||||
|
});
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
items: () => [],
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<template v-for="item in items" :key="item.title">
|
||||||
|
<Card class="w-full">
|
||||||
|
<div class="flex items-center justify-between font-semibold">
|
||||||
|
<div class="text-foreground text-xl">{{ item.title }}</div>
|
||||||
|
<div class="rounded-md bg-gray-100/50 px-2 py-1 text-sm">
|
||||||
|
<span class="mr-1 text-gray-500/90">{{ item.totalTitle }}</span>
|
||||||
|
<VbenCountToAnimator
|
||||||
|
:end-val="item.totalValue"
|
||||||
|
:start-val="0"
|
||||||
|
:suffix="item.suffix"
|
||||||
|
class="text-red-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<VbenCountToAnimator
|
||||||
|
:end-val="item.value"
|
||||||
|
:start-val="0"
|
||||||
|
class="text-foreground text-2xl font-semibold"
|
||||||
|
/>
|
||||||
|
<img :src="item.imgUrl" class="mb-2 size-14 flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,105 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
path: '/operations/protocol',
|
||||||
|
imgUrl: './images/dashboard/operations1.png',
|
||||||
|
title: '协议管理',
|
||||||
|
description:
|
||||||
|
'根据业务需求自定义开发对应的产品(设备模型)接入协议,并上传到平台。',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// imgUrl: './images/dashboard/operations2.png',
|
||||||
|
// title: '证书管理',
|
||||||
|
// description: '统一维护平台内的证书,用于数据通信加密。',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
path: '/operations/network',
|
||||||
|
imgUrl: './images/dashboard/operations3.png',
|
||||||
|
title: '网络组件',
|
||||||
|
description: '根据不同的传输类型配置平台底层网络组件相关参数。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/operations/gateway',
|
||||||
|
imgUrl: './images/dashboard/operations4.png',
|
||||||
|
title: '设备接入网关',
|
||||||
|
description: '根据不同的传输类型,关联消息协议,配置设备接入网关相关参数。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
imgUrl: './images/dashboard/operations5.png',
|
||||||
|
title: '日志管理',
|
||||||
|
description: '监控系统日志,及时处理系统异常。',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const goPath = (path) => {
|
||||||
|
if (path) {
|
||||||
|
router.push(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="device-steps-card">
|
||||||
|
<div class="steps-container">
|
||||||
|
<div
|
||||||
|
v-for="(step, index) in steps"
|
||||||
|
:key="index"
|
||||||
|
class="step-item"
|
||||||
|
:class="index < steps.length - 1 ? 'flex-[2]' : 'flex-1'"
|
||||||
|
>
|
||||||
|
<Tooltip :title="step.description" color="#0F46B2">
|
||||||
|
<div class="step-box" @click="goPath(step.path)">
|
||||||
|
<div class="step-img">
|
||||||
|
<img :src="step.imgUrl" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="step-title">{{ step.title }}</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div v-if="index < steps.length - 1" class="step-arrow">
|
||||||
|
<img src="/images/dashboard/arrow.png" alt="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.device-steps-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.steps-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.step-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
.step-box {
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,282 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ type: string }>(), {
|
||||||
|
type: () => 'week',
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
// 生成随机数据的函数
|
||||||
|
const generateRandomData = (count: number, min: number, max: number) => {
|
||||||
|
return Array.from(
|
||||||
|
{ length: count },
|
||||||
|
() => Math.floor(Math.random() * (max - min + 1)) + min,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成日期标签的函数
|
||||||
|
const generateDateLabels = (type: string) => {
|
||||||
|
const today = new Date();
|
||||||
|
const labels: string[] = [];
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'halfyear': {
|
||||||
|
// 半年(180天,每10天一个数据点)
|
||||||
|
for (let i = 17; i >= 0; i--) {
|
||||||
|
const date = new Date(today);
|
||||||
|
date.setDate(date.getDate() - i * 10);
|
||||||
|
labels.push(
|
||||||
|
`${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'month': {
|
||||||
|
// 近一个月(30天)
|
||||||
|
for (let i = 29; i >= 0; i--) {
|
||||||
|
const date = new Date(today);
|
||||||
|
date.setDate(date.getDate() - i);
|
||||||
|
labels.push(
|
||||||
|
`${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'week': {
|
||||||
|
// 近7天
|
||||||
|
for (let i = 6; i >= 0; i--) {
|
||||||
|
const date = new Date(today);
|
||||||
|
date.setDate(date.getDate() - i);
|
||||||
|
labels.push(
|
||||||
|
`${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
// 默认近7天
|
||||||
|
for (let i = 6; i >= 0; i--) {
|
||||||
|
const date = new Date(today);
|
||||||
|
date.setDate(date.getDate() - i);
|
||||||
|
labels.push(
|
||||||
|
`${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getDate().toString().padStart(2, '0')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据类型生成数据
|
||||||
|
const chartData = computed(() => {
|
||||||
|
const labels = generateDateLabels(props.type);
|
||||||
|
const dataCount = labels.length;
|
||||||
|
|
||||||
|
let trafficData: number[];
|
||||||
|
let deviceData: number[];
|
||||||
|
let maxValue: number;
|
||||||
|
|
||||||
|
switch (props.type) {
|
||||||
|
case 'halfyear': {
|
||||||
|
trafficData = generateRandomData(dataCount, 50, 800);
|
||||||
|
deviceData = generateRandomData(dataCount, 100, 1000);
|
||||||
|
maxValue = 1500;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'month': {
|
||||||
|
trafficData = generateRandomData(dataCount, 80, 600);
|
||||||
|
deviceData = generateRandomData(dataCount, 200, 900);
|
||||||
|
maxValue = 1200;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'week': {
|
||||||
|
trafficData = generateRandomData(dataCount, 100, 500);
|
||||||
|
deviceData = generateRandomData(dataCount, 300, 800);
|
||||||
|
maxValue = 1000;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
trafficData = generateRandomData(dataCount, 100, 500);
|
||||||
|
deviceData = generateRandomData(dataCount, 300, 800);
|
||||||
|
maxValue = 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels,
|
||||||
|
trafficData,
|
||||||
|
deviceData,
|
||||||
|
maxValue,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const data = chartData.value;
|
||||||
|
|
||||||
|
renderEcharts({
|
||||||
|
title: {
|
||||||
|
text: '设备流量',
|
||||||
|
left: '0%',
|
||||||
|
textStyle: {
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
bottom: 0,
|
||||||
|
containLabel: true,
|
||||||
|
left: '1%',
|
||||||
|
right: '1%',
|
||||||
|
top: '20%',
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['流量', '设备数'],
|
||||||
|
top: '3%',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'cross',
|
||||||
|
},
|
||||||
|
textStyle: {
|
||||||
|
align: 'left', // 关键配置:文字左对齐
|
||||||
|
},
|
||||||
|
formatter(params: any) {
|
||||||
|
let result = `${params[0].name}<br/>`;
|
||||||
|
params.forEach((item: any) => {
|
||||||
|
result += `${item.marker + item.seriesName}: ${item.value}<br/>`;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.labels,
|
||||||
|
boundaryGap: false, // 关键配置:取消坐标轴两边的留白
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
max: 1000,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#f0f0f0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '流量',
|
||||||
|
type: 'line',
|
||||||
|
data: data.trafficData,
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'emptyCircle',
|
||||||
|
symbolSize: 6,
|
||||||
|
showSymbol: false,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#2D72FF',
|
||||||
|
width: 3,
|
||||||
|
// 阴影设置
|
||||||
|
shadowBlur: 20, // 阴影模糊程度
|
||||||
|
shadowColor: 'rgba(45, 114, 255, 0.5)', // 阴影颜色(带透明度)
|
||||||
|
shadowOffsetX: 0, // 阴影水平偏移
|
||||||
|
shadowOffsetY: 10, // 阴影垂直偏移(向下4px)
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#2D72FF',
|
||||||
|
},
|
||||||
|
// areaStyle: {
|
||||||
|
// color: {
|
||||||
|
// type: 'linear',
|
||||||
|
// x: 0,
|
||||||
|
// y: 0,
|
||||||
|
// x2: 0,
|
||||||
|
// y2: 1,
|
||||||
|
// colorStops: [
|
||||||
|
// {
|
||||||
|
// offset: 0,
|
||||||
|
// color: 'rgba(45,114,255, 0.3.5)',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// offset: 1,
|
||||||
|
// color: 'rgba(45,114,255, 0.1)',
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '设备数',
|
||||||
|
type: 'line',
|
||||||
|
data: data.deviceData,
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'emptyCircle',
|
||||||
|
symbolSize: 6,
|
||||||
|
showSymbol: false,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#1FC5AE',
|
||||||
|
width: 3,
|
||||||
|
// 第二条线的阴影
|
||||||
|
shadowBlur: 20,
|
||||||
|
shadowColor: 'rgba(31, 197, 174, 0.5)',
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowOffsetY: 10,
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#1FC5AE',
|
||||||
|
},
|
||||||
|
// areaStyle: {
|
||||||
|
// color: {
|
||||||
|
// type: 'linear',
|
||||||
|
// x: 0,
|
||||||
|
// y: 0,
|
||||||
|
// x2: 0,
|
||||||
|
// y2: 1,
|
||||||
|
// colorStops: [
|
||||||
|
// {
|
||||||
|
// offset: 0,
|
||||||
|
// color: 'rgba(31,197,174, 0.3)',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// offset: 1,
|
||||||
|
// color: 'rgba(31,197,174, 0.1)',
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EchartsUI ref="chartRef" style="height: 390px" />
|
||||||
|
</template>
|