feat: 引入mock完成视频分析模块静态交互

dev
donghao 3 weeks ago
parent d8e35d30a0
commit d0a2edeb6e

@ -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": [
{
// 车号,生成随机的 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,
},
};

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: 6.9 KiB

@ -0,0 +1,169 @@
<!--
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-08-14 13:38:30
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-08-19 14:21: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">
<!-- //TODO -->
<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,62 @@
<!--
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-08-19 11:04:59
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-08-19 11:26:43
* @FilePath: \5G-Web\src\components\Swiper\SwiperPlayer.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<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="148"
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>

@ -94,7 +94,7 @@
color: white;
margin-left: 0;
height: 32px;
& .icon {
.icon {
width: 14px;
height: 14px;
background-image: url("@/assets/common/search_icon.png");
@ -148,6 +148,7 @@
width: 1202px;
height: 602px;
background-image: url("@/assets/common/dialog_bg2.png");
background-size: 100% 100%;
}
.fg-dialog-header-close {
width: 50px;
@ -167,9 +168,43 @@
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/alarm_title.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;
@ -205,6 +240,29 @@
}
}
}
//
.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%;

@ -290,7 +290,7 @@ onMounted(() => {
</el-button>
</div>
<div>
<!-- <VideoExport /> -->
<VideoExport />
</div>
</div>
<div class="flex justify-between appearance-monitor-banner">

@ -4,6 +4,8 @@ import { BaseDelete, BaseTable } from "@/components/CustomTable";
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,
@ -226,9 +228,10 @@ onMounted(() => {
</div>
</div>
<div class="digger-monitor-body">
<!-- 搜索区域 -->
<div class="digger-monitor-search-box">
<!-- <el-select
<div class="flex items-center justify-between">
<!-- 搜索区域 -->
<div class="digger-monitor-search-box">
<!-- <el-select
v-model="searchForm.station"
placeholder="钩机编号"
class="custom-select"
@ -237,19 +240,19 @@ onMounted(() => {
<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
<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"
@ -261,17 +264,22 @@ onMounted(() => {
<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>
<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">
@ -333,7 +341,7 @@ onMounted(() => {
</div>
</div>
</div>
<ul class="fg-footer-charts">
<ul class="fg-footer-charts">
<li>
<div class="fg-footer-charts-title">分时距离报警</div>
<div class="fg-footer-charts-content">

@ -7,6 +7,8 @@ import PoleDailyAlertChart from "@/components/Charts/poleDailyAlertChart.vue";
import PointModal from "./components/PointModal.vue";
import AlarmModal from "./components/AlarmModal.vue";
import DeleteModal from "./components/DeleteModal.vue";
import VideoExport from "./components/VideoExport.vue";
import {
getAppearanceMonitorApi,
getAppearanceMonitorDetailApi,
@ -224,54 +226,59 @@ onMounted(() => {
</div>
</div>
<div class="pole-monitor-body">
<!-- 搜索区域 -->
<div class="pole-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"
clearable
>
<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 class="flex items-center justify-between">
<!-- 搜索区域 -->
<div class="pole-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"
clearable
>
<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 pole-monitor-banner">
<!-- 左侧视频与缩略图区域 -->
<div class="left-panel">
<SwiperMonitor :fileList="currFileList" />
<SwiperMonitor :fileList="currFileList" />
</div>
<!-- 右侧表格区域 -->
<div class="right-panel">
@ -290,10 +297,7 @@ onMounted(() => {
>
<template v-slot:actionBar="{ row }">
<ul class="flex table_action_box">
<li
class="flex items-center mr-[8px]"
@click="openPoint()"
>
<li class="flex items-center mr-[8px]" @click="openPoint()">
<div class="fg-button-primary1">点云</div>
</li>

@ -0,0 +1,823 @@
<script lang="ts" setup>
import Player from "@/components/videoPlayer/Player.vue";
import SwiperPlayer from "@/components/Swiper/swiperPlayer.vue";
import SwiperFile from "@/components/Swiper/swiperFile.vue";
import { isSuccessApi } from "@/utils/forApi";
// TODO
//
const longText = `
const xData = ref(["1月", "2月", "3月", "4月", "5月"]);
const legendArr = ["车体检测", "撑杆检测"];
const datas = ref([
[1528, 1266.02, 2468.39, 2982.67, 3165.91],
[2844.44, 6505.07, 8016.12, 6350.87, 1474.61],
]);
const colorArr = [
["#3B9FFE", "#5070F2"],
["#FFDA8D", "#FFAC06"],
];
const deviceStatus = ref({
onlineCount: 50,
errorCount: 10,
outlineCount: 10,
});
const searchForm = reactive({
car: "1",
pole: "1",
});
const deviceTotal = ref(0);
const carFaultTotal = ref([]);
const poleFaultTotal = 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 = reactive({
list: [
{
name: "车体检测设备",
bindVal: {
total: 0,
},
icon: car_device_icon,
},
{
name: "撑杆检测设备",
bindVal: {
total: 0,
},
icon: pole_device_icon,
},
{
name: "钩机检测设备",
bindVal: {
total: 0,
},
icon: excavator_device_icon,
},
],
});
const websocketStore = useWebSocketStore();
// messages
watch(
() => websocketStore.messages,
(newMessages: string[], oldMessages: string[]) => {
if (newMessages?.length > 0 && !isAlarmOpen.value) {
currentRow.value = newMessages[newMessages?.length - 1];
currFileList.value = newMessages[newMessages?.length - 1]?.images;
isAlarmOpen.value = true;
}
},
{ deep: true, immediate: true }
);
const getList = async (dateType: string = "month") => {
activeBtn.value = dateType;
const res = await getDataOverviewApi({ dateType });
if (isSuccessApi(res)) {
const { data } = res;
datas.value[0] = data.appearance;
datas.value[1] = data.pole;
if (dateType === "month") {
xData.value = data.dateArr.map((item: any) => {
if (dateType === "month") {
return item + "月";
}
});
} else {
xData.value = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
}
// console.log(data, 'getList_data')
}
};
const getDeviceInfo = async () => {
try {
const res = await getDeviceInfowApi();
if (isSuccessApi(res)) {
const { data } = res;
deviceTotal.value = data.deviceTotal;
deviceInfo.list = [
{
name: "车体检测设备",
bindVal: data?.appearance,
icon: car_device_icon,
},
{
name: "撑杆检测设备",
bindVal: data?.pole,
icon: pole_device_icon,
},
{
name: "钩机检测设备",
bindVal: data?.excavator,
icon: excavator_device_icon,
},
];
}
} catch (error) {
console.error("获取设备信息出错:", error);
}
};
const getPoleFault = async () => {
try {
const res = await getRecordFaultApi({
dateType: "month",
value: searchForm.pole,
type: "pole",
});
if (isSuccessApi(res)) {
const { data } = res;
poleFaultTotal.value = data;
// deviceStatus.value = data
console.log(data);
}
} catch (error) {
console.error("获取设备信息出错:", error);
}
};
const getCarFault = async () => {
try {
const res = await getRecordFaultApi({
dateType: "month",
value: searchForm.car,
type: "appearance",
});
if (isSuccessApi(res)) {
const { data } = res;
carFaultTotal.value = data;
// deviceStatus.value = data
console.log(data);
}
} catch (error) {
console.error("获取设备信息出错:", error);
}
};
const getRealTime = async () => {
try {
const res = await getRealTimeApi({ deviceType: "" });
if (isSuccessApi(res)) {
const { data } = res;
imageFault.value = data;
// deviceStatus.value = data
console.log(data);
}
} catch (error) {
console.error("获取设备信息出错:", error);
}
};
onBeforeRouteLeave(() => {
isAlarmOpen.value = false;
currentRow.value = {};
currFileList.value = [];
});
onMounted(() => {
getList();
getDeviceInfo();
getCarFault();
getPoleFault();
getRealTime();
});
<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>
<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>
</HomeSubTitle>
</div>
<div class="chart-container chart-pie-bg">
<!-- <PieChart :data="carFaultTotal" :colors="[
'#FF7C09',
'#0032FF',
'#04FFF2',
'#D19EFF',
'#FF0103',
'#9EFFF3',
]" /> -->
<VehicleMonitorChart />
</div>
</div>
<div class="grid-item">
<div class="module-header">
<HomeSubTitle title="撑杆监测">
<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>
</HomeSubTitle>
</div>
<div class="chart-container chart-pie-bg">
<PoleMonitorChart />
<!-- <PieChartSmall :data="poleFaultTotal" :colors="['#9DFFF3', '#FFA179']" /> -->
</div>
</div>
</div>
<!-- 中部检测模块 -->
<div class="realTime-monitor-box">
<div class="monitor-images">
<div class="w-full h-[35px] fg-title ">实时监测画面</div>
<div class="monitor-images-left monitor-images-item">
<img :src="imageFault[0]?.url" />
<div class="fault-info">{{ imageFault[0]?.fault_type }}</div>
</div>
<div class="monitor-images-right monitor-images-item">
<img :src="imageFault[1]?.url" />
<div class="fault-info">{{ imageFault[1]?.fault_type }}</div>
</div>
<div class="monitor-images-left monitor-images-item">
<img :src="imageFault[0]?.url" />
<div class="fault-info">{{ imageFault[0]?.fault_type }}</div>
</div>
<div class="monitor-images-right monitor-images-item">
<img :src="imageFault[1]?.url" />
<div class="fault-info">{{ imageFault[1]?.fault_type }}</div>
</div>
</div>
</div>
<!-- 设备信息 -->
<div class="device-info-box">
<div class="module-header">
<HomeSubTitle title="设备信息"> </HomeSubTitle>
</div>
<div class="device-info">
<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">
{{ deviceTotal }}
</div>
<div class="text-[14px]">设备总数</div>
</div>
</div>
<ul class="device-list">
<li class="device-card" v-for="(v, k) in deviceInfo.list" :key="k">
<div
class="device-card-head text-[16px] flex items-center mb-[12px]"
>
<img :src="v.icon" alt="" class="w-[32px]" />
<span class="ml-[12px]">{{ v.name }}</span>
<span class="font-bold">{{ v.bindVal?.total || 0 }}</span>
</div>
<DeviceStatus :deviceStatus="v.bindVal" />
</li>
</ul>
</div>
</div>
<AlarmModal
v-model:value="isAlarmOpen"
:info="currentRow"
:image="currFileList"
@close="isAlarmOpen = false"
/>
</div>
分析完成`;
const show = ref<boolean>(false);
const currentVideo = ref<Record<string, any>>({
video_url:
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
});
const isPlaying = ref<boolean>(false);
const activeIndex = ref<number>(-1);
const fileList = ref<Record<string, any>[]>([]);
// const paused = ref(false);
const isGenerator = ref<boolean>(false);
const isGenerating = ref<boolean>(false);
const displayedText = ref("");
const displayedFiles = ref<Record<string, any>[]>([]);
const textContainer = ref(null);
const textInterval = ref(null);
const imageInterval = ref(null);
const currentIndex = ref(0);
const handleExport = () => {
show.value = true;
};
// ||
// const toggleGeneration = () => {
// console.log(
// "generateNext",
// longText.length,
// longText.length / (1000 / textGenerationSpeed.value)
// );
// if (isGenerating.value) {
// // paused.value = !paused.value;
// } else {
// isGenerating.value = true;
// // paused.value = false;
// startGeneration();
// }
// };
//
// const resetGeneration = () => {
// clearTimeout(textTimer.value);
// displayedText.value = "";
// isGenerating.value = false;
// // paused.value = false;
// };
//
const textGenerationSpeed = computed(() => {
//
return 10;
// return 100;
});
//
const scrollToBottom = () => {
if (textContainer.value) {
textContainer.value.scrollTop = textContainer.value.scrollHeight;
}
};
//
const generateText = () => {
if (currentIndex.value < longText.length) {
displayedText.value += longText.charAt(currentIndex.value);
currentIndex.value++;
scrollToBottom(); //
} else {
isGenerating.value = false;
clearInterval(textInterval.value);
}
};
//
const loadImages = () => {
if (displayedFiles.value.length >= fileList.value.length) {
clearInterval(imageInterval.value);
return
}
if (
(displayedText.value.length / longText.length) * fileList.value.length >
displayedFiles.value.length
) {
const nextImage = fileList.value[displayedFiles.value.length];
displayedFiles.value = [...displayedFiles.value, nextImage];
}
};
//
const startGeneration = () => {
isGenerator.value = true;
isGenerating.value = true;
//
textInterval.value = setInterval(generateText, textGenerationSpeed.value);
//
imageInterval.value = setInterval(loadImages, 300);
// getFileList(); //
// // if (paused.value) {
// // paused.value = false;
// // }
// const startTime = new Date().getTime();
// const generateNext = () => {
// // if (paused.value) return;
// if (displayedText.value.length < longText.length) {
// displayedText.value = longText.slice(0, displayedText.value.length + 1);
// scrollToBottom();
// textTimer.value = setTimeout(generateNext, textGenerationSpeed.value);
// } else {
// isGenerating.value = false;
// const endTime = new Date().getTime();
// console.log(
// longText.length / (1000 / textGenerationSpeed.value),
// "generateNext",
// Number((endTime - startTime) / 1000)
// );
// // paused.value = false;
// }
// };
// generateNext();
};
// TODO mock
const getFileList = async () => {
try {
const resAll = await fetch("/api/v1/record/generator_result/", {
method: "POST",
});
const res = await resAll.json();
if (isSuccessApi(res)) {
console.log(res, "getFileList");
fileList.value = res.data.data;
// TODO
}
} catch (error) {
console.error("获取数据失败:", error);
}
};
//
const handelVideoTab = () => {
activeIndex.value = -1;
isPlaying.value = !isPlaying.value;
};
//
// watch(textGenerationSpeed, (newVal) => {
// if (isGenerating.value && !paused.value) {
// clearTimeout(textTimer.value);
// startGeneration();
// }
// });
watch(activeIndex, (newVal) => {
if (activeIndex.value !== -1) {
isPlaying.value = false;
}
});
onMounted(() => {
getFileList();
});
//
onUnmounted(() => {
clearInterval(textInterval.value);
clearInterval(imageInterval.value);
});
</script>
<template>
<el-button
type="primary"
@click="handleExport"
class="fg-info-button video_export_btn"
>
<span class="icon"></span> 视频分析
</el-button>
<el-dialog
class="fg-dialog fg-dialog2 videoExportModal-wrap"
v-model="show"
align-center
:show-close="false"
>
<!-- 自定义标题栏 -->
<template #header="{ close, titleId, titleClass }">
<div class="flex items-center justify-between fg-dialog-header">
<div
class="flex items-center justify-center fg-dialog-header-icon-title"
>
<div class="header-icon"></div>
<p class="header-text">视频分析</p>
</div>
<div class="fg-dialog-header-close" @click="close"></div>
</div>
</template>
<div class="video-export-body py-[16px] px-[24px]">
<!-- 播放器 -->
<div class="flex generator-container">
<div class="generator-start-video">
<Player
:src="currentVideo?.video_url"
:is-playing="isPlaying"
@play="isPlaying = true"
@pause="isPlaying = false"
v-show="activeIndex === -1"
/>
<div class="big-image-container" v-show="activeIndex !== -1">
<img :src="fileList?.[activeIndex]?.image_url" alt="" />
</div>
</div>
<div class="generator-pannel">
<div class="generator-text" ref="textContainer" v-show="isGenerator">
<div class="text-content">
<span
v-for="(char, index) in displayedText"
:key="index"
:class="{
'last-char':
index === displayedText.length - 1 && isGenerating,
}"
>
{{ char }}
</span>
<span class="cursor" :class="{ blink: !isGenerating }">|</span>
</div>
</div>
<div
class="flex flex-col items-center justify-center w-full h-[100%] generator-controls"
v-show="!isGenerator"
>
<!-- <button
@click="toggleGeneration"
:disabled="isGenerating && paused"
>
{{ generateButtonText }}
</button> -->
<div class="bg-no-generator"></div>
<p class="no-data-text">暂未开始分析~</p>
<el-button
type="primary"
class="fg-info-button mt-[16px]"
@click="startGeneration"
>开始分析</el-button
>
<!-- <button
@click="resetGeneration"
class="reset-btn"
:disabled="!displayedText.length"
>
重置
</button>
<div class="slider-container">
<label>生成速度</label>
<input
type="range"
min="10"
max="150"
v-model="textGenerationSpeed"
/>
<span>{{ 150 - textGenerationSpeed }}ms/字符</span>
</div> -->
</div>
</div>
</div>
<div class="flex video-export-footer">
<div class="video-pannel">
<div class="fg-border-left-title">原始视频</div>
<div
class="video-pannel-item"
:class="activeIndex === -1 ? 'active-slide' : ''"
>
<SwiperPlayer
@click="handelVideoTab()"
class="cursor-pointer"
:videoUrl="currentVideo?.video_url"
:isPlaying="isPlaying && activeIndex === -1"
/>
</div>
</div>
<div class="results-pannel">
<div class="fg-border-left-title">分析结果</div>
<div class="results-content">
<SwiperFile
:fileList="displayedFiles"
v-model:value="activeIndex"
v-if="displayedFiles?.length"
/>
<div class="h-full fg-no-data" v-else>
<div class="bg-no-data"></div>
<p class="no-data-text">暂无数据~</p>
</div>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<style lang="scss">
.video_export_btn {
.icon {
background-image: url("@/assets/common/generator_video_btn_icon.png");
}
}
.videoExportModal-wrap {
&.fg-dialog.fg-dialog2 {
background-image: url("@/assets/common/dialog_video_export_bg.png");
height: 795px;
}
.generator-container {
height: 476px;
padding: 16px;
border-radius: 4px;
overflow: hidden;
border: 1px solid #2982e2;
display: flex;
.generator-start-video {
width: 720px;
height: 100%;
margin-right: 16px;
overflow: hidden;
border-radius: 4px;
overflow: hidden;
video {
width: 100%;
height: 100%;
}
.big-image-container {
width: 100%;
height: 100%;
}
}
.generator-pannel {
width: calc(100% - 720px - 16px);
height: 100%;
}
.generator-text {
flex: 1;
border-radius: 12px;
overflow-y: auto;
margin-bottom: 20px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
font-size: 14px;
color: white;
line-height: 1.7;
height: 100%;
.text-content {
min-height: 100%;
position: relative;
white-space: pre-wrap;
}
.cursor {
display: inline-block;
font-weight: bold;
color: #2ecce0;
animation: blink 1s infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.blink {
animation: blink 1s infinite;
}
.last-char {
color: #2ecce0;
}
}
.generator-controls {
.bg-no-generator {
width: 63px;
background-image: url("@/assets/common/generator_video_icon.png");
background-size: contain;
height: 58px;
background-position: center;
background-repeat: no-repeat;
}
.no-data-text {
padding-top: 16px;
font-size: 14px;
color: #999999;
}
}
/* 滚动条样式 */
.generator-text::-webkit-scrollbar {
width: 10px;
}
.generator-text::-webkit-scrollbar-track {
background: rgba(26, 32, 44, 0.4);
border-radius: 5px;
}
.generator-text::-webkit-scrollbar-thumb {
background: linear-gradient(to bottom, #4299e1, #3182ce);
border-radius: 5px;
}
.generator-text::-webkit-scrollbar-thumb:hover {
background: linear-gradient(to bottom, #3182ce, #2b6cb0);
}
}
.video-export-footer {
.fg-border-left-title {
margin: 16px 0;
}
.video-pannel-item {
box-sizing: border-box;
width: 216px;
margin-right: 16px;
height: 148px;
position: relative;
border-radius: 4px;
overflow: hidden;
&.active-slide {
border: 2px solid #2ecce0;
}
video {
width: 216px;
height: 148px;
object-fit: cover;
overflow: hidden;
}
}
.results-pannel {
display: flex;
flex-direction: column;
flex: 1;
.results-content {
height: 100%;
padding-left: 16px;
border-left: 1px dashed rgba(255, 255, 255, 0.6);
}
img {
width: 216px;
height: 148px;
}
.swiper {
width: calc(216px * 4 + 12px * 3);
height: 100%;
.swiper-slide {
width: 216px;
height: 148px;
}
}
}
}
}
</style>
Loading…
Cancel
Save