Compare commits

...

14 Commits
main ... dev

@ -144,7 +144,7 @@
### 1.2 请求参数
| 参数名 | 类型 | 是否必填 | 说明 | 示例值 |
|-----------------------|---------|----------|--------------------------|--------------|
| train_number | string | 否 | 车(精确/模糊查询) | "JZ20250301" |
| train_number | string | 否 | 车辆ID(精确/模糊查询) | "JZ20250301" |
| train_model | string | 否 | 车型 | "货车" |
| train_carriage_number | string | 否 | 车厢号(精确查询) | "C001" |
| alarm_type | string | 否 | 告警类型(如:倾斜、断裂) | "倾斜" |
@ -164,7 +164,7 @@
| ├─ total | int | 总记录数 |
| ├─ list | array | 撑杆监测列表 |
| └─ ├─ id | string | 唯一标识 |
| └─ ├─ carNo | string | 车 |
| └─ ├─ carNo | string | 车辆ID |
| └─ ├─ carType | string | 车型 |
| └─ ├─ carriageNo | string | 车厢号 |
| └─ ├─ warnType | string | 告警类型 |
@ -239,7 +239,7 @@
| message | string | 响应信息 |
| data | object | 数据体 |
| ├─ id | string | 监测数据 ID |
| ├─ carNo | string | 车 |
| ├─ carNo | string | 车辆ID |
| ├─ carType | string | 车型 |
| ├─ carriageNo | string | 车厢号 |
| ├─ warnType | string | 告警类型 |
@ -312,7 +312,7 @@
### 1.2 请求参数
| 参数名 | 类型 | 是否必填 | 说明 | 示例值 |
|-----------------------|---------|----------|--------------------------|--------------|
| train_number | string | 否 | 车(精确/模糊查询) | "JZ20250301" |
| train_number | string | 否 | 车辆ID(精确/模糊查询) | "JZ20250301" |
| train_model | string | 否 | 车型 | "货车" |
| train_carriage_number | string | 否 | 车厢号(精确查询) | "C001" |
| alarm_type | string | 否 | 告警类型(如:倾斜、断裂) | "倾斜" |
@ -333,7 +333,7 @@
| ├─ total | int | 总记录数 |
| ├─ list | array | 外观监测列表 |
| └─ ├─ id | string | 唯一标识 |
| └─ ├─ carNo | string | 车 |
| └─ ├─ carNo | string | 车辆ID |
| └─ ├─ carType | string | 车型 |
| └─ ├─ carriageNo | string | 车厢号 |
| └─ ├─ warnType | string | 告警类型 |
@ -410,7 +410,7 @@
| message | string | 响应信息 |
| data | object | 数据体 |
| ├─ id | string | 监测数据 ID |
| ├─ carNo | string | 车 |
| ├─ carNo | string | 车辆ID |
| ├─ carType | string | 车型 |
| ├─ carriageNo | string | 车厢号 |
| ├─ warnType | string | 告警类型 |

@ -0,0 +1,22 @@
/*
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-03-11 11:29:02
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-08-19 11:39:52
* @FilePath: \5G-Loading-Bay-Web\mock\poleMonitor.ts
* @Description:
*/
import { MockMethod } from "vite-plugin-mock";
import { poleMonitorListData, fileListData } from "./pools/commonData";
import { fetchCurrPageByList, fetchMockSuccessFullByOther } from "./utils/apiMock";
export default [
{
url: "/api/v1/record/generator_result/",
method: "post",
response: req => {
// console.log(req);
return {...fetchMockSuccessFullByOther(fileListData)}
}
}
] as MockMethod[];

@ -0,0 +1,80 @@
/*
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-03-11 11:30:09
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-08-19 17:35:17
* @FilePath: \5G-Loading-Bay-Web\mock\pools\poleMonitorData.ts
* @Description: ,`customMade`, koroFileHeader : https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import Mock from "mockjs";
import { isImage } from "../utils/is";
const fileUrls = [
"https://t7.baidu.com/it/u=72176654,472254471&fm=193",
"https://img2.baidu.com/it/u=1319203967,453193481&fm=253&fmt=auto&app=120&f=JPEG?w=1023&h=685",
"https://img2.baidu.com/it/u=1876217523,3868046800&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=800",
"https://picx.zhimg.com/v2-a8b8217d6aad02940be44a657cb55a99_r.jpg?source=1940ef5c",
"https://img0.baidu.com/it/u=108226426,371429720&fm=253&fmt=auto&app=138&f=JPEG?w=749&h=500",
"https://img1.baidu.com/it/u=967792105,51180745&fm=253&fmt=auto?w=1200&h=800",
"https://picx.zhimg.com/v2-026d3f92c9984a21dc217018d3b228a7_r.jpg?source=1def8aca",
];
const mockListData = Mock.mock({
// 生成 10 条数据,可以根据需要调整数量
"data|140": [
{
// 车辆ID生成随机的 4 位字母和数字组合
train_number: /[A-Z0-9]{10}/,
// 车型,从预定义的数组中随机选择一个
train_model: () => Mock.Random.pick(["轿车", "SUV", "客车", "货车"]),
// 车厢号,生成 1 到 10 的随机整数
"train_carriage_number|1-10": 1,
// 告警类型,从预定义的数组中随机选择一个
alarm_type: () =>
Mock.Random.pick(["超速告警", "碰撞告警", "低电量告警"]),
// 故障类型,从预定义的数组中随机选择一个
faultType: () => Mock.Random.pick(["撑杆弯曲", "撑杆断折"]),
// 等级,生成 1 到 3 的随机整数
"level|1-3": 1,
// 复核,随机生成 '是' 或 '否'
is_reviewed: () => Mock.Random.pick([true, false]),
// 时间,生成过去一个月内的随机日期和时间
created_at: () =>
Mock.Random.date("yyyy-MM-dd") + " " + Mock.Random.time("HH:mm:ss"),
},
],
});
const mockFilesData = Mock.mock({
[`list|${fileUrls.length}`]: [
{
"id|+1": 10,
key: "@id",
name: "@animal",
image_url: function () {
// 依次取出视频链接
const currFile = fileUrls[this.id - 10];
return fileUrls[this.id - 10];
return null;
},
created_at: '@datetime("yyyy-MM-dd HH:mm:ss")',
updated_at: '@datetime("yyyy-MM-dd HH:mm:ss")',
length: "@float(0.1, 10, 2, 2)",
width: "@float(0.1, 10, 2, 2)",
height: "@float(0.1, 10, 2, 2)",
weight: "@float(0.1, 1000, 1, 2)",
volume: function () {
return (this.length * this.width * this.height).toFixed(2);
},
record: 1,
},
],
});
// console.log(mockListData, 'mockListData');
const currentData = mockListData.data;
const currentFilesData = mockFilesData.list;
export const fileListData = {
data: {
data: currentFilesData,
},
};

@ -24,7 +24,7 @@ const mockListData = Mock.mock({
// 生成 10 条数据,可以根据需要调整数量
"data|140": [
{
// 车,生成随机的 4 位字母和数字组合
// 车辆ID,生成随机的 4 位字母和数字组合
train_number: /[A-Z0-9]{10}/,
// 车型,从预定义的数组中随机选择一个
train_model: () => Mock.Random.pick(["轿车", "SUV", "客车", "货车"]),

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

@ -2,7 +2,7 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-03-07 15:09:18
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-03-14 11:14:15
* @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
*/
@ -94,3 +94,35 @@ export const getVehiclManagementApi = (params: any) => {
export const getBeforeMonitorDetailApi = (params: any) => {
return request.get(`/api/v1/record/before_arrive_record_list/`, params);
};
// 图表统计1 type有appearance、pole、distance。dateType有day、hour分别对应日和时
export const getRecordAmountDataApi = (params: any) => {
return request.get(`/api/v1/record/get_record_amount_data/`, params);
};
// 图表统计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);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 KiB

After

Width:  |  Height:  |  Size: 964 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

@ -0,0 +1,255 @@
<!--
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-08-14 10:33:46
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-08-27 16:22:40
* @FilePath: \5G-Web\src\components\Charts\appearanceDailyAlertChart.vue
* @Description: 外观日告警统计
-->
<script lang="ts" setup>
import * as echarts from "echarts";
const props = defineProps({
datas: {
type: Object,
default: () => {},
},
});
function initChart() {
const chartDom = document.getElementById("appearanceDailyAlertChart");
const mainChart = echarts.init(chartDom);
// TODO
let xAxisData = [];
let typesData = [];
for (let key in props.datas) {
xAxisData.push(key + "日");
if (props.datas[key] && Object.keys(props.datas[key])?.length) {
typesData.push(...Object.keys(props.datas[key]));
}
}
typesData = Array.from(new Set(typesData));
console.log(typesData, "typesData");
const colorArrs = [
{
name: "搭扣未扣",
color: "rgba(212, 145, 20, 0.9)",
color1: "rgba(212, 145, 20, 0.3)",
},
{
name: "小门搭扣丢失",
color: "rgba(0, 157, 255, 0.9)",
color1: "rgba(0, 157, 255, 0.3)",
},
{
name: "门折页座脱落",
color: "rgba(44, 188, 204, 0.9)",
color1: "rgba(44, 188, 204, 0.3)",
},
{
name: "下侧门板缺失",
color: "rgba(13, 233, 138, 0.9)",
color1: "rgba(13, 233, 138, 0.3)",
},
{
name: "小门外胀",
color: "rgba(198, 81, 14, 0.9)",
color1: "rgba(198, 81, 14, 0.3)",
},
{
name: "小窗裂纹",
color: "rgba(255, 209, 92, 0.9)",
color1: "rgba(255, 209, 92, 0.3)",
},
];
// const startData = [
// {
// name: "",
// data: [
// 80, 30, 120, 80, 100, 60, 70, 50, 40, 60, 110, 30, 80, 100, 120, 150,
// ],
// color: "rgba(212, 145, 20, 0.9)",
// color1: "rgba(212, 145, 20, 0.3)",
// },
// {
// name: "",
// data: [
// 100, 20, 220, 180, 120, 180, 100, 120, 100, 140, 160, 20, 180, 140, 80,
// 170,
// ],
// color: "rgba(0, 157, 255, 0.9)",
// color1: "rgba(0, 157, 255, 0.3)",
// },
// {
// name: "",
// data: [
// 150, 180, 200, 190, 180, 50, 80, 100, 80, 140, 120, 190, 150, 60, 100,
// 120,
// ],
// color: "rgba(44, 188, 204, 0.9)",
// color1: "rgba(44, 188, 204, 0.3)",
// },
// {
// name: "",
// data: [
// 120, 140, 180, 170, 80, 140, 130, 110, 140, 100, 100, 140, 120, 80, 140,
// 140,
// ],
// color: "rgba(13, 233, 138, 0.9)",
// color1: "rgba(13, 233, 138, 0.3)",
// },
// {
// name: "",
// data: [
// 100, 120, 80, 100, 50, 70, 150, 120, 100, 140, 60, 100, 60, 150, 120,
// 170,
// ],
// color: "rgba(198, 81, 14, 0.9)",
// color1: "rgba(198, 81, 14, 0.3)",
// },
// {
// name: "",
// data: [
// 150, 160, 180, 170, 150, 80, 100, 140, 120, 80, 140, 80, 120, 80, 100,
// 150,
// ],
// color: "rgba(255, 209, 92, 0.9)",
// color1: "rgba(255, 209, 92, 0.3)",
// },
// ];
let finalData = [];
colorArrs.forEach((lineItem, lineIndex) => {
const currInitValues = new Array(xAxisData.length).fill(0);
Object.keys(props.datas).forEach((key, index) => {
if (
props.datas[key] &&
Object.keys(props.datas[key]).includes(lineItem.name)
) {
currInitValues[index] = props.datas[key][lineItem.name];
}
});
// for (let key in props.datas) {
// if(props.datas[key] && Object.keys(props.datas[key]).includes(lineItem.name)){
// currInitValues[Number(key)-1] = props.datas[key][lineItem.name];
// }
// }
finalData.push({
...lineItem,
data: currInitValues,
});
});
const seriesOptions = [];
const legendData = [];
finalData.forEach((item) => {
legendData.push(item.name);
seriesOptions.push({
name: item.name,
type: "line",
color: item.color,
symbolSize: 9,
symbol: "none",
areaStyle: {
color: {
type: "line",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: item.color1,
},
{
offset: 1,
color: "rgba(255, 209, 92, 0)",
},
],
global: false,
},
},
data: item.data,
});
});
let option = {
backgroundColor: "transparent",
tooltip: {
trigger: "axis",
},
legend: {
orient: "horizontal",
right: 0,
top: "5%",
icon: "rect",
textStyle: {
fontFamily: "PingFangSC-Semibold",
fontSize: 14,
color: "#FFFFFF",
padding: [0, 2],
},
data: legendData,
},
grid: {
left: "1%",
right: "1%",
bottom: "5%",
top: "25%",
containLabel: true,
},
xAxis: {
type: "category",
boundaryGap: false,
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
color: "#B4C0CC",
},
axisTick: {
show: false,
},
data: xAxisData,
},
yAxis: {
type: "value",
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
color: "#B4C0CC",
},
splitLine: {
show: true,
lineStyle: {
type: "dashed",
color: "rgba(255,255,255,0.1)",
width: 2,
},
},
},
series: seriesOptions,
};
mainChart.setOption(option);
}
watch(
() => props.datas,
(newVal) => {
if (newVal) {
initChart();
}
},
{
deep: true,
}
);
</script>
<template>
<div id="appearanceDailyAlertChart" style="width: 960px; height: 100%"></div>
</template>

@ -0,0 +1,242 @@
<!--
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-08-14 10:33:22
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-08-26 17:09:24
* @FilePath: \5G-Web\src\components\Charts\appearanceDailyDetectionChart.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<script setup lang="ts">
import * as echarts from "echarts";
const props = defineProps({
datas: {
type: Object,
default: () => {},
},
});
function initChart() {
const chartDom = document.getElementById("appearanceDailyDetectionChart");
const mainChart = echarts.init(chartDom);
let finalData = [];
for (let key in props.datas) {
finalData.push({
name: key + "日",
value: props.datas[key],
});
}
// TODO mock
// const finalData = [
// {
// name: "1",
// value: 175,
// },
// {
// name: "2",
// value: 148,
// },
// {
// name: "3",
// value: 195,
// },
// {
// name: "4",
// value: 115,
// },
// {
// name: "5",
// value: 148,
// },
// {
// name: "6",
// value: 95,
// },
// {
// name: "7",
// value: 56,
// },
// {
// name: "8",
// value: 45,
// },
// {
// name: "9",
// value: 15,
// },
// {
// name: "10",
// value: 48,
// },
// {
// name: "11",
// value: 95,
// },
// {
// name: "12",
// value: 128,
// },
// {
// name: "13",
// value: 34,
// },
// {
// name: "14",
// value: 123,
// },
// {
// name: "15",
// value: 175,
// },
// {
// name: "16",
// value: 148,
// },
// ];
const createSvg = (shadowColor, shadowBlur) => `
<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
x="0px" y="0px"
viewBox="0 0 32 128"
xml:space="preserve"
>
<style>
.st2 {
fill: transparent;
stroke: ${shadowColor};
stroke-width: ${shadowBlur}px;
filter: url(#chart-inset-shadow);
}
</style>
<defs>
<filter id="chart-inset-shadow" width="200%" height="200%" x="-50%" y="-50%">
<feGaussianBlur in="SourceGraphic" result="gass" stdDeviation="${
shadowBlur * 0.75
}" />
<feMerge>
<feMergeNode in="gass" />
</feMerge>
</filter>
</defs>
<path class="st2" d="M0 0 L32 0 L32 128 L0 128 Z" />
</svg>
`;
const svgString = createSvg("rgba(7, 131, 250, 1)", 8);
const svg = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
const barWidth = 16;
const DOMURL = window.URL || window.webkitURL || window;
const insetShadowUrl = DOMURL.createObjectURL(svg);
let xAxisData = [];
let seriesData1 = [];
finalData.forEach((item) => {
xAxisData.push(item.name);
seriesData1.push(item.value);
});
const option = {
backgroundColor: "",
tooltip: {
trigger: "axis",
},
grid: {
left: "1%",
right: "1%",
bottom: "5%",
top: "15%",
containLabel: true,
},
xAxis: {
type: "category",
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
color: "#B4C0CC",
},
splitLine: {
show: false,
},
data: xAxisData,
},
yAxis: {
type: "value",
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
color: "#B4C0CC",
},
splitLine: {
show: true,
lineStyle: {
type: "dashed",
color: "rgba(255,255,255,0.1)",
width: 1,
},
},
},
series: [
{
data: seriesData1,
type: "pictorialBar",
symbol: "image://" + insetShadowUrl,
barWidth: barWidth,
legendHoverLink: false,
},
{
tooltip: {
show: false,
},
data: seriesData1,
type: "bar",
barWidth: barWidth,
itemStyle: {
color: "transparent",
borderWidth: 1,
borderColor: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(45, 230, 255, 1)",
},
{
offset: 1,
color: "rgba(7, 131, 250, 1)",
},
]),
shadowColor: "blue",
shadowBlur: 12,
shadowOffsetX: 0,
shadowOffsetY: 0,
},
},
],
};
mainChart.setOption(option);
}
watch(
() => props.datas,
(newVal) => {
if (newVal) {
initChart();
}
},
{
deep: true,
}
);
</script>
<template>
<div
class="main-chart"
ref="mainChartRef"
id="appearanceDailyDetectionChart"
style="width: 862px; height: 100%"
></div>
</template>

@ -0,0 +1,216 @@
<script setup lang="ts">
import * as echarts from "echarts";
const props = defineProps({
datas: {
type: Object,
default: () => {},
},
});
function initChart() {
const chartDom = document.getElementById("diggerDailyDistanceChart");
const mainChart = echarts.init(chartDom);
let finalData = [];
for (let key in props.datas) {
finalData.push({
name: key + "日",
value: props.datas[key],
});
}
// let finalData = [
// {
// name: "1",
// value: 175,
// },
// {
// name: "2",
// value: 148,
// },
// {
// name: "3",
// value: 195,
// },
// {
// name: "4",
// value: 115,
// },
// {
// name: "5",
// value: 148,
// },
// {
// name: "6",
// value: 95,
// },
// {
// name: "7",
// value: 56,
// },
// {
// name: "8",
// value: 45,
// },
// {
// name: "9",
// value: 15,
// },
// {
// name: "10",
// value: 48,
// },
// {
// name: "11",
// value: 95,
// },
// {
// name: "12",
// value: 128,
// },
// {
// name: "13",
// value: 34,
// },
// {
// name: "14",
// value: 123,
// },
// {
// name: "15",
// value: 175,
// },
// {
// name: "16",
// value: 148,
// },
// ];
let xAxisData = [];
let seriesData2 = [];
finalData.forEach((item) => {
xAxisData.push(item.name);
seriesData2.push(item.value);
});
let option = {
tooltip: {
textStyle: {
color: "#000",
},
padding: [10, 10],
trigger: "axis",
backgroundColor: "#fff",
borderColor: "rgba(112, 119, 242, 0.8)",
borderWidth: 1,
},
grid: {
left: "1%",
right: "1%",
bottom: "5%",
top: "15%",
containLabel: true,
},
toolbox: {
show: false,
},
xAxis: {
type: "category",
data: xAxisData,
axisLine: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
show: false,
},
axisLabel: {
color: "#B4C0CC",
},
},
yAxis: [
{
type: "value",
axisLine: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
lineStyle: {
color: "#35414D",
type: "dashed",
},
},
axisLabel: {
color: "#B4C0CC",
},
},
],
series: [
{
type: "line",
data: seriesData2,
symbolSize: 10,
symbol: "circle",
smooth: false,
yAxisIndex: 0,
label: {
show: false,
// textStyle: {
// color: "#FFD15C",
// fontSize: 20,
// fontFamily: "DIN",
// fontWeight: "bold",
// },
position: "top",
formatter: function (p) {
return p.value > 0 ? p.value : "";
},
},
lineStyle: {
width: 2,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(255, 209, 92, 1)",
},
{
offset: 1,
color: "rgba(255, 209, 92, 1)",
},
]),
shadowColor: "rgba(255, 209, 92, 0.4)",
shadowBlur: 10,
shadowOffsetY: 10,
},
itemStyle: {
color: "rgba(255, 209, 92, 1)",
borderColor: "",
borderWidth: 3,
shadowColor: "rgba(255, 209, 92, 01)",
shadowBlur: 5,
},
},
],
};
mainChart.setOption(option);
}
watch(
() => props.datas,
(newVal) => {
if (newVal) {
initChart();
}
},
{
deep: true,
}
);
</script>
<template>
<div id="diggerDailyDistanceChart" style="width: 960px; height: 100%"></div>
</template>

@ -0,0 +1,303 @@
<script setup lang="ts">
import * as echarts from "echarts";
const props = defineProps({
datas: {
type: Object,
default: () => {},
},
});
function generateData(totalNum, bigvalue, smallvalue, color) {
let dataArr = [];
for (var i = 0; i < totalNum; i++) {
if (i % 2 === 0) {
dataArr.push({
name: (i + 1).toString(),
value: bigvalue,
itemStyle: {
color: color,
borderWidth: 0,
},
});
} else {
dataArr.push({
name: (i + 1).toString(),
value: smallvalue,
itemStyle: {
color: "rgba(0,0,0,0)",
borderWidth: 0,
},
});
}
}
return dataArr;
}
function initChart() {
const chartDom = document.getElementById("diggerHourlyDistanceChart");
const mainChart = echarts.init(chartDom);
let finalData = [];
for (let key in props.datas) {
finalData.push({
name: key + ":00",
value: props.datas[key],
});
}
// let finalData = [
// {
// name: "05:00",
// value: 175,
// },
// {
// name: "06:00",
// value: 148,
// },
// {
// name: "07:00",
// value: 95,
// },
// {
// name: "08:00",
// value: 175,
// },
// {
// name: "09:00",
// value: 148,
// },
// {
// name: "10:00",
// value: 95,
// },
// {
// name: "11:00",
// value: 56,
// },
// {
// name: "12:00",
// value: 45,
// },
// {
// name: "13:00",
// value: 34,
// },
// {
// name: "14:00",
// value: 123,
// },
// {
// name: "15:00",
// value: 175,
// },
// {
// name: "16:00",
// value: 148,
// },
// {
// name: "17:00",
// value: 95,
// },
// {
// name: "18:00",
// value: 123,
// },
// {
// name: "19:00",
// value: 175,
// },
// {
// name: "20:00",
// value: 148,
// },
// {
// name: "21:00",
// value: 95,
// },
// {
// name: "22:00",
// value: 148,
// },
// {
// name: "23:00",
// value: 95,
// },
// ];
let xAxisData = [];
let seriesData1 = [];
finalData.forEach((item) => {
xAxisData.push(item.name);
seriesData1.push(item.value);
});
const dom = 200;
const barWidth = dom / 10;
const colors = [
{
type: "linear",
x: 1,
x2: 1,
y: 0,
y2: 1,
colorStops: [
{
offset: 1,
color: "rgba(216,216,216,0)",
},
{
offset: 0.5,
color: "rgba(103,236,228,0.53)",
},
{
offset: 0,
color: "#00FEEE",
},
],
},
];
const colors1 = [
{
type: "linear",
x: 0,
x2: 0,
y: 1,
y2: 0,
colorStops: [
{
offset: 0.3,
color: "#4bcde4",
},
{
offset: 1,
color: "#4b88e4",
},
],
},
];
const option = {
backgroundColor: "transparent",
//X
xAxis: {
name: "X",
nameTextStyle: {
color: "#FFFFFF",
padding: [0, 0, 0, 50],
},
data: xAxisData,
show: true,
type: "category",
axisLine: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
show: false,
},
axisLabel: {
color: "#B4C0CC",
},
},
yAxis: {
type: "value",
axisLine: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
lineStyle: {
color: "#35414D",
type: "dashed",
},
},
axisLabel: {
color: "#B4C0CC",
},
},
/**区域位置*/
grid: {
// Adjust grid for spacing
left: "1%",
right: "1%",
bottom: "5%",
top: "15%",
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
},
series: [
{
type: "bar",
barWidth: barWidth,
symbolOffset: ["10%", "50%"],
itemStyle: {
color: function (params) {
return colors[0];
},
},
data: seriesData1,
tooltip: {
show: false,
},
},
{
//
z: 4,
type: "pictorialBar",
data: seriesData1,
symbolOffset: ["0%", "0%"],
symbolSize: [barWidth, barWidth * 0.3],
itemStyle: {
color: function (params) {
return "rgba(0,255,252,0.1)";
},
},
tooltip: {
show: false,
},
},
{
//
z: 3,
type: "pictorialBar",
symbolPosition: "end",
data: seriesData1,
symbolOffset: ["0%", "-50%"],
symbolSize: [barWidth, barWidth * 0.3],
itemStyle: {
borderWidth: 0,
color: function (params) {
return colors1[0];
},
},
},
],
};
mainChart.setOption(option);
}
watch(
() => props.datas,
(newVal) => {
if (newVal) {
initChart();
}
},
{
deep: true,
}
);
</script>
<template>
<div id="diggerHourlyDistanceChart" style="width: 862px; height: 100%"></div>
</template>

@ -0,0 +1,231 @@
<script setup lang="ts">
import * as echarts from "echarts";
function generateData(totalNum, bigvalue, smallvalue, color) {
let dataArr = [];
for (const i = 0; i < totalNum; i++) {
if (i % 2 === 0) {
dataArr.push({
name: (i + 1).toString(),
value: bigvalue,
itemStyle: {
color: color,
borderWidth: 0,
},
});
} else {
dataArr.push({
name: (i + 1).toString(),
value: smallvalue,
itemStyle: {
color: "rgba(0,0,0,0)",
borderWidth: 0,
},
});
}
}
return dataArr;
}
function initChart() {
const chartDom = document.getElementById("dvDeviceInfoChart");
const mainChart = echarts.init(chartDom);
const value = [965, 1155, 872];
const value1 = [0.65, 0.12, -0.33];
const max1 = Math.max.apply(null, value) * 1.2;
const data1 = [];
//data1for使value
for (let i = value.length - 1; i >= 0; i--) {
data1.push(max1);
}
const option = {
backgroundColor: "transparent",
grid: {
left: "5%",
top: "5%",
bottom: "5%",
right: "5%",
containLabel: true,
},
xAxis: {
type: "value",
max: max1,
splitLine: {
show: false,
},
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
show: false,
},
},
yAxis: {
type: "category",
splitLine: {
show: false,
},
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
show: true,
interval: 0,
margin: 0,
verticalAlign: "right",
align: "left",
padding: [-30, 0, 0, 0],
textStyle: {
color: "#ffffff",
fontSize: 16,
},
},
data: ["周一", "周二", "周三"],
},
series: [
{
//
name: "真实值",
type: "bar",
animation: false,
barWidth: "5%",
label: {
show: true,
position: "insideLeft",
padding: [50, 0, 0, -15],
distance: 15,
//color: co1,
formatter: function (params) {
//console.info(params);
if (value1[params.dataIndex] < 0) {
return (
"{a|较上年}{b|" +
(value1[params.dataIndex] * 100).toFixed(0) +
"%}"
);
} else {
return (
"{a|较上年}{c|+" +
(value1[params.dataIndex] * 100).toFixed(0) +
"%}"
);
}
},
rich: {
a: {
fontSize: 12,
color: "#fff",
//padding: [20, -30, 0, -30],
},
b: {
fontSize: 12,
color: "#13C2C2",
fontWeight: "bold",
padding: [0, 0, 0, 15],
},
c: {
fontSize: 12,
color: "#FF4242",
fontWeight: "bold",
padding: [0, 0, 0, 15],
},
},
},
itemStyle: {
normal: {
color: {
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [
{
offset: 0,
color: "rgba(2,50,147,1)", // 0%
},
{
offset: 1,
color: "rgba(36,237,255,1)", // 100%
},
],
},
},
},
data: value,
z: 1,
},
{
//1
name: "真实值圆点",
type: "scatter",
symbolOffset: ["10%", "0"],
symbolSize: 8,
itemStyle: {
borderWidth: 0,
color: "rgba(255,255,255,1)",
},
data: value,
z: 5,
},
{
//2
name: "真实值圆环",
type: "scatter",
symbolOffset: ["10%", "0"],
symbolSize: 15,
itemStyle: {
borderWidth: 1,
borderColor: "rgba(101,224,255,1)",
//shadowBlur: 15,
//shadowColor: 'rgba(101,224,255,1)',
color: "rgba(255,255,255,0.43)",
},
data: value,
z: 10,
},
{
//3
name: "背景条",
type: "bar",
animation: false,
barGap: "-100%",
barWidth: "5%",
label: {
show: true,
position: "insideRight",
verticalAlign: "right",
padding: [-30, -10, 0, 0],
distance: 15,
color: "#fff",
formatter: function (params) {
//console.info(params);
return " " + value[params.dataIndex].toFixed(0);
},
},
itemStyle: {
normal: {
color: "rgba(62,78,123,1)",
},
},
data: data1,
z: 0,
},
],
};
mainChart.setOption(option);
}
onMounted(() => {
initChart();
});
</script>
<template>
<div id="dvDeviceInfoChart" style="width: 100%; height: 100%"></div>
</template>

@ -0,0 +1,285 @@
<!--
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-08-14 10:31:16
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-08-27 13:44:55
* @FilePath: \5G-Web\src\components\Charts\poleDailyAlertChart.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<script lang="ts" setup>
import * as echarts from "echarts";
const props = defineProps({
datas: {
type: Object,
default: () => {},
},
});
// const propsData = {
// xAxisData: [
// "1",
// "2",
// "3",
// "4",
// "5",
// "6",
// "7",
// "8",
// "9",
// "10",
// "11",
// "12",
// "13",
// "14",
// "15",
// "16",
// ],
// legendData: ["", "", "穿"],
// seriesData: {
// : [
// 210, 80, 120, 100, 90, 120, 110, 140, 130, 120, 120, 125, 130, 160, 150,
// 100,
// ],
// : [
// 70, 110, 140, 120, 140, 70, 80, 170, 160, 140, 140, 130, 140, 120, 90,
// 110,
// ],
// 穿: [
// 80, 150, 180, 160, 80, 60, 70, 180, 190, 170, 160, 140, 130, 80, 100,
// 150,
// ],
// },
// };
const initChart = () => {
const chartDom = document.getElementById("poleDailyAlertChart");
const mainChart = echarts.init(chartDom);
const seriesBaseData = [
{
// name: "",
// data: propsData.seriesData[""],
type: "line",
smooth: true, //true线; false线
symbol: "none",
itemStyle: {
color: "#ffc600", //线
lineStyle: {
color: "#ffc600", //线
type: "solid",
},
},
areaStyle: {
//线
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "rgba(255,198,0, 0.5)", // 0%
},
{
offset: 1,
color: "rgba(255,198,0, 0.1)", // 100%
},
],
global: false, // false
},
},
},
{
// name: "",
// data: propsData.seriesData[""],
type: "line",
smooth: true, //true线; false线
symbol: "none",
itemStyle: {
color: "#24adfe", //线
lineStyle: {
color: "#24adfe", //线
type: "solid",
},
},
areaStyle: {
//线
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "rgba(36,173,254, 0.5)", // 0%
},
{
offset: 1,
color: "rgba(52,112,252, 0.1)", // 100%
},
],
global: false, // false
},
},
},
{
// name: "穿",
// data: propsData.seriesData["穿"],
type: "line",
smooth: true, //true线; false线
symbol: "none",
itemStyle: {
color: "#0DE98A", //线
lineStyle: {
color: "#0DE98A", //线
type: "solid",
},
},
areaStyle: {
//线
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "rgba(13, 233, 138, 0.5)", // 0%
},
{
offset: 1,
color: "rgba(13, 233, 138, 0.1)", // 100%
},
],
global: false, // false
},
},
},
];
const maxTypesLength = seriesBaseData.length;
let xAxisData = [];
let typesData = [];
for (let key in props.datas) {
xAxisData.push(key + "日");
if (props.datas[key] && Object.keys(props.datas[key])?.length) {
typesData.push(...Object.keys(props.datas[key]));
}
}
const legendData = Array.from(new Set(typesData)).slice(0, maxTypesLength);
let finalSeriesData = [];
legendData.forEach((v, vIndex) => {
const currInitValues = new Array(xAxisData.length).fill(0);
Object.keys(props.datas).forEach((key, index) => {
if (props.datas[key] && Object.keys(props.datas[key]).includes(v)) {
currInitValues[index] = props.datas[key][v];
}
});
finalSeriesData.push({
...seriesBaseData[vIndex],
name: v,
data: currInitValues,
});
});
const option = {
backgroundColor: "#061740",
grid: {
x: 0,
y: 0,
x2: 0,
y2: 0,
left: "1%",
right: "1%",
bottom: "5%",
top: "25%",
containLabel: true,
},
tooltip: {
trigger: "axis",
},
legend: {
left: "right",
top: "1",
padding: [10, 0, 0, 0],
data: legendData,
//
// type: 'scroll',
//
textStyle: {
color: "#fff",
fontSize: "14px",
},
},
xAxis: [
{
type: "category",
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
color: "#B4C0CC",
},
data: xAxisData,
},
],
yAxis: [
{
type: "value",
interval: 1,
axisLabel: {
formatter: "{value} ",
},
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
color: "#B4C0CC",
},
//线
splitLine: {
show: true,
lineStyle: {
type: "solid", //solid线;dashed线
color: "rgba(36, 173, 254, 0.2)",
},
},
},
],
series: finalSeriesData,
};
mainChart.setOption(option);
};
watch(
() => props.datas,
(newVal) => {
if (newVal) {
initChart();
}
},
{
deep: true,
}
);
</script>
<template>
<div id="poleDailyAlertChart" style="width: 960px; height: 100%"></div>
</template>

@ -0,0 +1,222 @@
<script lang="ts" setup>
import * as echarts from "echarts";
const props = defineProps({
datas: {
type: Object,
default: () => {},
},
});
const initChart = () => {
const chartDom = document.getElementById("poleDailyDetectionChart");
const mainChart = echarts.init(chartDom);
let finalData = [];
for (let key in props.datas) {
finalData.push({
name: key + "日",
value: props.datas[key],
});
}
// const data1 = [
// {
// name: "1",
// value: 220,
// },
// {
// name: "2",
// value: 148,
// },
// {
// name: "3",
// value: 195,
// },
// {
// name: "4",
// value: 245,
// },
// {
// name: "5",
// value: 148,
// },
// {
// name: "6",
// value: 95,
// },
// {
// name: "7",
// value: 56,
// },
// {
// name: "8",
// value: 45,
// },
// {
// name: "9",
// value: 250,
// },
// {
// name: "10",
// value: 48,
// },
// {
// name: "11",
// value: 95,
// },
// {
// name: "12",
// value: 128,
// },
// {
// name: "13",
// value: 34,
// },
// {
// name: "14",
// value: 123,
// },
// {
// name: "15",
// value: 175,
// },
// {
// name: "16",
// value: 148,
// },
// ];
let xAxisData = [];
let seriesData1 = [];
finalData.forEach((item) => {
xAxisData.push(item.name);
seriesData1.push(item.value);
});
var barWidth = 16;
var barWidthBg = 25;
const color = [
{
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 1,
color: "rgba(16, 53, 41, 0.10)",
},
{
offset: 0,
color: "rgba(57, 161, 59, 1)",
},
],
},
];
const seriesData = xAxisData.map((item, index) => {
return {
value: seriesData1[index],
itemStyle: {
borderRadius: [2, 2, 0, 0],
color: color[0],
},
};
});
const option = {
grid: {
left: "1%",
right: "1%",
bottom: "5%",
top: "15%",
containLabel: true,
},
xAxis: {
data: xAxisData,
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
show: true,
color: "#B4C0CC",
fontSize: 12,
},
},
yAxis: {
axisLine: {
show: true,
lineStyle: {
color: "#B4C0CC",
},
},
axisLabel: {
show: true,
color: "#B4C0CC",
fontSize: 12,
},
splitLine: {
lineStyle: {
color: "#B4C0CC",
opacity: 0.2,
},
},
},
series: [
{
type: "bar",
data: seriesData,
barWidth: barWidth,
},
{
type: "bar",
data: seriesData,
barWidth: barWidthBg,
barGap: "-120%",
itemStyle: {
opacity: 0.2,
},
},
{
type: "pictorialBar",
data: seriesData,
symbolPosition: "end",
symbolSize: [barWidth, 7],
z: 12,
symbolOffset: [-3, -3],
label: {
show: true,
position: "top",
},
},
{
type: "pictorialBar",
data: seriesData,
symbolPosition: "end",
symbolSize: [barWidthBg, 6],
itemStyle: {
opacity: 0.2,
},
z: 11,
symbolOffset: [-3, -3],
},
],
};
mainChart.setOption(option);
};
watch(
() => props.datas,
(newVal) => {
if (newVal) {
initChart();
}
},
{
deep: true,
}
);
</script>
<template>
<div id="poleDailyDetectionChart" style="width: 862px; height: 100%"></div>
</template>

@ -0,0 +1,396 @@
<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;
color: string;
pieData: number[]; // [65, 35]
pieColors: string[]; //
}
//
const mainChartRef = ref<HTMLDivElement | null>(null);
//
let mainChart: echarts.ECharts | null = null;
function Pie() {
let dataArr = [];
for (var i = 0; i < 150; i++) {
if (i % 3 === 0) {
dataArr.push({
name: (i + 1).toString(),
value: 10,
itemStyle: {
color: "#fff",
borderWidth: 0,
borderColor: "rgba(0,0,0,0)",
},
});
} else {
dataArr.push({
name: (i + 1).toString(),
value: 25,
itemStyle: {
color: "rgba(0,0,0,0)",
borderWidth: 0,
borderColor: "rgba(0,0,0,0)",
},
});
}
}
return dataArr;
}
//
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: {
trigger: "item",
formatter: " {b} : {d}% <br/> {c}",
},
legend: {
orient: "horizontal",
bottom: 20,
selectedMode: false, //
x: "center",
textStyle: {
color: "#fff",
},
data: legendData,
},
series: [
{
type: "pie",
radius: ["45%", "68%"],
center: centerPoint,
color: colors,
itemStyle: {
borderColor: "#031845",
borderWidth: 10,
//
color: function (params) {
return {
//type: 'linear',
x: 0,
y: 1,
x2: 1,
y2: 0,
colorStops: colors[params.dataIndex], // 100%
//globalCoord: false // false
};
},
},
data: seriesData,
labelLine: {
show: false,
},
label: {
show: false,
},
},
{
type: "pie",
radius: ["0%", "15%"],
center: centerPoint,
color: ["#ffffff", "#ffffff"],
startAngle: 360,
silent: true, //
data: [
{
value: 270,
name: "1",
itemStyle: {
color: "transparent",
borderWidth: 2,
borderColor: "#ffffff",
},
},
{
value: 90,
name: "2",
itemStyle: {
color: "transparent",
},
},
],
labelLine: {
show: false,
},
label: {
show: false,
},
},
{
type: "pie",
radius: [0, "14%"],
center: ["51%", "39%"],
startAngle: 90,
silent: true, //
data: [
{
value: 25,
name: "1",
itemStyle: {
color: "transparent",
borderWidth: 2,
borderColor: "#ffffff",
},
},
{
value: 75,
name: "2",
itemStyle: {
color: "transparent",
},
},
],
// selectedOffset: 10,
labelLine: {
show: false,
},
label: {
show: false,
},
},
{
type: "pie",
radius: ["70%", "71%"],
center: centerPoint,
color: [
"blue",
"transparent",
"blue",
"transparent",
"blue",
"transparent",
],
data: [
{
value: 17,
name: "11",
},
{
value: 17,
name: "22",
},
{
value: 17,
name: "33",
},
{
value: 17,
name: "44",
},
{
value: 17,
name: "55",
},
{
value: 17,
name: "66",
},
],
labelLine: {
show: false,
},
label: {
show: false,
},
},
{
type: "pie",
zlevel: 0,
silent: true,
radius: ["31%", "33%"],
center: centerPoint,
z: 10,
label: {
show: false,
},
labelLine: {
show: false,
},
data: Pie(),
},
{
type: "pie",
zlevel: 0,
silent: true,
radius: ["43%", "45%"],
center: centerPoint,
z: 10,
startAngle: 90,
label: {
show: false,
},
color: ["red", "blue", "red", "blue"],
labelLine: {
show: false,
},
data: [
{
name: "r1",
value: 25,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{
offset: 0,
color: "rgba(51,149,191,0.5)",
},
{
offset: 1,
color: "rgba(51,149,191,0)",
},
]),
},
},
{
name: "r2",
value: 25,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{
offset: 0,
color: "rgba(0,0,0,0)",
},
{
offset: 1,
color: "rgba(51,149,191,0.5)",
},
]),
},
},
{
name: "r3",
value: 25,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{
offset: 0,
color: "rgba(51,149,191,0)",
},
{
offset: 1,
color: "rgba(51,149,191,0.5)",
},
]),
},
},
{
name: "r4",
value: 25,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{
offset: 0,
color: "rgba(51,149,191,0.5)",
},
{
offset: 1,
color: "rgba(0,0,0,0)",
},
]),
},
},
],
},
],
};
mainChart.setOption(option);
//
mainChart.on("click", (params) => {
//
params?.name && router.push({
name: "PoleMonitor",
query: {
name: params?.name
}
});
console.log(params, "点击事件");
});
};
//
const handleResize = () => {
mainChart?.resize();
};
//
const cleanup = () => {
window.removeEventListener("resize", handleResize);
mainChart?.dispose();
};
//
watchEffect((onCleanup) => {
onCleanup(cleanup);
});
watch(
() => props.datas,
(newVal) => {
if (newVal) {
initMainChart();
window.addEventListener("resize", handleResize);
}
},
{
deep: true,
}
);
</script>
<template>
<div
class="main-chart"
ref="mainChartRef"
id="poleMonitorChart"
style="width: 100%; height: 100%"
></div>
</template>

@ -186,26 +186,24 @@ const updateChart = () => {
name: props.legendArr[index],
barWidth: 25,
// label: {
// normal: {
// show: true,
// position: "top",
// fontSize: 16,
// color: "#fff",
// offset: [0, -10],
// },
// },
itemStyle: { color: "transparent" },
data: item,
yAxisIndex: 0, // 使y
},
{
type: "bar",
name: props.legendArr[index],
barWidth: 25,
itemStyle: { color: "transparent" },
data: item,
yAxisIndex: 1, // 使y
},
// {
// type: "bar",
// name: props.legendArr[index],
// barWidth: 25,
// itemStyle: { color: "transparent" },
// data: item,
// yAxisIndex: 1, // 使y
// },
])
.flat();
@ -238,13 +236,13 @@ const updateChart = () => {
grid: {
left: "5%",
right: "5%",
top: "5%",
bottom: "10%",
top: "15%",
bottom: "20%",
containLabel: true,
},
legend: {
left: "center",
top: "90%",
top: "85%",
itemWidth: 12, //
itemHeight: 8,
textStyle: { color: "#fff", fontSize: 12 },
@ -267,7 +265,7 @@ const updateChart = () => {
splitLine: {
lineStyle: { type: "dashed", color: "rgba(80,112,242,0.3)" },
},
axisLabel: { textStyle: { color: "#8C8C8C" }, fontSize: 12 },
axisLabel: { color: "#8C8C8C", fontSize: 12 },
// axisLine: { lineStyle: { color: "#8C8C8C" } },
// scale: true, //
},
@ -275,7 +273,7 @@ const updateChart = () => {
// name: "kWh",
// nameTextStyle: { color: "#fff", fontSize: 12 },
splitLine: { lineStyle: { color: "transparent" } },
axisLabel: { textStyle: { color: "#8C8C8C" }, fontSize: 12 },
axisLabel: { color: "#8C8C8C", fontSize: 12 },
// axisLine: { lineStyle: { color: "#8C8C8C" } },
position: "right",
// scale: true, //

@ -0,0 +1,178 @@
<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++) {
if (i % 2 === 0) {
dataArr.push({
name: (i + 1).toString(),
value: bigvalue,
itemStyle: {
color: color,
borderWidth: 0,
},
});
} else {
dataArr.push({
name: (i + 1).toString(),
value: smallvalue,
itemStyle: {
color: "rgba(0,0,0,0)",
borderWidth: 0,
},
});
}
}
return dataArr;
}
function initChart() {
const chartDom = document.getElementById("vehicleMonitorChart");
const mainChart = echarts.init(chartDom);
let bgColor = "";
let title = "总量";
let color = [
"#3862d8",
"#219CF9",
"#2DE6FF",
"#40c057",
"#ffd351",
"#ff8e43",
"#ff5151",
"#8e2de6",
];
let echartData = props.datas;
let formatNumber = function (num) {
let reg = /(?=(\B)(\d{3})+$)/g;
return num.toString().replace(reg, ",");
};
let total = echartData.reduce((a, b) => {
return a + b.value * 1;
}, 0);
const option = {
backgroundColor: "transparent",
color: color, // color
tooltip: {
trigger: "item",
},
title: {
text: "",
top: 20,
left: 20,
textStyle: {
fontSize: 14,
color: "#666666",
fontWeight: 400,
},
show: false,
},
series: [
{
type: "pie",
zlevel: 2,
roseType: "radius",
radius: ["10%", "65%"],
center: ["50%", "50%"],
data: echartData,
itemStyle: {}, // normal
labelLine: {
length: 18,
length2: 32,
lineStyle: {}, // normal
},
label: {
show: true,
position: "outside",
formatter: "{name|{b}}\n{rate|{d}%}\n{icon|}",
rich: {
icon: {
backgroundColor: "inherit",
borderRadius: 3,
width: 3,
height: 3,
padding: [3, 3, 0, -12],
},
name: {
fontSize: 10, //
padding: [-12, 5, -20, 2],
color: "#fff",
},
rate: {
fontSize: 10, //
align: "left",
padding: [-12, 5, -40, 2], //
color: "#fff",
},
},
},
},
{
type: "pie",
zlevel: 1,
silent: true,
radius: ["0%", "75%"],
center: ["50%", "50%"],
itemStyle: {
color: "#0E4E89", //
},
labelLine: {
show: false,
},
label: {
show: false,
},
data: [{ value: 100, name: "" }], //
},
{
type: "pie",
zlevel: 1,
silent: true,
radius: ["74%", "75%"],
label: {
show: false,
},
itemStyle: {
color: "#3F7D97", //
},
data: [{ value: 100, name: "" }], //
},
],
};
mainChart.setOption(option);
//
mainChart.on("click", (params) => {
//
params?.name && router.push({
name: "AppearanceMonitor",
query: {
name: params?.name
}
});
console.log(params, "点击事件");
});
}
watch(
() => props.datas,
(newVal) => {
if (newVal) {
initChart();
}
},
{
deep: true,
}
);
</script>
<template>
<div id="vehicleMonitorChart" style="width: 100%; height: 100%"></div>
</template>

@ -1,9 +1,15 @@
<template>
<el-dialog class="deleteModal-wrap" v-model="show" @close="handleClose">
<el-dialog
class="deleteModal-wrap"
v-model="show"
@close="handleClose"
align-center
:show-close="false"
>
<!-- 标题栏 -->
<template #header="{ close, titleId, titleClass }">
<div class="flex items-center justify-between delete-dialog-header">
<div class="flex items-center justify-center header-left">
<div class="flex items-center header-left">
<div class="header-icon mr-[12px]"></div>
<p
class="overflow-hidden whitespace-nowrap text-ellipsis max-w-[650px]"
@ -11,6 +17,16 @@
{{ "确定删除吗?" }}
</p>
</div>
<div class="deleteModal-header-close" @click="close">
<i class="el-icon el-dialog__close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"
></path>
</svg>
</i>
</div>
</div>
</template>
<!-- 内容区域 -->
@ -55,7 +71,7 @@ const handleClose = () => {
emit("update:value", false);
};
//
const deleteData = () => {
const deleteData = () => {
emit("delete-confirm", props.info);
};
const show = computed({
@ -70,47 +86,34 @@ const show = computed({
<style lang="scss">
.deleteModal-wrap.el-dialog {
border: none;
overflow: hidden;
box-shadow: none;
background-color: transparent;
background-image: url("@/assets/common/bg_delete_dialog.png");
background-image: url("@/assets/common/info_dialog_bg.png");
background-size: contain;
background-position: center;
background-repeat: no-repeat;
width: 560px;
height: 226px;
background-color: transparent;
width: 573px;
height: 248px;
padding: 0;
margin-top: calc(50vh - 316px);
.el-dialog__header.show-close {
padding: 0;
}
.el-dialog__close {
width: 56px;
height: 56px;
width: 32px;
height: 32px;
color: white;
font-size: 18px;
padding-top: 4px;
padding-right: 20px;
font-size: 18px !important;
padding-top: 0px;
padding-right: 0px;
cursor: pointer;
}
.delete-dialog-header {
color: white;
padding: 0;
padding-top: 32px;
padding-left: 16px;
padding: 16px 16px 0;
.header-left {
padding: 0 24px;
// font-weight: bold;
font-size: 18px;
padding: 0 8px;
width: 266px;
font-size: 16px;
background: linear-gradient(90deg, #0e3c8f 0%, rgba(14, 60, 143, 0) 100%);
.header-icon {
// margin-top: 4px;
width: 24px;
height: 48px;
height: 32px;
background-image: url("@/assets/common/warn_icon.png");
background-size: contain;
background-position: center;
@ -121,8 +124,9 @@ const show = computed({
.delete-content {
box-sizing: border-box;
margin-bottom: 30px;
padding-left: 76px;
margin-bottom: 20px;
margin-top: 30px;
padding-left: 24px;
font-weight: 400;
font-size: 14px;
color: #ffffff;

@ -64,9 +64,15 @@
#0c4fad 53%,
rgba(65, 117, 190, 0) 100%
);
.table_action_box{
color: #fff;
}
}
&.selected-row {
color: #37dbff;
.table_action_box{
color: #fff;
}
background: linear-gradient(
90deg,
rgba(30, 54, 88, 0) 0%,
@ -82,22 +88,25 @@
.el-table__header > thead {
color: #9fb5d7;
background-color: #104284 !important;
background-color: #0841A0 !important;
tr {
background-color: #104284 !important;
background-color: #0841A0 !important;
}
th {
background-color: #104284 !important;
background-color: #0841A0 !important;
}
}
}
.fixed_pagination {
padding: 12px 20px 0;
padding: 12px 0px 0;
}
/* full_table */
&.full_table {
.el-table--large .el-table__cell {
padding: 4px 0;
padding: 8.5px 0;
}
.el-table__body .el-table__cell {
padding: 10px 0;
}
.baseTable_box {
cursor: default;
@ -118,19 +127,18 @@
}
&:hover td {
background-color: transparent;
}
}
}
.el-table__header > thead {
color: #9fb5d7;
background-color: #104284 !important;
background-color: #0841A0 !important;
tr {
background-color: #104284 !important;
background-color: #0841A0 !important;
}
th {
background-color: #104284 !important;
background-color: #0841A0 !important;
}
}
}

@ -6,7 +6,7 @@ import {
defineComponent,
nextTick,
reactive,
type PropType
type PropType,
} from "vue";
import "./baseTable.scss";
@ -17,7 +17,7 @@ function getDefaultSort(attrs: Record<string, any>): any {
export default defineComponent({
name: "XTable",
directives: {
loading: ElLoading.directive
loading: ElLoading.directive,
},
inheritAttrs: false,
props: {
@ -26,7 +26,7 @@ export default defineComponent({
*/
columns: {
type: Array as PropType<XTableColumn[]>,
required: true
required: true,
},
/**
@ -34,7 +34,7 @@ export default defineComponent({
*/
dataSource: {
type: Array as PropType<XTableData[]>,
required: true
required: true,
},
/**
@ -42,7 +42,7 @@ export default defineComponent({
*/
loading: {
type: Boolean,
default: false
default: false,
},
/**
@ -50,7 +50,7 @@ export default defineComponent({
*/
maxHeight: {
type: [Number, String] as PropType<number | "auto">,
default: "auto"
default: "auto",
},
/**
@ -64,7 +64,7 @@ export default defineComponent({
default: true,
validator(value: boolean | "always") {
return ["always", true, false].includes(value);
}
},
},
/**
@ -76,11 +76,11 @@ export default defineComponent({
validator(value: string) {
return value
.split(",")
.map(item => item.trim())
.every(item =>
.map((item) => item.trim())
.every((item) =>
["total", "sizes", "prev", "pager", "next", "jumper"].includes(item)
);
}
},
},
/**
@ -88,7 +88,7 @@ export default defineComponent({
*/
total: {
type: Number,
default: 0
default: 0,
},
/**
@ -96,7 +96,7 @@ export default defineComponent({
*/
pageSize: {
type: Number,
default: 10
default: 10,
},
/**
@ -104,7 +104,7 @@ export default defineComponent({
*/
page: {
type: Number,
default: 1
default: 1,
},
/**
@ -114,7 +114,7 @@ export default defineComponent({
type: Array as PropType<number[]>,
default() {
return [10, 20, 30, 50];
}
},
},
/**
@ -124,7 +124,7 @@ export default defineComponent({
type: [Function, String] as PropType<
(row: XTableData) => string | string
>,
default: "id"
default: "id",
},
/**
@ -132,7 +132,7 @@ export default defineComponent({
*/
visibleColumn: {
type: Boolean,
default: undefined
default: undefined,
},
/**
@ -140,17 +140,8 @@ export default defineComponent({
*/
isFixedPagination: {
type: Boolean,
default: true
}
// handleDel: {
// type: Function,
// default: () => {}
// }
// customActions: {
// type: Function,
// default: () => {}
// }
default: true,
},
},
emits: ["change", "columnChange", "update:visibleColumn", "actions"],
setup(props, { slots, attrs, emit }) {
@ -159,7 +150,7 @@ export default defineComponent({
const tableState = reactive<XTableState>({
tid: 0,
sortBy,
sortOrder
sortOrder,
});
const showPagination = computed(() => {
@ -203,7 +194,7 @@ export default defineComponent({
pageSize,
prop: sortBy,
order: sortOrder,
type: "number"
type: "number",
});
}
@ -220,7 +211,7 @@ export default defineComponent({
pageSize,
prop: sortBy,
order: sortOrder,
type: "size"
type: "size",
});
});
}
@ -261,6 +252,30 @@ export default defineComponent({
return col;
}
/**
*
*/
function renderBaseColumn(column: XTableColumn) {
const columnSlots: {
default?: (scope: Record<string, any>) => any;
header?: (scope: Record<string, any>) => any;
} = {};
const slot = getSlot(column);
const headerSlot = getSlot(column, "header");
if (slot) {
columnSlots.default = (scope) => slot(scope);
}
if (headerSlot) {
columnSlots.header = (scope) => headerSlot(scope);
}
return (
<ElTableColumn {...getColumnProps(column)}>{columnSlots}</ElTableColumn>
);
}
/**
*
*/
@ -272,7 +287,7 @@ export default defineComponent({
default: (scope: Record<string, any>) => {
const slot = getSlot(column);
return slot?.(scope);
}
},
}}
</ElTableColumn>
);
@ -285,74 +300,21 @@ export default defineComponent({
return (
<div>
{slots.actionBar && <div>{slots.actionBar({ row })}</div>}
{/* <ul>
<li
class="flex items-center"
style={{ fontSize: "14px", color: "#E80D0D" }}
onClick={() => handleDel(row)}
>
<i class="iconfont icon-shanchu pr-[8px]"></i>
<el-button type="text">
<span
style={{
fontSize: "14px",
color: "#E80D0D"
}}
>
</span>
</el-button>
</li>
</ul> */}
</div>
);
}
},
}}
</ElTableColumn>
);
}
return <ElTableColumn {...getColumnProps(column)} />;
}
/**操作按钮事件 */
// function handleDel(row) {
// console.log(row, "handleDel");
// emit("actions", {
// type: "delete",
// data: { ...row }
// });
// }
/**
*
*/
function renderBaseColumn(column: XTableColumn) {
const columnSlots: {
default?: (scope: Record<string, any>) => any;
header?: (scope: Record<string, any>) => any;
} = {};
const slot = getSlot(column);
const headerSlot = getSlot(column, "header");
if (slot) {
columnSlots.default = scope => slot(scope);
}
if (headerSlot) {
columnSlots.header = scope => headerSlot(scope);
}
return (
<ElTableColumn {...getColumnProps(column)}>{columnSlots}</ElTableColumn>
);
return renderBaseColumn(column);
}
/**
*
*/
function renderTableColumn(column: XTableColumn) {
if (column.hidden) return;
if (column.hidden) return null;
if (column.type) {
return renderTypeColumn(column);
}
@ -369,7 +331,7 @@ export default defineComponent({
if (column.hidden) return;
return (
<ElTableColumn {...getColumnProps(column)}>
{children.map(column => renderTableColumn(column))}
{children.map((column) => renderTableColumn(column))}
</ElTableColumn>
);
}
@ -387,7 +349,7 @@ export default defineComponent({
pageSizes: props.pageSizes,
currentPage: props.page,
onSizeChange: handlePageSizeChange,
onCurrentChange: handlePageNumChange
onCurrentChange: handlePageNumChange,
};
return (
@ -396,7 +358,7 @@ export default defineComponent({
"x-table-pagination pagination_wrap",
props.isFixedPagination
? "fixed_pagination"
: "noneFixed_pagination"
: "noneFixed_pagination",
]}
>
<ElPagination {...paginationProps} />
@ -412,7 +374,7 @@ export default defineComponent({
columns: props.columns,
visible: props.visibleColumn,
onChange: handleColumnChange,
onVisibleChange: handleVisibleChange
onVisibleChange: handleVisibleChange,
};
return <BaseColumn {...customColumnProps} />;
@ -425,14 +387,13 @@ export default defineComponent({
maxHeight: mHeight.value,
data: props.dataSource,
rowKey: props.rowKey,
onSortChange: handleTableSortChange
onSortChange: handleTableSortChange,
};
const extraSlots: {
append?: () => any;
empty?: () => any;
} = {};
if (slots.append) {
extraSlots.append = () => slots.append?.();
}
@ -449,40 +410,20 @@ export default defineComponent({
v-loading={props.loading}
v-slots={extraSlots}
>
{props.columns.map(column => {
if (Array.isArray(column.children)) {
return renderColumnChildren(column, column.children);
}
return renderTableColumn(column);
})}
{/* 使用插槽引入操作栏的内容 */}
{/* <ElTableColumn label="">
{{
default: ({ row }: { row: Record<string, any> }) => {
return extraSlots.customActions?.({
row
});
}
}}
</ElTableColumn> */}
{/* <ElTableColumn label="">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.$index, scope.row)"
>Edit</el-button
>
<el-button
size="small"
type="danger"
@click="handleDelete(scope.$index, scope.row)"
>Delete</el-button>
>
</template>
</ElTableColumn> */}
{{
default: () => {
return props.columns.map((column) =>
Array.isArray(column.children)
? renderColumnChildren(column, column.children)
: renderTableColumn(column)
);
},
}}
</ElTable>
{showPagination.value && renderPagination()}
{!isUndefined(props.visibleColumn) && renderCustomColumn()}
</div>
);
};
}
});
},
});

@ -0,0 +1,140 @@
<template>
<span>
报表下载
</span>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import ExcelJS from 'exceljs';
import { saveAs } from 'file-saver-es';
// ()
const listData = [
{
trainCode: 'T-20250508',
trainType: 'C54K',
occurrenceTime: 5785.67354,
alarmType: '车辆损坏',
faultType: '搭扣未扣',
},
];
// URL (URL)
const imageUrl = ref(''); // Replace with the actual image URL
const placeholderImageUrl = 'https://via.placeholder.com/150'; // Fallback
const convertImageToBase64 = async (url: string) => {
try {
const response = await fetch(url);
const blob = await response.blob();
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (error) {
console.error('Error loading image:', error);
return null;
}
};
const saveToExcel = async () => {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Sheet1');
// 1. ()
worksheet.getColumn('A').width = 15;
worksheet.getColumn('B').width = 15;
worksheet.getColumn('C').width = 15;
worksheet.getColumn('D').width = 10; // Smaller width for ''
worksheet.getColumn('E').width = 15;
worksheet.getColumn('F').width = 15;
worksheet.getColumn('G').width = 15;
worksheet.getColumn('H').width = 10;
worksheet.getColumn('I').width = 10;
// 2.
worksheet.getCell('A1').value = '进场时间';
worksheet.getCell('B1').value = '出场时间';
worksheet.getCell('A3').value = '车厢编号';
worksheet.getCell('C3').value = '1234567890'; // Example Value
// 3.
worksheet.getCell('C5').value = '列车编号';
worksheet.getCell('D5').value = '车型';
worksheet.getCell('E5').value = '发生时间';
worksheet.getCell('F5').value = '告警类型';
worksheet.getCell('G5').value = '故障类型';
// 4.
listData.forEach((item, index) => {
const row = index + 6; // 6
worksheet.getCell(`C${row}`).value = item.trainCode;
worksheet.getCell(`D${row}`).value = item.trainType;
worksheet.getCell(`E${row}`).value = item.occurrenceTime;
worksheet.getCell(`F${row}`).value = item.alarmType;
worksheet.getCell(`G${row}`).value = item.faultType;
});
// 5.
// Load the image (using a data URL or URL)
try {
if (imageUrl.value) {
const base64Image = await convertImageToBase64(imageUrl.value);
if (base64Image) {
const imageId = workbook.addImage({
base64: base64Image,
extension: 'png', // Adjust extension if needed (jpeg, etc.)
});
// Add the image to the worksheet
worksheet.addImage(imageId, {
tl: { col: 2, row: 8 }, // Top left corner of the image (C9 in Excel)
br: { col: 7, row: 25 }, // Bottom right corner of the image (H26 in Excel)
});
}
}
} catch (error) {
console.error('Error adding image:', error);
}
// 6. ""
worksheet.getCell('A17').value = '车身缺陷';
// 7. H
for (let i = 8; i <= 28; i++) {
const cell = worksheet.getCell(`H${i}`);
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'ADD8E6' }, // Light blue color
};
}
// 8. (Optional) Style the header rows
const headerRange = 'A1:G5'; // Adjust as needed
worksheet.getCell(headerRange).font = { bold: true };
worksheet.getCell(headerRange).alignment = { vertical: 'middle', horizontal: 'center' }; // Center align headers
// 9. Excel
const buffer = await workbook.xlsx.writeBuffer();
saveAs(new Blob([buffer], { type: 'application/octet-stream' }), '车辆信息.xlsx');
};
onMounted(async () => {
// Pre-load the image (or use a placeholder if loading fails)
imageUrl.value = 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/65/No-Image-Placeholder.svg/1665px-No-Image-Placeholder.svg.png'
});
</script>

@ -2,7 +2,7 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-03-06 15:00:26
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-06-23 15:58:27
* @LastEditTime: 2025-09-04 16:55:32
* @FilePath: \vite-ai\data-dashboard\src\views\dashboard\components\footer.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
@ -64,7 +64,8 @@ const isActive = (path: string) => {
background-image: url('@/assets/footer/menu#{$i}.png');
//
background-repeat: no-repeat;
background-size: cover;
background-size: contain;
background-position: center;
//
width: 96px;
height: 96px;

@ -0,0 +1,53 @@
<!--
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-03-06 15:52:40
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-09-05 14:00:15
* @FilePath: \vite-ai\data-dashboard\src\components\contentHeader.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div class="flex items-center justify-between content_header_wrap"
:style="{ 'background-image': `url(${sub_title_bg})`, 'background-size': 'cover', 'background-repeat': 'no-repeat', 'background-position': 'left bottom' }">
<!-- 左侧标题区域 -->
<div class="flex items-center left-section pl-[16px]">
<img src="@/assets/common/sub_title_icon.png" class="w-[20px] h-[20px]">
<div class="fg-title ml-[12px]">
{{ title }}
<slot name="title"></slot>
</div>
</div>
<!-- 右侧内容 -->
<div>
<slot name="extra"></slot>
</div>
</div>
</template>
<script lang="ts" setup>
import sub_title_bg from '@/assets/home/sub_title_bg.png';
// props
const props = withDefaults(defineProps<{
// bgLayout: number | string; // 1855 918 800 450
title: string; //
}>(), {
// bgLayout: 1855,
title: ""
});
//
// const bgImageUrl = computed(() => {
// switch (Number(props.bgLayout)) {
// default:
// return sub_title_bg;
// }
// });
</script>
<style scoped lang="scss">
.content_header_wrap {
width: 431px;
height: 38px;
// border-image: linear-gradient(90deg, rgba(25, 61, 72, 0), rgba(67, 215, 226, 1), rgba(29, 65, 76, 0)) 1 1;
}
</style>

@ -0,0 +1,168 @@
<!--
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-08-14 13:38:30
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-08-20 09:46:29
* @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 {
value: number;
fileList: Record<string, any>[];
}
interface Emits {
(e: "update:value", val: boolean): void;
}
const props = withDefaults(defineProps<Props>(), {
value: false,
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 handleSlideClick = (index) => {
if (activeIndex.value === index) {
togglePlay(); //
} else {
isPlaying.value = false;
emit("update:value", index);
currFile.value = props.fileList[index];
}
};
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 }
);
watch(
() => props.value,
(newVal) => {
console.log("swiperFile_newVal", newVal);
activeIndex.value = newVal;
}
);
</script>
<template>
<div class="w-full h-full fg-swiper-file-wrap">
<!-- 缩略图区域 -->
<div class="flex" v-if="fileList?.length > 0">
<div class="thumbnail-container">
<swiper
ref="swiperRef"
:modules="modules"
:slides-per-view="4"
: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"
@click="handleSlideClick(index)"
:class="{ 'active-slide': activeIndex === index }"
>
<img
:src="file?.image_url"
v-if="file?.image_url"
class="cursor-pointer"
/>
<SwiperPlayer
class="cursor-pointer"
:videoUrl="file?.video_url"
v-else-if="file?.video_url"
:isPlaying="isPlaying && activeIndex === index"
/>
<div v-else>
<!-- 视频图片加载失败 -->
</div>
</swiper-slide>
</swiper>
</div>
</div>
</div>
</template>
<style lang="scss">
.fg-swiper-file-wrap {
.thumbnail-container {
overflow: visible;
margin-right: 16px;
.swiper {
width: 100%;
height: 100%;
.swiper-slide {
img,
video {
border-radius: 4px;
object-fit: cover;
overflow: hidden;
}
}
.active-slide img,
.active-slide video {
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,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,62 @@
<!--
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-08-19 11:04:59
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-08-20 09:46:44
* @FilePath: \5G-Web\src\components\Swiper\swiperPlayer.vue
* @Description: 作为轮播文件中的视频块
-->
<template>
<div class="flex items-center justify-center w-full h-full position-relative">
<video
ref="videoRef"
:controls="false"
muted
:src="videoUrl"
width="100%"
height="100%"
style="object-fit: cover"
@error="handleVideoError"
v-if="!isVideoError"
></video>
<div class="bg_error_img" v-if="isVideoError"></div>
<div :class="{ bg_icon: true, playing: isPlaying }" v-if="!isVideoError">
<!-- {{ isPlaying }} -->
</div>
</div>
</template>
<script lang="ts" setup>
const props = defineProps<{
videoUrl: string;
isPlaying: boolean;
}>();
const videoRef = ref<HTMLVideoElement | null>(null);
const isVideoError = ref<boolean>(false);
const handleVideoError = () => {
console.log("handleVideoError");
isVideoError.value = true;
};
</script>
<style lang="scss" scoped>
.bg_error_img {
width: 100%;
height: 100%;
background: url("@/assets/common/load_file_error.png") no-repeat center center;
background-size: 50%;
border: 1px dashed red;
}
.bg_icon {
position: absolute;
width: 100%;
height: 100%;
background: url("@/assets/common/player_icon_1.png") no-repeat center center;
background-size: 40px;
&.playing {
background: url("@/assets/common/player_icon_2.png") no-repeat center center;
background-size: 40px;
}
}
</style>

@ -7,203 +7,207 @@
* @Description: 视频播放器
-->
<template>
<!-- 视频播放器 -->
<div class="video-player-box">
<video ref="videoRef" class="video-element" controls @timeupdate="handleTimeUpdate"
@loadedmetadata="handleLoadedMetadata" @play="handlePlay" @pause="handlePause" @waiting="handleWaiting"
@canplay="handleCanPlay" style="object-fit: cover;" @error="handleVideoError">
<source :src="src" type="video/mp4">
您的浏览器不支持视频播放
</video>
<!-- 加载状态提示 -->
<div v-if="loading" class="loading-overlay">
<el-icon class="is-loading">
<Loading />
</el-icon>
</div>
<div class="bg_error_img" v-if="isVideoError">
</div>
<!-- 视频播放器 -->
<div class="video-player-box">
<video
ref="videoRef"
class="video-element"
controls
@timeupdate="handleTimeUpdate"
@loadedmetadata="handleLoadedMetadata"
@play="handlePlay"
@pause="handlePause"
@waiting="handleWaiting"
@canplay="handleCanPlay"
style="object-fit: cover"
@error="handleVideoError"
>
<source :src="src" type="video/mp4" />
您的浏览器不支持视频播放
</video>
<!-- 加载状态提示 -->
<div v-if="loading" class="loading-overlay">
<el-icon class="is-loading">
<Loading />
</el-icon>
</div>
<div class="bg_error_img" v-if="isVideoError"></div>
</div>
</template>
<script setup lang="ts">
import { ElIcon } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import { ElIcon } from "element-plus";
import { Loading } from "@element-plus/icons-vue";
type ProgressData = {
currentTime: number
duration: number
progress: number
}
currentTime: number;
duration: number;
progress: number;
};
const props = defineProps({
src: {
type: String,
required: true
},
isPlaying: {
type: Boolean,
default: false
}
})
src: {
type: String,
},
isPlaying: {
type: Boolean,
default: false,
},
});
const emit = defineEmits<{
(e: 'update:progress', data: ProgressData): void
(e: 'update:duration', duration: number): void
(e: 'play'): void
(e: 'pause'): void
}>()
const videoRef = ref<HTMLVideoElement | null>(null)
const loading = ref<boolean>(false)
(e: "update:progress", data: ProgressData): void;
(e: "update:duration", duration: number): void;
(e: "play"): void;
(e: "pause"): void;
}>();
const videoRef = ref<HTMLVideoElement | null>(null);
const loading = ref<boolean>(false);
const isVideoError = ref<boolean>(false);
let currentDuration = 0
let currentDuration = 0;
//
watch(() => props.isPlaying, (newVal) => {
if (!videoRef.value) return
newVal ? videoRef.value.play() : videoRef.value.pause()
})
watch(
() => props.isPlaying,
(newVal) => {
if (!videoRef.value) return;
newVal ? videoRef.value.play() : videoRef.value.pause();
}
);
//
watch(() => props.src, (newVal) => {
if (!videoRef.value) return
isVideoError.value = false
videoRef.value.pause()
videoRef.value.src = newVal
videoRef.value.load()
watch(
() => props.src,
(newVal) => {
if (!videoRef.value) return;
isVideoError.value = false;
videoRef.value.pause();
videoRef.value.src = newVal;
videoRef.value.load();
if (props.isPlaying) {
videoRef.value.play()
videoRef.value.play();
}
})
}
);
//
const handleTimeUpdate = () => {
if (!videoRef.value) return
const currentTime = videoRef.value.currentTime
const progress = currentDuration > 0
? (currentTime / currentDuration) * 100
: 0
emit('update:progress', {
currentTime,
duration: currentDuration,
progress
})
}
if (!videoRef.value) return;
const currentTime = videoRef.value.currentTime;
const progress =
currentDuration > 0 ? (currentTime / currentDuration) * 100 : 0;
emit("update:progress", {
currentTime,
duration: currentDuration,
progress,
});
};
//
const handleLoadedMetadata = () => {
if (!videoRef.value) return
currentDuration = videoRef.value.duration
emit('update:duration', currentDuration)
}
if (!videoRef.value) return;
currentDuration = videoRef.value.duration;
emit("update:duration", currentDuration);
};
const handleVideoError = () => {
console.log('handleVideoError')
isVideoError.value = true;
console.log("handleVideoError");
isVideoError.value = true;
};
//
const handlePlay = () => emit('play')
const handlePause = () => emit('pause')
const handlePlay = () => emit("play");
const handlePause = () => emit("pause");
//
const handleWaiting = () => loading.value = true
const handleCanPlay = () => loading.value = false
const handleWaiting = () => (loading.value = true);
const handleCanPlay = () => (loading.value = false);
onMounted(() => {
console.log('onMounted', props.src)
if (videoRef.value) {
videoRef.value.src = props.src
}
})
console.log("onMounted", props.src);
if (videoRef.value) {
videoRef.value.src = props.src;
}
});
onUnmounted(() => {
if (videoRef.value) {
videoRef.value.pause()
videoRef.value.removeAttribute('src')
videoRef.value.load()
}
})
if (videoRef.value) {
videoRef.value.pause();
videoRef.value.removeAttribute("src");
videoRef.value.load();
}
});
//
defineExpose({
play: () => videoRef.value?.play(),
pause: () => videoRef.value?.pause(),
seek: (time: number) => {
if (videoRef.value) {
videoRef.value.currentTime = time
}
play: () => videoRef.value?.play(),
pause: () => videoRef.value?.pause(),
seek: (time: number) => {
if (videoRef.value) {
videoRef.value.currentTime = time;
}
})
},
});
</script>
<style lang="scss">
.video-player-box {
position: relative;
position: relative;
width: 100%;
height: 100%;
margin: 0 auto;
.bg_error_img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin: 0 auto;
.bg_error_img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 99999999;
background-color: #090F48;
background-image: url("@/assets/common/load_file_error.png");
background-position: center;
background-repeat: no-repeat;
background-size: 25%;
border: 1px dashed red;
}
.video-element {
width: 100%;
height: auto;
border-radius: 8px;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.el-icon.is-loading {
font-size: 32px;
animation: rotating 2s linear infinite;
z-index: 99999999;
background-color: #090f48;
background-image: url("@/assets/common/load_file_error.png");
background-position: center;
background-repeat: no-repeat;
background-size: 25%;
border: 1px dashed red;
}
.video-element {
width: 100%;
height: auto;
border-radius: 8px;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.el-icon.is-loading {
font-size: 32px;
animation: rotating 2s linear infinite;
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
to {
transform: rotate(360deg);
}
}
}
</style>

@ -0,0 +1,52 @@
export const dataViewConfig = {
monthArr: [
{
name: "1月",
value: "1",
},
{
name: "2月",
value: "2",
},
{
name: "3月",
value: "3",
},
{
name: "4月",
value: "4",
},
{
name: "5月",
value: "5",
},
{
name: "6月",
value: "6",
},
{
name: "7月",
value: "7",
},
{
name: "8月",
value: "8",
},
{
name: "9月",
value: "9",
},
{
name: "10月",
value: "10",
},
{
name: "11月",
value: "11",
},
{
name: "12月",
value: "12",
},
],
};

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

@ -0,0 +1,446 @@
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_alarm_data: TrainAlarmDataProps[];
role_alarm_data: TrainAlarmDataProps[];
}
const formatDateTime = (dateTimeStr: string) => {
const date = new Date(dateTimeStr);
return date.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
};
// 使用您指定的图片URL
const DEFAULT_IMAGE_URL =
"https://img1.baidu.com/it/u=967792105,51180745&fm=253&fmt=auto?w=1200&h=800";
const convertImageToBase64 = async (url: string) => {
try {
const response = await fetch(url);
const blob = await response.blob();
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (error) {
console.error("Error loading image:", error);
return null;
}
};
const getImageDimensions = (
url: string
): Promise<{ width: number; height: number }> => {
return new Promise((resolve) => {
const img = new Image();
const timer = setTimeout(() => {
resolve({ width: 80, height: 60 }); // 默认尺寸
}, 3000);
img.crossOrigin = "Anonymous";
img.onload = () => {
clearTimeout(timer);
resolve({ width: img.width, height: img.height });
};
img.onerror = () => {
clearTimeout(timer);
resolve({ width: 80, height: 60 });
};
img.src = url;
});
};
// 下载图片并转换为ArrayBuffer
const downloadImage = async (url: string): Promise<ExcelJS.Image | null> => {
try {
const finalUrl = url || DEFAULT_IMAGE_URL;
let response: Response;
try {
response = await fetch(finalUrl);
if (!response.ok) throw new Error("直接下载失败");
} catch (directError) {
// 尝试使用CORS代理
try {
const proxyUrl = `https://corsproxy.io/?${encodeURIComponent(
finalUrl
)}`;
response = await fetch(proxyUrl);
if (!response.ok) throw new Error("代理下载失败");
} catch (proxyError) {
console.error("图片下载失败:", proxyError);
return null;
}
}
const arrayBuffer = await response.arrayBuffer();
const extension = finalUrl.split(".").pop()?.toLowerCase() || "jpeg";
const { width, height } = await getImageDimensions(finalUrl);
// 计算缩放比例(保持宽高比)
const maxWidth = 80;
const maxHeight = 60;
const scale = Math.min(maxWidth / width, maxHeight / height, 1);
return {
buffer: arrayBuffer, // 直接使用ArrayBuffer
extension: extension === "png" ? "png" : "jpeg",
width: width * scale,
height: height * scale,
};
} catch (error) {
console.error("图片处理失败:", error);
return null;
}
};
// 像素转换为Excel的EMU单位 (Excel使用EMU作为度量单位)
const pixelsToEMU = (pixels: number) => Math.round(pixels * 9525);
export const exportTrainReport = async (data: TrainReportData) => {
const workbook = new ExcelJS.Workbook();
workbook.creator = "Railway Inspection System";
workbook.created = new Date();
// 1. 创建"整车信息"工作表 (无表头行)
const trainSheet = workbook.addWorksheet("整车信息");
trainSheet.getColumn(1).width = 20;
trainSheet.getColumn(2).width = 30;
// 直接添加数据行
const trainInfoData = [
{ key: "车头编号", value: data.train_id },
{ key: "进场时间", value: formatDateTime(data.arrive_at) },
{
key: "出场时间",
value: data.leave_at ? formatDateTime(data.leave_at) : "未出场",
},
{ key: "车厢列表", value: "" },
];
trainInfoData.forEach((info, index) => {
const row = index + 1;
trainSheet.getCell(`A${row}`).value = info.key;
trainSheet.getCell(`B${row}`).value = info.value;
// 设置第一列加粗
trainSheet.getCell(`A${row}`).font = { bold: true };
// 设置第二列加粗(除车厢列表行)
if (row < 4) {
trainSheet.getCell(`B${row}`).font = { bold: true };
}
});
// 添加车厢列表
const startRow = trainInfoData.length + 1;
data.train_data.forEach((carriage, index) => {
const row = startRow + index;
trainSheet.getCell(`A${row}`).value = `车厢 ${index + 1}`;
trainSheet.getCell(`B${row}`).value = carriage.carriage_number;
});
// 创建缺陷表
// 2. 创建"车身缺陷记录"工作表
const appearanceSheet = workbook.addWorksheet("车身缺陷记录");
const alarmAppearanceData = data.appearance_alarm_data;
// 修改列定义
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: "image_url", width: 15 }, // 原图链接列
];
// 设置表头样式
appearanceSheet.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" },
};
});
// 设置所有行高60像素≈45磅
appearanceSheet.properties.defaultRowHeight = 45;
// 添加所有缺陷记录
alarmAppearanceData.forEach(async (item, i) => {
const rowIndex = i + 2; // 从第二行开始(第一行是表头)
// 添加文本数据
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.image_url || DEFAULT_IMAGE_URL;
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.)
});
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" },
};
});
// 设置所有行高60像素≈45磅
poleSheet.properties.defaultRowHeight = 45;
// 添加所有缺陷记录
for (let i = 0; i < alarmRoleData.length; i++) {
const item = alarmRoleData[i];
const rowIndex = i + 2; // 从第二行开始(第一行是表头)
// 添加文本数据
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,
});
// 使用指定图片URL
const imageUrl = item.image_url || DEFAULT_IMAGE_URL;
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,
height: 45,
},
editAs: "oneCell",
});
}
} catch (error) {
console.error(`图片添加失败 (行 ${rowIndex}):`, error);
const errorCell = poleSheet.getCell(`E${rowIndex}`);
errorCell.value = "图片加载失败";
errorCell.font = {
color: { argb: "FFFF0000" }, // 红色
italic: true,
};
}
}
// final 生成Excel文件
const buffer = await workbook.xlsx.writeBuffer();
try {
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "车辆信息.xlsx";
link.click();
URL.revokeObjectURL(url);
} catch (error) {
console.error("导出 Excel 文件时出错:", error);
}
};
// ... 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",
data: [
{ carriage_number: "CK-1001" },
{ carriage_number: "CK-1002" },
{ carriage_number: "CK-1003" },
],
appearance_alarm_data: [
{
train_carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
created_at: "2023-06-15 09:15:34",
alarm_type: "AI智能识别告警",
fault_type: "车体变形",
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
train_carriage_number: "CK-1002",
carriage_type: "P70型棚车",
created_at: "2023-06-15 10:22:18",
alarm_type: "人工巡检报告",
fault_type: "油漆剥落",
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
train_carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
created_at: "2023-06-15 09:15:34",
alarm_type: "AI智能识别告警",
fault_type: "车体变形",
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
train_carriage_number: "CK-1002",
carriage_type: "P70型棚车",
created_at: "2023-06-15 10:22:18",
alarm_type: "人工巡检报告",
fault_type: "油漆剥落",
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
train_carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
created_at: "2023-06-15 09:15:34",
alarm_type: "AI智能识别告警",
fault_type: "车体变形",
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
train_carriage_number: "CK-1002",
carriage_type: "P70型棚车",
created_at: "2023-06-15 10:22:18",
alarm_type: "人工巡检报告",
fault_type: "油漆剥落",
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
train_carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
created_at: "2023-06-15 09:15:34",
alarm_type: "AI智能识别告警",
fault_type: "车体变形",
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
train_carriage_number: "CK-1002",
carriage_type: "P70型棚车",
created_at: "2023-06-15 10:22:18",
alarm_type: "人工巡检报告",
fault_type: "油漆剥落",
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
train_carriage_number: "CK-1001",
carriage_type: "C80型运煤敞车",
created_at: "2023-06-15 09:15:34",
alarm_type: "AI智能识别告警",
fault_type: "车体变形",
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
{
train_carriage_number: "CK-1002",
carriage_type: "P70型棚车",
created_at: "2023-06-15 10:22:18",
alarm_type: "人工巡检报告",
fault_type: "油漆剥落",
image_url:
"http://192.168.10.14:8123/sftp/2025-07-01/20250701_131010.jpg",
},
],
};
return {
saveToExcel: () => exportTrainReport(data),
};
};

@ -1,6 +1,14 @@
@import url('./fonts.scss');
@import url('./element-plus.scss');
@import url("./fonts.scss");
@import url("./element-plus.scss");
:root {
--fg-box-margin: 16px;
// &
--fg-swiper-monitor-main-iamge-width: 581px;
--fg-swiper-monitor-main-iamge-height: 400px;
--fg-swiper-monitor-slide-iamge-width: 155px;
--fg-swiper-monitor-slide-iamge-height: 91px;
}
//
.bg_title {
background-size: contain;
@ -17,15 +25,253 @@
}
}
.bg_basic_content{
background: linear-gradient( 180deg, rgba(7,16,19,0) 0%, #081417 100%);;
.bg-basic-content {
background: linear-gradient(180deg, rgba(7, 16, 19, 0) 0%, #081417 100%);
}
.bg_error_picture {
.bg-error-picture {
width: 100%;
height: 100%;
background: url("@/assets/common/load_file_error.png") no-repeat center center;
background-size: 50%;
border: 1px dashed red;
}
}
//
.fg-title {
font-family: "DouyinSansBold"; //
//
font-size: 20px;
color: #fff; // ,
//
// background: linear-gradient(to bottom, #ffffff, #96e6ff);
// -webkit-background-clip: text;
// background-clip: text;
// -webkit-text-fill-color: transparent;
}
// 1
.fg-mark1 {
font-family: "DingTalk JinBuTi"; //
//
color: #1cecf5; // ,
}
.fg-f-1 {
font-family: "DingTalk JinBuTi"; //
}
//
.fg-icon {
width: 17px;
height: 17px;
background-image: url("@/assets/common/title_before_icon.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
/* 按钮 */
.fg-button {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0 10px;
height: 28px;
border-radius: 2px 2px 2px 2px;
}
.fg-basic-btn {
height: 32px;
width: 76px;
margin: 0;
&.el-button {
margin-left: 0;
}
}
.fg-info-button {
background: linear-gradient(180deg, #2589ff 0%, #46a9ed 100%);
border: 1px solid #42a5f5;
border-radius: 2px;
color: white;
margin-left: 0;
height: 32px;
.icon {
width: 14px;
height: 14px;
background-image: url("@/assets/common/search_icon.png");
background-size: contain;
background-position: center;
background-repeat: no-repeat;
margin-right: 5px;
}
}
.fg-reset-btn {
background: transparent;
border-radius: 2px;
border: 1px solid #088bd6;
color: white;
box-shadow: none;
& .icon {
width: 14px;
height: 14px;
background-image: url("@/assets/common/reset_icon.png");
background-size: contain;
background-position: center;
background-repeat: no-repeat;
margin-right: 5px;
}
}
.fg-button-primary {
@apply fg-button;
background: #009dff;
}
.fg-button-primary1 {
@apply fg-button;
background: #22bacb;
}
.fg-button-primary-danger {
@apply fg-button;
background: #f53f3f;
}
//
.fg-dialog {
background-color: transparent;
background-image: url("@/assets/common/dialog_bg1.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
padding: 0 !important;
width: 816px;
height: 640px;
&.fg-dialog2 {
width: 1202px;
height: 602px;
background-image: url("@/assets/common/dialog_bg2.png");
background-size: 100% 100%;
}
.fg-dialog-header-close {
width: 50px;
height: 50px;
position: absolute;
right: -17px;
top: -17px;
z-index: 999999 !important;
cursor: pointer;
background-image: url("@/assets/common/dialog_close_icon.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
.fg-dialog-header {
position: relative;
background-color: transparent;
background-image: url("@/assets/common/dialog_head_bg.png");
background-repeat: no-repeat;
//
.fg-dialog-header-icon-title {
padding: 0 24px;
font-weight: bold;
font-size: 18px;
.header-icon {
width: 24px;
height: 48px;
margin-right: 12px;
background-image: url("@/assets/common/sub_title_icon.png");
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}
.header-text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 650px;
}
}
}
}
//
.fg-border-left-title {
// box-sizing: border-box;
padding-left: 8px;
height: 22px;
font-weight: bold;
font-size: 14px;
color: #ffffff;
border-left: 3px solid;
border-image: linear-gradient(180deg, #2589ff 0%, #46a9ed 100%) 1;
line-height: 22px;
}
//
.fg-footer-charts {
display: flex;
justify-content: space-between;
margin-top: 16px;
gap: 16px;
width: 100%;
& > li {
// height: 230px;
background: url("@/assets/common/long_chart_bg.png") no-repeat center center;
background-size: 100% 100%;
// background-color: red;
&:nth-child(1) {
width: calc(50% - 50px - 8px);
// overflow: hidden;
}
&:nth-child(2) {
width: calc(50% + 50px - 8px);
// overflow: hidden;
}
.fg-footer-charts-title {
@apply fg-title;
font-size: 18px;
padding-left: 16px;
padding-top: 7px;
padding-bottom: 1px;
}
.fg-footer-charts-content {
// overflow: hidden;
max-width: 100%;
height: 195px;
// display: flex;
// justify-content: center;
// background-color: red;
}
}
}
//
.fg-no-data {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.bg-no-data {
width: 63px;
background-image: url("@/assets/common/no_data.png");
background-size: contain;
height: 58px;
background-position: center;
background-repeat: no-repeat;
}
.no-data-text {
padding-top: 16px;
font-size: 14px;
color: #999999;
}
}
.fg-empty-image {
width: 100%;
height: 100%;
background-image: url("@/assets/common/emptyBg.png");
background-size: 312px 204px;
background-position: center;
background-repeat: no-repeat;
}

@ -112,6 +112,11 @@
width: 150px;
background-color: #032b5c; /* 自定义背景色 */
border: none; /* 可选:去掉边框 */
--fg-select-height: 32px; // *
&.mini-size {
--fg-select-height: 28px; // *
width: 78px;
}
.el-select__selected-item {
color: white; /* 文字颜色 */
}
@ -120,7 +125,7 @@
background: rgba(74, 126, 191, 0.1); /* 下拉框背景色 */
border: none !important; /* 边框 */
color: white; /* 文字颜色 */
height: 32px;
height: var(--fg-select-height); /* 高度 */
box-shadow: none;
&::placeholder {
@ -128,8 +133,8 @@
}
}
.el-select__wrapper {
min-height: 32px;
line-height: 32px;
min-height: var(--fg-select-height);
line-height: var(--fg-select-height);
box-shadow: 0 0 0 0 !important;
}
.el-select__arrow {
@ -159,6 +164,9 @@
height: 32px;
}
}
.el-progress-bar__outer {
background: rgba(255, 255, 255, 0.3);
}
/* 修改下拉菜单背景色 */
.el-select-dropdown {
@ -204,47 +212,3 @@
background-color: transparent;
}
}
/* 按钮 */
.basic-btn {
height: 32px;
width: 76px;
margin: 0;
&.el-button {
margin-left: 0;
}
}
.query-btn {
background: linear-gradient(180deg, #2589ff 0%, #46a9ed 100%);
border: 1px solid #42a5f5;
border-radius: 2px;
color: white;
margin-left: 0;
& .icon {
width: 14px;
height: 14px;
background-image: url("@/assets/common/search_icon.png");
background-size: contain;
background-position: center;
background-repeat: no-repeat;
margin-right: 5px;
}
}
.reset-btn {
background: transparent;
border-radius: 2px;
border: 1px solid #088bd6;
color: white;
box-shadow: none;
& .icon {
width: 14px;
height: 14px;
background-image: url("@/assets/common/reset_icon.png");
background-size: contain;
background-position: center;
background-repeat: no-repeat;
margin-right: 5px;
}
}

@ -1,7 +1,14 @@
// src/assets/fonts.scss
@font-face {
font-family: 'DingTalk JinBuTi'; //
src: url('@/assets/fonts/DingTalk JinBuTi.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
font-family: "DingTalk JinBuTi"; // 1
src: url("@/assets/fonts/DingTalk JinBuTi.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "DouyinSansBold"; // 2
src: url("@/assets/fonts/DouyinSansBold.otf") format("truetype");
font-weight: normal;
font-style: normal;
}

@ -1,121 +1,43 @@
.appearance-monitor-warp {
box-sizing: border-box;
padding-top: 32px;
width: 100%;
height: 100%;
.appearance-monitor-main-content {
background-image: url("@/assets/appearanceMonitor/appearanceMonitor-main-bg.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
height: 584px;
margin-top: 20px;
}
.search-section {
padding: 16px 0;
}
.appearance-monitor-search-box {
display: flex;
justify-content: space-between;
gap: 20px;
// align-items: center;
.appearance-monitor-right {
box-sizing: border-box;
width:970px;
// display: flex;
background-image: url("@/assets/common/carbtmBg.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
align-items: center;
gap: 12px;
margin: 16px 0;
}
.appearance-monitor-body {
padding: 0 24px;
}
.right-panel {
width: calc(
100% - var(--fg-swiper-monitor-slide-iamge-width) -
var(--fg-swiper-monitor-main-iamge-width) - var(--fg-box-margin) * 2
);
.el-scrollbar__view {
background: transparent !important;
height: 350px;
}
}
.appearance-monitor-search-box {
display: flex;
align-items: center;
gap: 12px;
margin: 16px 0;
.appearance-monitor-banner {
.left-panel {
width: calc(
var(--fg-swiper-monitor-slide-iamge-width) +
var(--fg-swiper-monitor-main-iamge-width) + var(--fg-box-margin)
);
max-height: 480px;
margin-right: 16px;
}
.right-panel{
.el-scrollbar__view {
background: transparent !important;
height: 600px;
}
.fixed_pagination{
padding: 12px 20px 15px;
}
}
.appearance-monitor-left {
width: 49%;
background-image: url("@/assets/common/boderBg.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
.monitor-left-top {
box-sizing: border-box;
padding: 32px 16px 20px;
min-height: 600px;
.file-preview-screen {
width: 100%;
height: 590px;
display: flex;
justify-content: center;
align-items: center;
img {
max-width: 100%;
max-height: 460px;
object-fit: cover
}
video {
width: 100%;
max-height: calc(100%);
}
}
}
.monitor-left-bottom {
width: 100%;
padding: 0 16px;
margin-bottom: 29px;
overflow: visible;
.swiper {
width: 100%;
height: 100%;
.swiper-slide {
width: 20%;
border-radius:4px;
height: 144px;
img {
width: 100%;
height: 144px;
border-radius:4px;
object-fit: cover
}
}
.active-slide img,
.active-slide video {
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);
}
}
}
.empty-bg {
box-sizing: border-box;
width: 892px;
height: 815px;
background-image: url("@/assets/common/emptyBg.png");
background-size: 156px 102px;
background-position: center;
background-repeat: no-repeat;
}
}
}
}
}

@ -1,139 +1,47 @@
<template>
<div class="appearance-monitor-warp">
<div class="appearance-monitor-left h-[100%]">
<template v-if="currFileList?.length">
<div class="monitor-left-top">
<div class="file-preview-screen">
<Player :src="currFile?.video_url" :is-playing="isPlaying" v-if="currFile?.video_url"
@play="isPlaying = true" @pause="isPlaying = false" />
<img :src="currFile?.image_url" v-else-if="currFile?.image_url" >
<div v-else>
<!-- //TODO -->
</div>
</div>
</div>
<div class="monitor-left-bottom">
<swiper ref="swiperRef" :modules="modules" :slides-per-view="4" :space-between="10" navigation
:scrollbar="{ draggable: false }" :centered-slides="false" :observer="true" :observeParents="true"
@swiper="onSwiper" @slideChange="onSlideChange">
<swiper-slide v-for="(file, index) in currFileList" :key="index" @click="handleSlideClick(index)"
:class="{ 'active-slide': activeIndex === index }">
<img :src="file?.image_url" v-if="file?.image_url" class="cursor-pointer" />
<SwiperPlayer class="cursor-pointer" :videoUrl="file?.video_url" v-else-if="file?.video_url"
:isPlaying="isPlaying && (activeIndex === index)" />
<div v-else>
<!-- //TODO -->
</div>
</swiper-slide>
</swiper>
</div>
</template>
<div class="empty-bg" v-else></div>
</div>
<div class="appearance-monitor-right h-[100%]">
<div class="module-header">
<ContentHeader bgLayout="918">
<template #title>
<div class="w-[200px] bg_title bg_title_5"></div>
</template>
<template #extra>
<div></div>
</template>
</ContentHeader>
</div>
<!-- 表格区域 -->
<!-- 搜索区域 -->
<div class="px-[16px]">
<div class="appearance-monitor-search-box">
<el-select v-model="searchForm.station" placeholder="站点" class="custom-select">
<el-option label="小觉站" value="小觉站"></el-option>
<el-option label="东西站" value="东西站"></el-option>
<el-option label="立杆区" value="立杆区"></el-option>
</el-select>
<el-input v-model="searchForm.train_number" placeholder="请输入列车号" class="custom-input" clearable />
<el-input v-model="searchForm.train_carriage_number" placeholder="请输入车厢号" class="custom-input" clearable />
<el-select v-model="searchForm.fault_type" 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-select>
<el-button type="primary" @click="handleQuery" class="basic-btn query-btn">
<span class="icon"></span> 查询
</el-button>
<el-button @click="handleReset" class="basic-btn reset-btn">
<span class="icon"></span> 重置
</el-button>
</div>
<!-- 右侧表格区域 -->
<div class="w-full right-panel">
<div class="bg-transparent baseTable_wrap">
<template v-if="pagination.total > 0">
<BaseTable class="bg-transparent baseTable_box" :total="pagination.total" :pageSize="pagination.pageSize"
:dataSource="listData" :isFixedPagination="true" :columns="columns" :page="pagination.currentPage"
@change="handleTableChange" :row-class-name="handleRowClassName" @row-click="handleRowClick">
<template #created_at="{ row }">
<div>{{row}}</div>
</template>
</BaseTable>
</template>
</div>
</div>
</div>
</div>
<!-- <PointModal v-model:value="isPointOpen" :info="currentRow" @close="isPointOpen = false" /> -->
<AppearanceAlarmModal v-model:value="isAlarmOpen" :info="currentRow" :image="currFileList" :beforeImage="currBeforeFileList" @close="isAlarmOpen = false" />
<DeleteModal v-model:value="isDeleteOpen" @delete-success="getList()" :info="currentRow" @close="isDeleteOpen = false" />
</div>
</template>
<script lang="ts" setup>
import Player from '@/components/videoPlayer/Player.vue'
import SwiperPlayer from './components/SwiperPlayer.vue'
import ContentHeader from "@/components/ContentHeader.vue";
import { onBeforeRouteLeave, useRoute } from "vue-router";
import { BaseTable } from "@/components/CustomTable";
import { Swiper, SwiperSlide } from "swiper/vue";
import { Navigation, Scrollbar } from "swiper/modules";
import { getAppearanceMonitorApi, getAppearanceMonitorDetailApi, getBeforeMonitorDetailApi } from '@/api/dashboard';
import SwiperMonitor from "./components/SwiperMonitor.vue";
import AppearanceDailyDetectionChart from "@/components/Charts/appearanceDailyDetectionChart.vue";
import AppearanceDailyAlertChart from "@/components/Charts/appearanceDailyAlertChart.vue";
import AppearanceAlarmModal from "./components/AppearanceAlarmModal.vue";
import DeleteModal from "./components/DeleteModal.vue";
import VideoExport from "./components/VideoExport.vue";
import {
getAppearanceMonitorApi,
getAppearanceMonitorDetailApi,
getBeforeMonitorDetailApi,
getRecordAmountDataApi,
getRecordFaultTypeAmountDataApi,
} from "@/api/dashboard";
import { isSuccessApi } from "@/utils/forApi";
// import PointModal from './components/PointModal.vue'
import AppearanceAlarmModal from './components/AppearanceAlarmModal.vue'
import DeleteModal from './components/DeleteModal.vue'
import { useWebSocketStore } from '@/stores/websocketStore';
import { onBeforeRouteLeave } from 'vue-router';
import { useWebSocketStore } from "@/stores/websocketStore";
import { useDict } from "@/hooks/useDict";
import "swiper/css";
import 'swiper/scss';
import 'swiper/scss/navigation';
import { color } from 'echarts';
const modules = [Navigation, Scrollbar];
const activeIndex = ref(-1);
const swiperRef = ref(null);
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, (newMessages: string[], oldMessages: string[]) => {
// console.log(':', newMessages[newMessages?.length - 1].content);
// console.log(':', oldMessages);
// if (newMessages?.length > oldMessages?.length) {
// //
// const newMessage = newMessages[newMessages?.length - 1];
// // console.log(' WebSocket :', newMessage);
// //
// }
if(newMessages?.length > 0 && !isAlarmOpen.value) {
console.log(newMessages[newMessages?.length - 1],'newMessages[newMessages?.length - 1]');
currentRow.value = newMessages[newMessages?.length - 1];
currFileList.value = newMessages[newMessages?.length - 1]?.images;
isAlarmOpen.value = true;
watch(
() => websocketStore.messages,
(newMessages: string[], oldMessages: string[]) => {
if (newMessages?.length > 0 && !isAlarmOpen.value) {
console.log(
newMessages[newMessages?.length - 1],
"newMessages[newMessages?.length - 1]"
);
currentRow.value = newMessages[newMessages?.length - 1];
currFileList.value = newMessages[newMessages?.length - 1]?.images;
isAlarmOpen.value = true;
}
}, { deep: true, immediate: true });
},
{ deep: true, immediate: true }
);
const columns = [
{
label: "站点",
@ -141,7 +49,7 @@ const columns = [
width: 75,
},
{
label: "车",
label: "车辆ID",
property: "train_number",
width: 155,
},
@ -158,181 +66,81 @@ const columns = [
{
label: "告警类型",
property: "alarm_type",
width: 90,
// width: 90,
},
{
label: "故障类型",
property: "fault_type",
width: 90,
// width: 90,
},
{
label: "等级",
property: "level",
width: 60,
},
// {
// label: "",
// property: "is_reviewed",
// formatter: ({ is_reviewed }) => {
// return is_reviewed === true
// ? ""
// : "";
// },
// width: 60,
// },
{
label: "时间",
property: "created_at",
width: 180,
},
{
slot: "operation",
type: "action",
label: "操作",
width: 120,
formatter: (row) => {
return h(
"div",
{
style: {
fontSize: "14px",
color:"#37EBFF"
}
},
[
// h("i", {
// class: `iconfont icon-zishebeizu pr-[8px]`
// }),
h(
"span",
{
fontSize: "14px",
class: "pf-1",
},
[
// h("i",
// {
// style: {
// fontSize: "14px",
// letterSpacing: "2px",
// marginRight: "4px",
// color:"#37EBFF"
// },
// onClick: () => {
// // console.log(row.id);
// //
// isPointOpen.value = true;
// }
// },
// ""
// ),
h("i",
{
style: {
fontSize: "14px",
letterSpacing: "2px",
marginRight: "4px",
color:"#009DFF"
},
onClick: (row) => {
console.log('row.id');
//
isAlarmOpen.value = true;
console.log(isAlarmOpen.value);
currentRow.value = row;
}
},
"详情"
),
h("i",
{
style: {
fontSize: "14px",
letterSpacing: "2px",
marginRight: "4px",
color:"#FF2727"
},
onClick: (row) => {
// console.log(row.id);
//
isDeleteOpen.value = true;
currentRow.value = row;
}
},
"删除"
),
]
)
]
);
}
}
]
width: 180,
},
];
const pagination = ref({ currentPage: 1, pageSize: 10, total: 0 });
const listData = ref([]); //
const currentRow = ref<Record<string, any>>({}); //
const currFileList = ref<Record<string, any>[]>([]); //
const currBeforeFileList = ref<Record<string, any>[]>([]); //
const currFile = ref<Record<string, any>>({}); //
const isPlaying = ref<boolean>(false); //
const currFileList = ref<Record<string, any>[]>([]); //
const currBeforeFileList = ref<Record<string, any>[]>([]); //
//
const searchForm = reactive({
train_number: "",
train_carriage_number: "",
fault_type: "",
station:"",
type: "appearance"
fault_type: route?.query?.name || "",
station: "",
type: "appearance",
});
const dataLoading = ref(true);
const togglePlay = () => {
isPlaying.value = !isPlaying.value;
};
const handleSlideClick = (index) => {
if (activeIndex.value === index) {
togglePlay() //
} else {
activeIndex.value = index;
currFile.value = currFileList.value[index];
isPlaying.value = false;
}
};
const onSwiper = (swiper) => {
swiperRef.value = swiper;
console.log('Swiper 实例已获取:', swiper);
};
const onSlideChange = () => {
console.log("slide change");
};
const recordAmountData = ref({}); //
const recordFaultData = ref({}); //
//
const getFileList = async () => {
try {
const res = await getAppearanceMonitorDetailApi({ id: currentRow.value?.id, current: 1, pageSize: 1000 })
console.log(res.data, 'getDetailList_data')
if (isSuccessApi(res)) {
currFileList.value = res.data.data;
currFile.value = res.data.data[0];
activeIndex.value = 0;
}
} catch (error) {
console.log(error, 'getDetailList_error')
try {
const res = await getAppearanceMonitorDetailApi({
id: currentRow.value?.id,
current: 1,
pageSize: 1000,
});
console.log(res.data, "getDetailList_data");
if (isSuccessApi(res)) {
currFileList.value = res.data.data;
}
}
} catch (error) {
console.log(error, "getDetailList_error");
}
};
//
const getBeforeFileList = async () => {
try {
const res = await getBeforeMonitorDetailApi({ id: currentRow.value?.id, current: 1, pageSize: 1000 })
console.log(res.data, 'getDetailList_data')
if (isSuccessApi(res)) {
currBeforeFileList.value = res.data.data;
}
} catch (error) {
console.log(error, 'getDetailList_error')
try {
const res = await getBeforeMonitorDetailApi({
id: currentRow.value?.id,
current: 1,
pageSize: 1000,
});
console.log(res.data, "getDetailList_data");
if (isSuccessApi(res)) {
currBeforeFileList.value = res.data.data;
}
}
} catch (error) {
console.log(error, "getDetailList_error");
}
};
// TODO mock
// const getFileList = async () => {
// try {
@ -342,8 +150,6 @@ const getBeforeFileList = async () => {
// const res = await resAll.json()
// if (isSuccessApi(res)) {
// currFileList.value = res.data.data;
// currFile.value = res.data.data[0];
// activeIndex.value = 0;
// }
// } catch (error) {
// console.error(':', error)
@ -351,42 +157,76 @@ const getBeforeFileList = async () => {
// }
function loadDetail() {
currentRow.value = listData.value[0]
getFileList()
getBeforeFileList()
currentRow.value = listData.value[0];
getFileList();
getBeforeFileList();
}
//
const getList = async () => {
try {
const { currentPage, pageSize } = pagination.value;
const res = await getAppearanceMonitorApi({ ...searchForm, current: currentPage, pageSize })
console.log(res.data, 'getList_data')
const res = await getAppearanceMonitorApi({
...searchForm,
current: currentPage,
pageSize,
});
console.log(res.data, "getList_data");
if (isSuccessApi(res)) {
listData.value = res.data.data;
loadDetail()
loadDetail();
pagination.value = {
...pagination.value,
total: res.data.total
total: res.data.total,
};
}
} catch (error) {
console.error('获取数据失败:', error)
console.error("获取数据失败:", error);
}
};
// 1
const fetchRecordAmountData = async () => {
try {
const res = await getRecordAmountDataApi({
type: "appearance",
dateType: "day",
});
console.log(res.data, "fetchRecordAmountData_data");
if (isSuccessApi(res)) {
recordAmountData.value = res.data;
}
} catch (error) {
console.error("获取数据失败:", error);
}
};
// 2
const fetchRecordFaultTypeAmountData = async () => {
try {
const res = await getRecordFaultTypeAmountDataApi({
type: "appearance",
dateType: "day",
});
console.log(res.data, "fetchRecordFaultTypeAmountData_data");
if (isSuccessApi(res)) {
recordFaultData.value = res.data;
}
} catch (error) {
console.error("获取数据失败:", error);
}
};
//
const handleQuery = () => {
getList()
getList();
};
//
const handleReset = () => {
searchForm.train_number = '';
searchForm.station = '';
searchForm.train_carriage_number = '';
searchForm.fault_type = '';
getList()
searchForm.train_number = "";
searchForm.station = "";
searchForm.train_carriage_number = "";
searchForm.fault_type = "";
getList();
};
function handleTableChange(record) {
@ -394,33 +234,182 @@ function handleTableChange(record) {
pagination.value = {
...pagination.value,
currentPage: record.page,
pageSize: record.pageSize
pageSize: record.pageSize,
};
getList();
}
//
const handleRowClassName = ({ row }) => {
return row.id === currentRow.value.id ? 'selected-row' : '';
return row.id === currentRow.value.id ? "selected-row" : "";
};
/**查看详情 */
function openCurrent(row) {
console.log(row, "openCurrent");
currentRow.value = row;
isAlarmOpen.value = true;
}
//
function deleteCurrent(row) {
isDeleteOpen.value = true;
currentRow.value = row;
}
//
const handleRowClick = (row, event, rowIndex) => {
currentRow.value = row;
getFileList()
getBeforeFileList()
getFileList();
getBeforeFileList();
};
onBeforeRouteLeave(() => {
isAlarmOpen.value = false;
currentRow.value = {};
currFileList.value = [];
isAlarmOpen.value = false;
currentRow.value = {};
currFileList.value = [];
});
onMounted(() => {
getList();
fetchRecordAmountData();
fetchRecordFaultTypeAmountData();
});
</script>
<template>
<div class="appearance-monitor-warp">
<div class="appearance-monitor-main-content">
<div class="module-header">
<div class="fg-title pl-[16px] py-[16px]">
<span class="text-[18px]">外观监测</span>
</div>
</div>
<div class="appearance-monitor-body">
<!-- 搜索区域 -->
<div class="flex items-center justify-between">
<div class="appearance-monitor-search-box">
<el-select
v-model="searchForm.station"
placeholder="站点"
class="custom-select"
>
<el-option label="小觉站" value="小觉站"></el-option>
<el-option label="东西站" value="东西站"></el-option>
<el-option label="立杆区" value="立杆区"></el-option>
</el-select>
<el-input
v-model="searchForm.train_number"
placeholder="请输入列车辆ID"
class="custom-input"
clearable
/>
<el-input
v-model="searchForm.train_carriage_number"
placeholder="请输入车厢号"
class="custom-input"
clearable
/>
<el-select
v-model="searchForm.fault_type"
placeholder="故障类型"
class="custom-select"
>
<el-option
v-for="item in faultTypeOptions"
:key="item.value"
:label="item"
:value="item"
></el-option>
</el-select>
<el-button
type="primary"
@click="handleQuery"
class="fg-basic-btn fg-info-button"
>
<span class="icon"></span> 查询
</el-button>
<el-button @click="handleReset" class="fg-basic-btn fg-reset-btn">
<span class="icon"></span> 重置
</el-button>
</div>
<div>
<VideoExport />
</div>
</div>
<div class="flex justify-between appearance-monitor-banner">
<!-- 左侧视频与缩略图区域 -->
<div class="left-panel">
<SwiperMonitor :fileList="currFileList" />
</div>
<!-- 右侧表格区域 -->
<div class="right-panel">
<div class="bg-transparent baseTable_wrap">
<template v-if="pagination.total > 0">
<BaseTable
class="bg-transparent baseTable_box"
:total="pagination.total"
:pageSize="pagination.pageSize"
:dataSource="listData"
:isFixedPagination="true"
:columns="columns"
:page="pagination.currentPage"
@change="handleTableChange"
:row-class-name="handleRowClassName"
@row-click="handleRowClick"
>
<template v-slot:actionBar="{ row }">
<ul class="flex table_action_box">
<li
class="flex items-center mr-[8px]"
@click="openCurrent(row)"
>
<div class="fg-button-primary">详情</div>
</li>
<li
class="flex items-center mr-[8px]"
@click="deleteCurrent(row)"
>
<div class="fg-button-primary-danger">删除</div>
</li>
</ul>
</template>
</BaseTable>
</template>
</div>
</div>
</div>
</div>
</div>
<ul class="fg-footer-charts">
<li>
<div class="fg-footer-charts-title">日检出量</div>
<div class="fg-footer-charts-content">
<AppearanceDailyDetectionChart :datas="recordAmountData" />
</div>
</li>
<li>
<div class="fg-footer-charts-title">日告警分类</div>
<div class="fg-footer-charts-content">
<AppearanceDailyAlertChart :datas="recordFaultData" />
</div>
</li>
</ul>
<AppearanceAlarmModal
v-model:value="isAlarmOpen"
:info="currentRow"
:image="currFileList"
:beforeImage="currBeforeFileList"
@close="isAlarmOpen = false"
/>
<DeleteModal
v-model:value="isDeleteOpen"
@delete-success="getList()"
:info="currentRow"
@close="isDeleteOpen = false"
/>
</div>
</template>
<style lang="scss">
@import url('./AppearanceMonitor.scss');
@import url("./AppearanceMonitor.scss");
</style>

@ -1,149 +1,129 @@
.data-overview-wrap {
padding-top: 30px;
// background: #002a5c;
height: 852px;
color: white;
font-family: "Arial", sans-serif;
.grid-container {
display: flex;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 20px;
width: 100%;
display: grid;
gap: 16px;
.grid-item {
// background: rgba(74, 126, 191, 0.1);
background-image: url("@/assets/common/gridItemBg.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
// border-radius: 8px;
width: 50%;
&>li{
width: 50%;
}
&>li:last-child{
margin-left: 12px;
}
.module-header {
color: #4a7ebf;
margin-bottom: 15px;
margin-bottom: 4px;
.month-btn,
.week-btn {
width: 70px;
height: 28px;
font-size: 14px;
color: #FFF;
background: #0F2839;
color: #fff;
background: #0f2839;
border: none;
}
.week-btn {
margin-right: 20px;
}
.active-btn {
background: #0B345E;
border: 1px solid #3FDDEB;
.active-btn {
background: #0b345e;
border: 1px solid #3fddeb;
}
}
.chart-container {
// margin-top: 0px;
width: 100%;
height: 300px;
}
.chart-container-bar {
// margin-top: 0px;
width: 100%;
height: 340px;
width: 431px;
height: 221px;
background: url("@/assets/home/sub_content_bg.png") no-repeat center;
}
.chart-pie-bg{
margin-top: 19px;
background-image: url("@/assets/common/gridItemPieBg.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
}
.grid-item-pie {
background-image: none;
}
}
.device-info {
.total-device {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
.device-icon {
width: 80px;
height: 90px;
background: url('@/assets/common/deviceTotal.png')
no-repeat center;
background-size: 100%;
}
.device-count {
margin-left: 15px;
.count-number {
font-size: 24px;
font-weight: bold;
}
}
}
.device-list {
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(3, 1fr);
padding: 0 16px 20px;
gap: 10px;
.device-card {
box-sizing: border-box;
// background: rgba(74, 126, 191, 0.1);
background-image: url("@/assets/common/deviceCardBg.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
padding: 16px;
// border-radius: 4px;
.status-bar {
margin-top: 5px;
}
}
}
.realTime-monitor-box {
display: flex;
flex: 1;
align-items: center;
flex-direction: column;
margin: 0 24px;
background: url("@/assets/home/realTime_monitor_bg.png") no-repeat center;
background-size: 100% 100%;
}
.monitor-images {
width: 100%;
display: flex;
box-sizing: border-box;
gap: 16px;
padding: 20px 16px 20px 16px;
.monitor-images-left,
.monitor-images-right {
flex: 1;
padding: 32px 16px 15px;
flex-wrap: wrap;
.monitor-images-item {
width: calc(50% - 8px);
height: 349px;
border: 1px dashed #ccc;
position: relative;
img {
width: 100%;
height: 256px;
// height: 100%;
}
.fault-info {
position: absolute;
padding: 0 10px;
// miomwidth: 80px;
height: 28px;
background: rgba(0,0,0,0.6);
background: rgba(0, 0, 0, 0.6);
border-radius: 16px 16px 16px 16px;
top: 16px;
left: 16px;
font-size: 14px;
color: #FFF;
color: #fff;
text-align: center;
line-height: 28px;
}
}
}
}
.rightInfo-box {
// background: linear-gradient(180deg, rgba(7, 16, 19, 0) 0%, #081417 100%);
.right-info-content {
height: 359px;
background: linear-gradient(180deg, rgba(7, 16, 19, 0) 0%, #081417 100%);
box-shadow: inset 2px 2px 4px 0px rgba(32, 174, 254, 0.6),
inset -2px -2px 4px 0px rgba(32, 174, 254, 0.6);
border-radius: 0px 0px 0px 0px;
border: 1px solid #1fc6ff;
margin-top: 4px;
}
.device-info-box {
.total-device {
.device-total-icon {
width: 96px;
height: 102px;
background: url("@/assets/common/deviceTotal.png") no-repeat center;
background-size: 100%;
}
}
.device-list {
box-sizing: border-box;
display: grid;
padding: 24px 16px 24px;
gap: 16px;
.device-card {
box-sizing: border-box;
width: 399px;
height: 195px;
background-image: url("@/assets/common/device_info_bg.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
padding: 16px;
}
}
}
.train-info-box{
}
}
}

@ -1,179 +1,35 @@
<template>
<div class="data-overview-wrap">
<!-- 检测总量汇总 -->
<div class="grid-container">
<div class="grid-item">
<div class="module-header">
<ContentHeader bgLayout="918">
<template #title>
<div class="w-[200px] bg_title bg_title_0"></div>
</template>
<template #extra>
<div>
<el-button type="primary" class="month-btn" @click="getList('month')"
:class="{ 'active-btn': activeBtn === 'month' }">
</el-button>
<el-button class="week-btn" @click="getList('week')" :class="{ 'active-btn': activeBtn === 'week' }">
</el-button>
</div>
</template>
</ContentHeader>
</div>
<div class="chart-container chart-container-bar">
<BarChart :xData="xData" :legendArr="legendArr" :datas="datas" :colorArr="colorArr" />
</div>
</div>
<!-- 设备信息 -->
<div class="grid-item">
<div class="module-header">
<ContentHeader bgLayout="918">
<template #title>
<div class="w-[200px] bg_title bg_title_1"></div>
</template>
<template #extra>
<div></div>
</template>
</ContentHeader>
</div>
<div class="device-info">
<div class="total-device">
<div class="device-icon"></div>
<div class="device-count">
<div>设备总数</div>
<div class="count-number">{{ deviceTotal }}</div>
</div>
</div>
<div class="device-list">
<div class="device-card">
<div class="mb-3">车体检测设备: {{ carDevice?.total || 0 }}</div>
<DeviceStatus :deviceStatus="carDevice" />
</div>
<div class="device-card">
<div class="mb-3">撑杆检测设备: {{ poleDevice?.total || 0 }}</div>
<DeviceStatus :deviceStatus="poleDevice" />
</div>
<div class="device-card">
<div class="mb-3">钩机检测设备: {{ excavatorDevice?.total || 0 }}</div>
<DeviceStatus :deviceStatus="excavatorDevice" />
</div>
</div>
</div>
</div>
</div>
<!-- 中部检测模块 -->
<div class="grid-container">
<ul class="flex grid-item grid-item-pie">
<li>
<div class="module-header">
<ContentHeader bgLayout="450">
<template #title>
<div class="w-[200px] bg_title bg_title_2"></div>
</template>
<template #extra>
<el-select v-model="searchForm.car" placeholder="时间" class="custom-select" @change="getCarFault()">
<el-option label="1月" value="1"></el-option>
<el-option label="2月" value="2"></el-option>
<el-option label="3月" value="3"></el-option>
<el-option label="4月" value="4"></el-option>
<el-option label="5月" value="5"></el-option>
<el-option label="6月" value="6"></el-option>
<el-option label="7月" value="7"></el-option>
<el-option label="8月" value="8"></el-option>
<el-option label="9月" value="9"></el-option>
<el-option label="10月" value="10"></el-option>
<el-option label="11月" value="11"></el-option>
<el-option label="12月" value="12"></el-option>
</el-select>
</template>
</ContentHeader>
</div>
<div class="chart-container chart-pie-bg">
<PieChart :data="carFaultTotal" :colors="[
'#FF7C09',
'#0032FF',
'#04FFF2',
'#D19EFF',
'#FF0103',
'#9EFFF3',
]" />
</div>
</li>
<li>
<div class="module-header">
<ContentHeader bgLayout="450">
<template #title>
<div class="w-[200px] bg_title bg_title_3"></div>
</template>
<template #extra>
<el-select v-model="searchForm.pole" placeholder="时间" class="custom-select" @change="getPoleFault()">
<el-option label="1月" value="1"></el-option>
<el-option label="2月" value="2"></el-option>
<el-option label="3月" value="3"></el-option>
<el-option label="4月" value="4"></el-option>
<el-option label="5月" value="5"></el-option>
<el-option label="6月" value="6"></el-option>
<el-option label="7月" value="7"></el-option>
<el-option label="8月" value="8"></el-option>
<el-option label="9月" value="9"></el-option>
<el-option label="10月" value="10"></el-option>
<el-option label="11月" value="11"></el-option>
<el-option label="12月" value="12"></el-option>
</el-select>
</template>
</ContentHeader>
</div>
<div class="chart-container chart-pie-bg">
<PieChartSmall :data="poleFaultTotal" :colors="['#9DFFF3', '#FFA179']" />
</div>
</li>
</ul>
<div class="grid-item">
<div class="module-header">
<ContentHeader bgLayout="918">
<template #title>
<div class="w-[200px] bg_title bg_title_4"></div>
</template>
<template #extra>
<div></div>
</template>
</ContentHeader>
</div>
<div class="monitor-images">
<div class="monitor-images-left">
<img :src="imageFault[0]?.url" />
<div class="fault-info">{{ imageFault[0]?.fault_type }}</div>
</div>
<div class="monitor-images-right">
<img :src="imageFault[1]?.url"/>
<div class="fault-info">{{ imageFault[1]?.fault_type }}</div>
</div>
</div>
</div>
</div>
<AlarmModal v-model:value="isAlarmOpen" :info="currentRow" :image="currFileList" @close="isAlarmOpen = false" />
</div>
</template>
<script lang="ts" setup>
import * as echarts from "echarts";
import ContentHeader from "@/components/ContentHeader.vue";
import BarChart from "./components/BarChart.vue";
import PieChart from "./components/PieChart.vue";
import PieChartSmall from "./components/PieChartSmall.vue";
import { BaseTable } from "@/components/CustomTable";
import TotalChart from "@/components/Charts/totalChart.vue";
import PoleMonitorChart from "@/components/Charts/poleMonitorChart.vue";
import VehicleMonitorChart from "@/components/Charts/vehicleMonitorChart.vue";
import TrainAlarmInfoModel from "./components/TrainAlarmInfoModel.vue";
import HomeSubTitle from "@/components/HeaderBar/homeSubTitle.vue";
import DeviceStatus from "./components/DeviceStatus.vue";
import { getDataOverviewApi, getDeviceInfowApi, getRecordFaultApi, getRealTimeApi } from '@/api/dashboard';
import car_device_icon from "@/assets/home/car_device_icon.png";
import pole_device_icon from "@/assets/home/pole_device_icon.png";
import excavator_device_icon from "@/assets/home/excavator_device_icon.png";
import {
getDataOverviewApi,
getDeviceInfowApi,
getRecordFaultApi,
getRealTimeApi,
getHomeTrainDataApi,
getHomeTrainDetailDataApi,
} from "@/api/dashboard";
import { isSuccessApi } from "@/utils/forApi";
import AlarmModal from './components/AlarmModal.vue'
import { useWebSocketStore } from '@/stores/websocketStore';
import { onBeforeRouteLeave } from 'vue-router';
import { isArray } from "@/utils/is";
import AlarmModal from "./components/AlarmModal.vue";
import { dataViewConfig } from "@/config/dataView";
import { useWebSocketStore } from "@/stores/websocketStore";
import { onBeforeRouteLeave } from "vue-router";
defineOptions({
name: "DataOverviewWrap"
name: "DataOverviewWrap",
});
// TODO mock
const xData = ref(["1月", "2月", "3月", "4月", "5月"]);
const legendArr = ["车体检测", "撑杆检测"];
const datas = ref([
@ -184,126 +40,376 @@ const colorArr = [
["#3B9FFE", "#5070F2"],
["#FFDA8D", "#FFAC06"],
];
const deviceStatus = ref({
onlineCount: 50,
errorCount: 10,
outlineCount: 10
});
const searchForm = reactive({
car: "1",
pole: "1",
appearanceTime: "9",
poleTime: "9",
});
const deviceTotal = ref(0);
const carDevice = ref({});
const poleDevice = ref({});
const excavatorDevice = ref({});
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,
errorCount: 0,
outlineCount: 0,
});
const trainListData = ref([]);
const currTrainFaultInfo = ref({});
const trainColumns = [
{
label: "进场时间",
property: "arrive_at",
width: 170,
},
{
label: "缺陷数量",
property: "fault_count",
// width: 550,
width: 120,
},
{
type: "action",
label: "操作",
// width: 210,
},
];
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;
fetchTrainDetail();
};
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;
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 });
},
{ deep: true, immediate: true }
);
const getList = async (dateType: string = "month") => {
activeBtn.value = dateType
const res = await getDataOverviewApi({ dateType })
activeBtn.value = dateType;
const res = await getDataOverviewApi({ dateType });
if (isSuccessApi(res)) {
const { data } = res
datas.value[0] = data.appearance
datas.value[1] = data.pole
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 + '月'
if (dateType === "month") {
return item + "月";
}
})
});
} else {
xData.value = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
xData.value = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
}
// console.log(data, 'getList_data')
}
};
const getDeviceInfo = async () => {
try {
const res = await getDeviceInfowApi();
if (isSuccessApi(res)) {
const { data } = res;
deviceTotal.value = data.deviceTotal;
carDevice.value = data.appearance;
poleDevice.value = data.pole;
excavatorDevice.value = data.excavator;
// deviceStatus.value = data
deviceInfo.value = {
onlineCount:
data?.pole?.onlineCount +
data?.appearance?.outlineCount +
data?.excavator?.errorCount,
errorCount:
data?.pole?.errorCount +
data?.appearance?.errorCount +
data?.excavator?.errorCount,
outlineCount:
data?.pole?.outlineCount +
data?.appearance?.outlineCount +
data?.excavator?.outlineCount,
total: data?.deviceTotal,
};
}
} catch (error) {
console.error('获取设备信息出错:', error);
console.error("获取设备信息出错:", error);
}
}
const getPoleFault = async () => {
};
//
const fetchAppearanceMonitorData = async () => {
try {
const res = await getRecordFaultApi({ dateType: 'month', value: searchForm.pole, type: "pole" });
const res = await getRecordFaultApi({
dateType: "month",
value: searchForm.appearanceTime,
type: "appearance",
});
if (isSuccessApi(res)) {
const { data } = res;
poleFaultTotal.value = data;
// deviceStatus.value = data
console.log(data);
appearanceFaultData.value = data;
console.log(data, "fetchAppearanceMonitorData");
}
} catch (error) {
console.error('获取设备信息出错:', error);
console.error("获取设备信息出错:", error);
}
}
const getCarFault = async () => {
};
//
const fetchPoleMonitorData = async () => {
try {
const res = await getRecordFaultApi({ dateType: 'month', value: searchForm.car, type: "appearance" });
const res = await getRecordFaultApi({
dateType: "month",
value: searchForm.poleTime,
type: "pole",
});
if (isSuccessApi(res)) {
const { data } = res;
carFaultTotal.value = data;
// deviceStatus.value = data
console.log(data);
poleFaultData.value = data;
console.log(data, "fetchPoleMonitorData");
}
} catch (error) {
console.error('获取设备信息出错:', error);
console.error("获取设备信息出错:", error);
}
}
};
const getRealTime = async () => {
try {
const res = await getRealTimeApi({ deviceType: '' });
const res = await getRealTimeApi({ deviceType: "" });
if (isSuccessApi(res)) {
const { data } = res;
imageFault.value = data;
// deviceStatus.value = data
if (isArray(data)) {
// TODO 使
imageFault.value = data.concat(data).concat(data).splice(0, 4);
}
console.log(data);
}
} catch (error) {
console.error('获取设备信息出错:', error);
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 = {};
currFileList.value = [];
isAlarmOpen.value = false;
currentRow.value = {};
currFileList.value = [];
});
onMounted(() => {
getList()
getDeviceInfo()
getCarFault()
getPoleFault()
getRealTime()
getList();
getDeviceInfo();
fetchAppearanceMonitorData();
fetchPoleMonitorData();
getRealTime();
fetchHomeTrainData();
});
</script>
<template>
<div class="flex data-overview-wrap">
<!-- 检测总量汇总 -->
<div class="grid-container">
<div class="grid-item">
<div class="module-header">
<HomeSubTitle title="检测总量汇总">
<template #extra>
<div>
<el-button
type="primary"
class="month-btn"
@click="getList('month')"
:class="{ 'active-btn': activeBtn === 'month' }"
>
</el-button>
<el-button
class="week-btn"
@click="getList('week')"
:class="{ 'active-btn': activeBtn === 'week' }"
>
</el-button>
</div>
</template>
</HomeSubTitle>
</div>
<div class="chart-container">
<TotalChart
:xData="xData"
:legendArr="legendArr"
:datas="datas"
:colorArr="colorArr"
/>
</div>
</div>
<div class="grid-item">
<div class="module-header">
<HomeSubTitle title="车体监测">
<template #extra>
<div class="flex items-center text-[14px] px-[16px]">
<span>时间</span>
<el-select
v-model="searchForm.appearanceTime"
placeholder="时间"
class="custom-select mini-size"
@change="fetchAppearanceMonitorData()"
>
<el-option
v-for="v in dataViewConfig.monthArr"
:key="v?.value"
:label="v?.name"
:value="v?.value"
></el-option>
</el-select>
</div>
</template>
</HomeSubTitle>
</div>
<div class="chart-container chart-pie-bg">
<VehicleMonitorChart :datas="appearanceFaultData" />
</div>
</div>
<div class="grid-item">
<div class="module-header">
<HomeSubTitle title="撑杆监测">
<template #extra>
<div class="flex items-center text-[14px] px-[16px]">
<span>时间</span>
<el-select
v-model="searchForm.poleTime"
placeholder="时间"
class="custom-select mini-size"
@change="fetchPoleMonitorData()"
>
<el-option
v-for="v in dataViewConfig.monthArr"
:key="v?.value"
:label="v?.name"
:value="v?.value"
></el-option>
</el-select>
</div>
</template>
</HomeSubTitle>
</div>
<div class="chart-container chart-pie-bg">
<PoleMonitorChart :datas="poleFaultData" />
</div>
</div>
</div>
<!-- 中部检测模块 -->
<div class="realTime-monitor-box">
<div class="w-full h-[35px] fg-title px-[16px] py-[18px]">
实时监测画面
</div>
<ul class="flex monitor-images">
<li class="monitor-images-item" v-for="(v, i) in imageFault" :key="i">
<img :src="v?.url" />
<div class="fault-info">{{ v?.fault_type }}</div>
</li>
</ul>
</div>
<div class="rightInfo-box">
<!-- 设备信息 -->
<div class="cursor-pointer device-info-box" @click="() => $router.push({ name: 'DeviceStatus'})">
<div class="module-header">
<HomeSubTitle title="设备信息"> </HomeSubTitle>
</div>
<div class="device-info right-info-content">
<div class="flex items-center justify-center total-device">
<div class="device-total-icon mt-[24px]"></div>
<div class="device-count ml-[24px]">
<div class="fg-mark1 text-[32px] font-bold">
{{ deviceInfo?.total }}
</div>
<div class="text-[14px]">设备总数</div>
</div>
</div>
<ul class="device-list">
<li class="device-card">
<DeviceStatus :deviceStatus="deviceInfo" />
</li>
</ul>
</div>
</div>
<div class="train-info-box mt-[16px]">
<div class="module-header">
<HomeSubTitle title="近期车辆"> </HomeSubTitle>
</div>
<div class="train-info right-info-content">
<div class="p-[16px] baseTable_wrap full_table">
<BaseTable
class="bg-transparent baseTable_box"
:dataSource="trainListData"
:columns="trainColumns"
:pageable="false"
height="325"
>
<template v-slot:actionBar="{ row }">
<ul class="flex table_action_box">
<li class="flex items-center" @click="openTrainDetail(row)">
<div class="fg-button-primary">详情</div>
</li>
</ul>
</template>
</BaseTable>
</div>
</div>
</div>
</div>
<TrainAlarmInfoModel
v-model:value="isTrainOpen"
:info="currTrainFaultInfo"
@close="isTrainOpen = false"
/>
<AlarmModal
v-model:value="isAlarmOpen"
:info="currentRow"
:image="currFileList"
@close="isAlarmOpen = false"
/>
</div>
</template>
<style scoped lang="scss">
@import url("./DataOverview.scss");
</style>

@ -1,21 +1,59 @@
.device-status-wrap{
.device-status-wrap {
height: 813px;
background-image: url("@/assets/common/device_status_bg_line.png");
background-image: url("@/assets/deviceStatus/device_status_bg.png");
background-size: 100% 100%;
background-position: bottom;
background-repeat: no-repeat;
}
.device-status-content-box{
.device-status-content-box {
.el-scrollbar__view {
background: transparent !important;
height: 600px;
}
.el-table__inner-wrapper{
.el-table__inner-wrapper {
background-color: transparent !important;
}
.el-table__body-wrapper, .el-scrollbar__wrap, .el-scrollbar{
.el-table__body-wrapper,
.el-scrollbar__wrap,
.el-scrollbar {
background: transparent !important;
}
}
.device-status-tag {
width: 78px;
height: 24px;
border-radius: 14px;
border: 1px solid #52c41a;
color: #52c41a;
display: flex;
align-items: center;
justify-content: center;
.dot {
display: inline-block;
margin-right: 8px;
width: 8px;
height: 8px;
background: #52c41a;
border-radius: 50%;
}
}
.device-status-tag-online{
@extend .device-status-tag;
}
.device-status-tag-outline {
@extend .device-status-tag;
border: 1px solid #999999;
color: #999999;
.dot {
background: #999999;
}
}
.device-status-tag-error {
@extend .device-status-tag;
border: 1px solid #e80d0d;
color: #e80d0d;
.dot {
background: #e80d0d;
}
}
}

@ -1,108 +1,60 @@
<template>
<div class="bg_basic_content">
<div class="device-status-wrap">
<div class="device-status-header mt-[32px]">
<ContentHeader bgLayout="1855">
<template #title>
<div class="w-[200px] bg_title bg_title_6">
</div>
</template>
</ContentHeader>
</div>
<div class="px-[16px] device-status-content-box">
<div class="mt-[16px] bg-transparent baseTable_wrap full_table">
<BaseTable class="bg-transparent baseTable_box" :total="pagination.total"
:pageSize="pagination.pageSize" :dataSource="listData" :isFixedPagination="true"
:columns="columns" :page="pagination.currentPage" @change="handleTableChange">
<template v-slot:actionBar="{ row }">
<ul class="flex table_action_box">
<li class="flex items-center mr-[16px]" @click="openCurrent(row)">
<el-button text>
<span :style="{
fontSize: '14px',
color: '#37DBFF'
}">
即时视频
</span>
</el-button>
</li>
<li class="flex items-center" @click="openHistory(row)">
<el-button text>
<span :style="{
fontSize: '14px',
color: '#37DBFF'
}">
历史视频
</span>
</el-button>
</li>
</ul>
</template>
</BaseTable>
</div>
</div>
<RealVideoModal v-model:value="isRealOpen" :info="currentRow" @close="isRealOpen = false" />
<HistoryVideoModal ref="historyModalRef" v-model:value="isHistoryOpen" :info="currentRow"
:historyVideos="historyVideos" @close="isHistoryOpen = false" />
<AlarmModal v-model:value="isAlarmOpen" :info="currentDetailRow" :image="currFileList" @close="isAlarmOpen = false" />
</div>
</div>
</template>
<script setup lang="ts">
<script setup lang="tsx">
import { BaseTable } from "@/components/CustomTable";
import ContentHeader from '@/components/ContentHeader.vue';
import HistoryVideoModal from './components/HistoryVideoModal.vue';
import RealVideoModal from './components/RealVideoModal.vue';
import onLineIcon from '@/assets/common/online_icon.png';
import outLineIcon from '@/assets/common/outline_icon.png';
import errorIcon from '@/assets/common/error_icon.png';
import { getDeviceStatusApi } from '@/api/dashboard';
import ContentHeader from "@/components/ContentHeader.vue";
import HistoryVideoModal from "./components/HistoryVideoModal.vue";
import RealVideoModal from "./components/RealVideoModal.vue";
import onLineIcon from "@/assets/common/online_icon.png";
import outLineIcon from "@/assets/common/outline_icon.png";
import errorIcon from "@/assets/common/error_icon.png";
import { getDeviceStatusApi } from "@/api/dashboard";
import { isSuccessApi } from "@/utils/forApi";
import AlarmModal from './components/AlarmModal.vue'
import { useWebSocketStore } from '@/stores/websocketStore';
import { onBeforeRouteLeave } from 'vue-router';
import AlarmModal from "./components/AlarmModal.vue";
import { useWebSocketStore } from "@/stores/websocketStore";
import { onBeforeRouteLeave } from "vue-router";
defineOptions({
name: "DeviceStatusIndex"
name: "DeviceStatusIndex",
});
const alarmLevelStatusEnum = {
'online': {
color: "#52C41A",
value: "1",
label: "在线",
isDelete: true,
icon: onLineIcon,
id: "1"
},
'offline': {
color: "#999999",
value: "2",
label: "离线",
isDelete: false,
icon: outLineIcon,
id: "2"
},
'error': {
color: "#E80D0D",
value: "3",
label: "故障",
isDelete: false,
icon: errorIcon,
id: "3"
},
'notFound': { //
color: "#E80D0D",
value: "4444",
label: "未知",
isDelete: false,
icon: errorIcon,
id: "4444"
}
}
online: {
color: "#52C41A",
className: "device-status-tag-online",
value: "1",
label: "在线",
isDelete: true,
icon: onLineIcon,
id: "1",
},
offline: {
color: "#999999",
className: "device-status-tag-outline",
value: "2",
label: "离线",
isDelete: false,
icon: outLineIcon,
id: "2",
},
error: {
color: "#E80D0D",
className: "device-status-tag-error",
value: "3",
label: "故障",
isDelete: false,
icon: errorIcon,
id: "3",
},
notFound: {
//
color: "#E80D0D",
className: "device-status-tag-error",
value: "4444",
label: "未知",
isDelete: false,
icon: errorIcon,
id: "4444",
},
};
const currentRow = ref<Record<string, any>>({});
const isRealOpen = ref<Boolean>(false); //
const isHistoryOpen = ref<Boolean>(false); //
@ -114,102 +66,86 @@ const currFileList = ref<Record<string, any>[]>([]); // 详情的文件列表
const websocketStore = useWebSocketStore();
// messages
watch(() => websocketStore.messages, (newMessages: string[], oldMessages: string[]) => {
if(newMessages?.length > 0 && !isAlarmOpen.value) {
currentDetailRow.value = newMessages[newMessages?.length - 1];
currFileList.value = newMessages[newMessages?.length - 1]?.images;
isAlarmOpen.value = true;
watch(
() => websocketStore.messages,
(newMessages: string[], oldMessages: string[]) => {
if (newMessages?.length > 0 && !isAlarmOpen.value) {
currentDetailRow.value = newMessages[newMessages?.length - 1];
currFileList.value = newMessages[newMessages?.length - 1]?.images;
isAlarmOpen.value = true;
}
}, { deep: true, immediate: true });
},
{ deep: true, immediate: true }
);
const columns = [
{
label: "设备名称",
property: "device_name"
},
{
label: "设备ID",
property: "device_number"
},
{
label: "设备位置",
property: "device_position"
},
{
label: "设备状态",
property: "device_status",
formatter: val => {
console.log(val);
const currentLevelObj =
alarmLevelStatusEnum[val?.device_status] || alarmLevelStatusEnum.notFound;
return h(
"div",
{
style: {
fontSize: "14px",
display: "flex",
alignItems: "center",
lineHeight: "20px",
color: currentLevelObj?.color
}
},
[
h('img', {
src: currentLevelObj?.icon,
style: {
width: '20px',
height: '20px',
marginRight: "12px"
}
}),
h(
"span",
{
fontSize: "14px",
},
currentLevelObj?.label
)
]
);
}
{
label: "设备名称",
property: "device_name",
},
{
label: "设备ID",
property: "device_number",
},
{
label: "设备位置",
property: "device_position",
width: 550,
},
{
label: "设备状态",
property: "device_status",
formatter: (val) => {
console.log(val);
const currentLevelObj =
alarmLevelStatusEnum[val?.device_status] ||
alarmLevelStatusEnum.notFound;
return (
<div className={currentLevelObj.className}>
<span className="dot"></span>
<span>{currentLevelObj?.label}</span>
</div>
);
},
{
type: "action",
label: "操作"
}
},
{
type: "action",
label: "操作",
width: 210,
},
];
const pagination = ref({ currentPage: 1, pageSize: 10, total: 0 });
const listData = ref([]);
const getList = async () => {
try {
const { currentPage, pageSize } = pagination.value;
const res = await getDeviceStatusApi({ current: currentPage, pageSize })
console.log(res.data, 'getList_data')
if (isSuccessApi(res)) {
listData.value = res.data.data;
pagination.value = {
...pagination.value,
total: res.data.total
};
}
} catch (error) {
console.error('获取数据失败:', error)
try {
const { currentPage, pageSize } = pagination.value;
const res = await getDeviceStatusApi({ current: currentPage, pageSize });
console.log(res.data, "getList_data");
if (isSuccessApi(res)) {
listData.value = res.data.data;
pagination.value = {
...pagination.value,
total: res.data.total,
};
}
}
} catch (error) {
console.error("获取数据失败:", error);
}
};
function handleTableChange(record) {
console.log("handleTableChange_record", record);
pagination.value = {
...pagination.value,
currentPage: record.page,
pageSize: record.pageSize
};
getList();
console.log("handleTableChange_record", record);
pagination.value = {
...pagination.value,
currentPage: record.page,
pageSize: record.pageSize,
};
getList();
}
/**打开实时视频 */
function openCurrent(row) {
console.log(row, "openCurrent");
currentRow.value = row;
isRealOpen.value = true;
console.log(row, "openCurrent");
currentRow.value = row;
isRealOpen.value = true;
}
/**打开历史视频 */
// getDeviceHistoryDetailApi
@ -231,40 +167,97 @@ function openCurrent(row) {
// TODO mock
const fetchHistoryList = async () => {
try {
const resAll = await fetch('/api/v1/device/device_history/', {
method: 'POST'
})
const res = await resAll.json()
if (isSuccessApi(res)) {
historyVideos.value = res.data;
isHistoryOpen.value = true;
nextTick(() => {
historyModalRef.value.loadData()
})
console.log(res, 'fetchHistoryList_data')
}
} catch (error) {
console.error('获取数据失败:', error)
try {
const resAll = await fetch("/api/v1/device/device_history/", {
method: "POST",
});
const res = await resAll.json();
if (isSuccessApi(res)) {
historyVideos.value = res.data;
isHistoryOpen.value = true;
nextTick(() => {
historyModalRef.value.loadData();
});
console.log(res, "fetchHistoryList_data");
}
}
} catch (error) {
console.error("获取数据失败:", error);
}
};
function openHistory(row) {
console.log(row, "openHistory");
currentRow.value = row;
fetchHistoryList();
console.log(row, "openHistory");
currentRow.value = row;
fetchHistoryList();
}
onBeforeRouteLeave(() => {
isAlarmOpen.value = false;
currentDetailRow.value = {};
currFileList.value = [];
isAlarmOpen.value = false;
currentDetailRow.value = {};
currFileList.value = [];
});
onMounted(() => {
getList();
getList();
});
</script>
<template>
<div class="bg-basic-content">
<div class="device-status-wrap">
<div class="device-status-header mt-[32px]">
<div class="mt-[24px] py-[16px] pl-[69px] flex items-center">
<div class="fg-title">设备状态</div>
</div>
</div>
<div class="px-[24px] device-status-content-box">
<div class="mt-[16px] bg-transparent baseTable_wrap full_table">
<BaseTable
class="bg-transparent baseTable_box"
:total="pagination.total"
:pageSize="pagination.pageSize"
:dataSource="listData"
:isFixedPagination="true"
:columns="columns"
:page="pagination.currentPage"
@change="handleTableChange"
>
<template v-slot:actionBar="{ row }">
<ul class="flex table_action_box">
<li
class="flex items-center mr-[12px]"
@click="openCurrent(row)"
>
<div class="fg-button-primary1">即时视频</div>
</li>
<li class="flex items-center" @click="openHistory(row)">
<div class="fg-button-primary">查看详情</div>
</li>
</ul>
</template>
</BaseTable>
</div>
</div>
<RealVideoModal
v-model:value="isRealOpen"
:info="currentRow"
@close="isRealOpen = false"
/>
<HistoryVideoModal
ref="historyModalRef"
v-model:value="isHistoryOpen"
:info="currentRow"
:historyVideos="historyVideos"
@close="isHistoryOpen = false"
/>
<AlarmModal
v-model:value="isAlarmOpen"
:info="currentDetailRow"
:image="currFileList"
@close="isAlarmOpen = false"
/>
</div>
</div>
</template>
<style lang="scss">
@import url('./DeviceStatus.scss');
@import url("./DeviceStatus.scss");
</style>

@ -1,122 +1,38 @@
.digger-monitor-warp {
box-sizing: border-box;
padding-top: 32px;
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
gap: 20px;
// align-items: center;
.digger-monitor-right {
box-sizing: border-box;
width:970px;
// display: flex;
background-image: url("@/assets/common/carbtmBg.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
.digger-monitor-main-content {
background-image: url("@/assets/diggerMonitor/diggerMonitor-main-bg.png");
background-size: 100% 100%;
background-position: bottom;
background-repeat: no-repeat;
height: 586px;
margin-top: 20px;
.digger-monitor-search-box {
display: flex;
align-items: center;
gap: 12px;
margin: 16px 0;
}
.right-panel{
.el-scrollbar__view {
background: transparent !important;
height: 600px;
}
.fixed_pagination{
padding: 12px 20px 15px;
}
.digger-monitor-body {
padding: 0 24px;
}
.digger-monitor-search-box {
display: flex;
align-items: center;
gap: 12px;
margin: 24px 0 16px;;
}
.right-panel {
width: calc(
100% - var(--fg-swiper-monitor-slide-iamge-width) -
var(--fg-swiper-monitor-main-iamge-width) - var(--fg-box-margin) * 2
);
.el-scrollbar__view {
background: transparent !important;
height: 360px;
}
.digger-monitor-left {
width: 49%;
background-image: url("@/assets/common/boderBg.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
.monitor-left-top {
box-sizing: border-box;
padding: 32px 16px 20px;
min-height: 600px;
.file-preview-screen {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
height: 492px;
// background-color: red;
img {
max-width: 100%;
max-height: 100%;
object-fit: cover;
}
video {
width: 100%;
max-height: calc(100%);
}
}
}
.monitor-left-bottom {
width: 100%;
padding: 0 16px;
margin-bottom: 29px;
overflow: visible;
.swiper {
width: 100%;
height: 100%;
.swiper-slide {
width: 20%;
border-radius:4px;
height: 144px;
img {
width: 100%;
height: 144px;
border-radius:4px;
object-fit: cover
}
}
.active-slide img,
.active-slide video {
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);
}
}
}
.empty-bg {
box-sizing: border-box;
width: 892px;
height: 815px;
background-image: url("@/assets/common/emptyBg.png");
background-size: 156px 102px;
background-position: center;
background-repeat: no-repeat;
}
}
.digger-monitor-banner {
.left-panel {
width: calc(
var(--fg-swiper-monitor-slide-iamge-width) +
var(--fg-swiper-monitor-main-iamge-width) + var(--fg-box-margin)
);
max-height: 480px;
margin-right: 16px;
}
}
}
}

@ -1,163 +1,31 @@
<template>
<div class="digger-monitor-warp">
<div class="digger-monitor-left">
<template v-if="currFileList?.length">
<div class="flex items-center justify-center h-full monitor-left-top">
<div class="file-preview-screen">
<!-- // TODO -->
<img :src="currFileList?.[0]?.image_url" v-if="currFileList?.[0]?.image_url" />
<div v-else class="w-full h-full bg_error_picture"></div>
</div>
</div>
</template>
<div class="empty-bg" v-else></div>
</div>
<div class="digger-monitor-right h-[100%]">
<div class="module-header">
<ContentHeader bgLayout="918">
<template #title>
<div class="w-[200px] bg_title bg_title_8"></div>
</template>
<template #extra>
<div></div>
</template>
</ContentHeader>
</div>
<!-- 表格区域 -->
<!-- 搜索区域 -->
<div class="px-[16px]">
<div class="digger-monitor-search-box">
<!-- <el-select
v-model="searchForm.station"
placeholder="钩机编号"
class="custom-select"
>
<el-option label="小觉站" value="小觉站"></el-option>
<el-option label="东西站" value="东西站"></el-option>
<el-option label="立杆区" value="立杆区"></el-option>
</el-select> -->
<el-input
v-model="searchForm.train_number"
placeholder="请输入列车号"
class="custom-input"
clearable
/>
<el-input
v-model="searchForm.train_carriage_number"
placeholder="请输入车厢号"
class="custom-input"
clearable
/>
<!-- <el-select
v-model="searchForm.fault_type"
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-select> -->
<el-button
type="primary"
@click="handleQuery"
class="basic-btn query-btn"
>
<span class="icon"></span> 查询
</el-button>
<el-button @click="handleReset" class="basic-btn reset-btn">
<span class="icon"></span> 重置
</el-button>
</div>
<!-- 右侧表格区域 -->
<div class="w-full right-panel">
<div class="bg-transparent baseTable_wrap">
<template v-if="pagination.total > 0">
<BaseTable
class="bg-transparent baseTable_box"
:total="pagination.total"
:pageSize="pagination.pageSize"
:dataSource="listData"
:isFixedPagination="true"
:columns="columns"
:page="pagination.currentPage"
@change="handleTableChange"
:row-class-name="handleRowClassName"
@row-click="handleRowClick"
>
<template #created_at="{ row }">
<div>{{ row }}</div>
</template>
</BaseTable>
</template>
</div>
</div>
</div>
</div>
<DiggerAlarmModal
v-model:value="isAlarmOpen"
:info="currentRow"
:fileList="currFileList"
@close="isAlarmOpen = false"
/>
<BaseDelete
v-model:value="isDeleteOpen"
:deleteContent="`
<p>
确定删除
<span>${currentRow?.alarm_type}</span>
相关告警记录吗删除后将找不到此记录请谨慎操作.
</p>
`"
@delete-confirm="handleDeleteConfirm"
:info="currentRow"
@close="isDeleteOpen = false"
/>
</div>
</template>
<script lang="ts" setup>
import Player from "@/components/videoPlayer/Player.vue";
import SwiperPlayer from "./components/SwiperPlayer.vue";
import ContentHeader from "@/components/ContentHeader.vue";
import { onBeforeRouteLeave } from "vue-router";
import { BaseDelete, BaseTable } from "@/components/CustomTable";
import { Swiper, SwiperSlide } from "swiper/vue";
import { Navigation, Scrollbar } from "swiper/modules";
import DiggerAlarmModal from "./components/DiggerAlarmModal.vue";
import DiggerHourlyDistanceChart from "@/components/Charts/diggerHourlyDistanceChart.vue";
import DiggerDailyDistanceChart from "@/components/Charts/diggerDailyDistanceChart.vue";
import VideoExport from "./components/VideoExport.vue";
import {
getAppearanceMonitorApi,
getAppearanceMonitorDetailApi,
getBeforeMonitorDetailApi,
deleteAppearanceMonitorApi,
getRecordAmountDataApi,
} from "@/api/dashboard";
import { isSuccessApi } from "@/utils/forApi";
import DiggerAlarmModal from "./components/DiggerAlarmModal.vue";
import { useWebSocketStore } from "@/stores/websocketStore";
import { onBeforeRouteLeave } from "vue-router";
import "swiper/css";
import "swiper/scss";
import "swiper/scss/navigation";
import { color } from "echarts";
const modules = [Navigation, Scrollbar];
const activeIndex = ref(-1);
const swiperRef = ref(null);
const isAlarmOpen = ref<Boolean>(false); //
const isDeleteOpen = ref<Boolean>(false); //
const websocketStore = useWebSocketStore();
const recordDistanceHourData = ref({}); //
const recordDistanceDayData = ref({}); //
// messages
watch(
() => websocketStore.messages,
(newMessages: string[], oldMessages: string[]) => {
// console.log(':', newMessages[newMessages?.length - 1].content);
// console.log(':', oldMessages);
// if (newMessages?.length > oldMessages?.length) {
// //
// const newMessage = newMessages[newMessages?.length - 1];
// // console.log(' WebSocket :', newMessage);
// //
// }
if (newMessages?.length > 0 && !isAlarmOpen.value) {
console.log(
newMessages[newMessages?.length - 1],
@ -185,7 +53,7 @@ const columns = [
width: 90,
},
{
label: "车",
label: "车辆ID",
property: "train_number",
width: 185,
},
@ -222,76 +90,15 @@ const columns = [
property: "created_at",
},
{
slot: "operation",
type: "action",
label: "操作",
width: 140,
formatter: (row) => {
return h(
"div",
{
style: {
fontSize: "14px",
color: "#37EBFF",
},
},
[
h(
"span",
{
fontSize: "14px",
class: "pf-1",
},
[
h(
"i",
{
style: {
fontSize: "14px",
letterSpacing: "2px",
marginRight: "4px",
color: "#009DFF",
},
onClick: (row) => {
console.log("row.id");
//
isAlarmOpen.value = true;
console.log(isAlarmOpen.value);
currentRow.value = row;
},
},
"详情"
),
h(
"i",
{
style: {
fontSize: "14px",
letterSpacing: "2px",
marginRight: "4px",
color: "#FF2727",
},
onClick: (row) => {
// console.log(row.id);
//
isDeleteOpen.value = true;
currentRow.value = row;
},
},
"删除"
),
]
),
]
);
},
width: 170,
},
];
const listData = ref([]); //
const currentRow = ref<Record<string, any>>({}); //
const currFileList = ref<Record<string, any>[]>([]); //
const currBeforeFileList = ref<Record<string, any>[]>([]); //
const currFile = ref<Record<string, any>>({}); //
//
const searchForm = reactive({
train_number: "",
@ -300,7 +107,6 @@ const searchForm = reactive({
station: "",
type: "distance", // TODO distance
});
const dataLoading = ref(true);
//
const getFileList = async () => {
try {
@ -312,53 +118,15 @@ const getFileList = async () => {
console.log(res.data, "getDetailList_data");
if (isSuccessApi(res)) {
currFileList.value = res.data.data;
currFile.value = res.data.data[0];
activeIndex.value = 0;
}
} catch (error) {
console.log(error, "getDetailList_error");
}
};
//
const getBeforeFileList = async () => {
try {
const res = await getBeforeMonitorDetailApi({
id: currentRow.value?.id,
current: 1,
pageSize: 1000,
});
console.log(res.data, "getDetailList_data");
if (isSuccessApi(res)) {
currBeforeFileList.value = res.data.data;
}
} catch (error) {
console.log(error, "getDetailList_error");
}
};
// TODO mock
// const getFileList = async () => {
// try {
// const resAll = await fetch('/api/v1/record/record_detail_list/', {
// method: 'POST'
// })
// const res = await resAll.json()
// if (isSuccessApi(res)) {
// currFileList.value = res.data.data;
// currFile.value = res.data.data[0];
// activeIndex.value = 0;
// }
// } catch (error) {
// console.error(':', error)
// }
// }
function loadDetail() {
currentRow.value = listData.value[0];
console.log(currentRow.value, "currentRow");
getFileList();
getBeforeFileList();
}
//
@ -383,6 +151,36 @@ const getList = async () => {
console.error("获取数据失败:", error);
}
};
// 1
const fetchRecordHourData = async () => {
try {
const res = await getRecordAmountDataApi({
type: "distance",
dateType: "hour",
});
console.log(res.data, "fetchRecordHourData_data");
if (isSuccessApi(res)) {
recordDistanceHourData.value = res.data;
}
} catch (error) {
console.error("获取数据失败:", error);
}
};
// 2
const fetchRecordDayData = async () => {
try {
const res = await getRecordAmountDataApi({
type: "distance",
dateType: "day",
});
console.log(res.data, "fetchRecordDayData_data");
if (isSuccessApi(res)) {
recordDistanceDayData.value = res.data;
}
} catch (error) {
console.error("获取数据失败:", error);
}
};
//
const handleQuery = () => {
@ -426,11 +224,23 @@ const handleRowClassName = ({ row }) => {
return row.id === currentRow.value.id ? "selected-row" : "";
};
/**查看详情 */
function openCurrent(row) {
console.log(row, "openCurrent");
currentRow.value = row;
isAlarmOpen.value = true;
}
//
function deleteCurrent(row) {
isDeleteOpen.value = true;
currentRow.value = row;
}
//
const handleRowClick = (row, event, rowIndex) => {
currentRow.value = row;
getFileList();
getBeforeFileList();
};
onBeforeRouteLeave(() => {
@ -441,8 +251,168 @@ onBeforeRouteLeave(() => {
onMounted(() => {
getList();
fetchRecordHourData();
fetchRecordDayData();
});
</script>
<template>
<div class="digger-monitor-warp">
<div class="digger-monitor-main-content">
<div class="module-header">
<div class="fg-title pl-[32px] pt-[23px]">
<span class="text-[18px]">钩机监测</span>
</div>
</div>
<div class="digger-monitor-body">
<div class="flex items-center justify-between">
<!-- 搜索区域 -->
<div class="digger-monitor-search-box">
<!-- <el-select
v-model="searchForm.station"
placeholder="钩机编号"
class="custom-select"
>
<el-option label="小觉站" value="小觉站"></el-option>
<el-option label="东西站" value="东西站"></el-option>
<el-option label="立杆区" value="立杆区"></el-option>
</el-select> -->
<el-input
v-model="searchForm.train_number"
placeholder="请输入列车辆ID"
class="custom-input"
clearable
/>
<el-input
v-model="searchForm.train_carriage_number"
placeholder="请输入车厢号"
class="custom-input"
clearable
/>
<!-- <el-select
v-model="searchForm.fault_type"
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-select> -->
<el-button
type="primary"
@click="handleQuery"
class="fg-basic-btn fg-info-button"
>
<span class="icon"></span> 查询
</el-button>
<el-button @click="handleReset" class="fg-basic-btn fg-reset-btn">
<span class="icon"></span> 重置
</el-button>
</div>
<div>
<VideoExport />
</div>
</div>
<!-- 主体内容区域 -->
<div class="flex justify-between digger-monitor-banner">
<div class="left-panel">
<template v-if="currFileList?.length">
<div
class="flex items-center justify-center h-full monitor-left-top"
>
<!-- // TODO -->
<img
class="w-full h-full"
:src="currFileList?.[0]?.image_url"
v-if="currFileList?.[0]?.image_url"
/>
<div v-else class="w-full h-full fg-empty-image"></div>
</div>
</template>
<div class="empty-bg" v-else></div>
</div>
<!-- 表格区域 -->
<!-- 搜索区域 -->
<!-- 右侧表格区域 -->
<div class="right-panel">
<div class="bg-transparent baseTable_wrap">
<template v-if="pagination.total > 0">
<BaseTable
class="bg-transparent baseTable_box"
:total="pagination.total"
:pageSize="pagination.pageSize"
:dataSource="listData"
:isFixedPagination="true"
:columns="columns"
:page="pagination.currentPage"
@change="handleTableChange"
:row-class-name="handleRowClassName"
@row-click="handleRowClick"
>
<template v-slot:actionBar="{ row }">
<ul class="flex table_action_box">
<li
class="flex items-center mr-[8px]"
@click="openCurrent(row)"
>
<div class="fg-button-primary">详情</div>
</li>
<li
class="flex items-center mr-[8px]"
@click="deleteCurrent(row)"
>
<div class="fg-button-primary-danger">删除</div>
</li>
</ul>
</template>
</BaseTable>
</template>
</div>
</div>
</div>
</div>
</div>
<ul class="fg-footer-charts">
<li>
<div class="fg-footer-charts-title">分时距离报警</div>
<div class="fg-footer-charts-content">
<DiggerHourlyDistanceChart :datas="recordDistanceHourData" />
</div>
</li>
<li>
<div class="fg-footer-charts-title">日距离告警</div>
<div class="fg-footer-charts-content">
<DiggerDailyDistanceChart :datas="recordDistanceDayData" />
</div>
</li>
</ul>
<DiggerAlarmModal
v-model:value="isAlarmOpen"
:info="currentRow"
:fileList="currFileList"
@close="isAlarmOpen = false"
/>
<BaseDelete
v-model:value="isDeleteOpen"
:deleteContent="`
<p>
确定删除
<span>${currentRow?.alarm_type}</span>
相关告警记录吗删除后将找不到此记录请谨慎操作.
</p>
`"
@delete-confirm="handleDeleteConfirm"
:info="currentRow"
@close="isDeleteOpen = false"
/>
</div>
</template>
<style lang="scss">
@import url("./DiggerMonitor.scss");
</style>

@ -1,14 +1,13 @@
.pole-monitor-wrap {
background-image: url("@/assets/common/bg_banner_1.png");
background-size: cover;
.pole-monitor-main-content {
background-image: url("@/assets/poleMonitor/poleMonitor_main_bg.png");
background-size: 100% 100%;
background-position: bottom;
background-repeat: no-repeat;
height: 823px;
.search-section {
padding: 16px 0;
}
.pole-main-content {
width: 100%;
height: 586px;
margin-top: 20px;
.pole-monitor-body {
padding: 0 24px;
}
.pole-monitor-search-box {
@ -17,101 +16,26 @@
gap: 12px;
margin: 16px 0;
}
.right-panel {
width: calc(
100% - var(--fg-swiper-monitor-slide-iamge-width) -
var(--fg-swiper-monitor-main-iamge-width) - var(--fg-box-margin) * 2
);
.el-scrollbar__view {
background: transparent !important;
height: 600px;
height: 360px;
}
}
.pole-monitor-main {
.pole-monitor-banner {
.left-panel {
width: 870px;
width: calc(
var(--fg-swiper-monitor-slide-iamge-width) +
var(--fg-swiper-monitor-main-iamge-width) + var(--fg-box-margin)
);
max-height: 480px;
margin-right: 16px;
&.empty-bg {
height: 680px;
background-image: url("@/assets/common/emptyBg.png");
background-size: 312px 204px;
background-position: center;
background-repeat: no-repeat;
}
.main-image {
box-sizing: border-box;
height: 511px;
position: relative;
background-color: #090f48;
border-radius: 4px;
.file-preview-screen {
height: calc(100%);
display: flex;
justify-content: center;
align-items: center;
img {
max-width: 100%;
max-height: 460px;
object-fit: cover;
}
}
video {
width: 100%;
max-height: calc(100%);
}
.image-info {
position: absolute;
height: 52px;
line-height: 52px;
bottom: 0;
font-size: 14px;
padding: 0 16px;
& > span {
margin-right: 10px;
}
}
}
.thumbnail-container {
width: 100%;
overflow: visible;
.swiper {
width: 100%;
height: 100%;
.swiper-slide {
width: 20%;
border-radius: 4px;
height: 144px;
img {
width: 100%;
height: 144px;
border-radius: 4px;
object-fit: cover;
}
}
.active-slide img,
.active-slide video {
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);
}
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save