feat: 视频分析对接、首页完善联调数据

dev
donghao 1 day ago
parent d8c58dd183
commit f00434a820

@ -2,7 +2,7 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-03-07 15:09:18
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-08-26 16:54:54
* @LastEditTime: 2025-09-10 15:38:28
* @FilePath: \5G-Loading-Bay-Web\src\api\dashboard.ts
* @Description: ,`customMade`, koroFileHeader : https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
@ -104,4 +104,25 @@ export const getRecordAmountDataApi = (params: any) => {
// 图表统计2 ?type=pole&dateType=day 这个是日异常类型的接口
export const getRecordFaultTypeAmountDataApi = (params: any) => {
return request.get(`/api/v1/record/get_record_fault_type_amount_data/`, params);
};
// 近期车辆缺陷列表 ?count=6
export const getHomeTrainDataApi = (params: any) => {
return request.get(`/api/v1/record/get_home_train_data/`, params);
};
// 近期车辆缺陷详情 /api/v1/record/get_train_detail_data/?id=1
export const getHomeTrainDetailDataApi = (params: any) => {
return request.get(`/api/v1/record/get_train_detail_data/`, params);
};
// 导出excel /api/v1/record/get_excel_data/?id=1
export const getExcelDataApi = (params: any) => {
return request.get(`/api/v1/record/get_excel_data/`, params);
};
// /api/v1/record/get_detect_record_data/?type=appearance 视频分析
export const getDetectRecordDataApi = (params: any) => {
return request.get(`/api/v1/record/get_detect_record_data/`, params);
};

@ -1,5 +1,13 @@
<script setup lang="ts">
import * as echarts from "echarts";
import { useRouter } from 'vue-router'
const router = useRouter()
const props = defineProps({
datas: {
type: Array as Record<string, any>[],
default: () => [],
},
});
interface ChartItem {
percent: number;
desc: string;
@ -11,34 +19,7 @@ interface ChartItem {
const mainChartRef = ref<HTMLDivElement | null>(null);
//
let mainChart: echarts.ECharts | null = null;
//
const colors = [
[
{
offset: 0,
color: "rgba(14, 189, 232, 1)", // 0%
},
{
offset: 1,
color: "#195EE9", // 100%
},
],
[
{
offset: 0,
color: "#FFD682", // 0%
},
{
offset: 1,
color: "#FEB217", // 100%
},
],
];
const chartData = [
{ value: 65, name: "撑杆折断", color: "#488AFE" },
{ value: 35, name: "撑杆弯曲", color: "#F7D36D" },
];
function Pie() {
let dataArr = [];
for (var i = 0; i < 150; i++) {
@ -70,7 +51,39 @@ function Pie() {
const initMainChart = () => {
const chartDom = document.getElementById("poleMonitorChart");
const mainChart = echarts.init(chartDom);
const centerPoint = ["50%", "41%"];
//
const colors = [
[
{
offset: 0,
color: "rgba(14, 189, 232, 1)", // 0%
},
{
offset: 1,
color: "#195EE9", // 100%
},
],
[
{
offset: 0,
color: "#FFD682", // 0%
},
{
offset: 1,
color: "#FEB217", // 100%
},
],
];
const seriesData = props.datas;
const legendData = [];
seriesData.map((item) => {
legendData.push(item.name);
});
const option = {
backgroundColor: "transparent",
tooltip: {
@ -80,11 +93,12 @@ const initMainChart = () => {
legend: {
orient: "horizontal",
bottom: 20,
selectedMode: false, //
x: "center",
textStyle: {
color: "#fff",
},
data: ["撑杆折断", "撑杆弯曲"],
data: legendData,
},
series: [
{
@ -108,16 +122,7 @@ const initMainChart = () => {
};
},
},
data: [
{
value: 735,
name: "撑杆折断",
},
{
value: 310,
name: "撑杆弯曲",
},
],
data: seriesData,
labelLine: {
show: false,
},
@ -339,6 +344,17 @@ const initMainChart = () => {
};
mainChart.setOption(option);
//
mainChart.on("click", (params) => {
//
params?.name && router.push({
name: "PoleMonitor",
query: {
name: params?.name
}
});
console.log(params, "点击事件");
});
};
//
@ -346,11 +362,6 @@ const handleResize = () => {
mainChart?.resize();
};
onMounted(() => {
initMainChart();
window.addEventListener("resize", handleResize);
});
//
const cleanup = () => {
window.removeEventListener("resize", handleResize);
@ -361,6 +372,18 @@ const cleanup = () => {
watchEffect((onCleanup) => {
onCleanup(cleanup);
});
watch(
() => props.datas,
(newVal) => {
if (newVal) {
initMainChart();
window.addEventListener("resize", handleResize);
}
},
{
deep: true,
}
);
</script>
<template>

@ -1,5 +1,14 @@
<script setup lang="ts">
import * as echarts from "echarts";
import { useRouter } from 'vue-router'
const router = useRouter()
const props = defineProps({
datas: {
type: Array as Record<string, any>[],
default: () => [],
},
});
function generateData(totalNum, bigvalue, smallvalue, color) {
let dataArr = [];
for (var i = 0; i < totalNum; i++) {
@ -38,33 +47,10 @@ function initChart() {
"#40c057",
"#ffd351",
"#ff8e43",
"#ff5151",
"#8e2de6",
];
let echartData = [
{
name: "小门搭扣丢失",
value: "3720",
},
{
name: "门折页座脱落",
value: "2920",
},
{
name: "小窗裂纹",
value: "2200",
},
{
name: "小门外胀",
value: "1420",
},
{
name: "搭扣未搭",
value: "3200",
},
{
name: "下侧门板缺失",
value: "2420",
},
];
let echartData = props.datas;
let formatNumber = function (num) {
let reg = /(?=(\B)(\d{3})+$)/g;
return num.toString().replace(reg, ",");
@ -162,10 +148,29 @@ function initChart() {
],
};
mainChart.setOption(option);
//
mainChart.on("click", (params) => {
//
params?.name && router.push({
name: "AppearanceMonitor",
query: {
name: params?.name
}
});
console.log(params, "点击事件");
});
}
onMounted(() => {
initChart();
});
watch(
() => props.datas,
(newVal) => {
if (newVal) {
initChart();
}
},
{
deep: true,
}
);
</script>
<template>

@ -0,0 +1,147 @@
<!--
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-08-14 13:38:30
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-09-10 11:42:43
* @FilePath: \5G-Web\src\views\dashboard\components\SwpierMonitor.vue
* @Description: 外观撑杆统一封装轮播图&视频组件
-->
<script lang="ts" setup>
import Player from "@/components/videoPlayer/Player.vue";
import SwiperPlayer from "./swiperPlayer.vue";
import { Swiper, SwiperSlide } from "swiper/vue";
import { Navigation, Scrollbar, Mousewheel, Pagination } from "swiper/modules";
import "swiper/css";
import "swiper/scss";
import "swiper/scss/navigation";
interface Props {
fileList: string[];
}
interface Emits {
(e: "update:value", val: boolean): void;
}
const props = withDefaults(defineProps<Props>(), {
fileList: [],
});
const emit = defineEmits<Emits>();
const modules = [Navigation, Scrollbar];
const activeIndex = ref(-1);
const swiperRef = ref(null);
// const isPlaying = ref<boolean>(false); //
const currFile = ref<Record<string, any>>({}); //
// const togglePlay = () => {
// isPlaying.value = !isPlaying.value;
// };
const onSwiper = (swiper) => {
swiperRef.value = swiper;
console.log("Swiper 实例已获取:", swiper);
};
const onSlideChange = () => {
console.log("slide change");
};
watch(
() => props.fileList,
(newVal) => {
currFile.value = newVal?.[0];
},
{ deep: true }
);
</script>
<template>
<div class="w-full h-full swiper-images-wrap">
<!-- 缩略图区域 -->
<div class="flex" v-if="fileList?.length > 0">
<div class="thumbnail-container">
<swiper
ref="swiperRef"
:modules="modules"
:slides-per-view="2"
:space-between="12"
:navigation="true"
:scrollbar="{ draggable: false }"
:centered-slides="false"
:observer="true"
:observeParents="true"
@swiper="onSwiper"
@slideChange="onSlideChange"
>
<swiper-slide
v-for="(file, index) in fileList"
:key="index"
:class="{ 'active-slide': activeIndex === index }"
>
<img
:src="file"
v-if="file"
class="cursor-pointer"
/>
<div v-else>
<!-- 视频图片加载失败 -->
</div>
</swiper-slide>
</swiper>
</div>
</div>
</div>
</template>
<style lang="scss">
.swiper-images-wrap {
.thumbnail-container {
overflow: visible;
margin-right: 16px;
.swiper {
width: 100%;
height: 100%;
max-width: 1150px;
.swiper-slide {
img,
video {
border-radius: 4px;
object-fit: cover;
overflow: hidden;
}
}
.active-slide img,
.active-slide video {
width: 568px;
border-radius: 4px;
border: 2px solid #2ecce0;
}
.swiper-button-prev,
.swiper-button-next {
background-color: rgba(0, 0, 0, 0.5);
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
}
.swiper-button-prev::after,
.swiper-button-next::after {
font-size: 12px;
color: #fff;
}
/* 修改按钮悬停样式 */
.swiper-button-prev:hover,
.swiper-button-next:hover {
background-color: rgba(0, 0, 0, 0.8);
}
}
}
}
</style>

@ -0,0 +1,23 @@
export const useDict = () => {
// 外观监测故障类型
const fetchAppearanceFaultTypeList = () => {
return [
"侧开门门板变形",
"侧墙板外胀",
"扶手变形",
"扶梯变形",
"配属标记不清",
"下侧门折页圆销丢失",
"载重标记不清",
"搭扣未搭",
];
};
const fetchPoleFaultTypeList = () => {
return ["钢管破损", "钢管锈蚀"];
};
return {
fetchAppearanceFaultTypeList,
fetchPoleFaultTypeList
};
};

@ -1,18 +1,19 @@
import ExcelJS from "exceljs";
interface TrainAlarmDataProps {
train_carriage_number: string;
created_at: string;
alarm_type: string;
fault_type: string;
image_url: string;
}
interface TrainReportData {
train_id: string;
arrive_at: string;
leave_at: string | null;
train_data: { carriage_number: string }[];
appearance_data: {
carriage_number: string;
carriage_type: string;
occurrence_time: string;
warning_type: string;
fault_type: string;
defect_image: string;
}[];
appearance_alarm_data: TrainAlarmDataProps[];
role_alarm_data: TrainAlarmDataProps[];
}
const formatDateTime = (dateTimeStr: string) => {
@ -92,7 +93,6 @@ const downloadImage = async (url: string): Promise<ExcelJS.Image | null> => {
return null;
}
}
const arrayBuffer = await response.arrayBuffer();
const extension = finalUrl.split(".").pop()?.toLowerCase() || "jpeg";
const { width, height } = await getImageDimensions(finalUrl);
@ -159,23 +159,22 @@ export const exportTrainReport = async (data: TrainReportData) => {
trainSheet.getCell(`A${row}`).value = `车厢 ${index + 1}`;
trainSheet.getCell(`B${row}`).value = carriage.carriage_number;
});
// 创建缺陷表
// 2. 创建"车身缺陷记录"工作表
const defectSheet = workbook.addWorksheet("车身缺陷记录");
const appearanceSheet = workbook.addWorksheet("车身缺陷记录");
const alarmAppearanceData = data.appearance_alarm_data;
// 修改列定义
defectSheet.columns = [
{ header: "车厢号", key: "carriage_number", width: 15 },
// { header: "车型", key: "carriage_type", width: 20 },
{ header: "发生时间", key: "occurrence_time", width: 20 },
{ header: "告警类型", key: "warning_type", width: 15 },
appearanceSheet.columns = [
{ header: "车厢号", key: "train_carriage_number", width: 15 },
{ header: "发生时间", key: "created_at", width: 20 },
{ header: "告警类型", key: "alarm_type", width: 15 },
{ header: "缺陷类型", key: "fault_type", width: 15 },
{ header: "缺陷图片", key: "thumbnail", width: 15 }, // 缩略图列
// { header: "查看原图", key: "defect_image", width: 15 }, // 原图链接列
// { header: "查看原图", key: "image_url", width: 15 }, // 原图链接列
];
// 设置表头样式
defectSheet.getRow(1).eachCell((cell) => {
appearanceSheet.getRow(1).eachCell((cell) => {
cell.font = { bold: true };
cell.fill = {
type: "pattern",
@ -191,25 +190,21 @@ export const exportTrainReport = async (data: TrainReportData) => {
});
// 设置所有行高60像素≈45磅
defectSheet.properties.defaultRowHeight = 45;
appearanceSheet.properties.defaultRowHeight = 45;
// 添加所有缺陷记录
for (let i = 0; i < data.appearance_data.length; i++) {
const item = data.appearance_data[i];
alarmAppearanceData.forEach(async (item, i) => {
const rowIndex = i + 2; // 从第二行开始(第一行是表头)
// 添加文本数据
defectSheet.addRow({
carriage_number: item.carriage_number,
carriage_type: item.carriage_type,
occurrence_time: formatDateTime(item.occurrence_time),
warning_type: item.warning_type,
appearanceSheet.addRow({
train_carriage_number: item.train_carriage_number,
created_at: formatDateTime(item.created_at),
alarm_type: item.alarm_type,
fault_type: item.fault_type,
});
// 使用指定图片URL
const imageUrl = item.defect_image || DEFAULT_IMAGE_URL;
const imageUrl = item.image_url || DEFAULT_IMAGE_URL;
try {
// 下载图片
// const image = await downloadImage(imageUrl);
@ -220,29 +215,90 @@ export const exportTrainReport = async (data: TrainReportData) => {
base64: base64Image,
extension: "png", // Adjust extension if needed (jpeg, etc.)
});
appearanceSheet.addImage(imageId, {
tl: { col: appearanceSheet.columns.length - 1, row: rowIndex - 1 }, // F列是第5列0-based
// br: { col: 6, row: rowIndex },
ext: {
width: 80,
height: 45,
},
editAs: "oneCell",
});
}
} catch (error) {
console.error(`图片添加失败 (行 ${rowIndex}):`, error);
const errorCell = appearanceSheet.getCell(`E${rowIndex}`);
errorCell.value = "图片加载失败";
errorCell.font = {
color: { argb: "FFFF0000" }, // 红色
italic: true,
};
}
});
// for (let i = 0; i < alarmAppearanceData.length; i++) {
// const item = alarmAppearanceData[i];
// const rowIndex = i + 2; // 从第二行开始(第一行是表头)
// }
// 3. 创建"撑杆缺陷记录"工作表
const poleSheet = workbook.addWorksheet("撑杆缺陷记录");
const alarmRoleData = data.role_alarm_data;
// 修改列定义
poleSheet.columns = [
{ header: "车厢号", key: "train_carriage_number", width: 15 },
{ header: "发生时间", key: "created_at", width: 20 },
{ header: "告警类型", key: "alarm_type", width: 15 },
{ header: "缺陷类型", key: "fault_type", width: 15 },
{ header: "缺陷图片", key: "thumbnail", width: 15 }, // 缩略图列
// { header: "查看原图", key: "image_url", width: 15 }, // 原图链接列
];
// 设置表头样式
poleSheet.getRow(1).eachCell((cell) => {
cell.font = { bold: true };
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFDDEBF7" }, // 浅蓝色背景
};
cell.border = {
top: { style: "thin" },
left: { style: "thin" },
bottom: { style: "thin" },
right: { style: "thin" },
};
});
// TODO : 调整图片大小和位置 (根据需要调整)
// // 计算缩放比例和显示尺寸
// const maxSize = 30; // 最大边长30像素
// const scale = Math.min(maxSize / image.width, maxSize / image.height);
// const displayWidth = image.width * scale;
// const displayHeight = image.height * scale;
// 设置所有行高60像素≈45磅
poleSheet.properties.defaultRowHeight = 45;
// 添加所有缺陷记录
for (let i = 0; i < alarmRoleData.length; i++) {
const item = alarmRoleData[i];
const rowIndex = i + 2; // 从第二行开始(第一行是表头)
// // 列宽(像素)和行高(像素)
// const colWidthPixels = 105; // 15字符 * 7
// const rowHeightPixels = 60; // 45磅 * (96/72) = 60像素
// 添加文本数据
poleSheet.addRow({
train_carriage_number: item.train_carriage_number,
created_at: formatDateTime(item.created_at),
alarm_type: item.alarm_type,
fault_type: item.fault_type,
});
// // 计算偏移量(居中)
// const offsetXPixels = (colWidthPixels - displayWidth) / 2;
// const offsetYPixels = (rowHeightPixels - displayHeight) / 2;
// 使用指定图片URL
const imageUrl = item.image_url || DEFAULT_IMAGE_URL;
// // 转换为EMU
// const offsetXEMU = pixelsToEMU(offsetXPixels);
// const offsetYEMU = pixelsToEMU(offsetYPixels);
// const widthEMU = pixelsToEMU(displayWidth);
// const heightEMU = pixelsToEMU(displayHeight);
defectSheet.addImage(imageId, {
tl: { col: defectSheet.columns.length - 1, row: rowIndex - 1 }, // F列是第5列0-based
try {
// 下载图片
// const image = await downloadImage(imageUrl);
// console.log("图片下载成功", image);
const base64Image = await convertImageToBase64(imageUrl);
if (base64Image) {
const imageId = workbook.addImage({
base64: base64Image,
extension: "png", // Adjust extension if needed (jpeg, etc.)
});
poleSheet.addImage(imageId, {
tl: { col: poleSheet.columns.length - 1, row: rowIndex - 1 }, // F列是第5列0-based
// br: { col: 6, row: rowIndex },
ext: {
width: 80,
@ -250,12 +306,10 @@ export const exportTrainReport = async (data: TrainReportData) => {
},
editAs: "oneCell",
});
}
} catch (error) {
console.error(`图片添加失败 (行 ${rowIndex}):`, error);
const errorCell = defectSheet.getCell(`E${rowIndex}`);
const errorCell = poleSheet.getCell(`E${rowIndex}`);
errorCell.value = "图片加载失败";
errorCell.font = {
color: { argb: "FFFF0000" }, // 红色
@ -263,8 +317,7 @@ export const exportTrainReport = async (data: TrainReportData) => {
};
}
}
// 3. 生成Excel文件
// final 生成Excel文件
const buffer = await workbook.xlsx.writeBuffer();
try {
const blob = new Blob([buffer], {
@ -283,110 +336,111 @@ export const exportTrainReport = async (data: TrainReportData) => {
// ... formatDateTime 函数保持不变 ...
export const useTrainSaveToExcel = (data) => {
console.log("useTrainSaveToExcel_data", data);
// 模拟数据 (实际应用中应通过API获取)
const reportData = {
train_id: "DF4-1234",
arrive_at: "2023-06-15 08:30:00",
leave_at: "2023-06-15 16:45:22",
train_data: [
data: [
{ carriage_number: "CK-1001" },
{ carriage_number: "CK-1002" },
{ carriage_number: "CK-1003" },
],
appearance_data: [
appearance_alarm_data: [
{
carriage_number: "CK-1001",
train_carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
occurrence_time: "2023-06-15 09:15:34",
warning_type: "AI智能识别告警",
created_at: "2023-06-15 09:15:34",
alarm_type: "AI智能识别告警",
fault_type: "车体变形",
defect_image:
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1002",
train_carriage_number: "CK-1002",
carriage_type: "P70型棚车",
occurrence_time: "2023-06-15 10:22:18",
warning_type: "人工巡检报告",
created_at: "2023-06-15 10:22:18",
alarm_type: "人工巡检报告",
fault_type: "油漆剥落",
defect_image:
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1001",
{
train_carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
occurrence_time: "2023-06-15 09:15:34",
warning_type: "AI智能识别告警",
created_at: "2023-06-15 09:15:34",
alarm_type: "AI智能识别告警",
fault_type: "车体变形",
defect_image:
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1002",
train_carriage_number: "CK-1002",
carriage_type: "P70型棚车",
occurrence_time: "2023-06-15 10:22:18",
warning_type: "人工巡检报告",
created_at: "2023-06-15 10:22:18",
alarm_type: "人工巡检报告",
fault_type: "油漆剥落",
defect_image:
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1001",
{
train_carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
occurrence_time: "2023-06-15 09:15:34",
warning_type: "AI智能识别告警",
created_at: "2023-06-15 09:15:34",
alarm_type: "AI智能识别告警",
fault_type: "车体变形",
defect_image:
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1002",
train_carriage_number: "CK-1002",
carriage_type: "P70型棚车",
occurrence_time: "2023-06-15 10:22:18",
warning_type: "人工巡检报告",
created_at: "2023-06-15 10:22:18",
alarm_type: "人工巡检报告",
fault_type: "油漆剥落",
defect_image:
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1001",
{
train_carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
occurrence_time: "2023-06-15 09:15:34",
warning_type: "AI智能识别告警",
created_at: "2023-06-15 09:15:34",
alarm_type: "AI智能识别告警",
fault_type: "车体变形",
defect_image:
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1002",
train_carriage_number: "CK-1002",
carriage_type: "P70型棚车",
occurrence_time: "2023-06-15 10:22:18",
warning_type: "人工巡检报告",
created_at: "2023-06-15 10:22:18",
alarm_type: "人工巡检报告",
fault_type: "油漆剥落",
defect_image:
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1001",
{
train_carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
occurrence_time: "2023-06-15 09:15:34",
warning_type: "AI智能识别告警",
created_at: "2023-06-15 09:15:34",
alarm_type: "AI智能识别告警",
fault_type: "车体变形",
defect_image:
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
carriage_number: "CK-1002",
train_carriage_number: "CK-1002",
carriage_type: "P70型棚车",
occurrence_time: "2023-06-15 10:22:18",
warning_type: "人工巡检报告",
created_at: "2023-06-15 10:22:18",
alarm_type: "人工巡检报告",
fault_type: "油漆剥落",
defect_image:
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
],
};
return {
saveToExcel: () => exportTrainReport(reportData),
saveToExcel: () => exportTrainReport(data),
};
};

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { onBeforeRouteLeave } from "vue-router";
import { onBeforeRouteLeave, useRoute } from "vue-router";
import { BaseTable } from "@/components/CustomTable";
import SwiperMonitor from "./components/SwiperMonitor.vue";
import AppearanceDailyDetectionChart from "@/components/Charts/appearanceDailyDetectionChart.vue";
@ -12,17 +12,20 @@ import {
getAppearanceMonitorDetailApi,
getBeforeMonitorDetailApi,
getRecordAmountDataApi,
getRecordFaultTypeAmountDataApi
getRecordFaultTypeAmountDataApi,
} from "@/api/dashboard";
import { isSuccessApi } from "@/utils/forApi";
import { useWebSocketStore } from "@/stores/websocketStore";
import { useDict } from "@/hooks/useDict";
const route = useRoute();
// const isPointOpen = ref<Boolean>(false); //
const isAlarmOpen = ref<Boolean>(false); //
const isDeleteOpen = ref<Boolean>(false); //
const websocketStore = useWebSocketStore();
const { fetchAppearanceFaultTypeList } = useDict();
const faultTypeOptions = fetchAppearanceFaultTypeList();
// messages
watch(
() => websocketStore.messages,
@ -63,12 +66,12 @@ const columns = [
{
label: "告警类型",
property: "alarm_type",
width: 90,
// width: 90,
},
{
label: "故障类型",
property: "fault_type",
width: 90,
// width: 90,
},
{
label: "等级",
@ -78,6 +81,7 @@ const columns = [
{
label: "时间",
property: "created_at",
width: 180,
},
{
type: "action",
@ -95,7 +99,7 @@ const currBeforeFileList = ref<Record<string, any>[]>([]); // 选中行历史文
const searchForm = reactive({
train_number: "",
train_carriage_number: "",
fault_type: "",
fault_type: route?.query?.name || "",
station: "",
type: "appearance",
});
@ -104,7 +108,6 @@ const dataLoading = ref(true);
const recordAmountData = ref({}); //
const recordFaultData = ref({}); //
//
const getFileList = async () => {
try {
@ -183,34 +186,34 @@ const getList = async () => {
};
// 1
const fetchRecordAmountData = async () => {
try {
try {
const res = await getRecordAmountDataApi({
type: "appearance",
dateType: "day",
});
console.log(res.data, "fetchRecordAmountData_data");
if (isSuccessApi(res)) {
recordAmountData.value = res.data;
recordAmountData.value = res.data;
}
} catch (error) {
console.error("获取数据失败:", error);
}
}
};
// 2
const fetchRecordFaultTypeAmountData = async () => {
try {
try {
const res = await getRecordFaultTypeAmountDataApi({
type: "appearance",
dateType: "day",
});
console.log(res.data, "fetchRecordFaultTypeAmountData_data");
if (isSuccessApi(res)) {
recordFaultData.value = res.data;
recordFaultData.value = res.data;
}
} catch (error) {
console.error("获取数据失败:", error);
}
}
};
//
const handleQuery = () => {
@ -269,8 +272,8 @@ onBeforeRouteLeave(() => {
onMounted(() => {
getList();
fetchRecordAmountData()
fetchRecordFaultTypeAmountData()
fetchRecordAmountData();
fetchRecordFaultTypeAmountData();
});
</script>
<template>
@ -311,12 +314,12 @@ onMounted(() => {
placeholder="故障类型"
class="custom-select"
>
<el-option label="下侧门板缺失" value="下侧门板缺失"></el-option>
<el-option label="门折页座脱落" value="门折页座脱落"></el-option>
<el-option label="小门塔扣丢失" value="小门塔扣丢失"></el-option>
<el-option label="小窗裂纹" value="小窗裂纹"></el-option>
<el-option label="搭扣未搭" value="搭扣未搭"></el-option>
<el-option label="小门外胀" value="小门外胀"></el-option>
<el-option
v-for="item in faultTypeOptions"
:key="item.value"
:label="item"
:value="item"
></el-option>
</el-select>
<el-button
type="primary"

@ -17,6 +17,8 @@ import {
getDeviceInfowApi,
getRecordFaultApi,
getRealTimeApi,
getHomeTrainDataApi,
getHomeTrainDetailDataApi,
} from "@/api/dashboard";
import { isSuccessApi } from "@/utils/forApi";
import { isArray } from "@/utils/is";
@ -40,18 +42,17 @@ const colorArr = [
];
const searchForm = reactive({
car: "1",
pole: "1",
appearanceTime: "9",
poleTime: "9",
});
const carFaultTotal = ref([]);
const poleFaultTotal = ref([]);
const appearanceFaultData = ref([]);
const poleFaultData = ref([]);
const imageFault = ref([]);
const activeBtn = ref("month");
const isAlarmOpen = ref<Boolean>(false); //
const currentRow = ref<Record<string, any>>({}); //
const currFileList = ref<Record<string, any>[]>([]); //
const deviceInfo = ref({
total: 0,
onlineCount: 0,
@ -59,331 +60,17 @@ const deviceInfo = ref({
outlineCount: 0,
});
const trainListData = ref([
{
arrive_img_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
created_at: "2023-10-01 10:00:00",
device_position: "车体检测设备",
data: {
train_data: [
{
model: "C54K",
carriage_number: "01-48937",
},
{
model: "C54K",
carriage_number: "01-34895",
},
{
model: "C54K",
carriage_number: "00-50912",
},
{
model: "C54K",
carriage_number: "02-18734",
},
{
model: "C54K",
carriage_number: "04-99681",
},
{
model: "C54K",
carriage_number: "04-71120",
},
{
model: "C54K",
carriage_number: "04-50236",
},
{
model: "C54K",
carriage_number: "02-17084",
},
{
model: "C54K",
carriage_number: "00-12068",
},
{
model: "C54K",
carriage_number: "00-26203",
},
{
model: "C54K",
carriage_number: "00-67639",
},
{
model: "C54K",
carriage_number: "01-31401",
},
{
model: "C54K",
carriage_number: "00-33740",
},
{
model: "C54K",
carriage_number: "01-74883",
},
{
model: "C54K",
carriage_number: "02-32217",
},
{
model: "C54K",
carriage_number: "00-52434",
},
{
model: "C54K",
carriage_number: "02-32892",
},
{
model: "C54K",
carriage_number: "03-82204",
},
{
model: "C54K",
carriage_number: "04-20857",
},
{
model: "C54K",
carriage_number: "03-14256",
},
{
model: "C54K",
carriage_number: "03-28256",
},
{
model: "C54K",
carriage_number: "02-27349",
},
{
model: "C54K",
carriage_number: "02-49452",
},
{
model: "C54K",
carriage_number: "01-37996",
},
{
model: "C54K",
carriage_number: "02-81300",
},
{
model: "C54K",
carriage_number: "01-37644",
},
{
model: "C54K",
carriage_number: "03-66450",
},
{
model: "C54K",
carriage_number: "04-17540",
},
{
model: "C54K",
carriage_number: "00-83994",
},
{
model: "C54K",
carriage_number: "03-13204",
},
{
model: "C54K",
carriage_number: "00-67918",
},
{
model: "C54K",
carriage_number: "00-26850",
},
{
model: "C54K",
carriage_number: "03-28183",
},
{
model: "C54K",
carriage_number: "00-85336",
},
{
model: "C54K",
carriage_number: "02-32747",
},
{
model: "C54K",
carriage_number: "01-89385",
},
{
model: "C54K",
carriage_number: "01-97885",
},
{
model: "C54K",
carriage_number: "04-79829",
},
{
model: "C54K",
carriage_number: "00-27585",
},
{
model: "C54K",
carriage_number: "03-56310",
},
{
model: "C54K",
carriage_number: "02-62317",
},
{
model: "C54K",
carriage_number: "01-55888",
},
{
model: "C54K",
carriage_number: "04-32385",
},
{
model: "C54K",
carriage_number: "01-75685",
},
{
model: "C54K",
carriage_number: "03-45210",
},
{
model: "C54K",
carriage_number: "01-35511",
},
{
model: "C54K",
carriage_number: "00-30912",
},
{
model: "C54K",
carriage_number: "03-61047",
},
{
model: "C54K",
carriage_number: "00-24265",
},
{
model: "C54K",
carriage_number: "01-24135",
},
{
model: "C54K",
carriage_number: "00-16077",
},
{
model: "C54K",
carriage_number: "01-75557",
},
{
model: "C54K",
carriage_number: "03-81207",
},
{
model: "C54K",
carriage_number: "04-17201",
},
{
model: "C54K",
carriage_number: "03-22895",
},
{
model: "C54K",
carriage_number: "00-20144",
},
{
model: "C54K",
carriage_number: "01-37853",
},
{
model: "C54K",
carriage_number: "01-19641",
},
{
model: "C54K",
carriage_number: "04-66344",
},
{
model: "C54K",
carriage_number: "01-79470",
},
{
model: "C54K",
carriage_number: "04-39271",
},
{
model: "C54K",
carriage_number: "02-35471",
},
{
model: "C54K",
carriage_number: "03-54817",
},
{
model: "C54K",
carriage_number: "03-43384",
},
{
model: "C54K",
carriage_number: "02-41045",
},
],
},
},
{
created_at: "2023-10-01 10:00:00",
device_position: "撑杆检测设备",
},
{
created_at: "2023-10-01 10:00:00",
device_position: "钩机检测设备",
},
{
created_at: "2023-10-01 10:00:00",
device_position: "车体检测设备",
},
{
created_at: "2023-10-01 10:00:00",
device_position: "车体检测设备",
},
{
created_at: "2023-10-01 10:00:00",
device_position: "撑杆检测设备",
},
{
created_at: "2023-10-01 10:00:00",
device_position: "钩机检测设备",
},
{
created_at: "2023-10-01 10:00:00",
device_position: "车体检测设备",
},
{
created_at: "2023-10-01 10:00:00",
device_position: "车体检测设备",
},
{
created_at: "2023-10-01 10:00:00",
device_position: "撑杆检测设备",
},
{
created_at: "2023-10-01 10:00:00",
device_position: "钩机检测设备",
},
{
created_at: "2023-10-01 10:00:00",
device_position: "车体检测设备",
},
]);
const trainListData = ref([]);
const currTrainFaultInfo = ref({});
const trainColumns = [
{
label: "进场时间",
property: "created_at",
property: "arrive_at",
width: 170,
},
{
label: "缺陷数量",
property: "device_position",
property: "fault_count",
// width: 550,
width: 120,
},
@ -397,10 +84,27 @@ const isTrainOpen = ref<Boolean>(false); //详情弹窗
const currentTrainRow = ref<Record<string, any>>({}); //
const fetchTrainDetail = async () => {
try {
const res = await getHomeTrainDetailDataApi({
id: currentTrainRow.value?.id,
});
if (isSuccessApi(res)) {
const { data } = res;
currTrainFaultInfo.value = data;
console.log(data, 'getHomeTrainDetailDataApi');
isTrainOpen.value = true;
}
} catch (error) {
console.error("获取设备信息出错:", error);
}
};
//
const openTrainDetail = (row: record<string, any>) => {
console.log(row, "openTrainDetail");
currentTrainRow.value = row;
isTrainOpen.value = true;
fetchTrainDetail();
};
const websocketStore = useWebSocketStore();
@ -461,40 +165,42 @@ const getDeviceInfo = async () => {
console.error("获取设备信息出错:", error);
}
};
const fetchPoleMonitorData = async () => {
//
const fetchAppearanceMonitorData = async () => {
try {
const res = await getRecordFaultApi({
dateType: "month",
value: searchForm.pole,
type: "pole",
value: searchForm.appearanceTime,
type: "appearance",
});
if (isSuccessApi(res)) {
const { data } = res;
poleFaultTotal.value = data;
console.log(data);
appearanceFaultData.value = data;
console.log(data, "fetchAppearanceMonitorData");
}
} catch (error) {
console.error("获取设备信息出错:", error);
}
};
//
const fetchTrainMonitorData = async () => {
//
const fetchPoleMonitorData = async () => {
try {
const res = await getRecordFaultApi({
dateType: "month",
value: searchForm.car,
type: "appearance",
value: searchForm.poleTime,
type: "pole",
});
if (isSuccessApi(res)) {
const { data } = res;
carFaultTotal.value = data;
console.log(data);
poleFaultData.value = data;
console.log(data, "fetchPoleMonitorData");
}
} catch (error) {
console.error("获取设备信息出错:", error);
}
};
const getRealTime = async () => {
try {
const res = await getRealTimeApi({ deviceType: "" });
@ -510,6 +216,21 @@ const getRealTime = async () => {
console.error("获取设备信息出错:", error);
}
};
const fetchHomeTrainData = async () => {
try {
const res = await getHomeTrainDataApi({
count: 6,
});
if (isSuccessApi(res)) {
const { data } = res;
trainListData.value = data?.data;
console.log(data, "fetchHomeTrainData");
}
} catch (error) {
console.error("获取设备信息出错:", error);
}
};
onBeforeRouteLeave(() => {
isAlarmOpen.value = false;
currentRow.value = {};
@ -518,9 +239,10 @@ onBeforeRouteLeave(() => {
onMounted(() => {
getList();
getDeviceInfo();
fetchTrainMonitorData();
fetchAppearanceMonitorData();
fetchPoleMonitorData();
getRealTime();
fetchHomeTrainData();
});
</script>
<template>
@ -567,10 +289,10 @@ onMounted(() => {
<div class="flex items-center text-[14px] px-[16px]">
<span>时间</span>
<el-select
v-model="searchForm.pole"
v-model="searchForm.appearanceTime"
placeholder="时间"
class="custom-select mini-size"
@change="fetchPoleMonitorData()"
@change="fetchAppearanceMonitorData()"
>
<el-option
v-for="v in dataViewConfig.monthArr"
@ -584,15 +306,7 @@ onMounted(() => {
</HomeSubTitle>
</div>
<div class="chart-container chart-pie-bg">
<!-- <PieChart :data="carFaultTotal" :colors="[
'#FF7C09',
'#0032FF',
'#04FFF2',
'#D19EFF',
'#FF0103',
'#9EFFF3',
]" /> -->
<VehicleMonitorChart />
<VehicleMonitorChart :datas="appearanceFaultData" />
</div>
</div>
<div class="grid-item">
@ -602,7 +316,7 @@ onMounted(() => {
<div class="flex items-center text-[14px] px-[16px]">
<span>时间</span>
<el-select
v-model="searchForm.pole"
v-model="searchForm.poleTime"
placeholder="时间"
class="custom-select mini-size"
@change="fetchPoleMonitorData()"
@ -619,8 +333,7 @@ onMounted(() => {
</HomeSubTitle>
</div>
<div class="chart-container chart-pie-bg">
<PoleMonitorChart />
<!-- <PieChartSmall :data="poleFaultTotal" :colors="['#9DFFF3', '#FFA179']" /> -->
<PoleMonitorChart :datas="poleFaultData" />
</div>
</div>
</div>
@ -638,7 +351,7 @@ onMounted(() => {
</div>
<div class="rightInfo-box">
<!-- 设备信息 -->
<div class="device-info-box">
<div class="cursor-pointer device-info-box" @click="() => $router.push({ name: 'DeviceStatus'})">
<div class="module-header">
<HomeSubTitle title="设备信息"> </HomeSubTitle>
</div>
@ -686,7 +399,7 @@ onMounted(() => {
</div>
<TrainAlarmInfoModel
v-model:value="isTrainOpen"
:info="currentTrainRow"
:info="currTrainFaultInfo"
@close="isTrainOpen = false"
/>
<AlarmModal

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { onBeforeRouteLeave } from "vue-router";
import { onBeforeRouteLeave, useRoute } from "vue-router";
import { BaseTable } from "@/components/CustomTable";
import SwiperMonitor from "./components/SwiperMonitor.vue";
import PoleDailyDetectionChart from "@/components/Charts/poleDailyDetectionChart.vue";
@ -17,6 +17,9 @@ import {
} from "@/api/dashboard";
import { isSuccessApi } from "@/utils/forApi";
import { useWebSocketStore } from "@/stores/websocketStore";
import { useDict } from "@/hooks/useDict";
const route = useRoute();
defineOptions({
name: "PoleMonitorIndex",
@ -76,7 +79,7 @@ const currFileList = ref<Record<string, any>[]>([]); // 详情的文件列表
const searchForm = reactive({
train_number: "",
train_carriage_number: "",
fault_type: "",
fault_type: route?.query?.name || "",
station: "",
type: "pole",
});
@ -86,6 +89,8 @@ const isAlarmOpen = ref<Boolean>(false); //详情弹窗
const isDeleteOpen = ref<Boolean>(false); //
const websocketStore = useWebSocketStore();
const { fetchPoleFaultTypeList } = useDict();
const faultTypeOptions = fetchPoleFaultTypeList()
const recordAmountData = ref({}); //
const recordFaultData = ref({}); //
// messages
@ -292,8 +297,7 @@ onMounted(() => {
class="custom-select"
clearable
>
<el-option label="撑杆弯曲" value="撑杆弯曲"></el-option>
<el-option label="撑杆断折" value="撑杆断折"></el-option>
<el-option v-for="item in faultTypeOptions" :key="item.value" :label="item" :value="item"></el-option>
</el-select>
<el-button
type="primary"

@ -1,16 +1,16 @@
<script setup lang="ts">
import { BaseTable } from "@/components/CustomTable";
import { getVehiclManagementApi } from "@/api/dashboard";
import { getVehiclManagementApi, getExcelDataApi, getHomeTrainDetailDataApi } from "@/api/dashboard";
import { isSuccessApi } from "@/utils/forApi";
import AlarmModal from "./components/AlarmModal.vue";
import TrainInfoModel from "./components/TrainInfoModel.vue";
import TrainAlarmInfoModel from "./components/TrainAlarmInfoModel.vue";
import { useTrainSaveToExcel } from "@/hooks/useTrainSaveToExcel";
import { useWebSocketStore } from "@/stores/websocketStore";
import { onBeforeRouteLeave } from "vue-router";
defineOptions({
name: "VehiclManagementWrap",
});
const currentRow = ref<Record<string, any>>({}); //
// const currentRow = ref<Record<string, any>>({}); //
const isAlarmOpen = ref<Boolean>(false); //
const isTrainOpen = ref<Boolean>(false); //
const currentDetailRow = ref<Record<string, any>>({}); //
@ -29,6 +29,8 @@ watch(
},
{ deep: true, immediate: true }
);
const currTrainFaultInfo = ref({});
const currentTrainRow = ref<Record<string, any>>({}); //
const columns = [
{
@ -77,6 +79,59 @@ const getList = async () => {
console.error("获取数据失败:", error);
}
};
//
const fetchTrainDetail = async () => {
try {
const res = await getHomeTrainDetailDataApi({
id: currentTrainRow.value?.id,
});
if (isSuccessApi(res)) {
const { data } = res;
currTrainFaultInfo.value = data;
console.log(data, 'getHomeTrainDetailDataApi');
isTrainOpen.value = true;
}
} catch (error) {
console.error("获取设备信息出错:", error);
}
};
//
const openTrainDetail = (row: record<string, any>) => {
console.log(row, "openTrainDetail");
currentTrainRow.value = row;
fetchTrainDetail();
};
const fetchExcelData = async (row) => {
try {
const res = await getExcelDataApi({
id: row?.id,
});
console.log(res.data, "fetchExcelData");
if (isSuccessApi(res)) {
const appearance_alarm_data = [];
const role_alarm_data = [];
res.data?.fault_data?.map((item) => {
if (item.alarm_type === "车辆损坏") {
appearance_alarm_data.push(item);
} else {
role_alarm_data.push(item);
}
});
const fullData = {
...res.data,
appearance_alarm_data,
role_alarm_data,
train_data: res.data?.data?.train_data,
};
useTrainSaveToExcel(fullData)?.saveToExcel();
}
} catch (error) {
console.error("获取数据失败:", error);
}
};
function handleTableChange(record) {
console.log("handleTableChange_record", record);
pagination.value = {
@ -87,13 +142,6 @@ function handleTableChange(record) {
getList();
}
/**查看详情 */
function openCurrent(row) {
console.log(row, "openCurrent");
currentRow.value = row;
isTrainOpen.value = true;
}
onBeforeRouteLeave(() => {
isAlarmOpen.value = false;
currentDetailRow.value = {};
@ -126,12 +174,15 @@ onMounted(() => {
>
<template v-slot:actionBar="{ row }">
<ul class="flex table_action_box">
<li class="flex items-center mr-[8px]" @click="useTrainSaveToExcel(row)?.saveToExcel()">
<li
class="flex items-center mr-[8px]"
@click="fetchExcelData(row)"
>
<div class="fg-button-primary1">报表下载</div>
</li>
<li
class="flex items-center mr-[16px]"
@click="openCurrent(row)"
@click="openTrainDetail(row)"
>
<div class="fg-button-primary">查看详情</div>
</li>
@ -140,12 +191,17 @@ onMounted(() => {
</BaseTable>
</div>
</div>
<TrainInfoModel
<!-- <TrainInfoModel
v-model:value="isTrainOpen"
:info="currentRow"
:image="currFileList"
@close="isTrainOpen = false"
/>
/> -->
<TrainAlarmInfoModel
v-model:value="isTrainOpen"
:info="currTrainFaultInfo"
@close="isTrainOpen = false"
/>
<AlarmModal
v-model:value="isAlarmOpen"
:info="currentDetailRow"

@ -2,13 +2,13 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-06-23 15:50:30
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-09-08 16:47:07
* @LastEditTime: 2025-09-10 14:58:45
* @FilePath: \5G-Web\src\views\dashboard\components\TrainInfoModel.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<script lang="ts" setup>
import { ref } from 'vue';
import SwiperImages from '@/components/Swiper/swiperImages.vue';
interface Props {
/** 弹窗显隐 */
value: boolean;
@ -31,9 +31,26 @@ const show = computed({
emit("update:value", val);
}
});
const activeIndex = ref(-1); //
const activeImages = ref([]); //
//
const isFault = computed(()=>{
return function get(val) {
return Object.keys(props.info?.fault_data).includes(val?.carriage_number);
}
})
//
const fetchImageList = (record, index) => {
if(!isFault.value(record)){
// activeIndex.value = -1;
return
}
activeIndex.value = index;
activeImages.value = props.info?.fault_data[record.carriage_number];
console.log(activeImages.value,"fetchImageList",record)
}
//
const itemCount = 16; //
</script>
<template>
<el-dialog class="trainModal-wrap fg-dialog fg-dialog2" v-model="show" align-center :show-close="false" >
@ -52,8 +69,8 @@ const itemCount = 16; // 这里可以根据实际需求动态设置
<!-- 图片区域 -->
<div class="train-content">
<div class="train-content-top">
<span class="train-content-bottom-title">列车ID:</span>
<div class="train-content-top-img">
<span class="train-content-bottom-title">列车ID: {{ info?.train_id }}</span>
<div class="train-content-top-img" v-if="activeIndex === -1">
<div class="train-content-top-img-box">
<img :src="info.arrive_img_url" alt=""></img>
</div>
@ -61,23 +78,26 @@ const itemCount = 16; // 这里可以根据实际需求动态设置
<img :src="info.leave_img_url" alt=""></img>
</div>
</div>
<div v-else class="w-full h-full min-h-[370px]">
<SwiperImages class="train-content-top-img" :fileList="activeImages" />
</div>
</div>
<div class="train-content-bottom">
<span class="train-content-bottom-title">列车与车厢号</span>
<div class="flex items-center train-content-bottom-train">
<div class="train-card-item mb-[8px] mr-[8px] flex items-center justify-center ">
<div class="train-card-item mb-[8px] mr-[8px] flex items-center justify-center cursor-pointer" @click="activeIndex=-1">
<div class="train_head_icon"></div>
</div>
<ul :class="['train-card-item-list', { 'high-height': info?.data?.train_data?.length > 15 }]">
<li class="flex flex-1 cursor-pointer train-card-item train-card-item-alarm" :class="{ ' train-card-item-default cursor-not-allowed': index === 0 }" v-for="(item, index) in info?.data?.train_data" :key="index">
<ul class="train-card-item-list" >
<li @click="fetchImageList(item, index)" class="flex flex-1 train-card-item train-card-item-alarm" :class="[!isFault(item) ? 'train-card-item-default cursor-not-allowed': 'cursor-allowed cursor-pointer', activeIndex === index ? 'train-card-item-active' : '']" v-for="(item, index) in info?.data?.train_data" :key="index">
<div class="w-[80px] px-[8px] flex flex-col justify-center">
<div>{{ item.model }}</div>
<div>
<!-- <span class="mr-3">04</span> -->
<span>{{ item.carriage_number }}</span>
</div>
<div>
<!-- <span class="mr-3">04</span> -->
<span>{{ item.carriage_number }}</span>
</div>
</div>
<div class="default-tag" v-if="index===0">
<div class="default-tag" v-if="!isFault(item)">
无异常
</div>
</li>

@ -43,7 +43,7 @@
}
.train-content-bottom {
box-sizing: border-box;
margin-top: 12px;
padding-top: 12px;
.train-content-bottom-train {
box-sizing: border-box;
@ -73,10 +73,17 @@
}
.train-card-item-alarm {
background: rgba(245,63,63,0.3);
background: rgba(245, 63, 63, 0.3);
color: #f53f3f;
border: 1px solid #f53f3f;
}
.train-card-item-active {
background-image: url("@/assets/trainManage/train_card_bg.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
border: 1px solid transparent;
}
.train-card-item-default {
position: relative;
color: #cacbce;

@ -1,14 +1,21 @@
<script lang="ts" setup>
import { useRoute } from "vue-router";
import Player from "@/components/videoPlayer/Player.vue";
import SwiperPlayer from "@/components/Swiper/swiperPlayer.vue";
import SwiperFile from "@/components/Swiper/swiperFile.vue";
import { getDetectRecordDataApi } from "@/api/dashboard";
import { isSuccessApi } from "@/utils/forApi";
const route = useRoute();
// TODO
/**
* @使用数据结构 {"infer_result": [{"log": "一段文本", "pic_url": "图片路径"}, {"log": "", "pic_url": ""}]}
* @使用数据结构 {"infer_result": [{"log": "一段文本", "pic_url": "图片路径"}, {"log": "", "pic_url": ""}]}
*/
//
const longText = `
const longText = ref(`
const xData = ref(["1月", "2月", "3月", "4月", "5月"]);
const legendArr = ["车体检测", "撑杆检测"];
const datas = ref([
@ -37,65 +44,6 @@ const activeBtn = ref("month");
const isAlarmOpen = ref<Boolean>(false); //
const currentRow = ref<Record<string, any>>({}); //
const currFileList = ref<Record<string, any>[]>([]); //
const deviceInfo = reactive({
list: [
{
name: "车体检测设备",
bindVal: {
total: 0,
},
icon: car_device_icon,
},
{
name: "撑杆检测设备",
bindVal: {
total: 0,
},
icon: pole_device_icon,
},
{
name: "钩机检测设备",
bindVal: {
total: 0,
},
icon: excavator_device_icon,
},
],
});
const websocketStore = useWebSocketStore();
// messages
watch(
() => websocketStore.messages,
(newMessages: string[], oldMessages: string[]) => {
if (newMessages?.length > 0 && !isAlarmOpen.value) {
currentRow.value = newMessages[newMessages?.length - 1];
currFileList.value = newMessages[newMessages?.length - 1]?.images;
isAlarmOpen.value = true;
}
},
{ deep: true, immediate: true }
);
const getList = async (dateType: string = "month") => {
activeBtn.value = dateType;
const res = await getDataOverviewApi({ dateType });
if (isSuccessApi(res)) {
const { data } = res;
datas.value[0] = data.appearance;
datas.value[1] = data.pole;
if (dateType === "month") {
xData.value = data.dateArr.map((item: any) => {
if (dateType === "month") {
return item + "月";
}
});
} else {
xData.value = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
}
// console.log(data, 'getList_data')
}
};
const getDeviceInfo = async () => {
try {
const res = await getDeviceInfowApi();
@ -351,13 +299,12 @@ onMounted(() => {
/>
</div>
分析完成`;
分析完成`);
const finalJsonData = ref<any[]>([]);
const show = ref<boolean>(false);
const currentVideo = ref<Record<string, any>>({
video_url:
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
const currentInfo = ref({
video_path: "", // https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
});
const isPlaying = ref<boolean>(false);
const activeIndex = ref<number>(-1);
@ -371,17 +318,14 @@ const textContainer = ref(null);
const textInterval = ref(null);
const imageInterval = ref(null);
const currentIndex = ref(0);
const handleExport = () => {
show.value = true;
};
const currentDataIndex = ref(0);
// ||
// const toggleGeneration = () => {
// console.log(
// "generateNext",
// longText.length,
// longText.length / (1000 / textGenerationSpeed.value)
// longText.value.length,
// longText.value.length / (1000 / textGenerationSpeed.value)
// );
// if (isGenerating.value) {
@ -404,7 +348,7 @@ const handleExport = () => {
//
const textGenerationSpeed = computed(() => {
//
return 2;
return 50;
// return 100;
});
@ -416,40 +360,70 @@ const scrollToBottom = () => {
};
//
const generateText = () => {
if (currentIndex.value < longText.length) {
displayedText.value += longText.charAt(currentIndex.value);
const generateText = (nextText) => {
if (currentIndex.value < longText.value.length) {
displayedText.value += JSON.stringify(nextText).charAt(currentIndex.value);
currentIndex.value++;
scrollToBottom(); //
} else {
isGenerating.value = false;
clearInterval(textInterval.value);
clearInterval(imageInterval.value);
}
};
//
const loadImages = () => {
if (displayedFiles.value.length >= fileList.value.length) {
clearInterval(imageInterval.value);
return
}
if (
(displayedText.value.length / longText.length) * fileList.value.length >
displayedFiles.value.length
) {
const nextImage = fileList.value[displayedFiles.value.length];
displayedFiles.value = [...displayedFiles.value, nextImage];
// const loadImages = () => {
// if (displayedFiles.value.length >= fileList.value.length) {
// clearInterval(imageInterval.value);
// return;
// }
// if (
// (displayedText.value.length / longText.value.length) *
// fileList.value.length >
// displayedFiles.value.length
// ) {
// const nextImage = fileList.value[displayedFiles.value.length];
// displayedFiles.value = [...displayedFiles.value, nextImage];
// }
// };
const generatePreText = () => {
if (currentDataIndex.value < finalJsonData.value.length) {
const nextText = finalJsonData.value[currentDataIndex.value];
if (nextText.is_check) {
fileList.value = [
...fileList.value,
{
image_url:
currentInfo.value?.images_path + "/" + nextText.frame_id + ".jpg", //
},
];
}
console.log("新插入数据text", currentDataIndex.value)
// imageInterval.value = setInterval(generateText(nextText), 10);
displayedText.value += JSON.stringify(nextText);
currentDataIndex.value++;
scrollToBottom(); //
} else {
isGenerating.value = false;
clearInterval(textInterval.value);
}
};
//
const startGeneration = () => {
const startGeneration = async () => {
const resAll = await fetch(currentInfo.value?.json_path);
const res = await resAll.json();
console.log(res);
longText.value = JSON.stringify(res);
finalJsonData.value = res;
isGenerator.value = true;
isGenerating.value = true;
//
textInterval.value = setInterval(generateText, textGenerationSpeed.value);
//
imageInterval.value = setInterval(loadImages, 300);
isPlaying.value = true;
// //
textInterval.value = setInterval(generatePreText, textGenerationSpeed.value);
// //
// imageInterval.value = setInterval(loadImages, 300);
// getFileList(); //
// // if (paused.value) {
@ -459,8 +433,8 @@ const startGeneration = () => {
// const generateNext = () => {
// // if (paused.value) return;
// if (displayedText.value.length < longText.length) {
// displayedText.value = longText.slice(0, displayedText.value.length + 1);
// if (displayedText.value.length < longText.value.length) {
// displayedText.value = longText.value.slice(0, displayedText.value.length + 1);
// scrollToBottom();
// textTimer.value = setTimeout(generateNext, textGenerationSpeed.value);
// } else {
@ -468,7 +442,7 @@ const startGeneration = () => {
// const endTime = new Date().getTime();
// console.log(
// longText.length / (1000 / textGenerationSpeed.value),
// longText.value.length / (1000 / textGenerationSpeed.value),
// "generateNext",
// Number((endTime - startTime) / 1000)
// );
@ -479,16 +453,36 @@ const startGeneration = () => {
};
// TODO mock
const getFileList = async () => {
const handleExport = async () => {
try {
const resAll = await fetch("/api/v1/record/generator_result/", {
method: "POST",
let currentType = "appearance";
switch (route?.name) {
case "PoleMonitor":
currentType = "pole";
break;
case "DiggerMonitor":
currentType = "distance";
break;
default:
currentType = "appearance";
break;
}
const res = await getDetectRecordDataApi({
type: currentType,
});
const res = await resAll.json();
// const resAll = await fetch("/api/v1/record/generator_result/", {
// method: "POST",
// });
// const res = await resAll.json();
if (isSuccessApi(res)) {
console.log(res, "getFileList");
fileList.value = res.data.data;
// fileList.value = res.data.data;
currentInfo.value = {
...res.data
};
console.log(currentInfo.value, "getFileList");
nextTick(() => {
show.value = true;
});
// TODO
}
} catch (error) {
@ -516,10 +510,6 @@ watch(activeIndex, (newVal) => {
}
});
onMounted(() => {
getFileList();
});
//
onUnmounted(() => {
clearInterval(textInterval.value);
@ -559,7 +549,7 @@ onUnmounted(() => {
<div class="flex generator-container">
<div class="generator-start-video">
<Player
:src="currentVideo?.video_url"
:src="currentInfo?.video_path"
:is-playing="isPlaying"
@play="isPlaying = true"
@pause="isPlaying = false"
@ -634,7 +624,7 @@ onUnmounted(() => {
<SwiperPlayer
@click="handelVideoTab()"
class="cursor-pointer"
:videoUrl="currentVideo?.video_url"
:videoUrl="currentInfo?.video_path"
:isPlaying="isPlaying && activeIndex === -1"
/>
</div>
@ -644,9 +634,9 @@ onUnmounted(() => {
<div class="fg-border-left-title">分析结果</div>
<div class="results-content">
<SwiperFile
:fileList="displayedFiles"
:fileList="fileList"
v-model:value="activeIndex"
v-if="displayedFiles?.length"
v-if="fileList?.length"
/>
<div class="h-full fg-no-data" v-else>
<div class="bg-no-data"></div>

@ -1,92 +0,0 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from "vue";
import * as echarts from "echarts";
const props = defineProps({
data: {
type: Array as PropType<Array<{ value: number; name: string }>>,
required: true,
},
colors: { type: Array as PropType<Array<string>>, default: () => [] },
});
const chartContainer = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const colorsArr = ['#FFCC4A','#028FF5','#06EA7C','#8500FF','#FF7D05','#00D1FF']
//
const initChart = () => {
if (!chartContainer.value) return;
chartInstance = echarts.init(chartContainer.value);
setTimeout(() => {
chartInstance?.resize();
updateChart();
}, 500);
};
//
const updateChart = () => {
if (!chartInstance) return;
chartInstance.setOption({
legend: {
type: "scroll",
orient: "vertical",
left: "70%",
align: "left",
top: "middle",
itemWidth: 16, //
itemHeight: 8,
textStyle: { color: "#FFF" },
},
series: [
{
type: "pie",
radius: ["30%", "80%"],
center: ["35%", "50%"],
label: { show: false },
itemStyle: {
color: (params) =>
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: props.colors[params.dataIndex] },
{ offset: 1, color: colorsArr[params.dataIndex] },
]),
},
data: props.data,
},
],
});
};
//
onMounted(() => {
initChart();
});
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
//
watch(
() => props.data,
async () => {
await nextTick();
updateChart();
}
);
//
// onMounted(() => {
// window.addEventListener("resize", () => chartInstance?.resize());
// });
// onUnmounted(() => {
// window.removeEventListener("resize", () => chartInstance?.resize());
// });
</script>
<template>
<div ref="chartContainer" style="width: 100%; height: 100%" />
</template>

@ -1,92 +0,0 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from "vue";
import * as echarts from "echarts";
const props = defineProps({
data: {
type: Array as PropType<Array<{ value: number; name: string }>>,
required: true,
},
colors: { type: Array as PropType<Array<string>>, default: () => [] },
});
const chartContainer = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const colorsArr = ['#3FE3FA','#FF4D00']
//
const initChart = () => {
if (!chartContainer.value) return;
chartInstance = echarts.init(chartContainer.value);
setTimeout(() => {
chartInstance?.resize();
updateChart();
}, 500);
};
//
const updateChart = () => {
if (!chartInstance) return;
chartInstance.setOption({
legend: {
type: "scroll",
orient: "vertical",
left: "70%",
align: "left",
top: "middle",
itemWidth: 16, //
itemHeight: 8,
textStyle: { color: "#FFF" },
},
series: [
{
type: "pie",
radius: ["40%", "80%"],
center: ["35%", "50%"],
label: { show: false },
itemStyle: {
color: (params) =>
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: props.colors[params.dataIndex] },
{ offset: 1, color: colorsArr[params.dataIndex] },
]),
},
data: props.data,
},
],
});
};
//
onMounted(() => {
initChart();
});
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
//
watch(
() => props.data,
async () => {
await nextTick();
updateChart();
}
);
//
// onMounted(() => {
// window.addEventListener("resize", () => chartInstance?.resize());
// });
// onUnmounted(() => {
// window.removeEventListener("resize", () => chartInstance?.resize());
// });
</script>
<template>
<div ref="chartContainer" style="width: 100%; height: 100%" />
</template>

@ -2,7 +2,7 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-06-23 15:50:30
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-09-05 16:48:36
* @LastEditTime: 2025-09-10 10:47:08
* @FilePath: \5G-Web\src\views\dashboard\components\TrainInfoModel.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
@ -72,7 +72,7 @@ const itemCount = 16; // 这里可以根据实际需求动态设置
<div class="train-card-item mb-[8px] mr-[8px] flex items-center justify-center ">
<div class="train_head_icon"></div>
</div>
<ul :class="['train-card-item-list', { 'high-height': info?.data?.train_data?.length > 15 }]">
<ul class="train-card-item-list">
<li class="flex flex-1 train-card-item" v-for="(item, index) in info?.data?.train_data" :key="index">
<div class="w-[80px] px-[8px] flex flex-col justify-center">
<div>{{ item.model }}</div>

@ -0,0 +1,451 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue 3 + ECharts 饼图点击交互</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
width: 100%;
max-width: 1200px;
background: rgba(255, 255, 255, 0.92);
border-radius: 20px;
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.3);
overflow: hidden;
padding: 30px;
}
header {
text-align: center;
margin-bottom: 30px;
}
h1 {
color: #2c3e50;
font-size: 2.8rem;
margin-bottom: 10px;
background: linear-gradient(45deg, #3498db, #8e44ad);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
color: #7f8c8d;
font-size: 1.2rem;
max-width: 800px;
margin: 0 auto;
line-height: 1.6;
}
.content {
display: flex;
flex-wrap: wrap;
gap: 30px;
}
.chart-container {
flex: 1;
min-width: 350px;
height: 450px;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 15px;
transition: transform 0.3s ease;
}
.chart-container:hover {
transform: translateY(-5px);
}
.info-panel {
flex: 1;
min-width: 350px;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 25px;
display: flex;
flex-direction: column;
}
.info-header {
text-align: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 2px solid #f1f2f6;
}
.info-header h2 {
color: #2c3e50;
font-size: 1.8rem;
}
.default-info {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
color: #95a5a6;
}
.default-info i {
font-size: 5rem;
margin-bottom: 20px;
color: #ecf0f1;
}
.info-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.info-card {
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
}
.info-card:hover {
transform: translateX(5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
}
.info-title {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.info-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
font-size: 1.2rem;
color: white;
}
.info-name {
font-size: 1.5rem;
font-weight: bold;
color: #2c3e50;
}
.info-item {
display: flex;
margin-top: 10px;
color: #7f8c8d;
}
.info-label {
font-weight: 600;
width: 120px;
color: #34495e;
}
.instructions {
margin-top: 30px;
background: #e8f4fc;
border-radius: 15px;
padding: 20px;
color: #2c3e50;
}
.instructions h3 {
margin-bottom: 15px;
color: #2980b9;
display: flex;
align-items: center;
gap: 10px;
}
.instructions ul {
padding-left: 25px;
}
.instructions li {
margin: 10px 0;
line-height: 1.6;
}
.highlight {
background-color: #f1c40f;
color: #2c3e50;
padding: 0 5px;
border-radius: 4px;
font-weight: 600;
}
footer {
text-align: center;
margin-top: 30px;
color: #7f8c8d;
font-size: 0.9rem;
}
@media (max-width: 768px) {
.content {
flex-direction: column;
}
h1 {
font-size: 2.2rem;
}
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<header>
<h1>ECharts 饼图点击交互</h1>
<p class="subtitle">本示例展示如何在 Vue 3 中为 ECharts 饼图添加点击事件,点击不同扇区查看详情信息</p>
</header>
<div class="content">
<div class="chart-container">
<div ref="chartDom" style="width: 100%; height: 100%;"></div>
</div>
<div class="info-panel">
<div class="info-header">
<h2>点击详情</h2>
</div>
<div v-if="!clickData" class="default-info">
<i class="fas fa-mouse-pointer"></i>
<h3>点击饼图扇区查看详情</h3>
<p>点击左侧饼图的任何部分将会在此显示详细信息</p>
</div>
<div v-else class="info-content">
<div class="info-card">
<div class="info-title">
<div class="info-icon" :style="{ background: clickData.color }">
<i :class="getIcon(clickData.name)"></i>
</div>
<div class="info-name">{{ clickData.name }}</div>
</div>
<div class="info-item">
<span class="info-label">数值:</span>
<span>{{ clickData.value }}</span>
</div>
<div class="info-item">
<span class="info-label">百分比:</span>
<span>{{ clickData.percent }}%</span>
</div>
<div class="info-item">
<span class="info-label">描述:</span>
<span>{{ getDescription(clickData.name) }}</span>
</div>
</div>
<div class="info-card">
<div class="info-item">
<span class="info-label">所属系列:</span>
<span>{{ clickData.seriesName }}</span>
</div>
<div class="info-item">
<span class="info-label">数据索引:</span>
<span>{{ clickData.dataIndex }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="instructions">
<h3><i class="fas fa-lightbulb"></i> 实现说明</h3>
<ul>
<li>使用 ECharts 的 <span class="highlight">chart.on('click')</span> 方法监听饼图点击事件</li>
<li>点击事件参数包含名称(name)、值(value)、百分比(percent)等关键数据</li>
<li>通过 Vue 的响应式特性更新点击信息展示区域</li>
<li>添加了窗口大小变化监听器,确保图表响应式调整</li>
<li>在组件卸载时正确销毁 ECharts 实例和事件监听器</li>
</ul>
</div>
<footer>
<p>Vue 3 + ECharts 饼图交互示例 | 点击饼图扇区体验交互效果</p>
</footer>
</div>
</div>
<script>
const { createApp, ref, onMounted, onBeforeUnmount } = Vue;
createApp({
setup() {
const chartDom = ref(null);
let chartInstance = null;
const clickData = ref(null);
// 饼图数据
const chartData = [
{ value: 1048, name: '搜索引擎' },
{ value: 735, name: '直接访问' },
{ value: 580, name: '邮件营销' },
{ value: 484, name: '联盟广告' },
{ value: 300, name: '视频广告' }
];
// 颜色列表
const colors = ['#5470C6', '#91CC75', '#FAC858', '#EE6666', '#73C0DE'];
// 根据名称获取图标
const getIcon = (name) => {
const icons = {
'搜索引擎': 'fas fa-search',
'直接访问': 'fas fa-globe',
'邮件营销': 'fas fa-envelope',
'联盟广告': 'fas fa-handshake',
'视频广告': 'fas fa-video'
};
return icons[name] || 'fas fa-chart-pie';
};
// 获取描述信息
const getDescription = (name) => {
const descriptions = {
'搜索引擎': '通过Google、百度等搜索引擎带来的流量',
'直接访问': '用户直接输入网址或书签访问',
'邮件营销': '通过电子邮件营销活动带来的访问',
'联盟广告': '通过联盟广告网络获得的流量',
'视频广告': '来自YouTube、TikTok等平台的视频广告'
};
return descriptions[name] || '暂无描述信息';
};
// 初始化图表
const initChart = () => {
if (!chartDom.value) return;
chartInstance = echarts.init(chartDom.value);
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
formatter: (name) => {
const data = chartData.find(item => item.name === name);
return `${name}: ${data.value}`;
}
},
color: colors,
series: [
{
name: '访问来源',
type: 'pie',
radius: ['40%', '70%'],
center: ['40%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}: {c}',
fontWeight: 'bold'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
labelLine: {
show: true
},
data: chartData
}
]
};
chartInstance.setOption(option);
// 添加点击事件监听
chartInstance.on('click', (params) => {
clickData.value = {
name: params.name,
value: params.value,
percent: params.percent,
color: params.color,
seriesName: params.seriesName,
dataIndex: params.dataIndex
};
});
};
// 处理窗口大小变化
const handleResize = () => {
chartInstance && chartInstance.resize();
};
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.off('click');
chartInstance.dispose();
}
window.removeEventListener('resize', handleResize);
});
return {
chartDom,
clickData,
getIcon,
getDescription
};
}
}).mount('#app');
</script>
</body>
</html>

@ -0,0 +1,692 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JSON数据解析器 - DeepSeek风格</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
body {
background: linear-gradient(135deg, #0a192f, #0f2a4a, #0a192f);
color: #e6f1ff;
min-height: 100vh;
padding: 20px;
overflow-x: hidden;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
text-align: center;
padding: 30px 0;
margin-bottom: 20px;
position: relative;
z-index: 2;
}
h1 {
color: #64ffda;
font-size: 3rem;
margin-bottom: 15px;
text-shadow: 0 0 15px rgba(100, 255, 218, 0.3);
font-weight: 600;
letter-spacing: -0.5px;
}
.subtitle {
color: #a8b2d1;
font-size: 1.2rem;
max-width: 800px;
margin: 0 auto;
line-height: 1.6;
}
.main-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
gap: 25px;
margin-bottom: 40px;
}
.card {
background: rgba(17, 34, 64, 0.7);
border-radius: 12px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
border: 1px solid rgba(100, 255, 218, 0.1);
backdrop-filter: blur(10px);
position: relative;
}
.card::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(to right, #64ffda, #0a192f);
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.4);
border-color: rgba(100, 255, 218, 0.3);
}
.card-header {
padding: 22px 30px;
display: flex;
align-items: center;
position: relative;
border-bottom: 1px solid rgba(100, 255, 218, 0.1);
}
.card-header i {
margin-right: 15px;
font-size: 1.5rem;
color: #64ffda;
}
.card-header h2 {
font-size: 1.6rem;
font-weight: 500;
color: #ccd6f6;
}
.card-body {
padding: 30px;
}
.input-group {
margin-bottom: 25px;
position: relative;
}
.input-group label {
display: block;
margin-bottom: 12px;
font-weight: 500;
color: #a8b2d1;
font-size: 1.1rem;
}
.json-input {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.json-input input {
flex: 1;
padding: 14px 20px;
background: rgba(10, 25, 47, 0.7);
border: 1px solid rgba(100, 255, 218, 0.2);
border-radius: 8px;
font-size: 1rem;
color: #e6f1ff;
transition: all 0.3s;
}
.json-input input:focus {
outline: none;
border-color: #64ffda;
box-shadow: 0 0 0 3px rgba(100, 255, 218, 0.2);
}
.json-input button {
padding: 14px 24px;
background: linear-gradient(to right, #0a192f, #112240);
color: #64ffda;
border: 1px solid rgba(100, 255, 218, 0.3);
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.json-input button:hover {
background: linear-gradient(to right, #112240, #0a192f);
border-color: #64ffda;
transform: translateY(-2px);
}
.json-input button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.preview-container {
position: relative;
}
.json-preview {
min-height: 300px;
max-height: 400px;
padding: 20px;
border-radius: 10px;
background: rgba(10, 25, 47, 0.5);
border: 1px solid rgba(100, 255, 218, 0.1);
font-family: 'Fira Code', monospace;
font-size: 14px;
line-height: 1.5;
color: #a8b2d1;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
position: relative;
transition: all 0.3s;
}
.json-preview.error {
color: #ff6b6b;
}
.json-preview.loading::before {
content: "加载中...";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #64ffda;
font-size: 16px;
}
.results-container {
min-height: 300px;
display: flex;
flex-direction: column;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.stats {
color: #a8b2d1;
font-size: 1.1rem;
}
.stats span {
color: #64ffda;
font-weight: 500;
}
.copy-btn {
padding: 10px 18px;
background: rgba(10, 25, 47, 0.7);
color: #64ffda;
border: 1px solid rgba(100, 255, 218, 0.3);
border-radius: 8px;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.copy-btn:hover {
background: rgba(100, 255, 218, 0.1);
border-color: #64ffda;
}
.results-list {
flex: 1;
background: rgba(10, 25, 47, 0.5);
border-radius: 10px;
padding: 20px;
border: 1px solid rgba(100, 255, 218, 0.1);
overflow-y: auto;
max-height: 350px;
}
ul {
list-style: none;
}
li {
padding: 14px 20px;
margin-bottom: 12px;
background: rgba(17, 34, 64, 0.7);
border-radius: 8px;
border-left: 3px solid #64ffda;
transition: all 0.3s;
font-family: 'Fira Code', monospace;
color: #ccd6f6;
display: flex;
align-items: center;
justify-content: space-between;
}
li:last-child {
margin-bottom: 0;
}
li:hover {
background: rgba(17, 34, 64, 0.9);
transform: translateX(5px);
}
.item-id {
font-weight: 500;
}
.item-actions {
opacity: 0;
transition: opacity 0.3s;
}
li:hover .item-actions {
opacity: 1;
}
.item-actions button {
background: none;
border: none;
color: #64ffda;
cursor: pointer;
padding: 5px;
border-radius: 4px;
transition: all 0.2s;
}
.item-actions button:hover {
background: rgba(100, 255, 218, 0.1);
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #8892b0;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 20px;
color: #64ffda;
opacity: 0.5;
}
.progress-container {
height: 6px;
background: rgba(100, 255, 218, 0.1);
border-radius: 3px;
margin: 15px 0;
overflow: hidden;
position: relative;
}
.progress-bar {
height: 100%;
background: linear-gradient(to right, #64ffda, #0a192f);
border-radius: 3px;
width: 0%;
transition: width 0.5s;
position: relative;
}
.progress-bar::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: progressShimmer 1.5s infinite;
}
@keyframes progressShimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
footer {
text-align: center;
color: rgba(168, 178, 209, 0.7);
padding: 30px 0;
margin-top: 50px;
font-size: 0.9rem;
border-top: 1px solid rgba(100, 255, 218, 0.1);
}
.logo {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.logo-circle {
width: 24px;
height: 24px;
background: linear-gradient(135deg, #64ffda, #0a192f);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.logo-circle i {
font-size: 12px;
color: #0a192f;
}
.logo-text {
font-size: 1.5rem;
font-weight: 600;
background: linear-gradient(to right, #64ffda, #a8b2d1);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
h1 {
font-size: 2.2rem;
}
.json-input {
flex-direction: column;
}
.results-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
position: absolute;
right: 30px;
top: 50%;
transform: translateY(-50%);
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #8892b0;
}
.status-dot.connected {
background: #64ffda;
box-shadow: 0 0 10px rgba(100, 255, 218, 0.5);
}
.status-text {
color: #8892b0;
font-size: 0.9rem;
}
.notification {
position: fixed;
top: 30px;
right: 30px;
background: rgba(17, 34, 64, 0.95);
color: #64ffda;
padding: 15px 25px;
border-radius: 8px;
border-left: 4px solid #64ffda;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
transform: translateX(200%);
transition: transform 0.3s ease-out;
z-index: 1000;
display: flex;
align-items: center;
gap: 10px;
}
.notification.show {
transform: translateX(0);
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<header>
<div class="logo">
<div class="logo-circle">
<i class="fas fa-code"></i>
</div>
<div class="logo-text">JSON解析器</div>
</div>
<h1>JSON数据分析工具</h1>
<p class="subtitle">从指定URL加载JSON数据并提取is_check为true的frame_id值</p>
</header>
<div class="main-content">
<!-- JSON数据展示 -->
<div class="card">
<div class="card-header">
<i class="fas fa-database"></i>
<h2>JSON数据源</h2>
<div class="status-indicator">
<div class="status-dot" :class="{'connected': jsonData && !error}"></div>
<div class="status-text">{{ jsonData && !error ? '已连接' : '未连接' }}</div>
</div>
</div>
<div class="card-body">
<div class="input-group">
<label for="jsonUrl">JSON数据URL:</label>
<div class="json-input">
<input type="text" v-model="jsonUrl" placeholder="输入JSON数据URL..." id="jsonUrl">
<button @click="fetchData" :disabled="loading">
<i class="fas fa-sync-alt" :class="{'fa-spin': loading}"></i>
{{ loading ? '加载中...' : '加载数据' }}
</button>
</div>
</div>
<div class="progress-container" v-if="loading">
<div class="progress-bar" :style="{ width: progress + '%' }"></div>
</div>
<div class="preview-container">
<div class="json-preview" :class="{ 'loading': loading, 'error': error }">
<template v-if="error">
{{ error }}
</template>
<template v-else-if="jsonData">
{{ formattedJson }}
</template>
<template v-else-if="loading">
<!-- 加载中状态由CSS处理 -->
</template>
<template v-else>
等待加载JSON数据...
</template>
</div>
</div>
</div>
</div>
<!-- 解析结果展示 -->
<div class="card">
<div class="card-header">
<i class="fas fa-check-circle"></i>
<h2>解析结果</h2>
</div>
<div class="card-body">
<div class="results-container">
<div class="results-header">
<div class="stats">
共找到 <span>{{ checkedIds.length }}</span> 个符合条件的 frame_id
<span v-if="jsonData">(共解析 {{ jsonData.length }} 个对象)</span>
</div>
<button class="copy-btn" @click="copyResults">
<i class="fas fa-copy"></i> 复制结果
</button>
</div>
<div class="results-list">
<ul v-if="checkedIds.length">
<li v-for="(id, index) in checkedIds" :key="id">
<div class="item-id">frame_id: {{ id }}</div>
<div class="item-actions">
<button :title="'复制 ' + id" @click="copySingleItem(id)">
<i class="fas fa-copy"></i>
</button>
</div>
</li>
</ul>
<div v-else class="empty-state">
<i class="fas fa-file-alt"></i>
<h3>未找到匹配的数据</h3>
<p>请加载JSON数据并确保其中包含is_check为true的对象</p>
</div>
</div>
</div>
</div>
</div>
</div>
<footer>
<p>JSON数据分析工具 &copy; {{ currentYear }} - 基于DeepSeek风格设计</p>
</footer>
<div class="notification" :class="{ show: showNotification }">
<i class="fas fa-check-circle"></i>
<div>{{ notificationMessage }}</div>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
jsonData: null,
checkedIds: [],
jsonUrl: 'http://192.168.10.14:8123/sftp/detect_log/waiguan/yolo_log.json',
loading: false,
error: null,
progress: 0,
showNotification: false,
notificationMessage: ''
},
computed: {
currentYear() {
return new Date().getFullYear();
},
formattedJson() {
if (!this.jsonData) return '';
return JSON.stringify(this.jsonData, null, 2);
}
},
mounted() {
// 初始加载数据
this.fetchData();
// 每30秒自动刷新数据
setInterval(() => {
if (this.jsonUrl) {
this.fetchData();
}
}, 30000);
},
methods: {
fetchData() {
this.loading = true;
this.error = null;
this.progress = 0;
// 模拟进度条
const progressInterval = setInterval(() => {
if (this.progress < 90) {
this.progress += 10;
}
}, 200);
axios.get(this.jsonUrl)
.then(response => {
this.jsonData = response.data;
this.parseData();
this.progress = 100;
this.showNotificationFunc('数据加载成功!');
})
.catch(error => {
console.error('Error fetching JSON:', error);
this.error = '无法加载数据:' + (error.message || '网络错误或URL无效');
this.showNotificationFunc(this.error);
})
.finally(() => {
this.loading = false;
clearInterval(progressInterval);
setTimeout(() => this.progress = 0, 500);
});
},
parseData() {
this.checkedIds = [];
if (this.jsonData && Array.isArray(this.jsonData)) {
this.jsonData.forEach(item => {
if (item.is_check === true && item.frame_id) {
this.checkedIds.push(item.frame_id);
}
});
}
},
copyResults() {
if (this.checkedIds.length === 0) return;
const text = this.checkedIds.join(', ');
this.copyToClipboard(text);
this.showNotificationFunc('已复制所有frame_id到剪贴板');
},
copySingleItem(id) {
this.copyToClipboard(id);
this.showNotificationFunc(`已复制 frame_id: ${id}`);
},
copyToClipboard(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
},
showNotificationFunc(message) {
this.notificationMessage = message;
this.showNotification = true;
setTimeout(() => {
this.showNotification = false;
}, 3000);
}
}
});
</script>
</body>
</html>
Loading…
Cancel
Save