feat: 添加概览页并更新默认首页路径,隐藏无用页面和功能

- 新增概览页 (`/overview`) 相关组件和页面文件,包括 KPI 指标展示、设备流量图表、
  设备类型占比饼图、设备接入步骤引导和运维管理步骤引导。
- 更新路由配置,将默认首页重定向路径从 `/analytics` 改为 `/overview`。
- 注释掉部分原有菜单项与路由配置(如文档、Gitee 地址、Vben 官方地址等)。
- 设置 `defaultHomePath` 为 `/overview`,确保应用启动后默认进入概览页。
This commit is contained in:
fhysy 2025-09-19 15:16:36 +08:00
parent 78c7cff383
commit 92a7bab3e9
24 changed files with 833 additions and 96 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,17 +1,10 @@
<script lang="ts" setup>
import { computed, h, onMounted, watch } from 'vue';
import { computed, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { useWatermark } from '@vben/hooks';
import {
BookOpenText,
CircleHelp,
GiteeIcon,
GitHubOutlined,
UserOutlined,
} from '@vben/icons';
import { UserOutlined } from '@vben/icons';
import {
BasicLayout,
LockScreen,
@ -20,7 +13,6 @@ import {
} from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
import { message } from 'ant-design-vue';
@ -42,15 +34,15 @@ const { destroyWatermark, updateWatermark } = useWatermark();
const tenantStore = useTenantStore();
const menus = computed(() => {
const defaultMenus = [
{
handler: () => {
openWindow(VBEN_DOC_URL, {
target: '_blank',
});
},
icon: BookOpenText,
text: $t('ui.widgets.document'),
},
// {
// handler: () => {
// openWindow(VBEN_DOC_URL, {
// target: '_blank',
// });
// },
// icon: BookOpenText,
// text: $t('ui.widgets.document'),
// },
{
handler: () => {
router.push('/profile');
@ -58,33 +50,33 @@ const menus = computed(() => {
icon: UserOutlined,
text: $t('ui.widgets.profile'),
},
{
handler: () => {
openWindow('https://gitee.com/dapppp/ruoyi-plus-vben5', {
target: '_blank',
});
},
icon: () => h(GiteeIcon, { class: 'text-red-800' }),
text: 'Gitee项目地址',
},
{
handler: () => {
openWindow(VBEN_GITHUB_URL, {
target: '_blank',
});
},
icon: GitHubOutlined,
text: 'Vben官方地址',
},
{
handler: () => {
openWindow(`${VBEN_GITHUB_URL}/issues`, {
target: '_blank',
});
},
icon: CircleHelp,
text: $t('ui.widgets.qa'),
},
// {
// handler: () => {
// openWindow('https://gitee.com/dapppp/ruoyi-plus-vben5', {
// target: '_blank',
// });
// },
// icon: () => h(GiteeIcon, { class: 'text-red-800' }),
// text: 'Gitee',
// },
// {
// handler: () => {
// openWindow(VBEN_GITHUB_URL, {
// target: '_blank',
// });
// },
// icon: GitHubOutlined,
// text: 'Vben',
// },
// {
// handler: () => {
// openWindow(`${VBEN_GITHUB_URL}/issues`, {
// target: '_blank',
// });
// },
// icon: CircleHelp,
// text: $t('ui.widgets.qa'),
// },
];
/**
* 租户选中状态 不显示个人中心

View File

@ -34,6 +34,7 @@ export const overridesPreferences = defineOverridesPreferences({
enableCheckUpdates: false,
// 检查更新的时间间隔,单位为分钟
checkUpdatesInterval: 1,
defaultHomePath: '/overview',
},
footer: {
/**

View File

@ -2,10 +2,10 @@ import type { RouteRecordStringComponent } from '@vben/types';
import { $t } from '@vben/locales';
const {
version,
// vite inject-metadata 插件注入的全局变量
} = __VBEN_ADMIN_METADATA__ || {};
// const {
// version,
// // vite inject-metadata 插件注入的全局变量
// } = __VBEN_ADMIN_METADATA__ || {};
/**
*
@ -39,59 +39,68 @@ export const localMenuList: RouteRecordStringComponent[] = [
},
name: 'Dashboard',
path: '/',
redirect: '/analytics',
redirect: '/overview',
children: [
{
name: 'Analytics',
path: '/analytics',
component: '/dashboard/analytics/index',
name: 'Overview',
path: '/overview',
component: '/dashboard/overview/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',
title: '概览页',
},
},
// {
// 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',
meta: {
icon: 'lucide:copyright',
order: 9999,
title: $t('demos.vben.about'),
},
name: 'About',
path: '/vben-admin/about',
},
// {
// component: '/_core/about/index',
// meta: {
// icon: 'lucide:copyright',
// order: 9999,
// title: $t('demos.vben.about'),
// },
// name: 'About',
// path: '/vben-admin/about',
// },
...localRoutes,
];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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': {
// 18010
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>