|
|
|
|
@ -1,164 +1,485 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="app-container home">
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :sm="24" :lg="12" style="padding-left: 20px">
|
|
|
|
|
<h2>RuoYi-Vue-Plus多租户管理系统</h2>
|
|
|
|
|
<p>
|
|
|
|
|
RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 分布式集群 场景升级(不兼容原框架)
|
|
|
|
|
<br />
|
|
|
|
|
* 前端开发框架 Vue3、TS、Element Plus<br />
|
|
|
|
|
* 后端开发框架 Spring Boot<br />
|
|
|
|
|
* 容器框架 Undertow 基于 Netty 的高性能容器<br />
|
|
|
|
|
* 权限认证框架 Sa-Token 支持多终端认证系统<br />
|
|
|
|
|
* 关系数据库 MySQL 适配 8.X 最低 5.7<br />
|
|
|
|
|
* 缓存数据库 Redis 适配 6.X 最低 4.X<br />
|
|
|
|
|
* 数据库框架 Mybatis-Plus 快速 CRUD 增加开发效率<br />
|
|
|
|
|
* 数据库框架 p6spy 更强劲的 SQL 分析<br />
|
|
|
|
|
* 多数据源框架 dynamic-datasource 支持主从与多种类数据库异构<br />
|
|
|
|
|
* 序列化框架 Jackson 统一使用 jackson 高效可靠<br />
|
|
|
|
|
* Redis客户端 Redisson 性能强劲、API丰富<br />
|
|
|
|
|
* 分布式限流 Redisson 全局、请求IP、集群ID 多种限流<br />
|
|
|
|
|
* 分布式锁 Lock4j 注解锁、工具锁 多种多样<br />
|
|
|
|
|
* 分布式幂等 Lock4j 基于分布式锁实现<br />
|
|
|
|
|
* 分布式链路追踪 SkyWalking 支持链路追踪、网格分析、度量聚合、可视化<br />
|
|
|
|
|
* 分布式任务调度 SnailJob 高性能 高可靠 易扩展<br />
|
|
|
|
|
* 文件存储 Minio 本地存储<br />
|
|
|
|
|
* 文件存储 七牛、阿里、腾讯 云存储<br />
|
|
|
|
|
* 监控框架 SpringBoot-Admin 全方位服务监控<br />
|
|
|
|
|
* 校验框架 Validation 增强接口安全性 严谨性<br />
|
|
|
|
|
* Excel框架 FastExcel(原Alibaba EasyExcel) 性能优异 扩展性强<br />
|
|
|
|
|
* 文档框架 SpringDoc、javadoc 无注解零入侵基于java注释<br />
|
|
|
|
|
* 工具类框架 Hutool、Lombok 减少代码冗余 增加安全性<br />
|
|
|
|
|
* 代码生成器 适配MP、SpringDoc规范化代码 一键生成前后端代码<br />
|
|
|
|
|
* 部署方式 Docker 容器编排 一键部署业务集群<br />
|
|
|
|
|
* 国际化 SpringMessage Spring标准国际化方案<br />
|
|
|
|
|
</p>
|
|
|
|
|
<p><b>当前版本:</b> <span>v5.6.1</span></p>
|
|
|
|
|
<p>
|
|
|
|
|
<el-tag type="danger">¥免费开源</el-tag>
|
|
|
|
|
</p>
|
|
|
|
|
<p>
|
|
|
|
|
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')">访问码云</el-button>
|
|
|
|
|
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Vue-Plus')">访问GitHub</el-button>
|
|
|
|
|
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog')"
|
|
|
|
|
>更新日志</el-button
|
|
|
|
|
>
|
|
|
|
|
</p>
|
|
|
|
|
</el-col>
|
|
|
|
|
|
|
|
|
|
<el-col :sm="24" :lg="12" style="padding-left: 20px">
|
|
|
|
|
<h2>haiwei-Cloud-Plus多租户微服务管理系统</h2>
|
|
|
|
|
<p>
|
|
|
|
|
haiwei-Cloud-Plus 微服务通用权限管理系统 重写 haiwei-Cloud 全方位升级(不兼容原框架)
|
|
|
|
|
<br />
|
|
|
|
|
* 前端开发框架 Vue3、TS、Element UI<br />
|
|
|
|
|
* 后端开发框架 Spring Boot<br />
|
|
|
|
|
* 微服务开发框架 Spring Cloud、Spring Cloud Alibaba<br />
|
|
|
|
|
* 容器框架 Undertow 基于 XNIO 的高性能容器<br />
|
|
|
|
|
* 权限认证框架 Sa-Token、Jwt 支持多终端认证系统<br />
|
|
|
|
|
* 关系数据库 MySQL 适配 8.X 最低 5.7<br />
|
|
|
|
|
* 关系数据库 Oracle 适配 11g 12c<br />
|
|
|
|
|
* 关系数据库 PostgreSQL 适配 13 14<br />
|
|
|
|
|
* 关系数据库 SQLServer 适配 2017 2019<br />
|
|
|
|
|
* 缓存数据库 Redis 适配 6.X 最低 5.X<br />
|
|
|
|
|
* 分布式注册中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
|
|
|
|
|
* 分布式配置中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
|
|
|
|
|
* 服务网关 Spring Cloud Gateway 响应式高性能网关<br />
|
|
|
|
|
* 负载均衡 Spring Cloud Loadbalancer 负载均衡处理<br />
|
|
|
|
|
* RPC远程调用 Apache Dubbo 原生态使用体验、高性能<br />
|
|
|
|
|
* 分布式限流熔断 Alibaba Sentinel 无侵入、高扩展<br />
|
|
|
|
|
* 分布式事务 Alibaba Seata 无侵入、高扩展 支持 四种模式<br />
|
|
|
|
|
* 分布式消息队列 Apache Kafka 高性能高速度<br />
|
|
|
|
|
* 分布式消息队列 Apache RocketMQ 高可用功能多样<br />
|
|
|
|
|
* 分布式消息队列 RabbitMQ 支持各种扩展插件功能多样性<br />
|
|
|
|
|
* 分布式搜索引擎 ElasticSearch 业界知名<br />
|
|
|
|
|
* 分布式链路追踪 Apache SkyWalking 链路追踪、网格分析、度量聚合、可视化<br />
|
|
|
|
|
* 分布式日志中心 ELK 业界成熟解决方案<br />
|
|
|
|
|
* 分布式监控 Prometheus、Grafana 全方位性能监控<br />
|
|
|
|
|
* 其余与 Vue 版本一致<br />
|
|
|
|
|
</p>
|
|
|
|
|
<p><b>当前版本:</b> <span>v2.6.1</span></p>
|
|
|
|
|
<p>
|
|
|
|
|
<el-tag type="danger">¥免费开源</el-tag>
|
|
|
|
|
</p>
|
|
|
|
|
<p>
|
|
|
|
|
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Cloud-Plus')">访问码云</el-button>
|
|
|
|
|
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Cloud-Plus')">访问GitHub</el-button>
|
|
|
|
|
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-cloud-plus/changlog')"
|
|
|
|
|
>更新日志</el-button
|
|
|
|
|
>
|
|
|
|
|
</p>
|
|
|
|
|
<!-- 欢迎栏 -->
|
|
|
|
|
<el-row :gutter="20" class="mb-4">
|
|
|
|
|
<el-col :span="24">
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
<div class="welcome-header">
|
|
|
|
|
<div>
|
|
|
|
|
<h2 class="welcome-title">{{ $t('dashboard.welcome') }}</h2>
|
|
|
|
|
<p class="welcome-desc">{{ $t('dashboard.description') }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="welcome-meta">
|
|
|
|
|
<el-tag type="primary">{{ currentDate }}</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<!-- 核心指标 -->
|
|
|
|
|
<el-row :gutter="20" class="mb-4">
|
|
|
|
|
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" v-for="item in statCards" :key="item.key">
|
|
|
|
|
<el-card shadow="hover" class="stat-card" @click="goTarget(item.path)">
|
|
|
|
|
<div class="stat-icon" :style="{ background: item.color }">
|
|
|
|
|
<svg-icon :icon-class="item.icon" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-info">
|
|
|
|
|
<div class="stat-value">{{ item.value }}</div>
|
|
|
|
|
<div class="stat-label">{{ item.label }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<!-- 状态分布图表 -->
|
|
|
|
|
<el-row :gutter="20" class="mb-4">
|
|
|
|
|
<el-col :xs="24" :md="12" :lg="8">
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
<template #header>
|
|
|
|
|
<span>{{ $t('dashboard.taskStatus') }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
<div ref="taskStatusChartRef" class="chart-box"></div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :xs="24" :md="12" :lg="8">
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
<template #header>
|
|
|
|
|
<span>{{ $t('dashboard.deviceStatus') }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
<div ref="deviceStatusChartRef" class="chart-box"></div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :xs="24" :md="12" :lg="8">
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
<template #header>
|
|
|
|
|
<span>{{ $t('dashboard.locationStatus') }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
<div ref="locationStatusChartRef" class="chart-box"></div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<!-- 最近任务与快捷入口 -->
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :xs="24" :lg="16" class="mb-4">
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<span>{{ $t('dashboard.recentTasks') }}</span>
|
|
|
|
|
<el-button link type="primary" @click="goTarget('/base/taskQueue')">{{ $t('dashboard.more') }}</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-table v-loading="loading" :data="recentTasks" stripe>
|
|
|
|
|
<el-table-column prop="taskCode" :label="$t('dashboard.taskCode')" min-width="120" />
|
|
|
|
|
<el-table-column prop="materialName" :label="$t('dashboard.materialName')" min-width="120" />
|
|
|
|
|
<el-table-column prop="startPoint" :label="$t('dashboard.startPoint')" min-width="100" />
|
|
|
|
|
<el-table-column prop="endPoint" :label="$t('dashboard.endPoint')" min-width="100" />
|
|
|
|
|
<el-table-column prop="taskStatus" :label="$t('dashboard.taskStatus')" width="100">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<dict-tag :options="wcs_task_status" :value="scope.row.taskStatus" />
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="createTime" :label="$t('dashboard.createTime')" width="170">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<span>{{ parseTime(scope.row.createTime, CREATED_TIME_DISPLAY_FORMAT) }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :xs="24" :lg="8">
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
<template #header>
|
|
|
|
|
<span>{{ $t('dashboard.quickLinks') }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
<div class="quick-link-list">
|
|
|
|
|
<div v-for="link in quickLinks" :key="link.path" class="quick-link" @click="goTarget(link.path)">
|
|
|
|
|
<svg-icon :icon-class="link.icon" class="quick-link-icon" />
|
|
|
|
|
<div class="quick-link-info">
|
|
|
|
|
<div class="quick-link-title">{{ link.title }}</div>
|
|
|
|
|
<div class="quick-link-desc">{{ link.desc }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<svg-icon icon-class="caret-forward" class="quick-link-arrow" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-divider />
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup name="Index" lang="ts">
|
|
|
|
|
const goTarget = (url: string) => {
|
|
|
|
|
window.open(url, '__blank');
|
|
|
|
|
import * as echarts from 'echarts';
|
|
|
|
|
import { parseTime } from '@/utils/ruoyi';
|
|
|
|
|
import { countTaskQueue, listTaskQueue } from '@/api/wcs/taskQueue';
|
|
|
|
|
import { countTaskDetail } from '@/api/wcs/taskDetail';
|
|
|
|
|
import { countDeviceInfo } from '@/api/wcs/deviceInfo';
|
|
|
|
|
import { countDeviceParam } from '@/api/wcs/deviceParam';
|
|
|
|
|
import { countLocationInfo } from '@/api/wcs/locationInfo';
|
|
|
|
|
import { countMaterialInfo } from '@/api/wcs/materialInfo';
|
|
|
|
|
import { countStoreInfo } from '@/api/wcs/storeInfo';
|
|
|
|
|
import { countPathInfo } from '@/api/wcs/pathInfo';
|
|
|
|
|
import { countDeviceHost } from '@/api/wcs/deviceHost';
|
|
|
|
|
import { TaskQueueVO } from '@/api/wcs/taskQueue/types';
|
|
|
|
|
import { CREATED_TIME_DISPLAY_FORMAT } from './wcs/utils/createTimeRange';
|
|
|
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
|
|
const { t } = useI18n();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
const { wcs_task_status } = toRefs<any>(proxy?.useDict('wcs_task_status'));
|
|
|
|
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const currentDate = ref('');
|
|
|
|
|
|
|
|
|
|
const statCards = ref([
|
|
|
|
|
{ key: 'taskQueue', label: t('dashboard.taskQueue'), value: 0, icon: 'list', color: '#3b82f6', path: '/base/taskQueue' },
|
|
|
|
|
{ key: 'taskDetail', label: t('dashboard.taskDetail'), value: 0, icon: 'component', color: '#6366f1', path: '/base/taskDetail' },
|
|
|
|
|
{ key: 'deviceHost', label: t('dashboard.deviceHost'), value: 0, icon: 'server', color: '#64748b', path: '/base/deviceHost' },
|
|
|
|
|
{ key: 'deviceInfo', label: t('dashboard.deviceInfo'), value: 0, icon: 'chart', color: '#10b981', path: '/base/deviceInfo' },
|
|
|
|
|
{ key: 'deviceParam', label: t('dashboard.deviceParam'), value: 0, icon: 'edit', color: '#0ea5e9', path: '/base/deviceParam' },
|
|
|
|
|
{ key: 'locationInfo', label: t('dashboard.locationInfo'), value: 0, icon: 'dict', color: '#f59e0b', path: '/base/locationInfo' },
|
|
|
|
|
{ key: 'materialInfo', label: t('dashboard.materialInfo'), value: 0, icon: 'excel', color: '#8b5cf6', path: '/base/materialInfo' },
|
|
|
|
|
{ key: 'storeInfo', label: t('dashboard.storeInfo'), value: 0, icon: 'tree', color: '#ec4899', path: '/base/storeInfo' },
|
|
|
|
|
{ key: 'pathInfo', label: t('dashboard.pathInfo'), value: 0, icon: 'build', color: '#14b8a6', path: '/base/pathInfo' }
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const quickLinks = ref([
|
|
|
|
|
{ title: t('dashboard.taskQueue'), desc: t('dashboard.taskQueueDesc'), icon: 'list', path: '/base/taskQueue' },
|
|
|
|
|
{ title: t('dashboard.taskDetail'), desc: t('dashboard.taskDetailDesc'), icon: 'component', path: '/base/taskDetail' },
|
|
|
|
|
{ title: t('dashboard.deviceHost'), desc: t('dashboard.deviceHostDesc'), icon: 'server', path: '/base/deviceHost' },
|
|
|
|
|
{ title: t('dashboard.deviceInfo'), desc: t('dashboard.deviceInfoDesc'), icon: 'chart', path: '/base/deviceInfo' },
|
|
|
|
|
{ title: t('dashboard.deviceParam'), desc: t('dashboard.deviceParamDesc'), icon: 'edit', path: '/base/deviceParam' },
|
|
|
|
|
{ title: t('dashboard.locationInfo'), desc: t('dashboard.locationInfoDesc'), icon: 'dict', path: '/base/locationInfo' },
|
|
|
|
|
{ title: t('dashboard.materialInfo'), desc: t('dashboard.materialInfoDesc'), icon: 'excel', path: '/base/materialInfo' },
|
|
|
|
|
{ title: t('dashboard.storeInfo'), desc: t('dashboard.storeInfoDesc'), icon: 'tree', path: '/base/storeInfo' },
|
|
|
|
|
{ title: t('dashboard.pathInfo'), desc: t('dashboard.pathInfoDesc'), icon: 'build', path: '/base/pathInfo' }
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const recentTasks = ref<TaskQueueVO[]>([]);
|
|
|
|
|
|
|
|
|
|
const taskStatusChartRef = ref<HTMLDivElement>();
|
|
|
|
|
const deviceStatusChartRef = ref<HTMLDivElement>();
|
|
|
|
|
const locationStatusChartRef = ref<HTMLDivElement>();
|
|
|
|
|
let taskStatusChart: echarts.ECharts | null = null;
|
|
|
|
|
let deviceStatusChart: echarts.ECharts | null = null;
|
|
|
|
|
let locationStatusChart: echarts.ECharts | null = null;
|
|
|
|
|
|
|
|
|
|
const goTarget = (path: string) => {
|
|
|
|
|
if (path.startsWith('http')) {
|
|
|
|
|
window.open(path, '__blank');
|
|
|
|
|
} else {
|
|
|
|
|
router.push(path);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatDate = (date: Date) => {
|
|
|
|
|
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|
|
|
|
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日 ${weekdays[date.getDay()]}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getCount = (res: any) => (res && typeof res.data === 'number' ? res.data : 0);
|
|
|
|
|
|
|
|
|
|
const loadCounts = async () => {
|
|
|
|
|
const [taskQueueRes, taskDetailRes, hostRes, deviceRes, deviceParamRes, locationRes, materialRes, storeRes, pathRes] = await Promise.all([
|
|
|
|
|
countTaskQueue(),
|
|
|
|
|
countTaskDetail(),
|
|
|
|
|
countDeviceHost(),
|
|
|
|
|
countDeviceInfo(),
|
|
|
|
|
countDeviceParam(),
|
|
|
|
|
countLocationInfo(),
|
|
|
|
|
countMaterialInfo(),
|
|
|
|
|
countStoreInfo(),
|
|
|
|
|
countPathInfo()
|
|
|
|
|
]);
|
|
|
|
|
statCards.value[0].value = getCount(taskQueueRes);
|
|
|
|
|
statCards.value[1].value = getCount(taskDetailRes);
|
|
|
|
|
statCards.value[2].value = getCount(hostRes);
|
|
|
|
|
statCards.value[3].value = getCount(deviceRes);
|
|
|
|
|
statCards.value[4].value = getCount(deviceParamRes);
|
|
|
|
|
statCards.value[5].value = getCount(locationRes);
|
|
|
|
|
statCards.value[6].value = getCount(materialRes);
|
|
|
|
|
statCards.value[7].value = getCount(storeRes);
|
|
|
|
|
statCards.value[8].value = getCount(pathRes);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadTaskStatusChart = async () => {
|
|
|
|
|
const statusOptions = wcs_task_status.value || [];
|
|
|
|
|
if (!statusOptions.length) {
|
|
|
|
|
taskStatusChart?.setOption({
|
|
|
|
|
title: { text: t('dashboard.noData'), left: 'center', top: 'center', textStyle: { fontSize: 14, color: '#999' } },
|
|
|
|
|
series: []
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const data = await Promise.all(
|
|
|
|
|
statusOptions.map(async (dict: any) => {
|
|
|
|
|
const res = await countTaskQueue({ taskStatus: parseInt(dict.value) });
|
|
|
|
|
return { name: dict.label, value: getCount(res) };
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
taskStatusChart?.setOption({
|
|
|
|
|
tooltip: { trigger: 'item' },
|
|
|
|
|
legend: { bottom: 0 },
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
type: 'pie',
|
|
|
|
|
radius: '65%',
|
|
|
|
|
data,
|
|
|
|
|
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadDeviceStatusChart = async () => {
|
|
|
|
|
const statuses = [
|
|
|
|
|
{ label: t('dashboard.deviceNormal'), value: 0 },
|
|
|
|
|
{ label: t('dashboard.deviceBusy'), value: 1 },
|
|
|
|
|
{ label: t('dashboard.deviceAbnormal'), value: 2 }
|
|
|
|
|
];
|
|
|
|
|
const data = await Promise.all(
|
|
|
|
|
statuses.map(async (status) => {
|
|
|
|
|
const res = await countDeviceInfo({ deviceStatus: status.value });
|
|
|
|
|
return { name: status.label, value: getCount(res) };
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
deviceStatusChart?.setOption({
|
|
|
|
|
tooltip: { trigger: 'item' },
|
|
|
|
|
legend: { bottom: 0 },
|
|
|
|
|
color: ['#10b981', '#f59e0b', '#ef4444'],
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
type: 'pie',
|
|
|
|
|
radius: ['40%', '65%'],
|
|
|
|
|
data,
|
|
|
|
|
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadLocationStatusChart = async () => {
|
|
|
|
|
const statuses = [
|
|
|
|
|
{ label: t('dashboard.locationUnused'), value: 0 },
|
|
|
|
|
{ label: t('dashboard.locationUsed'), value: 1 },
|
|
|
|
|
{ label: t('dashboard.locationLocked'), value: 2 },
|
|
|
|
|
{ label: t('dashboard.locationAbnormal'), value: 3 }
|
|
|
|
|
];
|
|
|
|
|
const data = await Promise.all(
|
|
|
|
|
statuses.map(async (status) => {
|
|
|
|
|
const res = await countLocationInfo({ locationStatus: status.value });
|
|
|
|
|
return { name: status.label, value: getCount(res) };
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
locationStatusChart?.setOption({
|
|
|
|
|
tooltip: { trigger: 'item' },
|
|
|
|
|
legend: { bottom: 0 },
|
|
|
|
|
color: ['#94a3b8', '#3b82f6', '#f59e0b', '#ef4444'],
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
type: 'pie',
|
|
|
|
|
radius: ['40%', '65%'],
|
|
|
|
|
data,
|
|
|
|
|
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadRecentTasks = async () => {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const res = await listTaskQueue({ pageNum: 1, pageSize: 5, params: {} });
|
|
|
|
|
recentTasks.value = res.rows || [];
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const initCharts = () => {
|
|
|
|
|
if (taskStatusChartRef.value) taskStatusChart = echarts.init(taskStatusChartRef.value);
|
|
|
|
|
if (deviceStatusChartRef.value) deviceStatusChart = echarts.init(deviceStatusChartRef.value);
|
|
|
|
|
if (locationStatusChartRef.value) locationStatusChart = echarts.init(locationStatusChartRef.value);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadCharts = async () => {
|
|
|
|
|
await Promise.all([loadTaskStatusChart(), loadDeviceStatusChart(), loadLocationStatusChart()]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleResize = () => {
|
|
|
|
|
taskStatusChart?.resize();
|
|
|
|
|
deviceStatusChart?.resize();
|
|
|
|
|
locationStatusChart?.resize();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
currentDate.value = formatDate(new Date());
|
|
|
|
|
initCharts();
|
|
|
|
|
Promise.all([loadCounts(), loadRecentTasks(), loadCharts()]);
|
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
window.removeEventListener('resize', handleResize);
|
|
|
|
|
taskStatusChart?.dispose();
|
|
|
|
|
deviceStatusChart?.dispose();
|
|
|
|
|
locationStatusChart?.dispose();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.home {
|
|
|
|
|
blockquote {
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
margin: 0 0 20px;
|
|
|
|
|
font-size: 17.5px;
|
|
|
|
|
border-left: 5px solid #eee;
|
|
|
|
|
}
|
|
|
|
|
hr {
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
border: 0;
|
|
|
|
|
border-top: 1px solid #eee;
|
|
|
|
|
}
|
|
|
|
|
.col-item {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ul {
|
|
|
|
|
padding: 0;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
font-family: 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #676a6c;
|
|
|
|
|
color: var(--el-text-color-regular);
|
|
|
|
|
overflow-x: hidden;
|
|
|
|
|
|
|
|
|
|
ul {
|
|
|
|
|
list-style-type: none;
|
|
|
|
|
.mb-4 {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h4 {
|
|
|
|
|
margin-top: 0px;
|
|
|
|
|
}
|
|
|
|
|
.welcome-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
|
|
|
|
h2 {
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
font-size: 26px;
|
|
|
|
|
font-weight: 100;
|
|
|
|
|
}
|
|
|
|
|
.welcome-title {
|
|
|
|
|
margin: 0 0 8px 0;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
.welcome-desc {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
b {
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
.welcome-meta {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.update-log {
|
|
|
|
|
ol {
|
|
|
|
|
display: block;
|
|
|
|
|
list-style-type: decimal;
|
|
|
|
|
margin-block-start: 1em;
|
|
|
|
|
margin-block-end: 1em;
|
|
|
|
|
margin-inline-start: 0;
|
|
|
|
|
margin-inline-end: 0;
|
|
|
|
|
padding-inline-start: 40px;
|
|
|
|
|
.stat-card {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition:
|
|
|
|
|
transform 0.2s,
|
|
|
|
|
box-shadow 0.2s;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
box-shadow: var(--app-shadow-md);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-card__body) {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-icon {
|
|
|
|
|
width: 48px;
|
|
|
|
|
height: 48px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
|
|
|
|
.svg-icon {
|
|
|
|
|
width: 26px;
|
|
|
|
|
height: 26px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-info {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-box {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 280px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.quick-link-list {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
|
|
.quick-link {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
border-radius: var(--app-radius-md);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.quick-link-icon {
|
|
|
|
|
width: 36px;
|
|
|
|
|
height: 36px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
background: var(--el-color-primary-light-9);
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.quick-link-info {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
|
|
|
|
.quick-link-title {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.quick-link-desc {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.quick-link-arrow {
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|