1
0
forked from dyf/dyf-vue-ui
Files
dyf-vue-ui/src/views/index.vue
2025-09-15 16:39:49 +08:00

847 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container home">
<!-- 数据总览卡片 -->
<div>
<h2>数据总览</h2>
<div class="data-item">
<div class="data_bck">
<div class="number"><span>{{ DataOverview.devicesNumber }}</span> </div>
<div class="title_number">设备数量</div>
</div>
<div class="data_green">
<div class="number"><span>{{ DataOverview.equipmentOnline }}</span> </div>
<div class="title_number">在线设备</div>
</div>
<div class="data_orgine">
<div class="number"><span>{{ DataOverview.binding }}</span> </div>
<div class="title_number">已绑定设备</div>
</div>
<div class="data_red">
<div class="number"><span>{{ DataOverview.equipmentAbnormal }}</span> </div>
<div class="title_number">异常设备</div>
</div>
</div>
<!-- 设备分类 + 快捷操作 -->
<el-row :gutter="20">
<el-col :span="12">
<div class="content-row">
<h2>设备分类</h2>
<div class="card-header">
<div v-for="(item, index) in deviceList" :key="index" class="progress-item"
style="display: inline-block; margin-right: 40px;">
<el-progress :stroke-width="7" type="circle" :width="100"
:percentage="item.total === 0 ? 0 : (item.current / item.total) * 100">
<template #default>
<div class="progress-text">
<span class="current">{{ item.current }}</span>
<span class="divider">/</span>
<span class="total">{{ item.total }}</span>
</div>
</template>
</el-progress>
<div class="progress-name">{{ item.name }}</div>
</div>
</div>
</div>
</el-col>
<el-col :span="12">
<div class="content-row">
<h2>快捷操作</h2>
<div class="card-header">
<div class="quick-item" @click="handledeviceTypeAdd">
<img src="../assets/index/device_type.png" class="quick-img" />
<div class="card_title">设备类型</div>
</div>
<div class="quick-item" @click="handledeviceAdd">
<img src="../assets/index/device_add.png" class="quick-img" />
<div class="card_title">设备添加</div>
</div>
<div class="quick-item" @click="handleGroup">
<img src="../assets/index/device_group.png" class="quick-img" />
<div class="card_title">分组管理</div>
</div>
<div class="quick-item" @click="handleControlPanel">
<img src="../assets/index/conton.png" class="quick-img" />
<div class="card_title">控制面板</div>
</div>
</div>
</div>
</el-col>
</el-row>
<!-- 图表区域设备使用频次 + 报警信息 -->
<el-row :gutter="20">
<el-col :span="12">
<div class="region-chart-card">
<div class="card-header">
<h2>设备使用频次</h2>
<div class="chart-controls">
<el-select v-model="deviceType" placeholder="设备类型" style="width: 150px;"
@change="handleDeviceTypeChange">
<el-option v-for="item in deviceTypeOptions" :key="item.value" :label="item.typeName"
:value="item.deviceTypeId" />
</el-select>
<div class="tab-group">
<div class="tab-item" :class="{ 'tab-item--active': activeTab === '1' }"
@click="updateFrequencyChart('1')">
近半年
</div>
<div class="tab-item" :class="{ 'tab-item--active': activeTab === '2' }"
@click="updateFrequencyChart('2')">
近一年
</div>
</div>
</div>
</div>
<div class="card-body">
<!-- 图表容器设备使用频次 -->
<div ref="frequencyChartRef" class="chart-container"></div>
</div>
</div>
</el-col>
<el-col :span="12">
<div class="region-chart-card">
<div class="card-header">
<h2>报警信息</h2>
</div>
<div class="card-body">
<el-row :gutter="16">
<el-col :span="8">
<div class="alarm-overview">
<!-- 环形图容器今日报警处理占比 -->
<div ref="alarmRingChartRef" class="chart-container"></div>
<div class="alarm-stats">
<div class="stat-item" v-if="alarmsData">
<div class="stat red">{{ alarmsData.alarmsTotal }}</div>
<div class="label">报警总数</div>
</div>
<div class="stat-item" v-if="alarmsData">
<div class="stat green">{{ alarmsData.processingAlarm }}</div>
<div class="label">总处理报警</div>
</div>
</div>
</div>
</el-col>
<!-- 报警事项 + 柱状图 -->
<el-col :span="16">
<div class="alarm-items">
<h3>报警事项</h3>
<!-- 柱状图容器各类型报警次数 -->
<div ref="alarmBarChartRef" class="chart-container"></div>
</div>
</el-col>
</el-row>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup name="Index" lang="ts">
import api from '@/api/home/index'
import { DataOverviewType } from '@/api/home/types'
import router from '@/router';
import * as echarts from 'echarts'; // 引入ECharts核心库
import apiTypeAll from '@/api/equipmentManagement/device/index';
const DataOverview = ref<DataOverviewType>({
devicesNumber: 0,
equipmentOnline: 0,
binding: 0,
equipmentAbnormal: 0
});
const deviceTypeOptions = ref([]); //设备类型
const deviceType = ref()
const activeTab = ref('1');
const alarmsData = ref()
// ---------------------- 基础数据 ----------------------
// 设备分类数据
const deviceList = ref([
{ name: "4G设备", current: 0, total: 0 },
{ name: "蓝牙设备", current: 0, total: 0 },
{ name: "4G&蓝牙设备", current: 0, total: 0 },
]);
// ---------------------- 图表Ref用于挂载图表实例 ----------------------
const frequencyChartRef = ref<HTMLDivElement | null>(null); // 设备使用频次折线图
const alarmRingChartRef = ref<HTMLDivElement | null>(null); // 报警环形图
const alarmBarChartRef = ref<HTMLDivElement | null>(null); // 报警柱状图
// ---------------------- 图表实例存储(用于销毁/更新) ----------------------
let frequencyChartInstance: echarts.ECharts | null = null;
let alarmRingChartInstance: echarts.ECharts | null = null;
let alarmBarChartInstance: echarts.ECharts | null = null;
// ---------------------- 快捷操作方法 ----------------------
const handledeviceTypeAdd = () => {
router.push('/equipmentManagement/deviceType');
};
const handledeviceAdd = () => {
router.push('/equipmentManagement/devices');
};
const handleGroup = () => {
router.push('/equipmentManagement/group');
};
const handleControlPanel = () => {
router.push('controlCenter/controlPanel');
};
// ---------------------- 图表初始化方法 ----------------------
const initFrequencyChart = async (range: any = '1', deviceTypeId: any) => {
if (!frequencyChartRef.value) return;
frequencyChartInstance = echarts.init(frequencyChartRef.value);
try {
let data = {
deviceTypeId: deviceTypeId
}
const res = await api.getEquipmentUsageData(range, data);
const monthData = res.data[0] || {};
const monthKeys = ['m1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm10', 'm11', 'm12'];
const monthNames = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
// 3. 计算时间范围(核心逻辑)
let filteredKeys, filteredNames, yAxisData;
const today = new Date();
const currentMonth = today.getMonth();
if (range === '1') {
const result = [];
for (let i = 0; i < 6; i++) {
const targetMonth = (currentMonth - i + 12) % 12;
result.push(targetMonth);
}
const recent6Months = result.reverse();
// 匹配接口字段和名称
filteredKeys = recent6Months.map(monthIndex => monthKeys[monthIndex]);
filteredNames = recent6Months.map(monthIndex => monthNames[monthIndex]);
yAxisData = filteredKeys.map(key => monthData[key] || 0);
} else {
// 近一年全部12个月1月→12月
filteredKeys = monthKeys;
filteredNames = monthNames;
yAxisData = filteredKeys.map(key => monthData[key] || 0);
}
const chartData = {
xAxis: filteredNames,
yAxis: yAxisData,
peak: { name: '峰值', value: 0, month: '' }
};
if (yAxisData.length) {
const maxVal = Math.max(...yAxisData);
const maxIndex = yAxisData.indexOf(maxVal);
chartData.peak = {
name: '峰值',
value: maxVal,
month: chartData.xAxis[maxIndex] || ''
};
}
const option = {
tooltip: { trigger: 'axis', formatter: '{b}: {c} 次' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
data: chartData.xAxis,
axisLabel: { interval: 0 }
},
yAxis: {
type: 'value',
name: '使用次数',
min: 0,
max: chartData.yAxis.length ? Math.max(...chartData.yAxis) + 100 : 100
},
series: [
{
name: '使用频次',
type: 'line',
data: chartData.yAxis,
smooth: false,
lineStyle: { width: 3, color: '#409eff' },
itemStyle: { color: '#409eff', radius: 5 },
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
{ offset: 1, color: 'rgba(64, 158, 255, 0)' }
]
}
},
markPoint: {
data: chartData.peak.value
? [{
name: chartData.peak.name,
value: chartData.peak.value,
xAxis: chartData.xAxis.indexOf(chartData.peak.month),
yAxis: chartData.peak.value,
itemStyle: { color: '#409eff' }
}]
: []
}
}
]
};
frequencyChartInstance.setOption(option);
} catch (error) {
}
};
/**
* 2. 报警环形图(今日报警处理占比)
*/
const initAlarmRingChart = async () => {
if (!alarmRingChartRef.value) return;
alarmRingChartInstance = echarts.init(alarmRingChartRef.value);
try {
const res = await api.getAlarmInformation({});
const { alarmsTotalToday = 0, processingAlarmToday = 0 } = res.data || {};
alarmsData.value = res.data || '0'
alarmRingChartInstance = echarts.init(alarmRingChartRef.value);
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} 次 ({d}%)' // 显示数量和百分比
},
legend: {
origin: 'vertical',
data: ['报警', '已处理']
},
series: [
{
type: 'pie',
radius: ['50%', '60%'], // 环形半径
center: ['50%', '50%'], // 居中显示
avoidLabelOverlap: false,
label: {
show: true,
position: 'center',
formatter: [
'{valueStyle|' + alarmsTotalToday + '/' + processingAlarmToday + '}',
'{textStyle|今日报警/处理}'
].join('\n'), // 换行
// 关键:配置 rich 定义样式
rich: {
valueStyle: {
color: '#333', // 数字颜色
fontSize: 18, // 数字字号
fontWeight: 'bold',// 数字加粗
lineHeight: 24 // 行高(控制与下一行间距)
},
textStyle: {
color: 'rgba(56, 64, 79, 0.6)', // 文字颜色(可自定义)
fontSize: 12, // 文字字号
lineHeight: 20 // 文字行高
}
},
fontSize: 16,
fontWeight: 'bold',
color: '#333'
},
labelLine: {
show: false // 隐藏标签连接线
},
data: [
{
value: processingAlarmToday,
name: '报警',
itemStyle: { color: '#F65757' } // 红色:未处理
},
{
value: alarmsTotalToday,
name: '已处理',
itemStyle: { color: '#07BE75' } // 绿色:已处理
},
]
}
]
};
alarmRingChartInstance.setOption(option);
// 报警柱状图
initAlarmBarChart()
} catch (error) {
}
};
/**
* 3. 报警柱状图(各类型报警次数)
*/
const initAlarmBarChart = () => {
if (!alarmBarChartRef.value) return;
const alarmTypeMap = [
{ name: '强制报警', field: 'alarmForced' }, // alarmForced
{ name: '撞击闯入', field: 'intrusionImpact' }, // intrusionImpact
{ name: '自动报警', field: 'alarmManual' }, // alarmManual
{ name: '电子围栏', field: 'fenceElectronic' } // fenceElectronic
];
const alarmTypes = alarmTypeMap.map(item => item.name);
const alarmCounts = alarmTypeMap.map(item => {
const value = alarmsData.value[item.field]; // 提取对应字段值
console.log(`${item.name}数值:`, value); // 打印每个类型的数值
return value;
});
const commonGradient = new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(246, 87, 87, 1)' }, // 渐变起点:#F65757不透明
{ offset: 1, color: 'rgba(224, 52, 52, 0)' } // 渐变终点:#E03434全透明
]);
const option = {
tooltip: {
trigger: 'axis',
formatter: '{b}: {c} 次'
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: alarmTypes,
axisLabel: {
interval: 0 // 强制显示所有标签
}
},
yAxis: {
type: 'value',
name: '报警次数',
min: 0
},
series: [
{
name: '报警次数',
type: 'bar',
data: alarmCounts,
barWidth: '20%',
itemStyle: {
color: commonGradient, // 所有柱子共用同一渐变色
borderRadius: 4 // 统一4px圆角
}
}
]
};
// 4. 初始化并渲染图表
alarmBarChartInstance = echarts.init(alarmBarChartRef.value);
alarmBarChartInstance.setOption(option);
};
// ---------------------- 图表更新方法(时间范围切换时) ----------------------
const updateFrequencyChart = (tabValue: any) => {
activeTab.value = tabValue;
if (frequencyChartInstance) {
frequencyChartInstance.dispose();
}
deviceType.value=''
initFrequencyChart(tabValue, '');
};
const handleDeviceTypeChange = (all) => {
initFrequencyChart(activeTab.value, all);
};
// 首页统计接口
const getData = async () => {
// 设备总览
api.getDataOverview({}).then(res => {
DataOverview.value = res.data
})
// 设备分类
try {
const res = await api.getEquipmentClassification({});
console.log(res, 'resss');
const { equipment4G, deviceBluetooth, devices4GAndBluetooth, total } = res.data;
// 映射数据current 为各类型设备数量total 为总设备数6
deviceList.value = [
{
name: "4G设备",
current: equipment4G,
total: total
},
{
name: "蓝牙设备",
current: deviceBluetooth,
total: total
},
{
name: "4G&蓝牙设备",
current: devices4GAndBluetooth,
total: total
},
];
} catch (error) {
console.log('获取设备分类数据失败:', error);
}
// 设备类型
apiTypeAll.deviceTypeAll().then(res => {
if (res.code == 200) {
const originalData = Array.isArray(res.data) ? res.data : [];
deviceTypeOptions.value = [{ typeName: '全部', deviceTypeId: ''}].concat(originalData);
}
}).catch(err => {
})
};
// ---------------------- 生命周期钩子(初始化/销毁图表) ----------------------
onMounted(() => {
// 页面加载时初始化所有图表
initFrequencyChart('1', '');
initAlarmRingChart();
getData()
// 监听窗口 resize自动调整图表大小
window.addEventListener('resize', () => {
frequencyChartInstance?.resize();
alarmRingChartInstance?.resize();
alarmBarChartInstance?.resize();
});
});
onUnmounted(() => {
// 页面销毁时销毁图表实例,避免内存泄漏
frequencyChartInstance?.dispose();
alarmRingChartInstance?.dispose();
//alarmBarChartInstance?.dispose();
window.removeEventListener('resize', () => { });
});
</script>
<style lang="scss" scoped>
.home {
padding: 10px 20px 10px 20px;
background-color: #f5f7fa;
min-height: calc(100vh - 84px);
// 数据总览卡片样式
.data-item {
display: flex;
justify-content: space-between;
width: 100%;
color: rgba(255, 255, 255, 1);
margin-bottom: 20px;
.data_bck,
.data_green,
.data_orgine,
.data_red {
width:23%;
height: 135px;
border-radius: 10px;
position: relative;
text-align: center;
}
.data_bck {
background: url('../assets/index/devices_online.png') no-repeat;
background-size: 100%; // 确保背景图充满容器
}
.data_green {
background: url('../assets/index/online.png') no-repeat;
background-size: 100%; // 确保背景图充满容器
}
.data_orgine {
background: url('../assets/index/add.png') no-repeat;
background-size: 100%; // 确保背景图充满容器
}
.data_red {
background: url('../assets/index/device_yc.png') no-repeat;
background-size: 100%; // 确保背景图充满容器
}
.number {
padding-top:30px;
font-size: 18px;
span {
font-size: 36px;
font-weight: 700;
margin-right: 5px;
}
}
.title_number {
margin-top: 5px;
font-size: 16px;
}
}
// 设备分类/快捷操作卡片样式
.content-row {
background-color: #fff;
height: 215px;
border-radius: 10px;
padding: 15px 25px;
margin-bottom: 20px;
h2 {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 15px 0;
}
.progress-item {
text-align: center;
.progress-text {
font-size: 20px;
color: #666;
.current {
font-weight: bold;
color: #000;
font-size: 23px;
}
}
.progress-name {
margin-top: 20px;
font-size: 16px;
color: #333;
}
}
.quick-item {
cursor: pointer;
text-align: center;
margin-top: 15px;
.quick-img {
width: 80px;
height: 80px;
}
.card_title {
margin-top: 20px;
font-size: 16px;
color: #333;
}
}
}
.tab-group {
display: flex;
gap: 12px;
/* 两个标签的间距 */
align-items: center;
margin-top: 15px;
}
.tab-item {
padding: 3px 10px;
cursor: pointer;
color: #666;
/* 未选中时的文字颜色 */
transition: all 0.2s ease;
font-size: 13px;
}
.tab-item--active {
border-color: #409eff;
/* 选中时的边框颜色(示例用 Element UI 主色) */
color: #409eff;
/* 选中时的文字颜色 */
background-color: rgba(64, 158, 255, 0.05);
/* 可选:选中时的浅背景 */
border: 1px solid rgba(2, 124, 251, 1);
border-radius: 4px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
h2 {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
}
// 图表卡片样式
.region-chart-card {
background: #fff;
border-radius: 10px;
padding: 16px;
height: 360px; // 固定图表卡片高度,避免布局错乱
.card-body {
height: calc(100% - 40px); // 卡片内容区高度减去header高度
}
// 图表容器通用样式(必须设置宽高,否则图表无法渲染)
.chart-container {
width: 100%;
height: 250px;
}
}
// 报警信息区域样式
.alarm-overview {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
.alarm-stats {
// width: 100%;
display: flex;
// flex-direction: column;
gap: 15px;
.stat-item {
text-align: center;
.stat {
font-size: 18px;
font-weight: bold;
}
.stat.red {
color: #ff4d4f;
}
.stat.green {
color: #07BE75;
}
.label {
font-size: 12px;
color: #666;
}
}
}
}
.alarm-items {
height: 100%;
h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 10px 0;
}
}
}
// 响应式适配(小屏幕下调整布局)
@media (max-width: 1200px) {
.home {
.data-item {
flex-wrap: wrap;
gap: 15px;
.data_bck,
.data_green,
.data_orgine,
.data_red {
width: calc(50% - 7.5px); // 小屏幕下2列布局
}
}
.el-col {
&:span-12 {
width: 100%;
margin-bottom: 20px;
}
}
}
// 新增媒体查询,处理更小屏幕尺寸
@media (max-width: 768px) {
.data-item {
.data_bck,
.data_green,
.data_orgine,
.data_red {
width: 100%; // 在更小屏幕上,每个卡片占据一行
margin-bottom: 20px;
}
}
.content-row {
height: auto; // 自动高度,避免固定高度导致内容溢出
padding: 15px;
h2 {
font-size: 16px;
}
.progress-item,
.quick-item {
margin-top: 10px;
}
}
.region-chart-card {
height: auto; // 自动高度,避免固定高度导致内容溢出
.card-body {
height: auto;
}
.chart-container {
height: 200px; // 减少图表高度,适应小屏幕
}
}
.alarm-overview {
.alarm-stats {
flex-direction: column; // 报警统计项改为垂直排列
gap: 10px;
}
}
}
// 针对超小屏幕尺寸的适配
@media (max-width: 480px) {
.data-item {
.data_bck,
.data_green,
.data_orgine,
.data_red {
width: 100%; // 每个卡片占据一行
margin-bottom: 15px;
}
}
.content-row {
padding: 10px;
h2 {
font-size: 14px;
}
.progress-item,
.quick-item {
margin-top: 5px;
}
}
.region-chart-card {
padding: 10px;
.chart-container {
height: 150px; // 进一步减少图表高度
}
}
.alarm-overview {
.alarm-stats {
gap: 5px;
}
}
}
}
</style>