Commit 9adbca23 authored by wangdonghao's avatar wangdonghao

新增功能

parent fb2fac6c
......@@ -41,3 +41,18 @@ export const getVideoUrl = (params) => {
export const getFlowSections = (data) => {
return request.post({ url: '/qxsd/tunnel/sectionCongestion', data })
}
// 获取事件告警详情
export const getEventDetail = (params) => {
return request.get({ url: '/qxsd/event-detail/alarm', params })
}
// 获取隧道统计
export const getTunnelReport = (data) => {
return request.post({ url: '/qxsd/report/tunnelStatisticalTodayByHole', data })
}
// 获取事件统计
export const getEventReport = (data) => {
return request.post({ url: '/qxsd/report/tunnelStatisticalByEventType', data })
}
\ No newline at end of file
......@@ -276,6 +276,7 @@ export const IconJson = {
'wallet-filled',
'warning',
'warning-filled',
'warn-triangle-filled',
'watch',
'watermelon',
'wind-power',
......
......@@ -13,13 +13,24 @@ export const EVENT_TYPES = [
{ name: '逆行', key: '4' },
{ name: '拥堵', key: '5' },
{ name: '可疑事件', key: '6' },
{ name: '追尾', key: '7' },
{ name: '停车', key: '8' },
{ name: '事故', key: '7' },
{ name: '违章停车', key: '8' },
{ name: '超速', key: '16' },
{ name: '其他', key: '100' },
{ name: '', key: '99' },
{ name: '机动车闯禁行', key: '22' },
{ name: '大型客车', key: '21' },
{ name: '缓慢行驶', key: '9' },
{ name: '超慢行驶', key: '10' },
{ name: '停驶', key: '11' },
{ name: '行人', key: '12' },
{ name: '抛洒物', key: '13' },
{ name: '火灾', key: '14' },
{ name: '异常天气', key: '15' },
{ name: '闯入', key: '17' },
{ name: '不按车道行驶', key: '18' },
{ name: '非机动车', key: '19' },
{ name: '危化品车辆', key: '20' },
]
export const MODEL_CLASS = [
......
import { store } from '@/store'
import { defineStore } from 'pinia'
interface EventNotify {
id?: string // 事件ID
eventType?: string // 事件类型(对应EVENT_TYPES)
eventStartTime?: string // 事件开始时间
eventEndTime?: string // 事件结束时间
eventCarPlate?: string // 车牌号
eventSituation?: string // 事件描述
eventStatus?: number // 事件状态(对应EVENT_STATUS)
}
interface TrafficType {
waves: string[], // 标注车辆,事件相关等
nowEvents: any[], // 当前事件标注
waves: string[] // 标注车辆,事件相关等
nowEvents: any[] // 当前事件标注
oldEvents: any[] // 回溯事件标注
eventCount: number // 事件数量
eventNotify: EventNotify | null // 事件通知
}
export const useTrafficStore = defineStore('traffic', {
state: (): TrafficType => ({
waves: [],
nowEvents: [],
oldEvents: []
oldEvents: [],
eventCount: 0,
eventNotify: null
}),
getters: {
getWaves () {
getWaves() {
return this.waves
},
getNowEvents () {
getNowEvents() {
return this.nowEvents
},
getOldEvents () {
getOldEvents() {
return this.oldEvents
}
},
getEventCount() {
return this.eventCount
},
actions: {
getEventNotify() {
return this.eventNotify
}
},
setWaves (e) {
actions: {
setWaves(e) {
this.waves = e || []
},
setNowEvents (e) {
setNowEvents(e) {
this.nowEvents = e || []
},
setOldEvents (e) {
setOldEvents(e) {
this.oldEvents = e || []
}
},
}
setEventCount(e) {
this.eventCount = e || 0
},
setEventNotify(e) {
this.eventNotify = e || null
}
}
})
export const useTrafficStoreOut = () => {
......
......@@ -251,14 +251,16 @@ export const useTunnelStore = defineStore('tunnel', {
this.faclities = tunnelPassages.map(item => {
const { leftHole, type } = item
const { leftHole, rightHole, type, width } = item
const classMap = {
x0: 'man-cross',
x1: 'car-cross'
}
return {
start: +leftHole,
leftStart: +leftHole,
rightStart: +rightHole,
width: +width,
type: classMap[type]
}
......
......@@ -160,7 +160,7 @@ onMounted(async () => {
params.carImages = (res2.bayonetImage || []).map(item => {
return {
name: item.carPlate,
imgs: item.imagePath
images: item.imagePath
}
})
params.evtImages = res2.eventImagesPath || []
......
......@@ -272,7 +272,8 @@
retainedTime: null, // 留存时间
eventStartTimeSearchStart: '', // 开始时间
eventStartTimeSearchEnd: '', // 结束时间
eventType: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '100'], // 事件类型
// eventType: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '100'], // 事件类型
eventType: [], // 事件类型
pageNo: 1,
pageSize: 10,
total: 0
......@@ -334,7 +335,8 @@
params.eventStartTimeSearchStart = ''
params.eventStartTimeSearchEnd = ''
// params.eventType = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '100']
params.eventType = EVENT_TYPES.map(item=> {return item.key})
// params.eventType = EVENT_TYPES.map(item=> {return item.key})
params.eventType = []
}
goGetList()
......@@ -400,7 +402,8 @@
}
onBeforeMount(() => {
params.eventType = EVENT_TYPES.map(item=> {return item.key})
// params.eventType = EVENT_TYPES.map(item=> {return item.key})
params.eventType = []
goGetList()
})
......
......@@ -36,7 +36,8 @@ import { useTunnelStoreWithOut } from '@/store/modules/tunnel'
import { getFocus, getRealCameras, setFocus } from '@/flash/Home'
const props = defineProps<{
direction
direction: number
holeType: string
}>()
const tunnelStore = useTunnelStoreWithOut()
......@@ -48,6 +49,10 @@ const focus = computed(() => {
})
const cameras = computed(() => {
if( props.holeType === 'new' ) {
// (新洞/老洞)目前还没有实现,需后续补功能
return getRealCameras(props.direction)
}
return getRealCameras(props.direction)
})
......
<template>
<div class="CarCard">
<div class="content">
<div class="images">
<el-image
:style="{ height: '100%', width: '100%', transform: 'rotateY(180deg)' }"
:src="IMAGE_BASE + carInfo.carImageUrl"
fit="contain"
>
<template #error>
<div class="image-error">
<Icon icon="ep:picture" />
</div>
</template>
</el-image>
</div>
<div class="lines">
<div class="line-ttl">
<div class="ttl">{{ carName }}</div>
<div
@click="collectChange"
:class="['icon', { collected: carInfo.isFocusMark || carInfo.isDangerous }]"
></div>
</div>
<div class="line-text">
<div class="name">隧道方向:</div>
<div class="text">{{ directionMap[carInfo.direction] }}</div>
</div>
<div class="line-text">
<div class="name">进入时间:</div>
<div class="text">{{ carInfo.inTunnelTime }}</div>
</div>
</div>
</div>
<div class="footer" v-if="carInfo.retainedMinutesTime">
<div class="cols">
<div class="col-x">
<div class="icon"></div>
<div class="text"> 留存 {{ carInfo.retainedMinutesTime }}分钟 </div>
</div>
<div class="col-x">
<div class="icon"></div>
<div class="text"> {{ carInfo.speed }}km/h </div>
</div>
<div class="col-x">
<div class="icon"></div>
<div class="text"> {{ numToStone(carInfo.carMilestone) }} </div>
</div>
</div>
<div v-if="!carInfo.mook" @click="goTracker" class="def-btn">轨迹追踪</div>
</div>
<div class="footer" v-else>车辆已驶出隧道</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { numToStone } from '@/utils/tunnel'
import { carMark, cancelMark } from '@/api/home'
import { ROAD_DIRECTION } from '@/config/enum'
import { ASYNC_DIRECTIONS } from '@/config/dict'
import { CAR_TYPES } from '@/config/dict'
const IMAGE_BASE = import.meta.env.VITE_IMAGE_BASE
interface CarInfo {
carId: string
type: number
carImageUrl?: string
carPlate?: string
isFocusMark?: boolean
isDangerous?: boolean
direction: string
inTunnelTime?: string
retainedMinutesTime?: number
speed?: number
carMilestone: string
mook?: boolean
}
const props = defineProps<{
carInfo: CarInfo
}>()
const carName = computed(() => {
return `${CAR_TYPES[props.carInfo.type].name}(${props.carInfo.carPlate})`
})
const router = useRouter()
// 收藏与取消
const collectChange = async () => {
const { carId, isFocusMark, isDangerous } = props.carInfo
if (isDangerous) {
return ElMessage.warning('危化品车不可取消关注!')
}
if (isFocusMark) {
await cancelMark({ carId })
} else {
await carMark({ carId })
}
}
// 方向
const directionMap = computed(() => {
return Object.fromEntries(
ASYNC_DIRECTIONS().map((item) => {
return [item.key, item.name]
})
)
})
// 轨迹追踪
const goTracker = () => {
const { carId, direction } = props.carInfo
router.push({
path: 'track',
query: {
id: carId,
direction: ROAD_DIRECTION[direction],
from: 'home'
}
})
}
</script>
<style lang="scss" scoped>
$base_url: '@/assets/images/home/';
.CarCard {
width: 445px;
height: 158px;
border-radius: 4px;
background: #0d3561;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
margin-right: 8px;
font-size: 14px;
font-weight: 600;
line-height: 22px;
color: #fff;
flex-shrink: 0;
&:last-child {
margin-right: 0;
}
.content {
display: flex;
align-items: center;
padding: 16px;
.images {
width: 74px;
height: 74px;
background: #eaeaea;
position: relative;
:deep(.image-error) {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #666666;
}
}
.lines {
flex-grow: 1;
margin-left: 12px;
.line-ttl {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
.ttl {
font-size: 18px;
line-height: 28px;
}
.icon {
width: 21px;
height: 20px;
background: url($base_url + 'collect_1.png') no-repeat;
background-size: cover;
cursor: pointer;
&.collected {
background: url($base_url + 'collect_2.png') no-repeat;
background-size: cover;
}
}
}
.line-text {
display: flex;
margin-bottom: 2px;
.name {
font-weight: normal;
color: rgba(240, 245, 255, 0.95);
}
}
}
}
.footer {
height: 47px;
background: rgba(69, 116, 182, 0.36);
display: flex;
align-items: center;
box-sizing: border-box;
padding: 10px 16px;
font-size: 14px;
font-weight: 600;
line-height: 22px;
color: rgba(240, 245, 255, 0.95);
.cols {
display: flex;
flex-grow: 1;
justify-content: space-between;
margin-right: 57px;
.col-x {
display: flex;
align-items: center;
// font-size: 12px;
justify-content: space-between;
.icon {
width: 20px;
height: 20px;
margin-right: 4px;
// background: salmon;
}
}
.col-x:nth-child(1) {
.icon {
background: url($base_url + 'remain.png') no-repeat;
background-size: cover;
}
}
.col-x:nth-child(2) {
.icon {
background: url($base_url + 'speed.png') no-repeat;
background-size: cover;
}
}
.col-x:nth-child(3) {
.icon {
background: url($base_url + 'distance.png') no-repeat;
background-size: cover;
}
}
}
.def-btn {
width: 72px;
height: 26px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
background: #1270ff;
box-sizing: border-box;
border: 1px solid rgba(155, 193, 255, 0.74);
}
}
}
</style>
<template>
<el-dialog v-model="isShow" width="740" class="event-alert-dialog" :align-center="true">
<template #header>
<div class="header">
事件告警
<span class="btn" @click="goToEvent">详情</span>
</div>
</template>
<div class="container">
<!-- 告警内容 -->
<div class="content-block">
<div class="title"> 告警内容 </div>
<div class="info">
<span>事件类型:{{ typeCompute(alertInfo).nameStr }}</span>
<span>事件时间:{{ alertInfo.eventStartTime }}</span>
<span>事件地点:{{ alertInfo.milestone }}</span>
</div>
</div>
<!-- 报警视频图片 -->
<div class="content-block">
<div class="title"> 报警视频图片 </div>
<div class="video-block">
<div class="video-item">
<span>实时视频</span>
<div class="video">
<VideoPlayer
v-bind="{
frameW: 220,
frameH: 148,
vcode: eventHisResp.cameraIndexCode,
pileNumber: ''
}"
/>
</div>
</div>
<div class="video-item">
<span>报警回放视频</span>
<div class="video">
<video
ref="videoRef"
:src="eventHisResp.videoPath"
:controls="true"
autoPlay
width="100%"
height="100%"
>
</video>
</div>
</div>
<div class="video-item">
<span>报警图片</span>
<div class="video">
<ImageSome :images="eventHisResp.eventImagesPath" />
</div>
</div>
</div>
</div>
<!-- 关联车辆 -->
<div class="content-block">
<div class="title"> 关联车辆({{ carList.length }}) </div>
<el-scrollbar>
<div class="car-block">
<CarCard v-for="car in carList" :key="car.id" :carInfo="car" />
</div>
</el-scrollbar>
</div>
<!-- 事件处置 -->
<div class="content-block">
<div class="title"> 事件处置 </div>
<el-form :label-width="100" v-if="[0, 3].includes(alertInfo.eventStatus)">
<el-form-item required label="处置方式:">
<el-radio-group v-model="handleType">
<el-radio :value="1">上报事件</el-radio>
<el-radio :value="2">消除事件</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="handleType === 2" required label="原因:">
<el-input
v-model="params.dispelCause"
placeholder="请输入"
:rows="5"
resize="none"
type="textarea"
/>
</el-form-item>
</el-form>
<div class="info mg-b" v-else>
<span>处置记录:{{ alertInfo.processingTime }}</span>
<span>处置人:{{ alertInfo.processingOperationsName }}</span>
<span>处置方式:{{ formatHandleType(alertInfo) }}</span>
</div>
</div>
</div>
<template #footer>
<div class="footer">
<div class="cancel-btn" @click="isShow = false">取消</div>
<div class="confirm-btn" v-loading="loading" @click="onSubmit">确定</div>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { cancelEvent, postEvent } from '@/api/event'
import { getEventDetail } from '@/api/home'
import VideoPlayer from '@/views/components/VideoPlayer/index.vue'
import ImageSome from '@/components/ImageSome/index.vue'
import CarCard from './components/CarCard.vue'
import { EVENT_TYPES } from '@/config/dict'
import { useRouter } from 'vue-router'
import { useUserStoreWithOut } from '@/store/modules/user'
const userStore = useUserStoreWithOut()
const carList = ref<any[]>([
{
carId: 'MEGCAM-90G-LS22033162-2016040732-92486',
direction: 1,
inTunnelTime: '2023-04-07 10:00:00',
retainedMinutesTime: 10,
speed: 60,
carMilestone: 1000,
type: 1,
carPlate: '粤B12345',
isFocusMark: true,
isDangerous: false
},
{
carId: 'MEGCAM-90G-LS22033162-2016040732-92486',
direction: 1,
inTunnelTime: '2023-04-07 10:00:00',
retainedMinutesTime: 10,
speed: 60,
carMilestone: 1000,
type: 1,
carPlate: '粤B12345',
isFocusMark: true,
isDangerous: false
},
{
carId: 'MEGCAM-90G-LS22033162-2016040732-92486',
direction: 1,
inTunnelTime: '2023-04-07 10:00:00',
retainedMinutesTime: 0,
speed: 60,
carMilestone: 1000,
type: 1,
carPlate: '粤B12345',
isFocusMark: true,
isDangerous: false
}
])
const isShow = ref<boolean>(false)
const alertInfo = ref<any>({})
const eventHisResp = ref<any>({})
const handleType = ref<number>(1)
const params = reactive<any>({
id: '',
dispelCause: '',
processingOperationsName: ''
})
const loading = ref<boolean>(false)
const router = useRouter()
const emits = defineEmits(['update'])
const EVENT_MAP = Object.fromEntries(
EVENT_TYPES.map((item) => {
return [item.key, item.name]
})
)
// 状态计算
const typeCompute = (row: any) => {
if (!row || Object.keys(row).length === 0) return { classStr: '', nameStr: '' }
const { eventCarPlate, eventType } = row
if (+eventType === 6) {
return {
classStr: 'type-1',
nameStr: EVENT_MAP[eventType]
}
}
if (eventCarPlate.length > 1) {
return {
classStr: 'type-2',
nameStr: EVENT_MAP[eventType] + '(多车)'
}
}
return {
classStr: 'type-3',
nameStr: EVENT_MAP[eventType] + '(单车)'
}
}
// 处置方式
const formatHandleType = (row: any) => {
if (!row || Object.keys(row).length === 0) return ''
const { processingType, dispelCause } = row
if (+processingType === 1) {
return '上报事件'
}
if (+processingType === 2) {
return `消除事件${dispelCause ? `(${dispelCause})` : ''}`
}
return ''
}
// 跳转事件回溯
const goToEvent = () => {
router.push({
path: 'detail',
query: { id: params.id, from: 'home' }
})
}
const open = async (id: string) => {
params.id = id
params.dispelCause = ''
params.processingOperationsName = userStore.getUser.nickname
handleType.value = 1
alertInfo.value = {}
eventHisResp.value = {}
await getDetail()
isShow.value = true
}
const onSubmit = () => {
if (loading.value) return
if (![0, 3].includes(alertInfo.value.eventStatus)) {
isShow.value = false
emits('update')
return
}
// 上报事件
if (handleType.value === 1) {
loading.value = true
postEvent(params)
.then(() => {
isShow.value = false
ElMessage.success('操作成功!')
emits('update', true)
})
.finally(() => {
loading.value = false
})
}
// 消除事件
if (handleType.value === 2) {
loading.value = true
cancelEvent(params)
.then((res) => {
if (res) {
isShow.value = false
ElMessage.success('操作成功!')
emits('update', true)
}
})
.finally(() => {
loading.value = false
})
}
}
// 获取详情
const getDetail = async () => {
const res = await getEventDetail({ id: params.id })
const { eventDetailRespVO, eventHisRespVO } = res
alertInfo.value = eventDetailRespVO
eventHisResp.value = eventHisRespVO
}
defineExpose({
open
})
</script>
<style lang="scss" scoped>
.event-alert-dialog {
.header {
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-size: 20px;
font-weight: 600;
line-height: 24px;
letter-spacing: 0em;
.btn {
cursor: pointer;
width: 56px;
height: 24px;
border-radius: 4px;
box-sizing: border-box;
border: 1px solid #ffffff;
font-size: 14px;
font-weight: normal;
text-align: center;
line-height: 22px;
margin-right: 48px;
}
}
.container {
padding: 0px 8px;
box-sizing: border-box;
font-family: PingFang SC;
font-size: 16px;
font-weight: normal;
line-height: 24px;
letter-spacing: 0em;
color: #ffffff;
.content-block {
margin-top: 24px;
.title {
font-size: 18px;
font-weight: 600;
line-height: 28px;
display: flex;
align-items: center;
margin-bottom: 8px;
&::before {
content: '';
display: inline-block;
width: 4px;
height: 20px;
border-radius: 2px;
background: #00ddff;
margin-right: 8px;
}
}
.info {
display: flex;
flex-wrap: wrap;
span {
width: 50%;
margin-bottom: 8px;
&:last-child {
width: 100%;
margin-bottom: 0px;
}
}
}
.mg-b {
margin-bottom: 24px;
}
.video-block {
display: flex;
justify-content: space-between;
.video-item {
width: 220px;
padding-bottom: 12px;
.video {
margin-top: 8px;
height: 148px;
background-color: black;
}
}
}
.car-block {
display: flex;
padding-bottom: 14px;
}
}
}
.footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 8px;
color: #fff;
.cancel-btn,
.confirm-btn {
cursor: pointer;
width: 70px;
height: 32px;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
padding: 4px 16px;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 0.4);
}
.confirm-btn {
background: #1270ff;
margin-left: 16px;
}
}
}
</style>
<style lang="scss">
.event-alert-dialog {
padding: 16px 16px 10px 16px;
background: #1b4f8f;
min-height: 800px;
box-sizing: border-box;
--el-text-color-regular: white;
.el-dialog__header {
padding: 0;
.el-dialog__headerbtn .el-dialog__close {
color: #fff;
font-size: 18px;
}
}
.el-dialog__footer {
padding-top: 0;
}
.el-textarea {
.el-textarea__inner {
border-radius: 0;
background: transparent;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.2);
}
}
}
</style>
<template>
<div :class="['ctrls', ctrlClass]">
<div :class="['ctrls', ctrlClass, holeCtrls]">
<div class="title">
<div class="text">
......@@ -38,7 +38,8 @@ import { useRouter } from 'vue-router'
import { ASYNC_DIRECTIONS } from '@/config/dict'
const props = defineProps<{
infos: any
infos: any,
holeTypeName: string
}>()
const emits = defineEmits(['car-filter'])
......@@ -46,6 +47,8 @@ const router = useRouter()
const ctrlClass = computed(() => props.infos.direction ? 'lower' : 'up')
const holeCtrls = computed(() => props.holeTypeName === '新洞' ? 'newTop' : 'oldButtom')
const act = ref('')
const filters = ref([
{ text: '危化品车', key: 'dangerousCount', type: '0'},
......@@ -58,7 +61,7 @@ const filters = ref([
const directionName = computed(() => {
const arrs = ASYNC_DIRECTIONS()
return arrs[props.infos.direction].name
return arrs[props.infos.direction].name + props.holeTypeName
})
......@@ -102,10 +105,10 @@ $car-url: '@/assets/images/cars/';
.ctrls {
display: inline-flex;
align-items: center;
height: 42px;
height: 40px;
.title {
color: white;
width: 316px;
width: 350px;
height: 38px;
display: flex;
align-items: center;
......@@ -129,7 +132,6 @@ $car-url: '@/assets/images/cars/';
}
&.up {
margin: 0 auto 15px;
.title {
background-image: url($base-url + 'arrow_lft.png');
background-repeat: no-repeat;
......@@ -145,7 +147,6 @@ $car-url: '@/assets/images/cars/';
}
&.lower {
margin: 15px auto 0;
.title {
background-image: url($base-url + 'arrow_rgt.png');
background-repeat: no-repeat;
......@@ -156,6 +157,14 @@ $car-url: '@/assets/images/cars/';
margin-right: 32px;
}
}
&.newTop {
margin: 0 auto 10px;
}
&.oldButtom {
margin: 0 auto;
}
}
.filters {
......
<template>
<div class="Overview">
<div v-for="info in tunnelHoles" :key="info.key">
<div class="title">{{ info.directionName }}今日流量</div>
<div class="flex">
<div class="flex-block m-width">
新洞<span class="number">{{ formatNumber(info.newCount) }}</span>
</div>
<div class="flex-block m-width">
老洞<span class="number">{{ formatNumber(info.oldCount) }}</span>
</div>
</div>
</div>
<div>
<div class="title">
事件统计
<div class="dateSelect-block">
<span
:class="['date-block', { active: date.key === dateSelected }]"
v-for="date in dateList"
:key="date.key"
@click="dateChange(date.key)"
>
{{ date.name }}
</span>
</div>
</div>
<div class="flex">
<div class="flex-block" v-for="accident in accidents" :key="accident.key">
<div :class="['icon', accident.icon]"></div>
<span>{{ accident.name }}</span>
<span class="number">{{ formatNumber(accident.count) }}</span>
</div>
<div class="btn-block" @click="packUp">
收起 <Icon color="rgba(240, 245, 255, 0.95)" :size="14" icon="ep:arrow-up" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ASYNC_DIRECTIONS } from '@/config/dict'
import { getTunnelReport, getEventReport } from '@/api/home'
const emit = defineEmits<{ (e: 'pack-up'): void }>()
const tunnelHoles = ref<any[]>([
{ key: 0, directionName: '杭向', newCount: 0, oldCount: 0 },
{ key: 1, directionName: '衢向', newCount: 0, oldCount: 0 }
])
const accidents = ref<any[]>([
{ key: '1', name: '交通事故', count: 0, icon: 'icon1' },
{ key: '2', name: '车辆故障', count: 0, icon: 'icon2' },
{ key: '3', name: '道路施工', count: 0, icon: 'icon3' },
{ key: '4', name: '路面状况', count: 0, icon: 'icon4' },
{ key: '5', name: '其他', count: 0, icon: 'icon5' }
])
// 日期选择列表
const dateList = ref<any[]>([
{ key: '1', name: '今天' },
{ key: '2', name: '本月' },
{ key: '3', name: '本年' }
])
const dateSelected = ref('1')
// 延迟时间 30分钟
const delay = 30*60*1000
const formatNumber = (num: number) => {
return num.toLocaleString()
}
// 收起
const packUp = () => {
emit('pack-up')
}
// 日期选择
const dateChange = (key: string) => {
dateSelected.value = key
getAccidents()
}
const tunnelHoleCount_timer = ref<NodeJS.Timeout | null>(null)
const accidents_timer = ref<NodeJS.Timeout | null>(null)
// 获取隧道统计
const getTunnelHoleCount = async () => {
if (tunnelHoleCount_timer.value) {
clearTimeout(tunnelHoleCount_timer.value)
tunnelHoleCount_timer.value = null
}
const res = await getTunnelReport({})
if (res) {
tunnelHoles.value[0].newCount = res.tunnelUpNew
tunnelHoles.value[0].oldCount = res.tunnelUpOld
tunnelHoles.value[1].newCount = res.tunnelDownNew
tunnelHoles.value[1].oldCount = res.tunnelDownOld
}
tunnelHoleCount_timer.value = setTimeout(() => {
getTunnelHoleCount()
}, delay)
}
// 获取事件统计
const getAccidents = async () => {
if (accidents_timer.value) {
clearTimeout(accidents_timer.value)
accidents_timer.value = null
}
const res = await getEventReport({ type: dateSelected.value })
if (res) {
accidents.value[0].count = res['交通事故'].count // 交通事故
accidents.value[1].count = res['车辆故障'].count // 车辆故障
accidents.value[2].count = res['道路施工'].count // 道路施工
accidents.value[3].count = res['路面状况'].count // 路面状况
accidents.value[4].count = res['其他'].count // 其他
}
accidents_timer.value = setTimeout(() => {
getAccidents()
}, delay)
}
watch(
() => ASYNC_DIRECTIONS(),
(directions) => {
directions.forEach((dir, index) => {
if (tunnelHoles.value[index]) {
tunnelHoles.value[index].directionName = dir.name
}
})
// 获取数据
getTunnelHoleCount()
},
{ deep: true, immediate: true }
)
onUnmounted(() => {
if (tunnelHoleCount_timer.value) {
clearTimeout(tunnelHoleCount_timer.value)
tunnelHoleCount_timer.value = null
}
if (accidents_timer.value) {
clearTimeout(accidents_timer.value)
accidents_timer.value = null
}
})
onMounted(() => {
getAccidents()
})
</script>
<style lang="scss" scoped>
$base-url: '@/assets/images/home/';
.Overview {
display: flex;
justify-content: space-between;
align-items: center;
font-family: PingFang SC;
font-size: 18px;
font-weight: 600;
line-height: 28px;
letter-spacing: 0em;
color: #ffffff;
.title {
display: flex;
align-items: center;
margin-bottom: 4px;
&::before {
content: '';
display: inline-block;
width: 4px;
height: 20px;
background-color: #00ddff;
border-radius: 2px;
margin-right: 8px;
}
.dateSelect-block {
display: flex;
margin-left: 16px;
font-size: 14px;
font-weight: 600;
line-height: 22px;
border-radius: 8px;
background: rgba(15, 95, 255, 0.2);
overflow: hidden;
cursor: pointer;
.date-block {
// width: 44px;
height: 26px;
display: flex;
justify-content: center;
align-items: center;
padding: 0px 8px;
&.active {
background: #0f60db;
}
}
}
}
.flex-block {
display: flex;
align-items: center;
min-width: 121px;
height: 40px;
border-radius: 4px;
background: #103f79;
font-size: 16px;
font-weight: normal;
line-height: 28px;
padding: 4px 14px;
box-sizing: border-box;
margin-left: 12px;
&.m-width {
min-width: 144px;
}
&:first-child {
margin-left: 0;
}
.number {
margin-left: 12px;
font-size: 24px;
font-weight: 600;
line-height: 32px;
}
.icon {
width: 24px;
height: 24px;
margin-right: 8px;
&.icon1 {
background: url($base-url + 'event_icon1.png') no-repeat;
background-size: 100% 100%;
}
&.icon2 {
background: url($base-url + 'event_icon2.png') no-repeat;
background-size: 100% 100%;
}
&.icon3 {
background: url($base-url + 'event_icon3.png') no-repeat;
background-size: 100% 100%;
}
&.icon4 {
background: url($base-url + 'event_icon4.png') no-repeat;
background-size: 100% 100%;
}
&.icon5 {
background: url($base-url + 'event_icon5.png') no-repeat;
background-size: 100% 100%;
}
}
}
.btn-block {
cursor: pointer;
display: flex;
align-items: center;
font-size: 14px;
font-weight: normal;
line-height: 22px;
font-variation-settings: 'opsz' auto;
color: rgba(240, 245, 255, 0.95);
margin-left: 24px;
}
}
</style>
<template>
<div :class="['realTime-block', { 'full-block': drawerShow }]">
<!-- 遮罩层 -->
<div class="mask" v-if="drawerShow" @click="closeRealTimeEvents"></div>
<div :class="['flex', { 'realTime-content': drawerShow }]">
<!-- 实时事件按钮 -->
<div class="realTime-btn-block" @click="openRealTimeEvents">
实时事件<span class="num-block">{{ eventsCount }}</span>
</div>
<!-- 实时事件抽屉弹窗 -->
<div :class="['list-block', { action: drawerShow }]">
<div :style="appStyle" class="realTimeEvents">
<!-- 搜索 -->
<div class="search-block">
<div class="inp-block">
<span class="i-name">关键字:</span>
<div class="i-content">
<el-input
v-model="params.keyWord2"
class="x-inp"
placeholder="输入隧道方向、桩号位置"
/>
</div>
</div>
<div class="inp-block">
<span class="i-name">告警时间:</span>
<div class="i-content">
<el-date-picker
class="x-inp"
v-model="alarmTime"
type="datetimerange"
range-separator="⇀"
value-format="YYYY-MM-DD HH:mm:ss"
:teleported="false"
start-placeholder="请选择"
end-placeholder="请选择"
/>
</div>
</div>
<div class="btns">
<div class="reset-btn" @click="reset">重置</div>
<div class="search-btn" @click="search">查询</div>
</div>
</div>
<!-- 列表 -->
<el-scrollbar ref="scrollbarRef" class="scrollbar-block" v-loading="loading">
<div class="list-item" v-for="item in listData" :key="item.id">
<Icon icon="ep:warn-triangle-filled" :size="25" color="#FF5151" />
<div class="content">
<div class="title">
{{ typeCompute(item).nameStr }}
<span class="time">
{{ item.eventStartTime.slice(5, -3) }}
</span>
</div>
<div class="text ellipsis">
{{ carPlateStr(item.eventCarPlate) + ' ' + item.eventSituation }}
</div>
<div class="foot">
<div :class="`status-${item.eventStatus}`">
{{ STATUS_MAP[item.eventStatus] }}
</div>
<div class="btn-block">
<span class="btn" @click="goEventDetail(item.id)">事件详情</span>
<span
class="btn-red"
v-if="[0, 3].includes(item.eventStatus)"
@click="goEventDetail(item.id)"
>
处置
</span>
<span class="btn mg-l" v-else @click="goToEvent(item.id)">事件回溯</span>
</div>
</div>
</div>
</div>
<div v-if="listData.length === 0 && !loading" class="empty">暂无数据</div>
</el-scrollbar>
<!-- 分页 -->
<el-pagination
background
class="pagination"
v-model:currentPage="params.pageNo"
:teleported="false"
:page-size="params.pageSize"
layout="total, prev, pager, next"
:total="params.total"
:pager-count="3"
@change="pageChange"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useAppStoreWithOut } from '@/store/modules/app'
import { getEventList } from '@/api/event'
import { debounce } from 'lodash-es'
import { EVENT_TYPES, EVENT_STATUS } from '@/config/dict'
import { Icon } from '@/components/Icon'
import { useTrafficStoreOut } from '@/store/modules/traffic'
import { ScrollbarInstance } from 'element-plus'
import { useRouter } from 'vue-router'
const appStore = useAppStoreWithOut()
const appStyle = computed(() => {
return {
height: appStore.fixHeight - 122 + 'px'
}
})
const trafficStore = useTrafficStoreOut()
const alarmTime = computed({
get() {
return [params.eventStartTimeSearchStart, params.eventStartTimeSearchEnd]
},
set(e) {
if (e) {
params.eventStartTimeSearchStart = e[0]
params.eventStartTimeSearchEnd = e[1]
} else {
params.eventStartTimeSearchStart = ''
params.eventStartTimeSearchEnd = ''
}
}
})
const params = reactive({
keyWord2: '',
eventStartTimeSearchStart: '', // 开始时间
eventStartTimeSearchEnd: '', // 结束时间
pageNo: 1,
pageSize: 10,
total: 0
})
const drawerShow = ref(false) // 实时事件弹窗打开开关
const listData = ref<any[]>([])
const loading = ref(false)
const scrollbarRef = ref<ScrollbarInstance>()
const router = useRouter()
const emit = defineEmits(['handle-event-alert'])
const STATUS_MAP = Object.fromEntries(
EVENT_STATUS.map((item) => {
return [item.key, item.name]
})
)
const EVENT_MAP = Object.fromEntries(
EVENT_TYPES.map((item) => {
return [item.key, item.name]
})
)
// 状态计算
const typeCompute = (row) => {
const { eventCarPlate, eventType } = row
if (+eventType === 6) {
return {
classStr: 'type-1',
nameStr: EVENT_MAP[eventType]
}
}
if (eventCarPlate.length > 1) {
return {
classStr: 'type-2',
nameStr: EVENT_MAP[eventType] + '(多车)'
}
}
return {
classStr: 'type-3',
nameStr: EVENT_MAP[eventType] + '(单车)'
}
}
// 车牌
const carPlateStr = (eventCarPlate) => {
return eventCarPlate?.join('')
}
// 搜索
const search = debounce(() => {
params.pageNo = 1
getList()
}, 100)
// 重置
const reset = () => {
params.keyWord2 = ''
params.eventStartTimeSearchStart = ''
params.eventStartTimeSearchEnd = ''
search()
}
// 分页
const pageChange = (e: number) => {
params.pageNo = e
getList()
}
// 获取列表
const getList = async () => {
loading.value = true
const res = await getEventList(params)
listData.value = res.list
params.total = res.total
loading.value = false
scrollbarRef.value!.setScrollTop(0)
}
// 跳转事件详情
const goEventDetail = (id: string, isClose: boolean = false) => {
emit('handle-event-alert', id)
if (isClose) {
closeNotification()
}
}
// 事件回溯
const goToEvent = (id: string) => {
router.push({
path: 'detail',
query: { id, from: 'home' }
})
}
// 初始化列表
const initList = () => {
params.pageSize = 10
params.total = 0
listData.value = []
reset()
}
// 获取事件数量
const getEventsCount = async () => {
const res = await getEventList({ pageNo: 1, pageSize: 1 })
trafficStore.setEventCount(res.total)
}
// 实时事件数量
const eventsCount = computed(() => {
return trafficStore.getEventCount > 99 ? '99+' : trafficStore.getEventCount
})
// 打开实时事件抽屉弹窗
const openRealTimeEvents = () => {
drawerShow.value = !drawerShow.value
if (drawerShow.value) {
initList()
}
}
// 关闭实时事件抽屉弹窗
const closeRealTimeEvents = () => {
drawerShow.value = false
}
const notificationRef = ref<any>(null)
// 右下角事件通知
const triggerEventNotify = (item) => {
notificationRef.value = ElNotification({
// title: 'Title',
position: 'bottom-right',
icon: h(Icon, { icon: 'ep:warn-triangle-filled', size: 25, color: '#FF5151' }),
duration: 10000,
appendTo: document.getElementsByClassName('home')[0] as HTMLElement,
customClass: 'event-notify',
message: h('div', { class: 'content' }, [
h('div', { class: 'title' }, [
h('span', null, `【${typeCompute(item).nameStr}】`),
h('span', { class: 'time' }, item.eventStartTime.slice(5, -3))
]),
h(
'div',
{ class: 'text ellipsis' },
carPlateStr(item.eventCarPlate) + ' ' + item.eventSituation
),
h('div', { class: 'foot' }, [
h('div', { class: `status-${item.eventStatus}` }, STATUS_MAP[item.eventStatus]),
h('div', { class: 'btn-block' }, [
h('span', { class: 'btn', onClick: () => goToEvent(item.id) }, '事件详情'),
h('span', { class: 'btn-red', onClick: () => goEventDetail(item.id, true) }, '处置')
])
])
])
})
trafficStore.setEventNotify(null)
}
// 关闭通知
const closeNotification = () => {
if (notificationRef.value) {
notificationRef.value.close()
notificationRef.value = null
}
}
// 触发事件通知
watch(
() => trafficStore.getEventNotify,
(e) => {
if (e) {
triggerEventNotify(e)
}
},
{ immediate: true, deep: true }
)
onBeforeMount(() => {
getEventsCount()
})
defineExpose({
initList,
search
})
</script>
<style lang="scss" scoped>
.full-block {
top: 0 !important;
left: 0;
z-index: 7 !important;
width: 100%;
height: 100%;
}
.realTime-block {
position: fixed;
right: 0;
top: 122px;
z-index: 5;
font-family: PingFang SC;
font-size: 18px;
font-weight: 600;
line-height: 22px;
text-align: center;
letter-spacing: 0px;
color: #ffffff;
display: flex;
.mask {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.realTime-content {
position: absolute;
right: 0;
top: 122px;
}
.realTime-btn-block {
margin-top: 16px;
width: 36px;
height: 140px;
border-radius: 6px 0px 0px 6px;
background: #0f81c9;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 12px 7px;
box-sizing: border-box;
cursor: pointer;
.num-block {
width: 32px;
height: 20px;
border-radius: 13px;
background: #ff5151;
font-family: DIN Alternate;
font-size: 14px;
line-height: 14px;
display: flex;
justify-content: center;
align-items: center;
padding: 1px 0px;
}
}
.list-block {
width: 0;
overflow: hidden;
visibility: hidden;
transition: all 0.5s;
&.action {
visibility: visible;
width: 400px;
}
.realTimeEvents {
width: 400px;
// height: 958px;
background: rgba(4, 27, 79, 0.75);
padding: 12px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
.search-block {
.inp-block {
margin-bottom: 12px;
display: flex;
align-items: center;
.i-name {
width: 70px;
font-family: PingFang SC;
font-size: 14px;
font-weight: normal;
text-align: left;
line-height: 22px;
letter-spacing: 0em;
color: rgba(240, 245, 255, 0.95);
}
.i-content {
flex: 1;
:deep(.el-input__wrapper) {
background-color: transparent;
box-shadow: none;
border: none;
}
.x-inp {
border-radius: 4px;
background-color: rgba(23, 42, 69, 0.6);
box-sizing: border-box;
border: 1px solid #2d5479;
}
}
}
.btns {
display: flex;
justify-content: flex-end;
align-items: center;
font-size: 16px;
font-weight: normal;
line-height: 24px;
letter-spacing: 0px;
color: rgba(255, 255, 255, 0.85);
margin-bottom: 10px;
.search-btn,
.reset-btn {
cursor: pointer;
width: 54px;
height: 28px;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
padding: 4px 8px;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 0.4);
}
.search-btn {
background: #1270ff;
margin-left: 8px;
}
}
}
.scrollbar-block {
flex: 1;
.list-item {
height: 136px;
display: flex;
justify-content: space-between;
padding: 12px 16px;
font-size: 16px;
font-weight: normal;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
color: #ffffff;
background: #1b4f8f;
box-sizing: border-box;
border-radius: 8px;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.content {
margin-left: 12px;
flex: 1;
.title {
margin-left: -6px;
font-size: 18px;
font-weight: 600;
.time {
margin-left: 10px;
}
}
.text {
margin: 8px 0;
}
.foot {
font-size: 14px;
line-height: 22px;
display: flex;
justify-content: space-between;
align-items: center;
.status-0,
.status-1,
.status-2 {
display: flex;
align-items: center;
&::before {
content: '';
width: 10px;
height: 10px;
border-radius: 10px;
margin-right: 4px;
}
}
.status-0,
.status-3 {
color: #e6a23c;
&::before {
background: #e6a23c;
}
}
.status-1 {
color: #70a9ff;
&::before {
background: #70a9ff;
}
}
.status-2 {
color: #67c23a;
&::before {
background: #67c23a;
}
}
.btn-block {
display: flex;
align-items: center;
.btn,
.btn-red {
padding: 1px 8px;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 2px;
cursor: pointer;
}
.btn-red {
background: #f56c6c;
border: 1px solid rgba(255, 108, 108, 0.4);
margin-left: 8px;
}
.mg-l {
margin-left: 8px;
}
}
}
}
}
.empty {
text-align: center;
color: #fff;
font-size: 14px;
line-height: 22px;
margin-top: 20px;
}
}
.pagination {
margin-top: 10px;
display: flex;
justify-content: center;
:deep(.btn-prev),
:deep(.btn-next),
:deep(.el-pager li),
:deep(.el-pagination__total) {
background-color: transparent;
color: #fff;
}
:deep(.el-pager .is-active) {
background-color: #409eff;
}
}
}
}
}
</style>
<style lang="scss">
.realTimeEvents {
.el-date-editor {
width: 100%;
height: 34px;
background-color: rgba(23, 42, 69, 0.6) !important;
border: 1px solid #2d5479 !important;
border-radius: 4px !important;
box-sizing: border-box;
.el-range-input {
background: transparent;
color: #fff;
}
.el-range-separator {
color: rgba(240, 245, 255, 0.95);
}
.el-range__close-icon {
color: rgba(240, 245, 255, 0.95);
}
}
}
.event-notify {
border-radius: 8px;
background: #1b4f8f;
box-sizing: border-box;
border: 0px solid #5499cf;
width: 368px;
height: 136px;
box-sizing: border-box;
padding: 12px 16px;
.el-notification__group {
margin-left: 12px;
margin-right: 0;
}
.el-notification__closeBtn {
color: #fff;
}
.content {
font-family: PingFang SC;
font-size: 16px;
font-weight: normal;
line-height: 24px;
letter-spacing: 0px;
color: #ffffff;
.title {
margin-left: -6px;
font-size: 18px;
font-weight: 600;
.time {
margin-left: 10px;
}
}
.text {
margin: 8px 0;
}
.foot {
font-size: 14px;
line-height: 22px;
display: flex;
justify-content: space-between;
align-items: center;
.status-0,
.status-1,
.status-2 {
display: flex;
align-items: center;
&::before {
content: '';
width: 10px;
height: 10px;
border-radius: 10px;
margin-right: 4px;
}
}
.status-0,
.status-3 {
color: #e6a23c;
&::before {
background: #e6a23c;
}
}
.status-1 {
color: #70a9ff;
&::before {
background: #70a9ff;
}
}
.status-2 {
color: #67c23a;
&::before {
background: #67c23a;
}
}
.btn-block {
display: flex;
align-items: center;
.btn,
.btn-red {
cursor: pointer;
padding: 1px 8px;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 2px;
}
.btn-red {
background: #f56c6c;
border: 1px solid rgba(255, 108, 108, 0.4);
margin-left: 8px;
}
}
}
}
}
.ellipsis {
display: -webkit-box;
-webkit-line-clamp: 2; /* 显示的行数 */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
<template>
<div class="traffic">
<FlowCarCtrl @car-filter="carFilter" :infos="inSummary" />
<div class="traffic">
<FlowCarCtrl @car-filter="carFilter" holeTypeName="新洞" :infos="newSummary" />
<div class="hole-select">
<div class="name">聚焦位置</div>
<el-select
v-model="holeAct"
popper-class="define-select"
:teleported="false"
>
<el-option v-for="({ name, key }) in HOLE_TYPES" :key v-bind="{ label: name, value: key }" />
<div class="name">聚焦方向</div>
<el-select v-model="holeDirection" popper-class="define-select" :teleported="false">
<el-option
v-for="{ name, inkey } in holeTypes"
:key="inkey"
v-bind="{ label: name, value: inkey }"
/>
</el-select>
</div>
<Scales
<!-- <Scales
@plate-change="plateChange"
v-model:sceneMode="sceneMode"
v-model:useMaterial="configs.useMaterial"
class="scale-ctrls"
/>
/> -->
<!-- 新洞监控 -->
<div class="cameras-block">
<flow-chart v-if="showFlow" :direction="holeDirection" />
<cameras ref="cameraRef1" v-else holeType="new" :direction="holeDirection" />
</div>
<Road
@go-section="goSection"
@go-track="goTrack"
@car-socket="e => carSocket(e, 1)"
v-bind="{ ...configs, direction: 0, traffic: inTraffic, act: holeAct === 'new' }"
@car-socket="(e) => carSocket(e, 'new')"
v-bind="{
...configs,
direction: holeDirection,
traffic: newTraffic,
act: true,
section: false,
roadHeight: newTraffic?.road_w
}"
/>
<Facilities :act="holeAct === 'new'" />
<Facilities v-bind="{ traffic: newTraffic, direction: holeDirection, act: true }" :act="true" />
<XDirection
v-if="inTraffic"
@go-section="goSection"
@go-track="goTrack"
@car-socket="e => carSocket(e, 0)"
v-bind="{ ...configs, direction: 0, traffic: inTraffic, act: holeAct === 'old', section: false }"
@car-socket="(e) => carSocket(e, 'old')"
v-bind="{
...configs,
direction: holeDirection,
traffic: oldTraffic,
act: true,
section: false,
roadHeight: oldTraffic?.road_w
}"
/>
<Facilities :act="holeAct === 'old'" />
<Facilities v-bind="{ traffic: oldTraffic, direction: holeDirection, act: true }" />
<XDirection
<!-- <XDirection
v-if="outTraffic"
@go-section="goSection"
@go-track="goTrack"
@car-socket="e => carSocket(e, 1)"
v-bind="{...configs, isDownCtrls: true, direction: 1, traffic: outTraffic, act: holeAct === 'old', section: false}"
@car-socket="(e) => carSocket(e, 1)"
v-bind="{
...configs,
isDownCtrls: true,
direction: 1,
traffic: outTraffic,
act: holeAct === 'old',
section: false
}"
/>
<Facilities :act="holeAct === 'new'" />
......@@ -55,19 +78,43 @@
<Road
@go-section="goSection"
@go-track="goTrack"
@car-socket="e => carSocket(e, 1)"
v-bind="{ ...configs, isDownCtrls: true, direction: 1, traffic: inTraffic, act: holeAct === 'new' }"
/>
<FlowCarCtrl @car-filter="carFilter" :infos="outSummary" />
</div>
@car-socket="(e) => carSocket(e, 1)"
v-bind="{
...configs,
isDownCtrls: true,
direction: 1,
traffic: inTraffic,
act: holeAct === 'new'
}"
/> -->
<div class="cameras-block">
<flow-chart v-if="showFlow" :direction="holeDirection" />
<cameras ref="cameraRef1" v-else holeType="old" :direction="holeDirection" />
</div>
<FlowCarCtrl @car-filter="carFilter" holeTypeName="老洞" :infos="oldSummary" />
<!-- 空间轴 -->
<div class="space-axis-block">
<span class="title">
<Icon icon="ep:info-filled" class="icon" :size="14" color="#0BC7FA" />
拖动滑块查看隧道局部
</span>
<SpaceAxis
ref="spaceAxisHNRef"
@drag-end="(scrollLeft) => spaceAxisScroll(scrollLeft, 'new')"
v-bind="{ traffic: newTraffic, direction: holeDirection }"
/>
<SpaceAxis
ref="spaceAxisHORef"
@drag-end="(scrollLeft) => spaceAxisScroll(scrollLeft, 'old')"
v-bind="{ traffic: oldTraffic, direction: holeDirection }"
/>
</div>
</div>
</template>
<script lang='ts' setup>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useTunnelStoreWithOut } from '@/store/modules/tunnel'
......@@ -77,26 +124,54 @@ import TrafficEngine from '@/views/traffic/roads/Engine/index'
import XDirection from '@/views/traffic/roads/XDirection/index.vue'
import FlowCarCtrl from '../FlowCarCtrls/index.vue'
import Facilities from '@/views/traffic/roads/Facilities/index.vue'
import Scales from '@/views/traffic/components/Scales/index.vue'
// import Scales from '@/views/traffic/components/Scales/index.vue'
import SpaceAxis from '@/views/traffic/roads/XDirection/comps/SpaceAxis/index.vue'
import Road from '@/views/traffic/roads/XDirection/Road.vue'
const HOLE_TYPES = [
{ name: '新洞', key: 'new' },
{ name: '老洞', key: 'old' }
]
import Cameras from '../Cameras/index.vue'
import FlowChart from '../FlowChart/index.vue'
import { ASYNC_DIRECTIONS } from '@/config/dict'
interface TrafficSummary<T = number> {
name: string
direction: T
dangerousCount: T
truckCount: T
passengerCount: T
sedanCount: T
otherCount: T
}
interface HoleTypes<T = string> {
name: T
key: T
inkey: number
seKey: T
}
const holeTypes = ref<HoleTypes[]>([])
const tunnelStore = useTunnelStoreWithOut()
const props = defineProps<{
useMaterial: boolean
showFlow: boolean
}>()
const eventRef = ref<any>(null) // 事件弹窗实例
const sceneMode = ref('panorama')
// const sceneMode = ref('panorama')
const router = useRouter()
const holeAct = ref('old')
const holeDirection = ref<number>(0)
const inNewTraffic = ref<TrafficEngine | null>(null) // 新洞上行隧道实例
const outNewTraffic = ref<TrafficEngine | null>(null) // 新洞下行隧道实例
const inTraffic = ref(null) // 上行隧道实例
const outTraffic = ref(null) // 下行隧道实例
const inOldTraffic = ref<TrafficEngine | null>(null) // 老洞上行隧道实例
const ouOldTraffic = ref<TrafficEngine | null>(null) // 老洞下行隧道实例
const inSummary = ref({
const newSummary = ref<TrafficSummary>({
name: '杭州方向',
direction: 0,
dangerousCount: 0,
......@@ -106,9 +181,9 @@ const inSummary = ref({
otherCount: 0
})
const outSummary = ref({
name: '宁波方向',
direction: 1,
const oldSummary = ref<TrafficSummary>({
name: '杭州方向',
direction: 0,
dangerousCount: 0,
truckCount: 0,
passengerCount: 0,
......@@ -120,48 +195,60 @@ const outSummary = ref({
const configs = reactive<any>({
plateType: 1, // 铭牌展示类型
filterType: null, // 类型筛选
useMaterial: false // 是否使用贴图
useMaterial: true // 是否使用贴图
})
// 新洞实例
const newTraffic = computed(() => {
return holeDirection.value === 1 ? outNewTraffic.value : inNewTraffic.value
})
// 老洞实例
const oldTraffic = computed(() => {
return holeDirection.value === 1 ? ouOldTraffic.value : inOldTraffic.value
})
// 铭牌切换
const plateChange = (e) => {
const plateChange = (e: number) => {
configs.plateType = e
}
// 车流筛选
const carFilter = ({e}) => {
const carFilter = ({ e }) => {
configs.filterType = e?.type || null
}
// 汇总同步
const carSocket = (e, direction) => {
const carSocket = (e, holeType: string) => {
const { dangerousCount, truckCount, passengerCount, sedanCount, otherCount } = e
const summary = [inSummary, outSummary][direction]
const summary = [newSummary, oldSummary][holeType === 'new' ? 0 : 1]
summary.value = {
...summary.value,
direction: holeDirection.value,
...{
dangerousCount, truckCount, passengerCount, sedanCount, otherCount,
dangerousCount,
truckCount,
passengerCount,
sedanCount,
otherCount,
total: dangerousCount + dangerousCount + passengerCount + sedanCount + otherCount
}
}
}
// 缎面跳转
const goSection = ({ x, direction }) => {
const goSection = ({ x, direction, scrollLeft, holeType }) => {
router.push({
path: '/section',
query: {
x, direction,
x,
direction,
scrollLeft,
holeType,
from: 'home'
}
})
}
// 车辆追踪
......@@ -169,7 +256,8 @@ const goTrack = ({ id, direction }) => {
router.push({
path: 'track',
query: {
id, direction,
id,
direction,
from: 'home'
}
})
......@@ -177,46 +265,92 @@ const goTrack = ({ id, direction }) => {
// 创建车流实例
const createTraffic = async () => {
const isAvl = await awaitGet(() => tunnelStore.getUpTunnel?.long)
// 隧道数据获取错误
if (!isAvl) return
const create = (direction) => {
const create = (direction, holeType) => {
const key = direction ? 'getDownTunnel' : 'getUpTunnel'
const { start, end, long } = tunnelStore[key]
return new TrafficEngine('panorama', {
direction,
holeType, // (新洞/老洞)目前还没有实现,需后续补功能
start: +start,
end: +end,
miles: +long
miles: +long,
road_w: holeType === 'new' ? 90 : 64
})
}
inTraffic.value = create(0)
outTraffic.value = create(1)
inNewTraffic.value = create(0, 'new')
outNewTraffic.value = create(1, 'new')
inTraffic.value.startTraffic(res => {
carSocket(res, 0)
})
outTraffic.value.startTraffic(res => {
carSocket(res, 1)
inOldTraffic.value = create(0, 'old')
ouOldTraffic.value = create(1, 'old')
// 后续接新洞后再做修改
// newTraffic.value?.startTraffic((res) => {
// carSocket(res, 'new')
// })
oldTraffic.value?.startTraffic((res) => {
carSocket(res, 'old')
})
}
const spaceAxisHNRef = ref<InstanceType<typeof SpaceAxis> | null>(null)
const spaceAxisHORef = ref<InstanceType<typeof SpaceAxis> | null>(null)
// 点击滚动轴
const spaceAxisScroll = (e: number, holeType: string) => {
if (holeType === 'new') {
spaceAxisHNRef.value?.updateThumb({ scrollLeft: e })
} else {
spaceAxisHORef.value?.updateThumb({ scrollLeft: e })
}
goSection({ x: 0, direction: holeDirection.value, scrollLeft: e, holeType })
}
// watch(
// () => sceneMode.value,
// (e) => {
// if (e === 'section') {
// goSection({ x: 0, direction: 0 })
// }
// }
// )
watch(
() => sceneMode.value,
e => {
if (e === 'section') {
goSection({ x: 0, direction: 0 })
() => holeDirection.value,
(e) => {
// 后续接新洞后再做修改
// newTraffic.value?.startTraffic((res) => {
// carSocket(res, 'new')
// })
newSummary.value.direction = e
oldTraffic.value?.startTraffic((res) => {
carSocket(res, 'old')
})
}
)
watch(
() => props.useMaterial,
(e) => {
configs.useMaterial = e
}
)
watch(
() => ASYNC_DIRECTIONS(),
(directions) => {
holeTypes.value = [...directions]
},
{ deep: true, immediate: true }
)
// 打开事件弹窗
provide('openEventPop', (event) => {
......@@ -224,15 +358,26 @@ provide('openEventPop', (event) => {
})
onMounted(() => {
createTraffic()
})
onBeforeUnmount(() => {
// 清理所有隧道实例的 WebSocket 连接
;[inNewTraffic.value, outNewTraffic.value, inOldTraffic.value, ouOldTraffic.value].forEach(
(instance) => {
if (instance) {
instance.cleanRoad() // 这会调用 socket.close()
}
}
)
})
defineExpose({
plateChange
})
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
.traffic {
position: relative;
text-align: center;
......@@ -248,7 +393,7 @@ onMounted(() => {
display: flex;
align-items: center;
width: 200px;
top: 15px;
top: 10px;
left: 85px;
.name {
font-size: 14px;
......@@ -264,7 +409,6 @@ onMounted(() => {
background: rgba(38, 67, 106);
border: none;
}
}
:deep(.el-select-dropdown__item) {
......@@ -274,8 +418,29 @@ onMounted(() => {
}
}
}
.cameras-block {
height: 200px;
display: flex;
flex-direction: column;
margin-bottom: 15px;
}
.space-axis-block {
display: flex;
flex-direction: column;
margin-top: 15px;
.title {
font-size: 14px;
line-height: 22px;
color: #fff;
display: flex;
align-items: center;
margin-left: 17px;
margin-bottom: 2px;
.icon {
margin-right: 5px;
}
}
}
}
</style>
<template>
<div class="home">
<div class="home">
<div class="flow-switch">
<div class="inner">
<div class="text-pre">隧道统计:</div>
<el-switch v-model="showOverview" />
<div class="text-aft">{{ showOverview ? '' : '' }}</div>
</div>
<div class="inner">
<div class="text-pre">实时交通流:</div>
<el-switch v-model="showFlow" />
<div class="text-aft">{{showFlow ? '' : ''}}</div>
<div class="text-aft">{{ showFlow ? '' : '' }}</div>
</div>
<div class="inner">
<Scales
:isShowSceneMode="false"
@plate-change="plateChange"
v-model:useMaterial="useMaterial"
class="scale-ctrls"
/>
</div>
</div>
<!-- 隧道统计 -->
<div class="overview-block" v-show="showOverview">
<Overview @pack-up="handlePackUp" />
</div>
<div class="bef-block">
<!-- <div class="bef-block">
<flow-chart v-if="showFlow" :direction="0" />
<cameras ref="cameraRef1" v-else :direction="0" />
</div>
</div> -->
<!-- 车流 -->
<Traffic />
<Traffic ref="trafficRef" :showFlow="showFlow" :useMaterial="useMaterial" />
<div class="aft-block">
<!-- <div class="aft-block">
<flow-chart v-if="showFlow" :direction="1" />
<cameras ref="cameraRef2" v-else :direction="1" />
</div>
</div> -->
<!-- 系统跳转 -->
<div @click="toSystem" class="sys-trans" v-if='menus.includes(8)'>
<div @click="toSystem" class="sys-trans" v-if="menus.includes(8)">
<div class="icon"></div>
<div class="text">隧道管控</div>
</div>
</div>
<!-- 实时事件 -->
<real-time-events ref="realTimeEvents" @handle-event-alert="openEventAlert" />
<!-- 事件告警弹窗 -->
<event-alert-pop @update="handleEventAlert" ref="eventAlertPop" />
</div>
</template>
<script lang='ts' setup>
<script lang="ts" setup>
import { ref } from 'vue'
import Traffic from './components/Traffic/index.vue'
import Cameras from './components/Cameras/index.vue'
import FlowChart from './components/FlowChart/index.vue'
// import Cameras from './components/Cameras/index.vue'
// import FlowChart from './components/FlowChart/index.vue'
import Overview from './components/Overview/index.vue'
import RealTimeEvents from './components/RealTimeEvents/index.vue'
import EventAlertPop from './components/EventAlertPop/index.vue'
import Scales from '@/views/traffic/components/Scales/index.vue'
import { useUserStoreWithOut } from '@/store/modules/user'
const userStore = useUserStoreWithOut()
import { getOrgainzations } from '@/api/system/role'
import {
getDetails,
} from '@/api/system/role'
import { getDetails } from '@/api/system/role'
import { getLoginPath } from '@/api/system/user'
import { getState, setState } from '@/flash/Home'
// const showFlow = ref(false) // 模式切换开关
const cameraRef1 = ref<any>(null) // 上行摄像头
const cameraRef2 = ref<any>(null) // 下行摄像头
const showOverview = ref(true) // 隧道统计显示开关
const realTimeEvents = ref<InstanceType<typeof RealTimeEvents> | null>(null)
const eventAlertPop = ref<InstanceType<typeof EventAlertPop> | null>(null)
const useMaterial = ref<boolean>(true) // 是否使用贴图
const trafficRef = ref<InstanceType<typeof Traffic> | null>(null)
// 权限菜单
const menus = ref([])
// 模式切换开关
const showFlow = computed({
get () {
get() {
return getState()
},
set (e) {
set(e) {
setState(e)
}
})
const cameraChange = (xcamera, direction) => {
if (direction === 0 && cameraRef1.value) {
cameraRef1.value.cameraChange(xcamera)
}
......@@ -75,7 +101,6 @@ const cameraChange = (xcamera, direction) => {
if (direction === 1 && cameraRef2.value) {
cameraRef2.value.cameraChange(xcamera)
}
}
// 系统免登录
......@@ -86,26 +111,42 @@ const toSystem = async () => {
provide('cameraChange', cameraChange)
const a = [{
}]
const a = [{}]
const setAuths = async () => {
const role = userStore.getRoles[0]
const res = await getOrgainzations({ code: role })
const resList = await getDetails({ id: res.list[0].id || '' })
menus.value = resList.menuIds || []
}
// 收起隧道统计
const handlePackUp = () => {
showOverview.value = false
}
// 打开事件告警
const openEventAlert = (id: string) => {
eventAlertPop.value?.open(id)
}
// 事件告警处置
const handleEventAlert = (isUpdate: boolean = false) => {
if (isUpdate) {
realTimeEvents.value?.search()
}
}
// 名牌切换
const plateChange = (e: number) => {
trafficRef.value?.plateChange(e)
}
onBeforeMount(() => {
setAuths()
})
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
$Common_Url: '@/assets/images/common/';
.flow-switch {
......@@ -115,6 +156,7 @@ $Common_Url: '@/assets/images/common/';
position: absolute;
right: 10px;
top: 100px;
display: flex;
justify-content: space-between;
align-items: center;
......@@ -122,17 +164,24 @@ $Common_Url: '@/assets/images/common/';
display: flex;
align-items: center;
justify-content: flex-end;
margin-right: 42px;
margin-right: 24px;
.text-aft {
margin-left: 6px;
}
.scale-ctrls {
margin-right: 20px;
}
}
:deep(.el-switch) {
--el-switch-on-color: #007DFA;
--el-switch-off-color: #203A60;
--el-switch-on-color: #007dfa;
--el-switch-off-color: #203a60;
}
}
.overview-block {
margin: 33px 135px 30px;
overflow: hidden;
}
.bef-block,
......
<template>
<div :class="['ctrls', ctrlClass]">
<div :class="['ctrls', ctrlClass]">
<div class="title">
<div class="text">
{{ DIRECTION_NAMES[infos.direction] }}
<div @click="directionChange" class="direction-change">
{{ DIRECTION_NAMES[infos.direction] + holeTypeName_l() }}
<div @click="holeTypeChange" class="direction-change">
<div class="icon"></div>
{{ DIRECTION_NAMES[1 - infos.direction].slice(0, -2) }}
{{ DIRECTION_NAMES[infos.direction] + holeTypeName_r() }}
</div>
</div>
<div class="more">
留存车辆:{{ infos.total }}
<div @click="goRemains" class="def-btn">
详情 <Icon icon="ep:arrow-right" />
</div>
<div @click="goRemains" class="def-btn"> 详情 <Icon icon="ep:arrow-right" /> </div>
</div>
</div>
......@@ -22,19 +18,16 @@
<div
v-for="item in filters"
:key="item.key"
:class="['x-car', {'act': item.key === act}]"
:class="['x-car', { act: item.key === act }]"
@click="goFilter(item)"
>
{{ item.text }}({{ infos[item.key] }})
</div>
</div>
</div>
</div>
</template>
<script lang='ts' setup>
<script lang="ts" setup>
import { ref, defineProps, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ASYNC_DIRECTIONS } from '@/config/dict'
......@@ -43,25 +36,31 @@ const props = defineProps<{
infos: any
}>()
const emits = defineEmits(['car-filter', 'direction-change'])
const emits = defineEmits(['car-filter', 'hole-type-change'])
const router = useRouter()
const ctrlClass = computed(() => props.infos.direction ? 'lower' : 'up')
const ctrlClass = computed(() => (props.infos.direction ? 'lower' : 'up'))
const act = ref('')
const filters = ref([
{ text: '危化品车', key: 'dangerousCount', type: '0'},
{ text: '危化品车', key: 'dangerousCount', type: '0' },
{ text: '货车', key: 'truckCount', type: '3' },
{ text: '客车', key: 'passengerCount', type: '1'},
{ text: '客车', key: 'passengerCount', type: '1' },
{ text: '轿车', key: 'sedanCount', type: '2' },
{ text: '其他', key: 'otherCount', type: '4' }
])
const DIRECTION_NAMES = computed(() => {
return ASYNC_DIRECTIONS().map((item) => item.name)
})
return ASYNC_DIRECTIONS().map(item => item.name)
const holeTypeName_l = () => {
return props.infos.holeType === 'old' ? '老洞' : '新洞'
}
})
const holeTypeName_r = () => {
return props.infos.holeType === 'old' ? '新洞' : '老洞'
}
const goFilter = (e) => {
if (act.value === e.key) {
......@@ -88,15 +87,13 @@ const goRemains = () => {
})
}
// 方向切换
const directionChange = () => {
emits('direction-change')
// 新老洞切换
const holeTypeChange = () => {
emits('hole-type-change')
}
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
$base-url: '@/assets/images/home/';
$car-url: '@/assets/images/cars/';
$common-url: '@/assets/images/common/';
......@@ -107,7 +104,7 @@ $common-url: '@/assets/images/common/';
height: 42px;
.title {
color: white;
width: 360px;
width: 460px;
height: 38px;
display: flex;
align-items: center;
......@@ -121,7 +118,7 @@ $common-url: '@/assets/images/common/';
align-items: center;
.direction-change {
font-size: 16px;
color: #42E7FF;
color: #42e7ff;
display: flex;
align-items: center;
cursor: pointer;
......@@ -193,9 +190,8 @@ $common-url: '@/assets/images/common/';
&.act {
background: rgba(124, 213, 255, 0.15);
box-sizing: border-box;
border: 1px solid #7CD5FF;
border: 1px solid #7cd5ff;
}
}
}
</style>
<template>
<div class="traffic">
<Scales
<div class="traffic">
<!-- <Scales
@plate-change="plateChange"
v-model:sceneMode="sceneMode"
v-model:useMaterial="commons.useMaterial"
class="scale-ctrls"
/>
/> -->
<FlowCarCtrl
@car-filter="carFilter"
@direction-change="handleDirectionChange"
:infos="{ ...summary, direction: commons.direction }"
@hole-type-change="handleHoleTypeChange"
:infos="{ ...summary, direction: commons.direction, holeType: commons.holeType }"
/>
<XDirectionSection
v-bind="{...commons, traffic}"
<!-- 新洞 -->
<XDirectionSectionRoad
ref="xDirectionSectionRoadRef"
v-bind="{
...commons,
traffic: newTraffic,
section: false,
roadHeight: newTraffic?.road_section_w,
act: commons.holeType === 'new'
}"
v-model:sectionAct="commons.sectionAct"
@car-socket="carSocket"
@section-change="sectionChange"
@go-track="goTrack"
@camera-change="e => emits('camera-change', e)"
@camera-change="(e) => emits('camera-change', e)"
@update-space-axis="(scrollLeft) => updateSpaceAxis('new', scrollLeft)"
/>
</div>
<!-- 老洞 -->
<XDirectionSection
ref="xDirectionSectionRef"
v-bind="{
...commons,
traffic,
section: false,
roadHeight: traffic?.road_section_w,
act: commons.holeType === 'old'
}"
v-model:sectionAct="commons.sectionAct"
@car-socket="carSocket"
@section-change="sectionChange"
@go-track="goTrack"
@camera-change="(e) => emits('camera-change', e)"
@update-space-axis="(scrollLeft) => updateSpaceAxis('old', scrollLeft)"
/>
<!-- 空间轴 -->
<div class="space-axis-block">
<span class="title">
<Icon icon="ep:info-filled" class="icon" :size="14" color="#0BC7FA" />
拖动滑块查看隧道局部
</span>
<SpaceAxis
ref="spaceAxisRoadRef"
@space-axis-scroll="(scrollLeft) => spaceAxisScroll('new', scrollLeft)"
v-bind="{
traffic: newTraffic,
direction: commons.direction,
holeType: 'new',
act: commons.holeType === 'new'
}"
/>
<SpaceAxis
ref="spaceAxisRef"
@space-axis-scroll="(scrollLeft) => spaceAxisScroll('old', scrollLeft)"
v-bind="{
traffic,
direction: commons.direction,
holeType: 'old',
act: commons.holeType === 'old'
}"
/>
</div>
</div>
</template>
<script lang='ts' setup>
<script lang="ts" setup>
import { ref, reactive, onBeforeMount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import FlowCarCtrl from '../FlowCarCtrls/index.vue'
import XDirectionSection from '@/views/traffic/roads/XDirectionSection/index.vue'
import XDirectionSection from '@/views/traffic/roads/XDirectionSectionV2/index.vue'
import XDirectionSectionRoad from '@/views/traffic/roads/XDirectionSectionV2/Road.vue'
import Scales from '@/views/traffic/components/Scales/index.vue'
// import Scales from '@/views/traffic/components/Scales/index.vue'
import Cameras from '../components/Cameras/index.vue'
import TrafficEngine from '@/views/traffic/roads/Engine/index'
import SpaceAxis from '@/views/traffic/roads/XDirection/comps/SpaceAxis/index.vue'
import { ElMessage } from 'element-plus'
......@@ -56,6 +108,7 @@ const tunnelStore = useTunnelStoreWithOut()
const commons = defineModel<any>()
const freshKey = ref(true)
const traffic = ref(null)
const newTraffic = ref(null)
const summary = ref<any>({
dangerousCount: 0,
......@@ -66,47 +119,47 @@ const summary = ref<any>({
total: 0
})
watch(
() => sceneMode.value,
(e) => {
if (e === 'panorama')
router.push({
path: 'home'
})
}
)
// watch(
// () => sceneMode.value,
// (e) => {
// if (e === 'panorama')
// router.push({
// path: 'home'
// })
// }
// )
// 汇总同步
const carSocket = (e) => {
const { dangerousCount, truckCount, passengerCount, sedanCount, otherCount } = e
const { dangerousCount, truckCount, passengerCount, sedanCount, otherCount } = e
summary.value = {
summary.value = {
...summary.value,
...{
dangerousCount, truckCount, passengerCount, sedanCount, otherCount,
dangerousCount,
truckCount,
passengerCount,
sedanCount,
otherCount,
total: dangerousCount + dangerousCount + passengerCount + sedanCount + otherCount
}
}
}
}
// 车流筛选
const carFilter = ({ e }) => {
commons.value.filterType = e?.type || null
}
// 展示牌切换
const plateChange = (e) => {
commons.value.plateType = e
}
// 方向切换
const handleDirectionChange = async () => {
// // 展示牌切换
// const plateChange = (e) => {
// commons.value.plateType = e
// }
commons.value.direction = 1 - commons.value.direction
// 新/老洞切换
const handleHoleTypeChange = async () => {
// commons.value.direction = 1 - commons.value.direction
commons.value.holeType = commons.value.holeType === 'old' ? 'new' : 'old'
summary.value.dangerousCount = 0
summary.value.truckCount = 0
......@@ -115,17 +168,26 @@ const handleDirectionChange = async () => {
summary.value.otherCount = 0
summary.value.total = 0
traffic.value.cleanRoad()
traffic.value?.cleanRoad()
newTraffic.value?.cleanRoad()
const configs = await getConfigs(commons.value.direction)
if (configs) {
if (commons.value.holeType === 'old') {
traffic.value = new TrafficEngine(sceneMode.value, configs)
traffic.value.startTraffic(res => {
traffic.value.startTraffic((res) => {
carSocket(res)
})
}
} else {
newTraffic.value = new TrafficEngine(sceneMode.value, configs)
// 新洞还未对接,需后完善
newTraffic.value.startTraffic((res) => {
carSocket(res)
})
}
}
}
// 段面切换
......@@ -138,7 +200,8 @@ const goTrack = ({ id, direction }) => {
router.push({
path: 'track',
query: {
id, direction,
id,
direction,
from: 'event'
}
})
......@@ -146,12 +209,10 @@ const goTrack = ({ id, direction }) => {
// 获取隧道数据
const getConfigs = (direct) => {
let wait = null
let stop = null
return new Promise(resolve => {
return new Promise((resolve) => {
wait = setTimeout(() => {
stop && stop()
resolve(false)
......@@ -165,41 +226,90 @@ const getConfigs = (direct) => {
direction: direct,
start,
end,
miles: long
miles: long,
road_section_w: commons.value.holeType === 'old' ? 134 : 201,
holeType: commons.value.holeType // 新老洞类型 old、new
})
}
})
})
}
const spaceAxisRef = ref<SpaceAxis>(null)
const spaceAxisRoadRef = ref<SpaceAxis>(null)
const xDirectionSectionRef = ref<XDirectionSection>(null)
const xDirectionSectionRoadRef = ref<XDirectionSectionRoad>(null)
// 空间轴滚动
const spaceAxisScroll = (holeType: string, scrollLeft: number) => {
if (holeType === 'old') {
xDirectionSectionRef.value.setRoadScroll(scrollLeft)
} else {
xDirectionSectionRoadRef.value.setRoadScroll(scrollLeft)
}
}
// 更新空间轴
const updateSpaceAxis = (holeType: string, scrollLeft: number) => {
if (holeType === 'old') {
spaceAxisRef.value.updateThumb({ scrollLeft })
} else {
spaceAxisRoadRef.value.updateThumb({ scrollLeft })
}
}
onBeforeMount(async () => {
const { x, direction } = route.query
const { x, direction, holeType } = route.query
if (isSomeNone([x, direction])) {
return ElMessage.error('地址错误!')
}
commons.value.direction = +(direction as string)
commons.value.holeType = holeType as string
const configs = await getConfigs(+direction)
if (configs) {
traffic.value = new TrafficEngine(sceneMode.value, configs)
traffic.value.startTraffic(res => {
traffic.value = new TrafficEngine(sceneMode.value, {
...configs,
road_section_w: 134,
holeType: 'old'
})
traffic.value.startTraffic((res) => {
carSocket(res)
})
}
newTraffic.value = new TrafficEngine(sceneMode.value, {
...configs,
road_section_w: 201,
holeType: 'new'
})
// 新洞还未对接,需后完善
// newTraffic.value.startTraffic((res) => {
// carSocket(res)
// })
carSocket({
dangerousCount: 0,
truckCount: 0,
passengerCount: 0,
sedanCount: 0,
otherCount: 0
})
}
})
onBeforeUnmount(() => {
// 清理所有隧道实例的 WebSocket 连接
[newTraffic.value, traffic.value].forEach(instance => {
if (instance) {
instance.cleanRoad() // 这会调用 socket.close()
}
})
})
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
.traffic {
position: relative;
text-align: center;
......@@ -209,6 +319,23 @@ onBeforeMount(async () => {
right: 85px;
top: 15px;
}
}
.space-axis-block {
display: flex;
flex-direction: column;
// margin-top: 15px;
.title {
font-size: 14px;
line-height: 22px;
color: #fff;
display: flex;
align-items: center;
margin-left: 17px;
margin-bottom: 2px;
.icon {
margin-right: 5px;
}
}
}
}
</style>
<template>
<div class="section">
<Cameras ref="videosRef" :commons />
<div class="section">
<div class="flow-switch">
<div class="inner">
<Scales
:isShowSceneMode="false"
@plate-change="plateChange"
v-model:useMaterial="commons.useMaterial"
class="scale-ctrls"
/>
</div>
</div>
<!-- 返回全局视图 -->
<div class="back-home" @click="backHome">
<div class="text-bef">返回全局视图</div>
</div>
<!-- <Cameras ref="videosRef" :commons /> -->
<Traffic @camera-change="cameraChange" v-model="commons" />
</div>
</div>
<!-- <Video ref="videoRef" /> -->
<!-- <Video ref="videoRef" /> -->
</template>
<script lang='ts' setup>
import Cameras from './components/Cameras/index.vue'
<script lang="ts" setup>
// import Cameras from './components/Cameras/index.vue'
import Traffic from './components/Traffic/index.vue'
// import Video from '@/components/Video/index.vue'
import { useRoute } from 'vue-router'
import Scales from '@/views/traffic/components/Scales/index.vue'
import { useRoute, useRouter } from 'vue-router'
import { ref } from 'vue'
// const videoRef = ref<any>(null)
const videosRef = ref<any>(null)
const route = useRoute()
const router = useRouter()
// 公共参数
const commons = reactive({
......@@ -31,30 +43,102 @@ const commons = reactive({
useMaterial: false, // 是否使用贴图
plateType: 1, // 铭牌展示类型
direction: 0, // 车流方向
sectionAct: 1 // 当前断面
sectionAct: 1, // 当前断面
scrollLeft: null, // 滚动轴位置
holeType: 'old' // 洞类型
})
const cameraChange = (e) => {
videosRef.value.cameraSet(e)
// videosRef.value.cameraSet(e)
}
// 展示牌切换
const plateChange = (e) => {
commons.plateType = e
}
// 返回全局视图
const backHome = () => {
router.push({
name: 'Home'
})
}
onBeforeMount(() => {
const querys = route.query
commons.sectionAct = +querys.x
commons.direction = +querys.direction
commons.scrollLeft = querys.scrollLeft ?? null
commons.holeType = querys.holeType
})
// provide('openVideoPop', (e) => {
// videoRef.value?.diaOpen(e)
// })
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
$base-url: '@/assets/images/section/';
.section {
text-align: center;
}
.flow-switch {
color: white;
font-size: 14px;
z-index: 6;
position: absolute;
right: 10px;
top: 100px;
display: flex;
justify-content: space-between;
align-items: center;
.inner {
display: flex;
align-items: center;
justify-content: flex-end;
margin-right: 24px;
.text-aft {
margin-left: 6px;
}
.scale-ctrls {
margin-right: 20px;
}
}
:deep(.el-switch) {
--el-switch-on-color: #007dfa;
--el-switch-off-color: #203a60;
}
}
.back-home {
display: flex;
align-items: center;
justify-content: center;
width: 140px;
height: 32px;
border-radius: 4px;
// padding: 4px 12px;
// box-sizing: border-box;
background: rgba(0, 125, 250, 0.15);
font-size: 16px;
font-weight: normal;
line-height: 24px;
color: #ffffff;
margin: 30px 0 27px 44px;
cursor: pointer;
&::before {
display: inline-block;
content: '';
width: 16px;
height: 16px;
background: url($base-url + 'back.png') no-repeat;
background-size: cover;
margin-right: 5px;
}
}
}
</style>
......@@ -36,12 +36,14 @@
</div>
</el-popover>
<template v-if="isShowSceneMode">
<div @click="sceneChange('panorama')" :class="['x-btn', 'reduce', { 'disabled': sceneMode === 'panorama' }]">
<div class="inner"></div>
</div>
<div @click="sceneChange('section')" :class="['x-btn', 'enlarge', { 'disabled': sceneMode === 'section' }]">
<div class="inner"></div>
</div>
</template>
<slot></slot>
......@@ -53,6 +55,12 @@
import { ref, defineModel } from 'vue'
const props = withDefaults(defineProps<{
isShowSceneMode?: boolean
}>(), {
isShowSceneMode: true
})
const emits = defineEmits(['plate-change'])
const carTip = ref(1)
......
......@@ -177,8 +177,10 @@ class TrafficEngine {
ratex = 1 // 换算比
miles = 1800 // 隧道总长
road_w = 90 // 路面宽
// section = 300 // 段面长
section = 100
road_section_w = 270 // 缎面模式路面宽
start = 0
end = 1800
......@@ -218,12 +220,14 @@ class TrafficEngine {
if (secne === 'panorama') {
this.ratex = (appStore.getDesign - 180) / this.miles
this.lane_w = 45
// this.lane_w = 45
this.lane_w = this.road_w / this.lanes
}
if (secne === 'section') {
this.ratex = appStore.getDesign / this.section
this.lane_w = 125
// this.lane_w = 125
this.lane_w = this.road_section_w / this.lanes - 10
}
this.cars = []
......@@ -374,9 +378,11 @@ class TrafficEngine {
// 设施换算
facilitiesFormate (facilities) {
return facilities.map(({ start, end, type, direction }) => {
return facilities.map(({ leftStart, rightStart, width, type, direction }) => {
return {
x: start * this.ratex,
left_x: leftStart * this.ratex,
right_x: rightStart * this.ratex,
width: width * this.ratex,
type,
direction
}
......@@ -387,8 +393,8 @@ class TrafficEngine {
// 停车带换算
parkingFormate (packings) {
return packings.map(({ start }) => {
return { x: start * this.ratex }
return packings.map(({ start, end }) => {
return { x: start * this.ratex, long: (end - start) * this.ratex }
})
}
......
<template>
<div :class="['facilities', act ? 'act' : 'disabled']">
<div :class="['facilities', act ? 'act' : 'disabled']">
<div
v-for="(item, x) in facilities"
:key="x"
:class="['x-facility', item.type]"
:style="item.style"
></div>
</div>
</div>
</template>
<script lang='ts' setup>
<script lang="ts" setup>
import { useTunnelStoreWithOut } from '@/store/modules/tunnel'
const props = withDefaults(defineProps<{
const props = withDefaults(
defineProps<{
act: boolean
}>(), {
traffic: any
direction: number
}>(),
{
act: true
})
}
)
const tunnelStore = useTunnelStoreWithOut()
const tunnel = computed(() => {
return tunnelStore.getUpTunnel
})
const facilities = computed(() => {
if (!props.traffic) return []
const rax = 1920 / +tunnel.value.long
return tunnelStore.getFacilities.map(item => {
const { start, type } = item
return {
style: { left: start * rax + 'px' },
type
}
})
return props.traffic.facilitiesFormate(tunnelStore.getFacilities).map((item) => {
const { left_x,right_x, width, type } = item
const style = {}
style.left = left_x + 'px'
// style.width = width + 'px'
})
const temp = { type, style }
return temp
})
})
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
$base-url: '@/assets/images/common/';
.facilities {
......@@ -79,8 +77,9 @@ $base-url: '@/assets/images/common/';
width: 40px;
height: 100%;
&::after {
content: "";
width: 40px;
content: '';
// width: 40px;
width: 100%;
height: 100%;
position: absolute;
left: 50%;
......@@ -110,8 +109,5 @@ $base-url: '@/assets/images/common/';
// background-size: cover;
// }
}
}
</style>
......@@ -7,7 +7,7 @@
act ? 'act' : 'disabled'
]">
<template v-if="!isDownCtrls">
<template v-if="!isDownCtrls && section">
<!-- 段面控制器 -->
......@@ -26,7 +26,7 @@
</template>
<div class="main-road">
<div class="main-road" :style="roadStyle">
<!-- 停车带层 -->
<Parking v-bind="{ scene: 'panorama', direction, tunnel, _traffic }" />
......@@ -55,7 +55,7 @@
</div>
</div>
<template v-if="isDownCtrls">
<template v-if="isDownCtrls && section">
<!-- 刻度线 -->
<Scale v-bind="{ data:_sections, isDownCtrls, direction, tunnel }" />
......@@ -121,16 +121,18 @@ const props = withDefaults(
evtConfigs: any, // 事件详情
act: boolean, // 是否激活
traffic?: any, // 车流引擎
section?: boolean
filterType?: null | string, // 筛选类型
isDownCtrls?: boolean, // 是否下方展示控制器
trackers?: any
roadHeight?: number
}>(),
{
filterType: null,
isDownCtrls: false,
act: true,
trackers: {}
trackers: {},
roadHeight: 120
}
)
......@@ -143,6 +145,11 @@ const _eventSelected= ref(null) // 选中车辆
// const sectionAct = ref(null) // 当前段面
const start = ref(true) // 是否初次追踪
const _lines = ref(3)
const roadStyle = computed(() => {
return {
height: `${props.roadHeight}px`
}
})
const sectionAct = defineModel('sectionAct') // 选中段面
......@@ -330,19 +337,16 @@ $base-common: '@/assets/images/common/';
background: url($base-url + 'entry.png') no-repeat;
background-size: 100% 100%;
position: absolute;
left: 0;
top: 0;
z-index: 1;
}
.export {
height: 100%;
width: 90px;
background: url($base-url + 'entry.png') no-repeat;
background: url($base-url + 'export.png') no-repeat;
background-size: 100% 100%;
position: absolute;
right: 0;
top: 0;
transform: rotateY(180deg);
z-index: 1;
}
.inner {
......@@ -433,6 +437,13 @@ $base-common: '@/assets/images/common/';
margin-top: -60px;
}
&.in {
.entry {
right: 0;
transform: rotateY(180deg);
}
.export {
left: 0;
}
.running-car {
right: -60px;
}
......@@ -442,6 +453,13 @@ $base-common: '@/assets/images/common/';
}
&.out {
.entry {
left: 0;
}
.export {
right: 0;
transform: rotateY(180deg);
}
.running-car {
left: -60px;
.core {
......
......@@ -34,6 +34,7 @@ import { EVENT_TYPE } from '@/config/enum'
import EVENT_IMAGE from '@/assets/images/event/event.png'
import TrafficSocket from '@/views/traffic/utils/socket'
import { useRoute } from 'vue-router'
import { useTrafficStoreOut } from '@/store/modules/traffic'
const props = defineProps<{
scene: string,
......@@ -46,6 +47,7 @@ const props = defineProps<{
const events = defineModel<any[]>('events') // 实时事件
const selected = defineModel<any>('selected') // 选中事件
const socketEvent = ref<any>(null) // 事件流
const trafficStore = useTrafficStoreOut()
const route = useRoute()
......@@ -109,7 +111,9 @@ const evtFlowStart = async () => {
)
socketEvent.value.start(res => {
events.value = props._traffic.eventFormate(res)
trafficStore.setEventCount(res.eventCount)
trafficStore.setEventNotify(res.eventDetailRespVO)
events.value = props._traffic.eventFormate(res.eventThirdEventStatusNotFinishList)
})
}
......@@ -170,6 +174,11 @@ onMounted(() => {
})
onBeforeUnmount(() => {
// 清理事件实例的 WebSocket 连接
evtFlowClose()
})
</script>
......
......@@ -37,6 +37,7 @@ const packings = computed(() => {
const style = {}
style[props.direction ? 'left' : 'right'] = item.x + 'px'
style.width = item.long + 'px'
const temp = {
...item,
......
<template>
<div
class="custom-scrollbar-track"
ref="track"
@click="jumpToPosition"
:class="[act ? 'act' : 'disabled']"
>
<div class="custom-scrollbar-thumb" ref="thumb" @mousedown="startDrag">
<span class="bg"></span>
</div>
</div>
</template>
<script lang="ts" setup>
import { useAppStoreWithOut } from '@/store/modules/app'
const props = withDefaults(
defineProps<{
traffic: any // 车流引擎
direction: number
act?: boolean
}>(),
{
isDownCtrls: false,
act: true
}
)
const emits = defineEmits(['space-axis-scroll', 'drag-end'])
const appStore = useAppStoreWithOut()
const thumb = ref(null)
const track = ref(null)
const scrollContainerWidth = computed(() => {
let _sections = props.traffic?.sectionsCompute()
if (!_sections) return 0
return (_sections.length - 1 + _sections.slice(-1)[0]?.grow) * appStore.getDesign
})
let isDragging = false
// 计算滑块宽度
const updateThumbWidth = () => {
if (!thumb.value || !track.value) return
const visibleWidth = track.value.clientWidth
const thumbWidth = (visibleWidth / scrollContainerWidth.value) * visibleWidth
thumb.value.style.width = `${thumbWidth}px`
}
// 更新滑块位置
const updateThumb = ({ scrollLeft }) => {
if (!thumb.value || !track.value) return
const trackWidth = track.value.clientWidth
const thumbWidth = thumb.value.clientWidth
const maxLeft = trackWidth - thumbWidth
const thumbLeft = (scrollLeft / scrollContainerWidth.value) * trackWidth
thumb.value.style.left = `${Math.min(thumbLeft, maxLeft)}px`
}
// 点击轨道跳转
const jumpToPosition = (e) => {
if (!track.value) return
const trackWidth = track.value.clientWidth
const trackRect = track.value.getBoundingClientRect()
const clickPosition = e.clientX - trackRect.left
const thumbWidth = thumb.value.clientWidth
// 点击位置需要先进行缩放
const clickPositionCenter = clickPosition / appStore.zoom - thumbWidth / 2
const contentToTrackRatio = clickPositionCenter / trackWidth
const scrollLeft = contentToTrackRatio * scrollContainerWidth.value
emits('space-axis-scroll', scrollLeft)
emits('drag-end', scrollLeft)
}
// 拖动滑块
const startDrag = (e) => {
e.preventDefault()
isDragging = true
const startX = e.clientX
const startLeft = parseFloat(thumb.value.style.left || '0')
let actualScrollLeft = 0
const moveHandler = (e) => {
if (!isDragging || !thumb.value || !track.value) return
// 拖动需要先进行缩放
const deltaX = (e.clientX - startX) / appStore.zoom
const newLeft = Math.min(
Math.max(0, startLeft + deltaX),
track.value.clientWidth - thumb.value.clientWidth
)
thumb.value.style.left = `${newLeft}px`
const contentToTrackRatio = scrollContainerWidth.value / track.value.clientWidth
actualScrollLeft = newLeft * contentToTrackRatio
emits('space-axis-scroll', actualScrollLeft)
}
const upHandler = () => {
isDragging = false
emits('drag-end', actualScrollLeft)
document.removeEventListener('mousemove', moveHandler)
document.removeEventListener('mouseup', upHandler)
}
document.addEventListener('mousemove', moveHandler)
document.addEventListener('mouseup', upHandler)
}
// 设置滑块位置
const setThumbPosition = (scrollLeft: number) => {
thumb.value.style.left = `${scrollLeft}px`
}
watch(
scrollContainerWidth,
(e) => {
updateThumbWidth()
},
{ immediate: true }
)
onMounted(() => {
// updateThumbWidth()
})
defineExpose({
updateThumb,
setThumbPosition
})
</script>
<style lang="scss" scoped>
.custom-scrollbar-track {
position: relative;
// right: 0;
// top: 0;
width: 100%;
height: 16px;
background: #486794;
border-radius: 5px;
margin-bottom: 4px;
&.disabled {
opacity: 0.3;
pointer-events: none;
}
.custom-scrollbar-thumb {
position: absolute;
width: 110px;
height: 100%;
cursor: pointer;
transition: opacity 0.2s;
display: flex;
align-items: center;
justify-content: space-between;
opacity: 0.65;
.bg {
flex-grow: 1;
height: 100%;
margin: 0 2px;
background: rgba(255, 255, 255, 0.25);
}
&::before,
&::after {
display: inline-block;
content: '';
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-right: 5px solid #fff;
}
&::after {
border-left: 5px solid #fff;
border-right: none;
}
}
.custom-scrollbar-thumb:hover {
opacity: 1;
}
}
</style>
<template>
<div :class="[
<div
:class="[
'x-direction',
isDownCtrls ? 'down' : 'up',
+direction ? 'out' : 'in',
act ? 'act' : 'disabled'
]">
]"
>
<template v-if="!isDownCtrls && section">
<!-- 段面控制器 -->
<div class="section-box">
<Section
v-bind="{ data:_sections, scene: 'section', direction, trackers }"
v-bind="{ data: _sections, scene: 'section', direction, trackers }"
v-model="sectionAct"
@go-section="goSection"
/>
......@@ -23,27 +21,27 @@
<!-- 刻度线 -->
<Scale class="scale-line" v-bind="{ data: _sections, isDownCtrls, direction, tunnel }" />
</template>
<div class="main-road">
<div class="main-road" :style="roadStyle">
<!-- 停车带层 -->
<Parking v-bind="{ scene: 'panorama', direction, tunnel, _traffic: traffic }" />
<!-- 路面层 -->
<div class="level-road">
<div class="entry"></div>
<div class="export"></div>
<div class="inner">
<!-- 热力图层 -->
<HeatRoad class="dis-tag" :direction />
<!-- 摄像机层 -->
<Camera class="dis-tag" v-bind="{ scene: 'panorama', direction, isDownCtrls, tunnel, _traffic: traffic }" @camera-change="e => emits('camera-change', e)" />
<Camera
class="dis-tag"
v-bind="{ scene: 'panorama', direction, isDownCtrls, tunnel, _traffic: traffic }"
@camera-change="(e) => emits('camera-change', e)"
/>
<!-- 虚实线 -->
<Line v-bind="{ direction, tunnel, _traffic: traffic }" />
......@@ -58,11 +56,17 @@
v-model:selected="_carSelected"
v-bind="{
scene: 'panorama',
_traffic: traffic, _events, tunnel, filterType,
direction, plateType, useMaterial,
trackers, evtConfigs
_traffic: traffic,
_events,
tunnel,
filterType,
direction,
plateType,
useMaterial,
trackers,
evtConfigs
}"
@car-socket="e => emits('car-socket', e)"
@car-socket="(e) => emits('car-socket', e)"
/>
<!-- 事件层 -->
......@@ -71,49 +75,39 @@
v-model:selected="_eventSelected"
v-bind="{
scene: 'panorama',
_traffic: traffic, direction, evtConfigs, tunnel
_traffic: traffic,
direction,
evtConfigs,
tunnel
}"
/>
</div>
</div>
</div>
<template v-if="isDownCtrls && section">
<!-- 刻度线 -->
<Scale v-bind="{ data:_sections, isDownCtrls, direction, tunnel }" />
<Scale v-bind="{ data: _sections, isDownCtrls, direction, tunnel }" />
<!-- 段面控制器 -->
<Section
v-bind="{ data:_sections, isDownCtrls, scene: 'panorama', direction }"
v-bind="{ data: _sections, isDownCtrls, scene: 'panorama', direction }"
@go-section="goSection"
/>
</template>
</div>
<CarCollect
v-model="_carSelected"
:cars="_cars"
ref="collectRef"
@go-track="e => emits('go-track', e)"
/>
<EventPop
:events="_events"
v-model="_eventSelected"
ref="eventRef"
@go-track="(e) => emits('go-track', e)"
/>
<EventPop :events="_events" v-model="_eventSelected" ref="eventRef" />
</template>
<script lang="ts" setup>
import Section from './comps/Section/index.vue'
import Scale from './comps/Scale/index.vue'
import Parking from './comps/Parking/index.vue'
......@@ -132,7 +126,14 @@ import tunnelConfigs from '@/config/tunnel'
import { useTunnelStoreWithOut } from '@/store/modules/tunnel'
const emits = defineEmits(['go-section', 'cars-update', 'open-event', 'go-track', 'car-socket', 'camera-change'])
const emits = defineEmits([
'go-section',
'cars-update',
'open-event',
'go-track',
'car-socket',
'camera-change'
])
const tunnelStore = useTunnelStoreWithOut()
const collectRef = ref<any>(null) // 收藏弹窗实例
......@@ -140,46 +141,49 @@ const eventRef = ref<any>(null) // 事件弹窗实例
const props = withDefaults(
defineProps<{
direction: number, // 道路方向
plateType: number, // 铭牌类型
useMaterial: boolean, // 是否使用动态车辆贴图
evtConfigs: any, // 事件详情
act: boolean, // 是否激活
traffic?: any, // 车流引擎
section?: boolean,
filterType?: null | string, // 筛选类型
isDownCtrls?: boolean, // 是否下方展示控制器
direction: number // 道路方向
plateType: number // 铭牌类型
useMaterial: boolean // 是否使用动态车辆贴图
evtConfigs: any // 事件详情
act: boolean // 是否激活
traffic?: any // 车流引擎
section?: boolean
filterType?: null | string // 筛选类型
isDownCtrls?: boolean // 是否下方展示控制器
trackers?: any
roadHeight?: number // 路面高度
}>(),
{
section: true,
filterType: null,
isDownCtrls: false,
act: true,
trackers: {}
trackers: {},
roadHeight: 90
}
)
const _traffic = ref<any>(null) // 车流引擎
const _sections = ref<any[]>([]) // 段面控制
const _cars = ref<any[]>([]) // 车流
const _carSelected= ref(null) // 选中车辆
const _carSelected = ref(null) // 选中车辆
const _events = ref<any[]>([]) // 事件列表
const _eventSelected= ref(null) // 选中车辆
const _eventSelected = ref(null) // 选中车辆
// const sectionAct = ref(null) // 当前段面
const start = ref(true) // 是否初次追踪
const _lines = ref(3)
const roadStyle = computed(() => {
return {
height: `${props.roadHeight}px`
}
})
const sectionAct = defineModel('sectionAct') // 选中段面
const tunnel = computed(() => {
return [
tunnelStore.getUpTunnel,
tunnelStore.getDownTunnel
][props.direction]
return [tunnelStore.getUpTunnel, tunnelStore.getDownTunnel][props.direction]
})
watch(
() => props.traffic,
(e) => {
......@@ -206,10 +210,10 @@ watch(
watch(
() => props.trackers.car,
(xcar) => {
if (xcar && props.trackers.tracking) {
const distance = props.direction ? xcar.originalCar.carMilestone - tunnel.value.start : tunnel.value.end - xcar.originalCar.carMilestone
const distance = props.direction
? xcar.originalCar.carMilestone - tunnel.value.start
: tunnel.value.end - xcar.originalCar.carMilestone
// const left = distance % 300
// const sections = (distance - left) / 300 + (left > 0 ? 1 : 0)
......@@ -234,9 +238,6 @@ const goSection = (x) => {
direction: props.direction
})
}
</script>
<style lang="scss" scoped>
......@@ -265,8 +266,6 @@ $base-common: '@/assets/images/common/';
}
}
.ctrls {
display: inline-flex;
align-items: center;
......@@ -332,25 +331,23 @@ $base-common: '@/assets/images/common/';
position: absolute;
background: url($base-url + 'road.png');
background-size: auto 100%;
.entry {
height: 100%;
width: 90px;
background: url($base-url + 'entry.png') no-repeat;
background-size: 100% 100%;
position: absolute;
left: 0;
top: 0;
z-index: 1;
}
.export {
height: 100%;
width: 90px;
background: url($base-url + 'entry.png') no-repeat;
background: url($base-url + 'export.png') no-repeat;
background-size: 100% 100%;
position: absolute;
right: 0;
top: 0;
transform: rotateY(180deg);
z-index: 1;
}
.inner {
......@@ -418,7 +415,6 @@ $base-common: '@/assets/images/common/';
}
.down {
.stone-num {
bottom: 12px;
}
......@@ -433,6 +429,13 @@ $base-common: '@/assets/images/common/';
margin-top: -60px;
}
&.in {
.entry {
right: 0;
transform: rotateY(180deg);
}
.export {
left: 0;
}
.running-car {
right: -60px;
}
......@@ -442,6 +445,13 @@ $base-common: '@/assets/images/common/';
}
&.out {
.entry {
left: 0;
}
.export {
right: 0;
transform: rotateY(180deg);
}
.running-car {
left: -60px;
.core {
......
<template>
<!-- {{ sectionTrans }} -->
<div :class="['x-direction', ['in', 'out'][direction], act ? 'act' : 'disabled']">
<template v-if="section">
<!-- 段面控制器 -->
<div class="section-box">
<Section
v-bind="{ data: _sections, scene: 'section', direction }"
v-model="sectionAct"
@go-section="handleChange"
/>
<TrackPoint v-bind="{ trackers, direction }" />
</div>
<!-- 刻度线 -->
<Scale v-bind="{ data: _sections, direction, tunnel }" />
</template>
<div class="container-block">
<div class="road-scroll" ref="roadScrollRef" @scroll="roadScroll">
<!-- 监控视频层 -->
<Cameras v-if="act" v-bind="{ direction, tunnel, _traffic: traffic }" ref="camerasRef" />
<div class="road-scale" :style="{ height: roadHeight + 'px' }">
<div :style="roadStyle" class="main-road">
<!-- 停车带层 -->
<Parking v-bind="{ scene: 'section', direction, tunnel, _traffic: traffic }" />
<!-- 摄像机层 -->
<Camera
v-if="act"
v-bind="{ direction, tunnel, _traffic: traffic }"
@camera-change="(e) => emits('camera-change', e)"
/>
<!-- 路面层 -->
<div class="level-road">
<!-- 热力图层 -->
<HeatRoad :direction />
<!-- 虚实线 -->
<Line class="line-1" v-bind="{ direction, tunnel, _traffic: traffic }" />
<Line class="line-2" v-bind="{ direction, tunnel, _traffic: traffic }" />
<!-- 路标 -->
<Arrow v-bind="{ direction, tunnel, _traffic: traffic }" />
<!-- 车流层 -->
<!-- <Cars
v-if="act"
v-model:cars="_cars"
v-model:selected="_carSelected"
v-bind="{
scene: 'section',
_traffic: traffic,
_events,
tunnel,
filterType,
direction,
plateType,
useMaterial,
trackers,
evtConfigs
}"
@car-socket="(e) => emits('car-socket', e)"
/> -->
<!-- 事件层 -->
<!-- <Events
v-model:events="_events"
v-model:selected="_eventSelected"
v-bind="{
scene: 'section',
_traffic: traffic,
direction,
evtConfigs,
tunnel
}"
/> -->
</div>
<!-- 桩号层 -->
<Pile v-bind="{ direction, tunnel, _traffic: traffic }" />
</div>
</div>
</div>
</div>
<CarCollect
v-model="_carSelected"
:cars="_cars"
ref="collectRef"
@go-track="(e) => emits('go-track', e)"
/>
<EventPop :events="_events" v-model="_eventSelected" ref="eventRef" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import Section from '../XDirection/comps/Section/index.vue'
import Scale from '../XDirection/comps/Scale/index.vue'
import Parking from '../XDirection/comps/Parking/index.vue'
import Camera from '../XDirection/comps/Camera/index.vue'
import Line from '../XDirection/comps/Line/index.vue'
import Arrow from '../XDirection/comps/Arrow/index.vue'
import Cars from '../XDirection/comps/Cars/index.vue'
import Events from '../XDirection/comps/Events/index.vue'
import Pile from '../XDirection/comps/Pile/index.vue'
import HeatRoad from '../XDirection/comps/HeatRoad/index.vue'
import TrackPoint from '../XDirection/comps/TrackPoint/index.vue'
import Cameras from './comps/Cameras/index.vue'
import CarCollect from '@/views/traffic/components/CarCollect/index.vue'
import EventPop from '@/views/traffic/components/EventPop/index.vue'
import tunnelConfigs from '@/config/tunnel'
import TrafficEngine from '@/views/traffic/roads/Engine/index'
import { useAppStoreWithOut } from '@/store/modules/app'
import { useTunnelStoreWithOut } from '@/store/modules/tunnel'
import { useRoute } from 'vue-router'
const props = withDefaults(
defineProps<{
filterType?: null | string // 筛选类型
trackers?: any // 追踪车辆
evtConfigs?: any // 事件详情
traffic?: any // 车流引擎
direction: number // 道路方向
plateType: number // 铭牌类型
useMaterial: boolean // 是否使用动态车辆贴图
section?: boolean // 是否显示段面控制器
scrollLeft?: number | null // 滚动条位置
roadHeight?: number // 路面高度
act?: boolean // 是否激活
}>(),
{
trackers: {},
filterType: null,
section: true,
act: true
}
)
const emits = defineEmits([
'open-video',
'cars-update',
'go-track',
'car-socket',
'section-change',
'camera-change',
'update-space-axis'
])
const tunnelStore = useTunnelStoreWithOut()
const appStore = useAppStoreWithOut()
const route = useRoute()
const collectRef = ref<any>(null) // 车辆 pop
const eventRef = ref<any>(null) // 事件 pop
const sectionTrans = ref<any>(null) // 路段动画
const start = ref(true) // 是否初次追踪
// const sectionAct = ref(0)
const firstSetRoadScroll = ref<boolean>(true) // 是否初次设置滚动条
const roadScrollLeft = ref<number | null>(null) // 滚动条位置
const _traffic = ref<any>(null) // 车流引擎
const _sections = ref<any[]>([]) // 段面控制
const _cars = ref<any>() // 车流
const _carSelected = ref(null) // 选中车辆
const _events = ref<any[]>([]) // 事件
const _eventSelected = ref(null) // 选中车辆
const sectionAct = defineModel('sectionAct') // 选中段面
const tunnel = computed(() => {
return [tunnelStore.getUpTunnel, tunnelStore.getDownTunnel][props.direction]
})
// 车道初始化
const trafficInit = (e) => {
if (_traffic.value) return
if (props.traffic) {
_traffic.value = props.traffic
} else {
const { start, end, long } = tunnel.value
_traffic.value = new TrafficEngine('section', {
direction: props.direction,
start: +start,
end: +end,
miles: +long
})
}
_sections.value = _traffic.value.sectionsCompute()
nextTick(() => {
sectionChange(sectionAct.value, true)
})
}
// 车流切面
watch(
() => _cars.value,
(e) => {
emits('cars-update', e)
}
)
// 事件段面定位
watch(
() => [props.evtConfigs?.infos, tunnel.value?.long],
([infos, _long]) => {
if (!infos || !_long) return
const { start, end, long } = tunnel.value
const num = +infos.milestone
const distance = props.direction ? num - start : end - num
// const left = distance % 300
// sectionAct.value = (distance - left) / 300 + (left > 0 ? 1 : 0) - 1
const left = distance % tunnelConfigs.perSection
sectionAct.value = (distance - left) / tunnelConfigs.perSection + (left > 0 ? 1 : 0) - 1
nextTick(() => {
sectionChange(sectionAct.value, true)
})
},
{
immediate: true
}
)
// 段面物理总长
const roadStyle = computed(() => {
if (!_sections.value.length) return
const x = _sections.value.length - 1 + _sections.value.slice(-1)[0].grow
return {
width: x * appStore.getDesign + 'px',
height: `${props.roadHeight}px`,
...sectionTrans.value
}
})
// 缎面切换
const sectionChange = (x, direct?: boolean, isScrollLeft?: boolean = false) => {
if (!props.act) return
emits('section-change', x)
let realx = x
let thumb_x = x
if (x >= _sections.value.length - 1) {
realx = _sections.value.length - 2 + _sections.value.slice(-1)[0].grow
thumb_x = _sections.value.length - 1 + _sections.value.slice(-1)[0].grow
}
const dx = Math.abs(sectionAct.value - x)
// 跳过动画
// if (direct) {
// sectionTrans.value = {
// transform: `translateX(${+props.direction ? '-' : ''}${realx * appStore.getDesign}px)`
// }
// } else {
// sectionTrans.value = {
// transform: `translateX(${+props.direction ? '-' : ''}${realx * appStore.getDesign}px)`,
// transitionProperty: 'all',
// transitionDuration: `${1 * dx}s`,
// transitionTimingFunction: 'ease-in-out'
// }
// }
let scrollLeft = !props.direction
? (_sections.value.length - 1 + _sections.value.slice(-1)[0].grow - thumb_x) *
appStore.getDesign
: thumb_x * appStore.getDesign
if (isScrollLeft) {
scrollLeft = roadScrollLeft.value
}
// 滚动条定位
setRoadScroll(scrollLeft, direct)
sectionAct.value = x
}
// 车辆追踪
watch(
() => props.trackers.car,
(xcar) => {
if (xcar && props.trackers.tracking) {
// const distance = props.direction ? xcar.originalCar.carMilestone - tunnel.value.start : tunnel.value.end - xcar.originalCar.carMilestone
const distance = xcar.distance
// const left = distance % 300
// const sections = (distance - left) / 300 + (left > 0 ? 1 : 0)
const left = distance % tunnelConfigs.perSection
const sections = (distance - left) / tunnelConfigs.perSection + (left > 0 ? 1 : 0)
sectionChange(sections - 1, start.value)
start.value = false
}
},
{
immediate: true,
deep: true
}
)
watch(
() => props.traffic,
(e) => {
if (e) {
_sections.value = e.sectionsCompute()
nextTick(() => {
if (firstSetRoadScroll.value) {
roadScrollLeft.value = props.scrollLeft
}
// const isScrollLeft = props.scrollLeft != null && firstSetRoadScroll.value ? true : false
const isScrollLeft = props.scrollLeft != null ? true : false
sectionChange(sectionAct.value, true, isScrollLeft)
firstSetRoadScroll.value = false
})
}
},
{
immediate: true
}
)
const handleChange = (x) => {
if (props.trackers.tracking) {
return ElMessage.warning('轨迹追踪过程中不支持此操作!')
} else {
sectionChange(x, true)
}
}
const roadScrollRef = ref<HTMLDivElement>(null)
const roadScroll = () => {
const scrollLeft = roadScrollRef.value?.scrollLeft || 0
emits('update-space-axis', scrollLeft)
}
// 设置路面滚动位置
const setRoadScroll = (scrollLeft, direct?: boolean = true) => {
roadScrollRef.value.scrollLeft = scrollLeft
roadScrollLeft.value = scrollLeft
// 是否平滑滚动
if (!direct) {
roadScrollRef.value.style['scroll-behavior'] = 'smooth'
}
}
defineExpose({
sectionChange,
setRoadScroll
})
// 打开单车弹窗
provide('openCarPop', (xcar) => {
return collectRef.value.diaOpen(xcar)
})
// onBeforeMount(() => {
// if (route.query.x) {
// sectionAct.value = +route.query.x
// }
// })
</script>
<style lang="scss" scoped>
@import url(@/views/traffic/css/car.scss);
$base-url: '@/assets/images/home/';
$base-event: '@/assets/images/event/';
$base-common: '@/assets/images/common/';
.x-direction {
text-align: center;
&.disabled {
.main-road {
opacity: 0.3;
pointer-events: none;
}
}
.section-box {
position: relative;
}
.custom-scrollbar-track {
position: relative;
// right: 0;
// top: 0;
width: 100%;
height: 16px;
background: #486794;
border-radius: 5px;
}
.custom-scrollbar-thumb {
position: absolute;
width: 110px;
height: 100%;
background: rgba(255, 255, 255, 0.1);
// border-radius: 5px;
cursor: pointer;
transition: background 0.2s;
}
.custom-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
}
}
.ctrls {
display: inline-flex;
align-items: center;
height: 42px;
margin: 0 auto;
.title {
color: white;
width: 316px;
height: 38px;
display: flex;
align-items: center;
justify-content: space-between;
background-image: url($base-url + 'arrow_lft.png');
background-repeat: no-repeat;
background-size: 100% auto;
background-position: 0 bottom;
padding-bottom: 4px;
.text {
font-size: 24px;
font-weight: 600;
margin-left: 32px;
}
.more {
font-size: 16px;
color: rgba(240, 245, 255, 0.95);
display: flex;
}
}
}
.filters {
display: flex;
margin-left: 15px;
.x-car {
height: 32px;
font-size: 14px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 16px;
color: #ffffff;
background: rgba(0, 125, 250, 0.15);
margin-left: 8px;
cursor: pointer;
&.act {
background: rgba(124, 213, 255, 0.15);
box-sizing: border-box;
border: 1px solid #7cd5ff;
}
}
}
.container-block {
overflow: hidden;
}
.road-scroll {
overflow-x: scroll;
scrollbar-width: none;
}
.road-scale {
padding-top: 30px;
height: 270px;
padding-bottom: 30px;
position: relative;
}
.main-road {
height: 270px;
position: relative;
position: absolute;
.line {
height: 2px;
position: absolute;
top: 50%;
width: 100%;
margin-top: -1px;
.x-line {
height: 2px;
position: absolute;
&.dash {
background: url($base-url + 'dash.png');
background-size: 30px 2px;
}
&.solid {
background: rgba($color: #ffffff, $alpha: 0.8);
}
}
}
.level-road {
top: 0;
left: 0;
height: 100%;
width: 100%;
position: absolute;
background: url($base-url + 'road.png');
background-size: auto 100%;
.entry {
height: 270px;
width: 267px;
background: url($base-url + 'entry.png') no-repeat;
background-size: 100%;
position: absolute;
background-position-y: 10px;
left: 0;
top: 0;
z-index: 1;
}
.export {
height: 270px;
width: 267px;
background: url($base-url + 'entry.png') no-repeat;
background-size: 100%;
position: absolute;
background-position-y: 10px;
right: 0;
top: 0;
transform: rotateY(180deg);
z-index: 1;
}
.line {
height: 2px;
position: absolute;
// top: 50%;
width: 100%;
margin-top: -1px;
.x-line {
height: 2px;
position: absolute;
&.dash {
background: url($base-url + 'dash.png');
background-size: 30px 2px;
}
&.solid {
background: rgba($color: #ffffff, $alpha: 0.8);
}
}
&.line-1 {
top: 33.33%;
}
&.line-2 {
top: 66.66%;
}
}
}
}
.x-direction {
&.in {
.running-car {
right: -60px;
}
.main-road {
// right: 0;
}
.sections {
flex-direction: row-reverse;
}
}
&.out {
.running-car {
left: -60px;
.core {
transform: rotate(180deg);
}
}
.main-road {
// left: 0;
}
.x-camera {
background: url($base-url + 'camera_3.png');
background-size: cover;
&:hover {
background: url($base-url + 'camera_4.png');
background-size: cover;
}
&.act {
background: url($base-url + 'camera_4.png');
background-size: cover;
}
}
.level-camera {
left: 0;
}
.stone-num {
&.first {
transform: translateX(0%);
}
&.last {
transform: translateX(-100%);
}
}
.level-parking {
left: 0;
}
.road-arrow {
transform: rotateY(180deg);
}
}
}
</style>
<template>
<div :class="['x-direction-section-camera', direction ? 'out' : 'in']">
<div
v-for="(item, x) in cameras"
:key="x"
:style="item.style"
:class="['x-camera', { act: cameraSelected.includes(x) || camerasFocus.includes(item.id) }]"
>
<div class="video-view">
<VideoPlayer
@click="() => videoFocus(x)"
v-bind="{
frameW: 368,
frameH: 234,
vcode: item.cameraIndexCode,
pileNumber: item.pileNumber
}"
/>
</div>
<div class="footer">
<div class="pile">{{ item.pileNumber }}</div>
<!-- <div class="speed">平均车速:80km/h</div> -->
</div>
<div @click="openVideo(item)" class="full-screen"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { getCurrentInstance } from 'vue'
import { useRoute } from 'vue-router'
import bus from '@/utils/eventBus'
import VideoPlayer from '@/views/components/VideoPlayer/index.vue'
// import { useCamera as useCameraHome } from '@/flash/Home'
import { getCameras, setCameras } from '@/flash/Home'
// store
const app = getCurrentInstance()
const route = useRoute()
// 事件
const emits = defineEmits(['camera-change', 'click-trigger'])
// 参数
const props = withDefaults(
defineProps<{
_traffic: any
tunnel: any
direction: number
}>(),
{}
)
const camerasFocus = ref<string[]>([])
// 选中摄像头计算
const cameraSelected = computed(() => {
const block = route.name
if (block === 'Home') {
return getCameras(props.direction)
}
return camerasFocus.value
})
// 路面摄像头铺设
const cameras = computed(() => {
if (!props._traffic) return []
return props._traffic.cameraFormate(props.tunnel.cameras, props.direction).map((item) => {
return {
...item,
style: {
left: item.x + 'px'
}
}
})
})
// 摄像头手动切换
const goSetCameras = (xcamera) => {
const block = route.name
if (block === 'Home') {
return setCameras(xcamera, props.direction)
} else {
emits('camera-change', xcamera)
}
}
onMounted(() => {
bus.on('focus-camera', (ids) => {
camerasFocus.value = ids
})
})
</script>
<style lang="scss" scoped>
$base-url: '@/assets/images/home/';
.x-direction-section-camera {
position: relative;
// left: 0;
width: 100%;
height: 256px;
margin: 27px 0 15px;
// z-index: 2;
.x-camera {
width: 368px;
height: 256px;
cursor: pointer;
position: absolute;
transform: translateX(-50%);
.video-view {
position: relative;
height: 234px;
box-sizing: border-box;
border: 1px solid #8eb8d1;
border-bottom: none;
border-radius: 6px 6px 0 0;
overflow: hidden;
}
.footer {
height: 24px;
display: flex;
align-items: center;
justify-content: space-between;
// position: absolute;
padding: 3px 8px;
box-sizing: border-box;
// left: 0;
// bottom: 0;
background: #35669c;
font-size: 12px;
font-weight: 600;
line-height: 18px;
color: rgba(240, 245, 255, 0.95);
border-radius: 0 0 6px 6px;
}
}
}
</style>
<template>
<!-- {{ sectionTrans }} -->
<div :class="['x-direction', ['in', 'out'][direction], act ? 'act' : 'disabled']">
<template v-if="section">
<!-- 段面控制器 -->
<div class="section-box">
<Section
v-bind="{ data: _sections, scene: 'section', direction }"
v-model="sectionAct"
@go-section="handleChange"
/>
<TrackPoint v-bind="{ trackers, direction }" />
</div>
<!-- 刻度线 -->
<Scale v-bind="{ data: _sections, direction, tunnel }" />
</template>
<div class="container-block">
<div class="road-scroll" ref="roadScrollRef" @scroll="roadScroll">
<!-- 监控视频层 -->
<Cameras v-if="act" v-bind="{ direction, tunnel, _traffic: traffic }" ref="camerasRef" />
<div class="road-scale" :style="{ height: roadHeight + 'px' }">
<div :style="roadStyle" class="main-road">
<!-- 停车带层 -->
<Parking v-bind="{ scene: 'section', direction, tunnel, _traffic: traffic }" />
<!-- 摄像机层 -->
<Camera
v-if="act"
v-bind="{ direction, tunnel, _traffic: traffic }"
@camera-change="(e) => emits('camera-change', e)"
/>
<!-- 路面层 -->
<div class="level-road">
<!-- 热力图层 -->
<HeatRoad :direction />
<!-- 虚实线 -->
<Line v-bind="{ direction, tunnel, _traffic: traffic }" />
<!-- 路标 -->
<Arrow v-bind="{ direction, tunnel, _traffic: traffic }" />
<!-- 车流层 -->
<Cars
v-if="act"
v-model:cars="_cars"
v-model:selected="_carSelected"
v-bind="{
scene: 'section',
_traffic: traffic,
_events,
tunnel,
filterType,
direction,
plateType,
useMaterial,
trackers,
evtConfigs
}"
@car-socket="(e) => emits('car-socket', e)"
/>
<!-- 事件层 -->
<Events
v-model:events="_events"
v-model:selected="_eventSelected"
v-bind="{
scene: 'section',
_traffic: traffic,
direction,
evtConfigs,
tunnel
}"
/>
</div>
<!-- 桩号层 -->
<Pile v-bind="{ direction, tunnel, _traffic: traffic }" />
</div>
</div>
</div>
</div>
<CarCollect
v-model="_carSelected"
:cars="_cars"
ref="collectRef"
@go-track="(e) => emits('go-track', e)"
/>
<EventPop :events="_events" v-model="_eventSelected" ref="eventRef" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import Section from '../XDirection/comps/Section/index.vue'
import Scale from '../XDirection/comps/Scale/index.vue'
import Parking from '../XDirection/comps/Parking/index.vue'
import Camera from '../XDirection/comps/Camera/index.vue'
import Line from '../XDirection/comps/Line/index.vue'
import Arrow from '../XDirection/comps/Arrow/index.vue'
import Cars from '../XDirection/comps/Cars/index.vue'
import Events from '../XDirection/comps/Events/index.vue'
import Pile from '../XDirection/comps/Pile/index.vue'
import HeatRoad from '../XDirection/comps/HeatRoad/index.vue'
import TrackPoint from '../XDirection/comps/TrackPoint/index.vue'
import Cameras from './comps/Cameras/index.vue'
import CarCollect from '@/views/traffic/components/CarCollect/index.vue'
import EventPop from '@/views/traffic/components/EventPop/index.vue'
import tunnelConfigs from '@/config/tunnel'
import TrafficEngine from '@/views/traffic/roads/Engine/index'
import { useAppStoreWithOut } from '@/store/modules/app'
import { useTunnelStoreWithOut } from '@/store/modules/tunnel'
import { useRoute } from 'vue-router'
const props = withDefaults(
defineProps<{
filterType?: null | string // 筛选类型
trackers?: any // 追踪车辆
evtConfigs?: any // 事件详情
traffic?: any // 车流引擎
direction: number // 道路方向
plateType: number // 铭牌类型
useMaterial: boolean // 是否使用动态车辆贴图
section?: boolean // 是否显示段面控制器
scrollLeft?: number | null // 滚动条位置
roadHeight?: number // 路面高度
act?: boolean // 是否激活
}>(),
{
trackers: {},
filterType: null,
section: true,
act: true
}
)
const emits = defineEmits([
'open-video',
'cars-update',
'go-track',
'car-socket',
'section-change',
'camera-change',
'update-space-axis'
])
const tunnelStore = useTunnelStoreWithOut()
const appStore = useAppStoreWithOut()
const route = useRoute()
const collectRef = ref<any>(null) // 车辆 pop
const eventRef = ref<any>(null) // 事件 pop
const sectionTrans = ref<any>(null) // 路段动画
const start = ref(true) // 是否初次追踪
// const sectionAct = ref(0)
const firstSetRoadScroll = ref<boolean>(true) // 是否初次设置滚动条
const roadScrollLeft = ref<number | null>(null) // 滚动条位置
const _traffic = ref<any>(null) // 车流引擎
const _sections = ref<any[]>([]) // 段面控制
const _cars = ref<any>() // 车流
const _carSelected = ref(null) // 选中车辆
const _events = ref<any[]>([]) // 事件
const _eventSelected = ref(null) // 选中车辆
const sectionAct = defineModel('sectionAct') // 选中段面
const tunnel = computed(() => {
return [tunnelStore.getUpTunnel, tunnelStore.getDownTunnel][props.direction]
})
// 车道初始化
const trafficInit = (e) => {
if (_traffic.value) return
if (props.traffic) {
_traffic.value = props.traffic
} else {
const { start, end, long } = tunnel.value
_traffic.value = new TrafficEngine('section', {
direction: props.direction,
start: +start,
end: +end,
miles: +long
})
}
_sections.value = _traffic.value.sectionsCompute()
nextTick(() => {
sectionChange(sectionAct.value, true)
})
}
// 车流切面
watch(
() => _cars.value,
(e) => {
emits('cars-update', e)
}
)
// 事件段面定位
watch(
() => [props.evtConfigs?.infos, tunnel.value?.long],
([infos, _long]) => {
if (!infos || !_long) return
const { start, end, long } = tunnel.value
const num = +infos.milestone
const distance = props.direction ? num - start : end - num
// const left = distance % 300
// sectionAct.value = (distance - left) / 300 + (left > 0 ? 1 : 0) - 1
const left = distance % tunnelConfigs.perSection
sectionAct.value = (distance - left) / tunnelConfigs.perSection + (left > 0 ? 1 : 0) - 1
nextTick(() => {
sectionChange(sectionAct.value, true)
})
},
{
immediate: true
}
)
// 段面物理总长
const roadStyle = computed(() => {
if (!_sections.value.length) return
const x = _sections.value.length - 1 + _sections.value.slice(-1)[0].grow
return {
width: x * appStore.getDesign + 'px',
height: `${props.roadHeight}px`,
...sectionTrans.value
}
})
// 缎面切换
const sectionChange = (x, direct?: boolean, isScrollLeft: boolean = false) => {
if (!props.act) return
emits('section-change', x)
let realx = x
let thumb_x = x
if (x >= _sections.value.length - 1) {
realx = _sections.value.length - 2 + _sections.value.slice(-1)[0].grow
thumb_x = _sections.value.length - 1 + _sections.value.slice(-1)[0].grow
}
const dx = Math.abs(sectionAct.value - x)
// 跳过动画
// if (direct) {
// sectionTrans.value = {
// transform: `translateX(${+props.direction ? '-' : ''}${realx * appStore.getDesign}px)`
// }
// } else {
// sectionTrans.value = {
// transform: `translateX(${+props.direction ? '-' : ''}${realx * appStore.getDesign}px)`,
// transitionProperty: 'all',
// transitionDuration: `${1 * dx}s`,
// transitionTimingFunction: 'ease-in-out'
// }
// }
let scrollLeft: number = !props.direction
? (_sections.value.length - 1 + _sections.value.slice(-1)[0].grow - thumb_x) *
appStore.getDesign
: thumb_x * appStore.getDesign
if (isScrollLeft) {
scrollLeft = roadScrollLeft.value
}
// 滚动条定位
setRoadScroll(scrollLeft, direct)
sectionAct.value = x
}
// 车辆追踪
watch(
() => props.trackers.car,
(xcar) => {
if (xcar && props.trackers.tracking) {
// const distance = props.direction ? xcar.originalCar.carMilestone - tunnel.value.start : tunnel.value.end - xcar.originalCar.carMilestone
const distance = xcar.distance
// const left = distance % 300
// const sections = (distance - left) / 300 + (left > 0 ? 1 : 0)
const left = distance % tunnelConfigs.perSection
const sections = (distance - left) / tunnelConfigs.perSection + (left > 0 ? 1 : 0)
sectionChange(sections - 1, start.value)
start.value = false
}
},
{
immediate: true,
deep: true
}
)
watch(
() => props.traffic,
(e) => {
if (e) {
_sections.value = e.sectionsCompute()
nextTick(() => {
if (firstSetRoadScroll.value) {
roadScrollLeft.value = props.scrollLeft
}
const isScrollLeft = props.scrollLeft != null ? true : false
sectionChange(sectionAct.value, true, isScrollLeft)
firstSetRoadScroll.value = false
})
}
},
{
immediate: true
}
)
const handleChange = (x) => {
if (props.trackers.tracking) {
return ElMessage.warning('轨迹追踪过程中不支持此操作!')
} else {
sectionChange(x, true)
}
}
const roadScrollRef = ref<HTMLDivElement>(null)
const roadScroll = () => {
const scrollLeft = roadScrollRef.value?.scrollLeft || 0
emits('update-space-axis', scrollLeft)
}
// 设置路面滚动位置
const setRoadScroll = (scrollLeft, direct?: boolean = true) => {
roadScrollRef.value.scrollLeft = scrollLeft
roadScrollLeft.value = scrollLeft
// 是否平滑滚动
if (!direct) {
roadScrollRef.value.style['scroll-behavior'] = 'smooth'
}
}
defineExpose({
sectionChange,
setRoadScroll
})
// 打开单车弹窗
provide('openCarPop', (xcar) => {
return collectRef.value.diaOpen(xcar)
})
// onBeforeMount(() => {
// if (route.query.x) {
// sectionAct.value = +route.query.x
// }
// })
</script>
<style lang="scss" scoped>
@import url(@/views/traffic/css/car.scss);
$base-url: '@/assets/images/home/';
$base-event: '@/assets/images/event/';
$base-common: '@/assets/images/common/';
.x-direction {
text-align: center;
&.disabled {
.main-road {
opacity: 0.3;
pointer-events: none;
}
}
.section-box {
position: relative;
}
.custom-scrollbar-track {
position: relative;
// right: 0;
// top: 0;
width: 100%;
height: 16px;
background: #486794;
border-radius: 5px;
}
.custom-scrollbar-thumb {
position: absolute;
width: 110px;
height: 100%;
background: rgba(255, 255, 255, 0.1);
// border-radius: 5px;
cursor: pointer;
transition: background 0.2s;
}
.custom-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
}
}
.ctrls {
display: inline-flex;
align-items: center;
height: 42px;
margin: 0 auto;
.title {
color: white;
width: 316px;
height: 38px;
display: flex;
align-items: center;
justify-content: space-between;
background-image: url($base-url + 'arrow_lft.png');
background-repeat: no-repeat;
background-size: 100% auto;
background-position: 0 bottom;
padding-bottom: 4px;
.text {
font-size: 24px;
font-weight: 600;
margin-left: 32px;
}
.more {
font-size: 16px;
color: rgba(240, 245, 255, 0.95);
display: flex;
}
}
}
.filters {
display: flex;
margin-left: 15px;
.x-car {
height: 32px;
font-size: 14px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 16px;
color: #ffffff;
background: rgba(0, 125, 250, 0.15);
margin-left: 8px;
cursor: pointer;
&.act {
background: rgba(124, 213, 255, 0.15);
box-sizing: border-box;
border: 1px solid #7cd5ff;
}
}
}
.container-block {
overflow: hidden;
}
.road-scroll {
overflow-x: scroll;
scrollbar-width: none;
}
.road-scale {
padding-top: 30px;
height: 270px;
padding-bottom: 30px;
position: relative;
}
.main-road {
height: 270px;
position: relative;
position: absolute;
.line {
height: 2px;
position: absolute;
top: 50%;
width: 100%;
margin-top: -1px;
.x-line {
height: 2px;
position: absolute;
&.dash {
background: url($base-url + 'dash.png');
background-size: 30px 2px;
}
&.solid {
background: rgba($color: #ffffff, $alpha: 0.8);
}
}
}
.level-road {
top: 0;
left: 0;
height: 100%;
width: 100%;
position: absolute;
background: url($base-url + 'road.png');
background-size: auto 100%;
.entry {
height: 270px;
width: 267px;
background: url($base-url + 'entry.png') no-repeat;
background-size: 100%;
position: absolute;
background-position-y: 10px;
left: 0;
top: 0;
z-index: 1;
}
.export {
height: 270px;
width: 267px;
background: url($base-url + 'entry.png') no-repeat;
background-size: 100%;
position: absolute;
background-position-y: 10px;
right: 0;
top: 0;
transform: rotateY(180deg);
z-index: 1;
}
}
}
.x-direction {
&.in {
.running-car {
right: -60px;
}
.main-road {
// right: 0;
}
.sections {
flex-direction: row-reverse;
}
}
&.out {
.running-car {
left: -60px;
.core {
transform: rotate(180deg);
}
}
.main-road {
// left: 0;
}
.x-camera {
background: url($base-url + 'camera_3.png');
background-size: cover;
&:hover {
background: url($base-url + 'camera_4.png');
background-size: cover;
}
&.act {
background: url($base-url + 'camera_4.png');
background-size: cover;
}
}
.level-camera {
left: 0;
}
.stone-num {
&.first {
transform: translateX(0%);
}
&.last {
transform: translateX(-100%);
}
}
.level-parking {
left: 0;
}
.road-arrow {
transform: rotateY(180deg);
}
}
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment