444 lines
16 KiB
Vue
444 lines
16 KiB
Vue
<script setup>
|
|
import { computed, ref } from "vue";
|
|
import Chart from "@/components/chart/index.vue";
|
|
const distributionRange = ref("近一周");
|
|
const reportData = [
|
|
{ id: "AL-001", type: "线缆破损", robot: "巡检机器人 R1", project: "西江隧道", status: "待研判", time: "2026-06-23 10:24:18" },
|
|
{ id: "AL-002", type: "局部高温", robot: "巡检机器人 Q1", project: "青云隧道", status: "待处理", time: "2026-06-23 10:18:42" },
|
|
{ id: "AL-003", type: "墙体裂缝", robot: "巡检机器人 N3", project: "南山隧道", status: "处置完成", time: "2026-06-23 09:56:11" },
|
|
{ id: "AL-004", type: "水渍渗漏", robot: "巡检机器人 R2", project: "西江隧道", status: "误判", time: "2026-06-23 09:20:36" },
|
|
{ id: "AL-005", type: "应急门开启异常", robot: "巡检机器人 N1", project: "南山隧道", status: "待处理", time: "2026-06-23 08:42:57" },
|
|
{ id: "AL-006", type: "局部高温", robot: "巡检机器人 N3", project: "南山隧道", status: "处置完成", time: "2026-06-22 19:12:06" },
|
|
{ id: "AL-007", type: "线缆破损", robot: "巡检机器人 R1", project: "西江隧道", status: "处置完成", time: "2026-06-22 17:46:28" },
|
|
{ id: "AL-008", type: "墙体裂缝", robot: "巡检机器人 Q2", project: "青云隧道", status: "待研判", time: "2026-06-22 16:08:40" },
|
|
{ id: "AL-009", type: "水渍渗漏", robot: "巡检机器人 R4", project: "西江隧道", status: "待处理", time: "2026-06-22 14:21:19" },
|
|
{ id: "AL-010", type: "局部高温", robot: "巡检机器人 N1", project: "南山隧道", status: "误判", time: "2026-06-22 11:03:50" },
|
|
{ id: "AL-011", type: "线缆破损", robot: "巡检机器人 R3", project: "西江隧道", status: "处置完成", time: "2026-06-16 13:14:09" },
|
|
{ id: "AL-012", type: "局部高温", robot: "巡检机器人 Q1", project: "青云隧道", status: "待处理", time: "2026-06-15 16:42:33" },
|
|
{ id: "AL-013", type: "墙体裂缝", robot: "巡检机器人 N2", project: "南山隧道", status: "处置完成", time: "2026-06-12 09:25:50" },
|
|
{ id: "AL-014", type: "水渍渗漏", robot: "巡检机器人 R2", project: "西江隧道", status: "误判", time: "2026-06-08 20:14:27" },
|
|
{ id: "AL-015", type: "应急门开启异常", robot: "巡检机器人 Q3", project: "青云隧道", status: "待研判", time: "2026-05-28 11:38:44" },
|
|
{ id: "AL-016", type: "局部高温", robot: "巡检机器人 N4", project: "南山隧道", status: "处置完成", time: "2026-05-19 15:07:18" },
|
|
{ id: "AL-017", type: "线缆破损", robot: "巡检机器人 R1", project: "西江隧道", status: "待处理", time: "2026-05-06 08:11:52" },
|
|
{ id: "AL-018", type: "墙体裂缝", robot: "巡检机器人 Q2", project: "青云隧道", status: "误判", time: "2026-04-23 14:54:06" },
|
|
{ id: "AL-019", type: "水渍渗漏", robot: "巡检机器人 N1", project: "南山隧道", status: "处置完成", time: "2026-04-12 10:03:16" }
|
|
];
|
|
const trendData = [
|
|
{ date: "06-09", project: "西江隧道", total: 1, closed: 1 },
|
|
{ date: "06-09", project: "青云隧道", total: 1, closed: 0 },
|
|
{ date: "06-09", project: "南山隧道", total: 1, closed: 1 },
|
|
{ date: "06-10", project: "西江隧道", total: 2, closed: 1 },
|
|
{ date: "06-10", project: "青云隧道", total: 1, closed: 0 },
|
|
{ date: "06-10", project: "南山隧道", total: 1, closed: 1 },
|
|
{ date: "06-11", project: "西江隧道", total: 2, closed: 1 },
|
|
{ date: "06-11", project: "青云隧道", total: 1, closed: 1 },
|
|
{ date: "06-11", project: "南山隧道", total: 2, closed: 1 },
|
|
{ date: "06-12", project: "西江隧道", total: 1, closed: 1 },
|
|
{ date: "06-12", project: "青云隧道", total: 1, closed: 1 },
|
|
{ date: "06-12", project: "南山隧道", total: 2, closed: 1 },
|
|
{ date: "06-13", project: "西江隧道", total: 2, closed: 1 },
|
|
{ date: "06-13", project: "青云隧道", total: 2, closed: 1 },
|
|
{ date: "06-13", project: "南山隧道", total: 2, closed: 2 },
|
|
{ date: "06-14", project: "西江隧道", total: 2, closed: 1 },
|
|
{ date: "06-14", project: "青云隧道", total: 1, closed: 1 },
|
|
{ date: "06-14", project: "南山隧道", total: 2, closed: 2 },
|
|
{ date: "06-15", project: "西江隧道", total: 3, closed: 2 },
|
|
{ date: "06-15", project: "青云隧道", total: 2, closed: 1 },
|
|
{ date: "06-15", project: "南山隧道", total: 2, closed: 2 },
|
|
{ date: "06-16", project: "西江隧道", total: 2, closed: 1 },
|
|
{ date: "06-16", project: "青云隧道", total: 2, closed: 1 },
|
|
{ date: "06-16", project: "南山隧道", total: 2, closed: 2 },
|
|
{ date: "06-17", project: "西江隧道", total: 3, closed: 2 },
|
|
{ date: "06-17", project: "青云隧道", total: 2, closed: 1 },
|
|
{ date: "06-17", project: "南山隧道", total: 3, closed: 3 },
|
|
{ date: "06-18", project: "西江隧道", total: 2, closed: 2 },
|
|
{ date: "06-18", project: "青云隧道", total: 2, closed: 1 },
|
|
{ date: "06-18", project: "南山隧道", total: 3, closed: 2 },
|
|
{ date: "06-19", project: "西江隧道", total: 3, closed: 2 },
|
|
{ date: "06-19", project: "青云隧道", total: 2, closed: 2 },
|
|
{ date: "06-19", project: "南山隧道", total: 4, closed: 3 },
|
|
{ date: "06-20", project: "西江隧道", total: 3, closed: 2 },
|
|
{ date: "06-20", project: "青云隧道", total: 2, closed: 2 },
|
|
{ date: "06-20", project: "南山隧道", total: 3, closed: 2 },
|
|
{ date: "06-21", project: "西江隧道", total: 2, closed: 2 },
|
|
{ date: "06-21", project: "青云隧道", total: 2, closed: 1 },
|
|
{ date: "06-21", project: "南山隧道", total: 3, closed: 2 },
|
|
{ date: "06-22", project: "西江隧道", total: 4, closed: 3 },
|
|
{ date: "06-22", project: "青云隧道", total: 2, closed: 1 },
|
|
{ date: "06-22", project: "南山隧道", total: 4, closed: 3 },
|
|
{ date: "06-23", project: "西江隧道", total: 4, closed: 3 },
|
|
{ date: "06-23", project: "青云隧道", total: 3, closed: 2 },
|
|
{ date: "06-23", project: "南山隧道", total: 4, closed: 3 }
|
|
];
|
|
const trendSeries = computed(() => {
|
|
const grouped = trendData.reduce(
|
|
(accumulator, item) => {
|
|
if (!accumulator[item.date]) {
|
|
accumulator[item.date] = { total: 0, closed: 0 };
|
|
}
|
|
accumulator[item.date].total += item.total;
|
|
accumulator[item.date].closed += item.closed;
|
|
return accumulator;
|
|
},
|
|
{}
|
|
);
|
|
return Object.entries(grouped).map(([date, value]) => ({
|
|
date,
|
|
total: value.total,
|
|
closed: value.closed
|
|
}));
|
|
});
|
|
const trendOption = computed(() => {
|
|
const dates = trendSeries.value.map((item) => item.date);
|
|
const totalValues = trendSeries.value.map((item) => item.total);
|
|
const closedValues = trendSeries.value.map((item) => item.closed);
|
|
return {
|
|
grid: {
|
|
left: 24,
|
|
right: 16,
|
|
top: 24,
|
|
bottom: 28,
|
|
containLabel: true
|
|
},
|
|
tooltip: {
|
|
trigger: "axis",
|
|
backgroundColor: "rgba(6, 20, 42, 0.92)",
|
|
borderColor: "rgba(96, 208, 255, 0.24)",
|
|
textStyle: {
|
|
color: "#eefcff"
|
|
}
|
|
},
|
|
legend: {
|
|
show: false
|
|
},
|
|
xAxis: {
|
|
type: "category",
|
|
boundaryGap: false,
|
|
data: dates,
|
|
axisLine: {
|
|
lineStyle: {
|
|
color: "rgba(96, 188, 242, 0.2)"
|
|
}
|
|
},
|
|
axisLabel: {
|
|
color: "rgba(181, 230, 249, 0.74)",
|
|
fontSize: 11
|
|
},
|
|
axisTick: {
|
|
show: false
|
|
}
|
|
},
|
|
yAxis: {
|
|
type: "value",
|
|
splitNumber: 5,
|
|
axisLine: {
|
|
show: false
|
|
},
|
|
axisTick: {
|
|
show: false
|
|
},
|
|
axisLabel: {
|
|
color: "rgba(181, 230, 249, 0.62)",
|
|
fontSize: 11
|
|
},
|
|
splitLine: {
|
|
lineStyle: {
|
|
color: "rgba(97, 189, 242, 0.12)"
|
|
}
|
|
}
|
|
},
|
|
series: [
|
|
{
|
|
name: "新增告警",
|
|
type: "line",
|
|
smooth: true,
|
|
symbol: "circle",
|
|
symbolSize: 7,
|
|
data: totalValues,
|
|
lineStyle: {
|
|
width: 3,
|
|
color: "#74ecff"
|
|
},
|
|
itemStyle: {
|
|
color: "#74ecff",
|
|
borderColor: "#081f3e",
|
|
borderWidth: 2
|
|
},
|
|
areaStyle: {
|
|
color: "rgba(116, 236, 255, 0.12)"
|
|
}
|
|
},
|
|
{
|
|
name: "闭环告警",
|
|
type: "line",
|
|
smooth: true,
|
|
symbol: "circle",
|
|
symbolSize: 7,
|
|
data: closedValues,
|
|
lineStyle: {
|
|
width: 3,
|
|
color: "#62e0a8"
|
|
},
|
|
itemStyle: {
|
|
color: "#62e0a8",
|
|
borderColor: "#081f3e",
|
|
borderWidth: 2
|
|
},
|
|
areaStyle: {
|
|
color: "rgba(98, 224, 168, 0.08)"
|
|
}
|
|
}
|
|
]
|
|
};
|
|
});
|
|
const typeStats = computed(() => {
|
|
const statsMap = /* @__PURE__ */ new Map();
|
|
reportData.forEach((item) => {
|
|
statsMap.set(item.type, (statsMap.get(item.type) ?? 0) + 1);
|
|
});
|
|
return Array.from(statsMap.entries()).map(([label, value]) => ({
|
|
label,
|
|
value
|
|
}));
|
|
});
|
|
const typeRadarOption = computed(() => {
|
|
const maxValue = Math.max(...typeStats.value.map((item) => item.value), 1);
|
|
const indicators = typeStats.value.map((item) => ({
|
|
name: item.label,
|
|
max: maxValue
|
|
}));
|
|
return {
|
|
tooltip: {
|
|
trigger: "item",
|
|
backgroundColor: "rgba(6, 20, 42, 0.92)",
|
|
borderColor: "rgba(96, 208, 255, 0.24)",
|
|
textStyle: {
|
|
color: "#eefcff"
|
|
}
|
|
},
|
|
radar: {
|
|
radius: "68%",
|
|
center: ["50%", "54%"],
|
|
indicator: indicators,
|
|
splitNumber: 4,
|
|
axisName: {
|
|
color: "#dff7ff",
|
|
fontSize: 12
|
|
},
|
|
axisLine: {
|
|
lineStyle: {
|
|
color: "rgba(97, 189, 242, 0.22)"
|
|
}
|
|
},
|
|
splitLine: {
|
|
lineStyle: {
|
|
color: "rgba(97, 189, 242, 0.16)"
|
|
}
|
|
},
|
|
splitArea: {
|
|
areaStyle: {
|
|
color: ["rgba(16, 52, 94, 0.10)", "rgba(16, 52, 94, 0.05)"]
|
|
}
|
|
}
|
|
},
|
|
series: [
|
|
{
|
|
type: "radar",
|
|
data: [
|
|
{
|
|
value: typeStats.value.map((item) => item.value),
|
|
name: "告警类型分布",
|
|
areaStyle: {
|
|
color: "rgba(73, 207, 255, 0.20)"
|
|
},
|
|
lineStyle: {
|
|
color: "#61dcff",
|
|
width: 2.5
|
|
},
|
|
itemStyle: {
|
|
color: "#8ff3ff",
|
|
borderColor: "#07203e",
|
|
borderWidth: 2
|
|
},
|
|
symbolSize: 8
|
|
}
|
|
]
|
|
}
|
|
]
|
|
};
|
|
});
|
|
const statusStats = computed(() => {
|
|
const order = ["待研判", "待处理", "处置完成", "误判"];
|
|
const total = reportData.length || 1;
|
|
return order.map((status) => {
|
|
const value = reportData.filter((item) => item.status === status).length;
|
|
return {
|
|
label: status,
|
|
value,
|
|
percent: `${Math.round(value / total * 100)}%`
|
|
};
|
|
});
|
|
});
|
|
const statusPieOption = computed(() => ({
|
|
tooltip: {
|
|
trigger: "item",
|
|
backgroundColor: "rgba(6, 20, 42, 0.92)",
|
|
borderColor: "rgba(96, 208, 255, 0.24)",
|
|
textStyle: {
|
|
color: "#eefcff"
|
|
}
|
|
},
|
|
legend: {
|
|
show: false
|
|
},
|
|
series: [
|
|
{
|
|
type: "pie",
|
|
radius: ["46%", "72%"],
|
|
center: ["50%", "50%"],
|
|
avoidLabelOverlap: false,
|
|
itemStyle: {
|
|
borderColor: "#081f3e",
|
|
borderWidth: 3
|
|
},
|
|
label: {
|
|
show: true,
|
|
color: "#dff7ff",
|
|
formatter: "{b}\n{d}%",
|
|
fontSize: 12
|
|
},
|
|
labelLine: {
|
|
lineStyle: {
|
|
color: "rgba(181, 230, 249, 0.52)"
|
|
}
|
|
},
|
|
data: statusStats.value.map((item) => ({
|
|
name: item.label,
|
|
value: item.value,
|
|
itemStyle: {
|
|
color: item.label === "待研判" ? "#ffd66e" : item.label === "待处理" ? "#ff8b78" : item.label === "处置完成" ? "#62e0a8" : "#8ea7ff"
|
|
}
|
|
}))
|
|
}
|
|
]
|
|
}));
|
|
const projectDistribution = computed(() => {
|
|
const now = /* @__PURE__ */ new Date("2026-06-23T23:59:59");
|
|
const rangeDaysMap = {
|
|
近一周: 7,
|
|
近一个月: 30,
|
|
近三个月: 90
|
|
};
|
|
const threshold = new Date(
|
|
now.getTime() - rangeDaysMap[distributionRange.value] * 24 * 60 * 60 * 1e3
|
|
);
|
|
const data = reportData.filter((item) => new Date(item.time.replace(" ", "T")) >= threshold);
|
|
const statsMap = /* @__PURE__ */ new Map();
|
|
data.forEach((item) => {
|
|
statsMap.set(item.project, (statsMap.get(item.project) ?? 0) + 1);
|
|
});
|
|
const maxValue = Math.max(...statsMap.values(), 1);
|
|
return Array.from(statsMap.entries()).map(([label, value]) => ({
|
|
label,
|
|
value,
|
|
width: `${value / maxValue * 100}%`
|
|
})).sort((a, b) => b.value - a.value);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="page-shell report-page report-dashboard-page">
|
|
<div class="report-dashboard">
|
|
<section class="report-main-grid">
|
|
<div class="page-card report-panel trend-panel">
|
|
<div class="report-panel-header">
|
|
<div class="page-card-title">告警趋势分析</div>
|
|
</div>
|
|
<div class="trend-chart">
|
|
<Chart :option="trendOption" />
|
|
</div>
|
|
<div class="trend-legend">
|
|
<span><i class="legend-dot total"></i>新增告警</span>
|
|
<span><i class="legend-dot closed"></i>闭环告警</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="page-card report-panel type-panel">
|
|
<div class="report-panel-header">
|
|
<div class="page-card-title">告警类型分布</div>
|
|
</div>
|
|
<div class="type-radar-layout">
|
|
<div class="type-radar-chart">
|
|
<Chart :option="typeRadarOption" />
|
|
</div>
|
|
<div class="type-radar-legend">
|
|
<div v-for="item in typeStats" :key="item.label" class="type-legend-item">
|
|
<span class="type-legend-name">{{ item.label }}</span>
|
|
<span class="type-legend-value">{{ item.value }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="page-card report-panel status-panel">
|
|
<div class="report-panel-header">
|
|
<div class="page-card-title">案件状态分布</div>
|
|
</div>
|
|
<div class="status-pie-layout">
|
|
<div class="status-pie-chart">
|
|
<Chart :option="statusPieOption" />
|
|
</div>
|
|
<div class="status-pie-legend">
|
|
<div v-for="item in statusStats" :key="item.label" class="status-legend-item">
|
|
<span
|
|
:class="[
|
|
'status-legend-dot',
|
|
item.label === '待研判'
|
|
? 'pending'
|
|
: item.label === '待处理'
|
|
? 'handling'
|
|
: item.label === '处置完成'
|
|
? 'finished'
|
|
: 'false',
|
|
]"
|
|
></span>
|
|
<span class="status-legend-name">{{ item.label }}</span>
|
|
<span class="status-legend-value">{{ item.value }}</span>
|
|
<span class="status-legend-percent">{{ item.percent }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="page-card report-panel ranking-panel">
|
|
<div class="report-panel-header">
|
|
<div class="page-card-title">项目告警分布</div>
|
|
<div class="report-filter-tabs">
|
|
<button
|
|
v-for="range in ['近一周', '近一个月', '近三个月']"
|
|
:key="range"
|
|
:class="['report-filter-tab', { active: distributionRange === range }]"
|
|
type="button"
|
|
@click="distributionRange = range"
|
|
>
|
|
{{ range }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="report-bar-list">
|
|
<div v-for="item in projectDistribution" :key="item.label" class="report-bar-row">
|
|
<div class="bar-row-head">
|
|
<span>{{ item.label }}</span>
|
|
<span>{{ item.value }} 次</span>
|
|
</div>
|
|
<div class="report-bar-track">
|
|
<span class="report-bar-fill blue" :style="{ width: item.width }"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</template>
|