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>
|
||||
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'),
|
||||
// },
|
||||
];
|
||||
/**
|
||||
* 租户选中状态 不显示个人中心
|
||||
|
|
|
@ -34,6 +34,7 @@ export const overridesPreferences = defineOverridesPreferences({
|
|||
enableCheckUpdates: false,
|
||||
// 检查更新的时间间隔,单位为分钟
|
||||
checkUpdatesInterval: 1,
|
||||
defaultHomePath: '/overview',
|
||||
},
|
||||
footer: {
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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>
|