go-viee-fetch-Demo/src/packages/components/Charts/ConfinedSpace/AlarmNowList/index.vue
Free-sss 4635366147 refactor(Charts): 重构有限空间组件并删除旧折线图
- 重构了 AlarmNowList 和 FiniteSpatialDistribution 组件的结构
- 更新了组件样式和布局
- 删除了未使用的 LineDropdownConfined 组件相关文件
2025-09-08 17:24:40 +08:00

536 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<PublicSmallBorder :title-text="chartConfig.option.titleText">
<div class="content_box">
<vue3-seamless-scroll v-if="option.dataset && option.dataset.length > 0" class="seamless" :list="option.dataset"
:limitScrollNum="rowNum" :hover="true" :step="waitTime" :wheel="true" :isWatch="true">
<div v-for="(item, index) in option.dataset" class="detail flex_column" :key="index">
<div class="flex_v cursor" :style="!showRankNum ? { paddingLeft: 0 } : { paddingLeft: '12px' }"
@click="handleOpenDialog(item)">
<div v-if="index === 0 && showRankNum" class="levelOneIcon flex_c">{{ index + 1 }}</div>
<div v-else-if="index === 1 && showRankNum" class="levelTwoIcon flex_c">{{ index + 1 }}</div>
<div v-else-if="showRankNum" class="levelOtherIcon flex_c">{{ index + 1 }}</div>
<div class="item_content">
<div class="item_header flex">
<div class="item_level_text">{{ item.alarmLevel }}</div>
<div class="item_title_text">{{ item.alarmDescname }}</div>
</div>
<div class="item_footer">
<div class="item_dept">{{ item.compName }}</div>
<div class="item_time">{{ item.alarmTime }}</div>
</div>
</div>
</div>
</div>
</vue3-seamless-scroll>
<div v-else style="
color: #fff;
font-size: 14px;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
">
暂无数据
</div>
<n-modal v-model:show="status.showDialog" :class="['custom-tab-modal']" title="报警详情" preset="card"
@on-after-leave="handleCloseDialog" :draggable="{ bounds: 'none' }"
:style="{ width: '800px', height: '516px' }">
<div v-if="isLoading" style="display: flex; width: 100%; justify-content: center; margin-top: 16px">
<n-spin size="large" />
</div>
<n-grid v-else cols="24" x-gap="12" style="overflow-y: auto; padding-bottom: 12px">
<!-- <div class="enterBtn" @click="handleMeetingBtnClick">会商</div> -->
<!-- 左侧详情 -->
<n-gi :span="14">
<!-- 基础信息 -->
<div class="detail-item">
<label>报警时间</label>
<span>{{ convertTimestampToDateTime((status.selectedRow as any)?.alarmRecord.alarmTime) }}</span>
</div>
<div class="detail-item">
<label>处置状态</label>
<span style="color: #ff9100">{{ (status.selectedRow as any)?.handleStatus }}</span>
</div>
<div class="detail-item">
<label>风险等级</label>
<span :style="{ color: '' }">{{ (status.selectedRow as any)?.alarmRecord.riskIdentName }}</span>
</div>
<div class="detail-item">
<label>报警等级</label>
<span class="highlight" style="color: #ff5454">{{
(status.selectedRow as any)?.alarmRecord.alarmLevel
}}</span>
</div>
<div class="detail-item">
<label>风险描述</label>
<span class="highlight">{{ (status.selectedRow as any)?.alarmRecord.alarmDesc }}</span>
</div>
<div class="detail-item">
<label>报警区域</label>
<span>{{
(status.selectedRow as any)?.pathName
.substring(1, (status.selectedRow as any)?.pathName.length)
.replaceAll('/', '-') + (((status.selectedRow as any)?.subareaName && (status.selectedRow as
any)?.subareaName !== '') ? (status.selectedRow as any)?.subareaName : '')
}}</span>
</div>
<div class="detail-item" style="display: flex; flex-direction: column"
v-if="(status.selectedRow as any)?.camerainfos.length > 0">
<label>警情关联设备:</label>
<div class="screenshot-placeholder">
<n-tabs type="line" animated>
<n-tab-pane v-for="(item, index) in (status.selectedRow as any)?.camerainfos" :name="item.deviceCode"
:tab="item.deviceName">
<div class="detail-item" style="display: flex; flex-direction: column">
<label>报警截图:</label>
<div style="display: flex">
<img :src="'/awimg/' + removeProtocolIpPort((status.selectedRow as any)?.alarmRecord.originImg)"
style="width: 180px; margin-right: 16px" />
<img :src="'/awimg/' + removeProtocolIpPort((status.selectedRow as any)?.alarmRecord.alarmImg)"
style="width: 180px" />
</div>
</div>
<div class="detail-item" style="display: flex; flex-direction: column">
<label>警情录像回放:</label>
<div class="screenshot-placeholder playback-placeholder" style="width: 350px; height: 200px">
<play-back :device-id="item.deviceCode"
:start="new Date((status.selectedRow as any)?.startTime)"
:end="new Date((status.selectedRow as any)?.endTime)"></play-back>
</div>
</div>
<div class="detail-item" style="display: flex; flex-direction: column">
<label>视频实时画面:</label>
<div class="screenshot-placeholder playback-placeholder" style="width: 350px; height: 200px">
<play-live :device-id="item.deviceCode"></play-live>
</div>
</div>
</n-tab-pane>
</n-tabs>
<br />
</div>
</div>
</n-gi>
<n-gi :span="10" style="height: 100%; display: flex; align-items: center">
<n-timeline size="medium">
<n-timeline-item class="timeLineClass" v-for="(item, index) in (status.selectedRow as any)?.taskLogs"
:key="index" type="info" :title="item.task_name" :content="item.task_detail" :time="item.task_begin" />
</n-timeline>
</n-gi>
</n-grid>
</n-modal>
</div>
</PublicSmallBorder>
</template>
<script setup lang="ts">
// @ts-nocheck
import { PropType, toRefs, shallowReactive, watch, computed, ref, reactive, onMounted, onUnmounted } from 'vue'
import { CreateComponentType } from '@/packages/index.d'
import { useChartDataFetch } from '@/hooks'
import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
import { option as configOption, styleConfig } from './config'
import { Vue3SeamlessScroll } from 'vue3-seamless-scroll'
import PlayBack from '@/components/Pages/yushiVideo/playback.vue'
import PlayLive from '@/components/Pages/yushiVideo/playLive.vue'
import axiosInstance from '@/api/axios'
import VChart from 'vue-echarts'
// import SmallBorder from '../SmallBorder01Co/index.vue'
import { height } from 'dom-helpers'
import axios from 'axios'
import PublicSmallBorder from '../../PublicSmallBorder/index.vue'
const props = defineProps({
chartConfig: {
type: Object as PropType<CreateComponentType & typeof option>,
required: true
}
})
const isLoading = ref(false)
const pointChartOption = ref(null)
const status = reactive({
showDialog: false,
selectedRow: null
})
const { rowNum, waitTime, showRankNum } = toRefs(props.chartConfig.option)
const { w, h } = toRefs(props.chartConfig.attr)
const option = shallowReactive({
dataset: configOption.dataset
})
const handleChangeDevice = (value: string | number) => {
status.selectedRow.cameraId = value
// if (status.selectedRow.alarmGroup.find(item => item.cameraId === value)) {
// status.selectedRow.startTime = status.alarmGroup.find(item => item.cameraId === value).startTime
// status.selectedRow.endTime = status.alarmGroup.find(item => item.cameraId === value).endTime
// }
}
const handleOpenDialog = async (row: any) => {
const alarmUUID = row.alarmUUID
status.showDialog = true
isLoading.value = true
//@ts-ignore
axiosInstance({
method: 'GET',
url: `${window.htconfig.API_IP}aw/bigScreen/qisuo/alarmRecordDetail/${alarmUUID}`,
responseType: 'json'
}).then((res: any) => {
//@ts-ignore
isLoading.value = false
const result = res.value
if (result) {
//@ts-ignore
status.selectedRow = {
...result,
cameraId: result.camerainfos[0]?.deviceCode,
startTime: (result.alarmRecord.alarmTime - 15 * 1000),
endTime: (result.alarmRecord.alarmTime + 15 * 1000)
}
}
})
//@ts-ignore
}
// 手动更新
watch(
() => props.chartConfig.option.dataset,
(newData: any) => {
option.dataset = newData
},
{
immediate: true,
deep: false
}
)
// 预览更新
useChartDataFetch(props.chartConfig, useChartEditStore, (newData: any) => {
option.dataset = newData
})
const removeProtocolIpPort = (url) => {
// 使用正则表达式匹配并移除协议、IP地址和端口
return url.replace(/^(?:https?:\/\/)?(?:[\d.]+|\[[\da-fA-F:.]+\])(?::\d+)?\/?/, '');
}
const convertTimestampToTime = timestamp => {
const date = new Date(timestamp)
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
const seconds = date.getSeconds().toString().padStart(2, '0')
return `${hours}:${minutes}:${seconds}`
}
const convertTimestampToDateTime = timestamp => {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
const seconds = date.getSeconds().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
const handleCloseDialog = () => {
status.showDialog = false
status.selectedRow = null
}
/**
* 格式化单个报警项的函数
* @param {Object} item 原始报警数据项
* @returns {Object} 格式化后的报警数据项
*/
const formatAlarmItem = (item) => {
let newAlarmDescname = item.alarmDescname;
const dateTimeRegexInDesc = /于\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
if (newAlarmDescname) {
newAlarmDescname = newAlarmDescname.replace(dateTimeRegexInDesc, '');
}
let newAlarmLevel = item.alarmLevel;
if (typeof newAlarmLevel === 'string') {
newAlarmLevel = newAlarmLevel.replace('风险', '');
}
return {
...item,
alarmTime: convertTimestampToDateTime(item.alarmTime), // 格式化 alarmTime
alarmDescname: newAlarmDescname,
alarmLevel: newAlarmLevel
};
};
const fetchRecentAlarms = async () => {
try {
isLoading.value = true
const res = await axiosInstance.get(`/awjt/space/getRecentAlarms/${props.chartConfig.option.sceneCode}`, { responseType: 'json', baseURL: '' });
if (res.state === true) {
let rawData = [];
if (Array.isArray(res.value.items)) {
rawData = res.value.items;
} else if (Array.isArray(res.value)) {
rawData = res.value;
}
option.dataset = rawData.map(formatAlarmItem);
} else {
console.warn('API返回数据格式不符合预期将保留原有数据并尝试重新格式化:', res);
if (Array.isArray(option.dataset)) {
option.dataset = option.dataset.map(formatAlarmItem);
}
}
} catch (error) {
console.error('获取最近报警数据失败,将保留原有数据并尝试重新格式化:', error);
if (Array.isArray(option.dataset)) {
option.dataset = option.dataset.map(formatAlarmItem);
}
} finally {
isLoading.value = false;
}
};
let alarmTimer = null;
onMounted(() => {
fetchRecentAlarms();
// 每5分钟执行一次
const interval = 5 * 60 * 1000;
alarmTimer = setInterval(fetchRecentAlarms, interval);
});
onUnmounted(() => {
clearInterval(alarmTimer);
});
</script>
<style lang="scss" scoped>
.content_box {
z-index: 20;
width: 100%;
height: 100%;
}
.seamless {
z-index: 1;
height: 240px;
margin: 0px 10px 30px 10px;
overflow: hidden;
}
.detail {
color: #fff;
position: relative;
font-size: 12px;
width: 100%;
}
.flex_column {
display: flex;
flex-direction: column;
}
.flex {
display: flex;
}
.flex_wrap {
display: flex;
flex-wrap: wrap;
}
.flex_v {
display: flex;
align-items: center;
// background-color: #0a0c1260;
border-bottom: 1.5px solid #123E54;
margin: 1px;
}
.item_level_text {
width: fit-content;
min-width: 2em;
height: 18px;
color: #ff9191;
border-radius: 12px;
background: #B10000;
font-size: 14px;
text-align: center;
line-height: 18px;
padding: 2px 12px;
margin-right: 20px;
}
// .item_title_text {
// color: #b10000;
// font-size: 18px;
// }
.item_title_text {
color: #b10000;
font-size: 18px;
white-space: nowrap;
/* 禁止换行 */
overflow: hidden;
/* 隐藏超出部分 */
text-overflow: ellipsis;
/* 显示省略号 */
flex-grow: 1; // 允许标题占据剩余空间
}
.flex_c {
display: flex;
justify-content: center;
align-items: center;
}
.levelOneIcon {
width: 18px;
height: 18px;
background: url(/src/assets/images/chart/tables/list_scroll_1_icon.png) 50% 50% / contain no-repeat;
}
.levelTwoIcon {
width: 18px;
height: 18px;
background: url(/src/assets/images/chart/tables/list_scroll_2_icon.png) 50% 50% / contain no-repeat;
}
.levelOtherIcon {
width: 18px;
height: 18px;
background: url(/src/assets/images/chart/tables/list_scroll_3_icon.png) 50% 50% / contain no-repeat;
}
.item_content {
width: 100%;
}
.item_header {
margin-left: 10px;
flex: 1;
width: fit-content;
max-width: 450px;
// height: 30px;
align-items: end;
white-space: nowrap;
/* 禁止换行 */
overflow: hidden;
/* 隐藏超出部分 */
text-overflow: ellipsis;
/* 显示省略号 */
margin-top: 2px;
// background-color: #123E54;
}
.item_footer {
margin-left: 20px;
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
}
.item_time {
// margin-right: 70px;
padding-right: 30px;
font-size: 18px;
color: #7E8990;
}
.item_dept {
color: #7E8990;
font-size: 12px;
}
.detail-item {
padding: 8px 0;
display: flex;
}
.cursor {
cursor: pointer;
}
.detail-item label {
min-width: 90px;
color: rgb(145 187 242);
}
.detail-item span {
font-weight: bold;
}
.info {
font-size: 1.25rem;
font-weight: 700;
width: fit-content;
margin: 0 16px 0 0;
display: flex;
align-items: center;
}
.info .sub-title {
padding: 0 0 0 24px;
background: url(./second_level.png) 0 50%/425px 31px no-repeat;
margin-right: 8px;
}
.info .info-num {
color: rgb(34 211 238);
font-size: 1.875rem;
}
.info .info-body {
padding-bottom: 6px;
}
.enterBtn {
color: rgb(62 200 244);
text-decoration: underline;
cursor: pointer;
font-size: 1.25rem;
position: absolute;
left: 87%;
top: -45px;
}
</style>
<style lang="scss">
.custom-tab-modal>.n-card-header {
background-color: rgba(26, 56, 113, 1) !important;
background-image: linear-gradient(to right, rgba(8, 100, 177, 0.7), transparent) !important;
padding: 16px !important;
border: 2px solid rgba(62, 200, 244, 1);
border-bottom-width: 0px !important;
}
.custom-tab-modal>.n-card__content {
background-color: rgba(26, 56, 113, 1) !important;
border: 2px solid rgba(62, 200, 244, 1);
padding-right: 0;
padding-bottom: 0;
}
::-webkit-scrollbar-thumb {
background-color: #2461db;
border-radius: 6px;
}
.custom-tab-modal .timeLineClass>div:last-child {
padding-left: 8px;
background: url(./text_bg.png) 0 50%/100% 100% no-repeat;
}
</style>