Commit 9adbca23 authored by wangdonghao's avatar wangdonghao

新增功能

parent fb2fac6c
...@@ -40,4 +40,19 @@ export const getVideoUrl = (params) => { ...@@ -40,4 +40,19 @@ export const getVideoUrl = (params) => {
// 获取隧道段面拥堵情况 // 获取隧道段面拥堵情况
export const getFlowSections = (data) => { export const getFlowSections = (data) => {
return request.post({ url: '/qxsd/tunnel/sectionCongestion', 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 = { ...@@ -276,6 +276,7 @@ export const IconJson = {
'wallet-filled', 'wallet-filled',
'warning', 'warning',
'warning-filled', 'warning-filled',
'warn-triangle-filled',
'watch', 'watch',
'watermelon', 'watermelon',
'wind-power', 'wind-power',
......
...@@ -13,13 +13,24 @@ export const EVENT_TYPES = [ ...@@ -13,13 +13,24 @@ export const EVENT_TYPES = [
{ name: '逆行', key: '4' }, { name: '逆行', key: '4' },
{ name: '拥堵', key: '5' }, { name: '拥堵', key: '5' },
{ name: '可疑事件', key: '6' }, { name: '可疑事件', key: '6' },
{ name: '追尾', key: '7' }, { name: '事故', key: '7' },
{ name: '停车', key: '8' }, { name: '违章停车', key: '8' },
{ name: '超速', key: '16' }, { name: '超速', key: '16' },
{ name: '其他', key: '100' }, { name: '其他', key: '100' },
{ name: '', key: '99' }, { name: '', key: '99' },
{ name: '机动车闯禁行', key: '22' }, { name: '机动车闯禁行', key: '22' },
{ name: '大型客车', key: '21' }, { 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 = [ export const MODEL_CLASS = [
......
import { store } from '@/store' import { store } from '@/store'
import { defineStore } from 'pinia' 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 { interface TrafficType {
waves: string[], // 标注车辆,事件相关等 waves: string[] // 标注车辆,事件相关等
nowEvents: any[], // 当前事件标注 nowEvents: any[] // 当前事件标注
oldEvents: any[] // 回溯事件标注 oldEvents: any[] // 回溯事件标注
eventCount: number // 事件数量
eventNotify: EventNotify | null // 事件通知
} }
export const useTrafficStore = defineStore('traffic', { export const useTrafficStore = defineStore('traffic', {
state: (): TrafficType => ({ state: (): TrafficType => ({
waves: [], waves: [],
nowEvents: [], nowEvents: [],
oldEvents: [] oldEvents: [],
eventCount: 0,
eventNotify: null
}), }),
getters: { getters: {
getWaves() {
getWaves () {
return this.waves return this.waves
}, },
getNowEvents () { getNowEvents() {
return this.nowEvents return this.nowEvents
}, },
getOldEvents () { getOldEvents() {
return this.oldEvents return this.oldEvents
} },
getEventCount() {
return this.eventCount
},
getEventNotify() {
return this.eventNotify
}
}, },
actions: { actions: {
setWaves(e) {
setWaves (e) {
this.waves = e || [] this.waves = e || []
}, },
setNowEvents (e) { setNowEvents(e) {
this.nowEvents = e || [] this.nowEvents = e || []
}, },
setOldEvents (e) { setOldEvents(e) {
this.oldEvents = e || [] this.oldEvents = e || []
} },
} setEventCount(e) {
this.eventCount = e || 0
},
setEventNotify(e) {
this.eventNotify = e || null
}
}
}) })
export const useTrafficStoreOut = () => { export const useTrafficStoreOut = () => {
return useTrafficStore(store) return useTrafficStore(store)
} }
\ No newline at end of file
...@@ -251,14 +251,16 @@ export const useTunnelStore = defineStore('tunnel', { ...@@ -251,14 +251,16 @@ export const useTunnelStore = defineStore('tunnel', {
this.faclities = tunnelPassages.map(item => { this.faclities = tunnelPassages.map(item => {
const { leftHole, type } = item const { leftHole, rightHole, type, width } = item
const classMap = { const classMap = {
x0: 'man-cross', x0: 'man-cross',
x1: 'car-cross' x1: 'car-cross'
} }
return { return {
start: +leftHole, leftStart: +leftHole,
rightStart: +rightHole,
width: +width,
type: classMap[type] type: classMap[type]
} }
......
...@@ -160,7 +160,7 @@ onMounted(async () => { ...@@ -160,7 +160,7 @@ onMounted(async () => {
params.carImages = (res2.bayonetImage || []).map(item => { params.carImages = (res2.bayonetImage || []).map(item => {
return { return {
name: item.carPlate, name: item.carPlate,
imgs: item.imagePath images: item.imagePath
} }
}) })
params.evtImages = res2.eventImagesPath || [] params.evtImages = res2.eventImagesPath || []
......
...@@ -272,7 +272,8 @@ ...@@ -272,7 +272,8 @@
retainedTime: null, // 留存时间 retainedTime: null, // 留存时间
eventStartTimeSearchStart: '', // 开始时间 eventStartTimeSearchStart: '', // 开始时间
eventStartTimeSearchEnd: '', // 结束时间 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, pageNo: 1,
pageSize: 10, pageSize: 10,
total: 0 total: 0
...@@ -334,7 +335,8 @@ ...@@ -334,7 +335,8 @@
params.eventStartTimeSearchStart = '' params.eventStartTimeSearchStart = ''
params.eventStartTimeSearchEnd = '' params.eventStartTimeSearchEnd = ''
// params.eventType = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '100'] // 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() goGetList()
...@@ -400,7 +402,8 @@ ...@@ -400,7 +402,8 @@
} }
onBeforeMount(() => { onBeforeMount(() => {
params.eventType = EVENT_TYPES.map(item=> {return item.key}) // params.eventType = EVENT_TYPES.map(item=> {return item.key})
params.eventType = []
goGetList() goGetList()
}) })
......
...@@ -36,7 +36,8 @@ import { useTunnelStoreWithOut } from '@/store/modules/tunnel' ...@@ -36,7 +36,8 @@ import { useTunnelStoreWithOut } from '@/store/modules/tunnel'
import { getFocus, getRealCameras, setFocus } from '@/flash/Home' import { getFocus, getRealCameras, setFocus } from '@/flash/Home'
const props = defineProps<{ const props = defineProps<{
direction direction: number
holeType: string
}>() }>()
const tunnelStore = useTunnelStoreWithOut() const tunnelStore = useTunnelStoreWithOut()
...@@ -48,6 +49,10 @@ const focus = computed(() => { ...@@ -48,6 +49,10 @@ const focus = computed(() => {
}) })
const cameras = computed(() => { const cameras = computed(() => {
if( props.holeType === 'new' ) {
// (新洞/老洞)目前还没有实现,需后续补功能
return getRealCameras(props.direction)
}
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> <template>
<div :class="['ctrls', ctrlClass]"> <div :class="['ctrls', ctrlClass, holeCtrls]">
<div class="title"> <div class="title">
<div class="text"> <div class="text">
...@@ -38,7 +38,8 @@ import { useRouter } from 'vue-router' ...@@ -38,7 +38,8 @@ import { useRouter } from 'vue-router'
import { ASYNC_DIRECTIONS } from '@/config/dict' import { ASYNC_DIRECTIONS } from '@/config/dict'
const props = defineProps<{ const props = defineProps<{
infos: any infos: any,
holeTypeName: string
}>() }>()
const emits = defineEmits(['car-filter']) const emits = defineEmits(['car-filter'])
...@@ -46,6 +47,8 @@ const router = useRouter() ...@@ -46,6 +47,8 @@ const router = useRouter()
const ctrlClass = computed(() => props.infos.direction ? 'lower' : 'up') const ctrlClass = computed(() => props.infos.direction ? 'lower' : 'up')
const holeCtrls = computed(() => props.holeTypeName === '新洞' ? 'newTop' : 'oldButtom')
const act = ref('') const act = ref('')
const filters = ref([ const filters = ref([
{ text: '危化品车', key: 'dangerousCount', type: '0'}, { text: '危化品车', key: 'dangerousCount', type: '0'},
...@@ -58,7 +61,7 @@ const filters = ref([ ...@@ -58,7 +61,7 @@ const filters = ref([
const directionName = computed(() => { const directionName = computed(() => {
const arrs = ASYNC_DIRECTIONS() 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/'; ...@@ -102,10 +105,10 @@ $car-url: '@/assets/images/cars/';
.ctrls { .ctrls {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
height: 42px; height: 40px;
.title { .title {
color: white; color: white;
width: 316px; width: 350px;
height: 38px; height: 38px;
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -129,7 +132,6 @@ $car-url: '@/assets/images/cars/'; ...@@ -129,7 +132,6 @@ $car-url: '@/assets/images/cars/';
} }
&.up { &.up {
margin: 0 auto 15px;
.title { .title {
background-image: url($base-url + 'arrow_lft.png'); background-image: url($base-url + 'arrow_lft.png');
background-repeat: no-repeat; background-repeat: no-repeat;
...@@ -145,7 +147,6 @@ $car-url: '@/assets/images/cars/'; ...@@ -145,7 +147,6 @@ $car-url: '@/assets/images/cars/';
} }
&.lower { &.lower {
margin: 15px auto 0;
.title { .title {
background-image: url($base-url + 'arrow_rgt.png'); background-image: url($base-url + 'arrow_rgt.png');
background-repeat: no-repeat; background-repeat: no-repeat;
...@@ -156,6 +157,14 @@ $car-url: '@/assets/images/cars/'; ...@@ -156,6 +157,14 @@ $car-url: '@/assets/images/cars/';
margin-right: 32px; margin-right: 32px;
} }
} }
&.newTop {
margin: 0 auto 10px;
}
&.oldButtom {
margin: 0 auto;
}
} }
.filters { .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> <template>
<div class="traffic">
<div class="traffic"> <FlowCarCtrl @car-filter="carFilter" holeTypeName="新洞" :infos="newSummary" />
<FlowCarCtrl @car-filter="carFilter" :infos="inSummary" /> <div class="hole-select">
<div class="name">聚焦方向:</div>
<div class="hole-select"> <el-select v-model="holeDirection" popper-class="define-select" :teleported="false">
<div class="name">聚焦位置:</div> <el-option
<el-select v-for="{ name, inkey } in holeTypes"
v-model="holeAct" :key="inkey"
popper-class="define-select" v-bind="{ label: name, value: inkey }"
:teleported="false" />
> </el-select>
<el-option v-for="({ name, key }) in HOLE_TYPES" :key v-bind="{ label: name, value: key }" /> </div>
</el-select>
<!-- <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, 'new')"
v-bind="{
...configs,
direction: holeDirection,
traffic: newTraffic,
act: true,
section: false,
roadHeight: newTraffic?.road_w
}"
/>
<Facilities v-bind="{ traffic: newTraffic, direction: holeDirection, act: true }" :act="true" />
<XDirection
@go-section="goSection"
@go-track="goTrack"
@car-socket="(e) => carSocket(e, 'old')"
v-bind="{
...configs,
direction: holeDirection,
traffic: oldTraffic,
act: true,
section: false,
roadHeight: oldTraffic?.road_w
}"
/>
<Facilities v-bind="{ traffic: oldTraffic, direction: holeDirection, act: true }" />
<!-- <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
}"
/>
<Facilities :act="holeAct === 'new'" />
<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'
}"
/> -->
<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> </div>
<Scales
@plate-change="plateChange"
v-model:sceneMode="sceneMode"
v-model:useMaterial="configs.useMaterial"
class="scale-ctrls"
/>
<Road
@go-section="goSection"
@go-track="goTrack"
@car-socket="e => carSocket(e, 1)"
v-bind="{ ...configs, direction: 0, traffic: inTraffic, act: holeAct === 'new' }"
/>
<Facilities :act="holeAct === 'new'" />
<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 }"
/>
<Facilities :act="holeAct === 'old'" />
<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}"
/>
<Facilities :act="holeAct === 'new'" />
<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>
</template> </template>
<script lang='ts' setup> <script lang="ts" setup>
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useTunnelStoreWithOut } from '@/store/modules/tunnel' import { useTunnelStoreWithOut } from '@/store/modules/tunnel'
...@@ -77,26 +124,54 @@ import TrafficEngine from '@/views/traffic/roads/Engine/index' ...@@ -77,26 +124,54 @@ import TrafficEngine from '@/views/traffic/roads/Engine/index'
import XDirection from '@/views/traffic/roads/XDirection/index.vue' import XDirection from '@/views/traffic/roads/XDirection/index.vue'
import FlowCarCtrl from '../FlowCarCtrls/index.vue' import FlowCarCtrl from '../FlowCarCtrls/index.vue'
import Facilities from '@/views/traffic/roads/Facilities/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' import Road from '@/views/traffic/roads/XDirection/Road.vue'
const HOLE_TYPES = [ import Cameras from '../Cameras/index.vue'
{ name: '新洞', key: 'new' }, import FlowChart from '../FlowChart/index.vue'
{ name: '老洞', key: 'old' }
] 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 tunnelStore = useTunnelStoreWithOut()
const props = defineProps<{
useMaterial: boolean
showFlow: boolean
}>()
const eventRef = ref<any>(null) // 事件弹窗实例 const eventRef = ref<any>(null) // 事件弹窗实例
const sceneMode = ref('panorama') // const sceneMode = ref('panorama')
const router = useRouter() const router = useRouter()
const holeAct = ref('old') const holeDirection = ref<number>(0)
const inTraffic = ref(null) // 上行隧道实例 const inNewTraffic = ref<TrafficEngine | null>(null) // 新洞上行隧道实例
const outTraffic = ref(null) // 下行隧道实例 const outNewTraffic = ref<TrafficEngine | null>(null) // 新洞下行隧道实例
const inSummary = ref({ const inOldTraffic = ref<TrafficEngine | null>(null) // 老洞上行隧道实例
const ouOldTraffic = ref<TrafficEngine | null>(null) // 老洞下行隧道实例
const newSummary = ref<TrafficSummary>({
name: '杭州方向', name: '杭州方向',
direction: 0, direction: 0,
dangerousCount: 0, dangerousCount: 0,
...@@ -106,9 +181,9 @@ const inSummary = ref({ ...@@ -106,9 +181,9 @@ const inSummary = ref({
otherCount: 0 otherCount: 0
}) })
const outSummary = ref({ const oldSummary = ref<TrafficSummary>({
name: '宁波方向', name: '杭州方向',
direction: 1, direction: 0,
dangerousCount: 0, dangerousCount: 0,
truckCount: 0, truckCount: 0,
passengerCount: 0, passengerCount: 0,
...@@ -120,48 +195,60 @@ const outSummary = ref({ ...@@ -120,48 +195,60 @@ const outSummary = ref({
const configs = reactive<any>({ const configs = reactive<any>({
plateType: 1, // 铭牌展示类型 plateType: 1, // 铭牌展示类型
filterType: null, // 类型筛选 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 configs.plateType = e
} }
// 车流筛选 // 车流筛选
const carFilter = ({e}) => { const carFilter = ({ e }) => {
configs.filterType = e?.type || null configs.filterType = e?.type || null
} }
// 汇总同步 // 汇总同步
const carSocket = (e, direction) => { const carSocket = (e, holeType: string) => {
const { dangerousCount, truckCount, passengerCount, sedanCount, otherCount } = e const { dangerousCount, truckCount, passengerCount, sedanCount, otherCount } = e
const summary = [inSummary, outSummary][direction] const summary = [newSummary, oldSummary][holeType === 'new' ? 0 : 1]
summary.value = { summary.value = {
...summary.value, ...summary.value,
direction: holeDirection.value,
...{ ...{
dangerousCount, truckCount, passengerCount, sedanCount, otherCount, dangerousCount,
truckCount,
passengerCount,
sedanCount,
otherCount,
total: dangerousCount + dangerousCount + passengerCount + sedanCount + otherCount total: dangerousCount + dangerousCount + passengerCount + sedanCount + otherCount
} }
} }
} }
// 缎面跳转 // 缎面跳转
const goSection = ({ x, direction }) => { const goSection = ({ x, direction, scrollLeft, holeType }) => {
router.push({ router.push({
path: '/section', path: '/section',
query: { query: {
x, direction, x,
direction,
scrollLeft,
holeType,
from: 'home' from: 'home'
} }
}) })
} }
// 车辆追踪 // 车辆追踪
...@@ -169,7 +256,8 @@ const goTrack = ({ id, direction }) => { ...@@ -169,7 +256,8 @@ const goTrack = ({ id, direction }) => {
router.push({ router.push({
path: 'track', path: 'track',
query: { query: {
id, direction, id,
direction,
from: 'home' from: 'home'
} }
}) })
...@@ -177,46 +265,92 @@ const goTrack = ({ id, direction }) => { ...@@ -177,46 +265,92 @@ const goTrack = ({ id, direction }) => {
// 创建车流实例 // 创建车流实例
const createTraffic = async () => { const createTraffic = async () => {
const isAvl = await awaitGet(() => tunnelStore.getUpTunnel?.long) const isAvl = await awaitGet(() => tunnelStore.getUpTunnel?.long)
// 隧道数据获取错误 // 隧道数据获取错误
if (!isAvl) return if (!isAvl) return
const create = (direction) => { const create = (direction, holeType) => {
const key = direction ? 'getDownTunnel' : 'getUpTunnel' const key = direction ? 'getDownTunnel' : 'getUpTunnel'
const { start, end, long } = tunnelStore[key] const { start, end, long } = tunnelStore[key]
return new TrafficEngine('panorama', { return new TrafficEngine('panorama', {
direction, direction,
holeType, // (新洞/老洞)目前还没有实现,需后续补功能
start: +start, start: +start,
end: +end, end: +end,
miles: +long miles: +long,
road_w: holeType === 'new' ? 90 : 64
}) })
} }
inTraffic.value = create(0) inNewTraffic.value = create(0, 'new')
outTraffic.value = create(1) outNewTraffic.value = create(1, 'new')
inTraffic.value.startTraffic(res => { inOldTraffic.value = create(0, 'old')
carSocket(res, 0) ouOldTraffic.value = create(1, 'old')
})
outTraffic.value.startTraffic(res => { // 后续接新洞后再做修改
carSocket(res, 1) // 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( watch(
() => sceneMode.value, () => holeDirection.value,
e => { (e) => {
if (e === 'section') { // 后续接新洞后再做修改
goSection({ x: 0, direction: 0 }) // 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) => { provide('openEventPop', (event) => {
...@@ -224,15 +358,26 @@ provide('openEventPop', (event) => { ...@@ -224,15 +358,26 @@ provide('openEventPop', (event) => {
}) })
onMounted(() => { onMounted(() => {
createTraffic() createTraffic()
})
onBeforeUnmount(() => {
// 清理所有隧道实例的 WebSocket 连接
;[inNewTraffic.value, outNewTraffic.value, inOldTraffic.value, ouOldTraffic.value].forEach(
(instance) => {
if (instance) {
instance.cleanRoad() // 这会调用 socket.close()
}
}
)
}) })
defineExpose({
plateChange
})
</script> </script>
<style lang='scss' scoped> <style lang="scss" scoped>
.traffic { .traffic {
position: relative; position: relative;
text-align: center; text-align: center;
...@@ -248,7 +393,7 @@ onMounted(() => { ...@@ -248,7 +393,7 @@ onMounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
width: 200px; width: 200px;
top: 15px; top: 10px;
left: 85px; left: 85px;
.name { .name {
font-size: 14px; font-size: 14px;
...@@ -264,7 +409,6 @@ onMounted(() => { ...@@ -264,7 +409,6 @@ onMounted(() => {
background: rgba(38, 67, 106); background: rgba(38, 67, 106);
border: none; border: none;
} }
} }
:deep(.el-select-dropdown__item) { :deep(.el-select-dropdown__item) {
...@@ -273,9 +417,30 @@ onMounted(() => { ...@@ -273,9 +417,30 @@ onMounted(() => {
background-color: rgb(54, 89, 128); background-color: rgb(54, 89, 128);
} }
} }
}
</style>
.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> <template>
<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>
<div class="inner">
<Scales
:isShowSceneMode="false"
@plate-change="plateChange"
v-model:useMaterial="useMaterial"
class="scale-ctrls"
/>
</div>
</div>
<div class="home"> <!-- 隧道统计 -->
<div class="overview-block" v-show="showOverview">
<div class="flow-switch"> <Overview @pack-up="handlePackUp" />
<div class="inner">
<div class="text-pre">实时交通流:</div>
<el-switch v-model="showFlow" />
<div class="text-aft">{{showFlow ? '' : ''}}</div>
</div> </div>
</div>
<div class="bef-block"> <!-- <div class="bef-block">
<flow-chart v-if="showFlow" :direction="0" /> <flow-chart v-if="showFlow" :direction="0" />
<cameras ref="cameraRef1" v-else :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" /> <flow-chart v-if="showFlow" :direction="1" />
<cameras ref="cameraRef2" v-else :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="icon"></div>
<div class="text">隧道管控</div> <div class="text">隧道管控</div>
</div> </div>
</div> <!-- 实时事件 -->
<real-time-events ref="realTimeEvents" @handle-event-alert="openEventAlert" />
<!-- 事件告警弹窗 -->
<event-alert-pop @update="handleEventAlert" ref="eventAlertPop" />
</div>
</template> </template>
<script lang='ts' setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import Traffic from './components/Traffic/index.vue' import Traffic from './components/Traffic/index.vue'
import Cameras from './components/Cameras/index.vue' // import Cameras from './components/Cameras/index.vue'
import FlowChart from './components/FlowChart/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' import { useUserStoreWithOut } from '@/store/modules/user'
const userStore = useUserStoreWithOut() const userStore = useUserStoreWithOut()
import { getOrgainzations } from '@/api/system/role' import { getOrgainzations } from '@/api/system/role'
import { import { getDetails } from '@/api/system/role'
getDetails,
} from '@/api/system/role'
import { getLoginPath } from '@/api/system/user' import { getLoginPath } from '@/api/system/user'
import { getState, setState } from '@/flash/Home' import { getState, setState } from '@/flash/Home'
// const showFlow = ref(false) // 模式切换开关 // const showFlow = ref(false) // 模式切换开关
const cameraRef1 = ref<any>(null) // 上行摄像头 const cameraRef1 = ref<any>(null) // 上行摄像头
const cameraRef2 = 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 menus = ref([])
// 模式切换开关 // 模式切换开关
const showFlow = computed({ const showFlow = computed({
get () { get() {
return getState() return getState()
}, },
set (e) { set(e) {
setState(e) setState(e)
} }
}) })
const cameraChange = (xcamera, direction) => { const cameraChange = (xcamera, direction) => {
if (direction === 0 && cameraRef1.value) { if (direction === 0 && cameraRef1.value) {
cameraRef1.value.cameraChange(xcamera) cameraRef1.value.cameraChange(xcamera)
} }
...@@ -75,7 +101,6 @@ const cameraChange = (xcamera, direction) => { ...@@ -75,7 +101,6 @@ const cameraChange = (xcamera, direction) => {
if (direction === 1 && cameraRef2.value) { if (direction === 1 && cameraRef2.value) {
cameraRef2.value.cameraChange(xcamera) cameraRef2.value.cameraChange(xcamera)
} }
} }
// 系统免登录 // 系统免登录
...@@ -86,26 +111,42 @@ const toSystem = async () => { ...@@ -86,26 +111,42 @@ const toSystem = async () => {
provide('cameraChange', cameraChange) provide('cameraChange', cameraChange)
const a = [{ const a = [{}]
}]
const setAuths = async () => { const setAuths = async () => {
const role = userStore.getRoles[0] const role = userStore.getRoles[0]
const res = await getOrgainzations({ code: role }) const res = await getOrgainzations({ code: role })
const resList = await getDetails({ id: res.list[0].id || '' }) const resList = await getDetails({ id: res.list[0].id || '' })
menus.value = resList.menuIds || [] 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(() => { onBeforeMount(() => {
setAuths() setAuths()
}) })
</script> </script>
<style lang='scss' scoped> <style lang="scss" scoped>
$Common_Url: '@/assets/images/common/'; $Common_Url: '@/assets/images/common/';
.flow-switch { .flow-switch {
...@@ -115,6 +156,7 @@ $Common_Url: '@/assets/images/common/'; ...@@ -115,6 +156,7 @@ $Common_Url: '@/assets/images/common/';
position: absolute; position: absolute;
right: 10px; right: 10px;
top: 100px; top: 100px;
display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
...@@ -122,17 +164,24 @@ $Common_Url: '@/assets/images/common/'; ...@@ -122,17 +164,24 @@ $Common_Url: '@/assets/images/common/';
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
margin-right: 42px; margin-right: 24px;
.text-aft { .text-aft {
margin-left: 6px; margin-left: 6px;
} }
.scale-ctrls {
margin-right: 20px;
}
} }
:deep(.el-switch) { :deep(.el-switch) {
--el-switch-on-color: #007DFA; --el-switch-on-color: #007dfa;
--el-switch-off-color: #203A60; --el-switch-off-color: #203a60;
} }
}
.overview-block {
margin: 33px 135px 30px;
overflow: hidden;
} }
.bef-block, .bef-block,
...@@ -169,4 +218,4 @@ $Common_Url: '@/assets/images/common/'; ...@@ -169,4 +218,4 @@ $Common_Url: '@/assets/images/common/';
margin-right: 5px; margin-right: 5px;
} }
} }
</style> </style>
\ No newline at end of file
<template> <template>
<div :class="['ctrls', ctrlClass]">
<div :class="['ctrls', ctrlClass]"> <div class="title">
<div class="text">
<div class="title"> {{ DIRECTION_NAMES[infos.direction] + holeTypeName_l() }}
<div class="text"> <div @click="holeTypeChange" class="direction-change">
{{ DIRECTION_NAMES[infos.direction] }} <div class="icon"></div>
<div @click="directionChange" class="direction-change"> {{ DIRECTION_NAMES[infos.direction] + holeTypeName_r() }}
<div class="icon"></div> </div>
{{ DIRECTION_NAMES[1 - infos.direction].slice(0, -2) }}
</div> </div>
</div> <div class="more">
<div class="more"> 留存车辆:{{ infos.total }}
留存车辆:{{ 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> </div>
</div>
<div class="filters"> <div class="filters">
<div <div
v-for="item in filters" v-for="item in filters"
:key="item.key" :key="item.key"
:class="['x-car', {'act': item.key === act}]" :class="['x-car', { act: item.key === act }]"
@click="goFilter(item)" @click="goFilter(item)"
> >
{{ item.text }}({{ infos[item.key] }}) {{ item.text }}({{ infos[item.key] }})
</div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script lang='ts' setup> <script lang="ts" setup>
import { ref, defineProps, computed } from 'vue' import { ref, defineProps, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ASYNC_DIRECTIONS } from '@/config/dict' import { ASYNC_DIRECTIONS } from '@/config/dict'
...@@ -43,25 +36,31 @@ const props = defineProps<{ ...@@ -43,25 +36,31 @@ const props = defineProps<{
infos: any infos: any
}>() }>()
const emits = defineEmits(['car-filter', 'direction-change']) const emits = defineEmits(['car-filter', 'hole-type-change'])
const router = useRouter() const router = useRouter()
const ctrlClass = computed(() => props.infos.direction ? 'lower' : 'up') const ctrlClass = computed(() => (props.infos.direction ? 'lower' : 'up'))
const act = ref('') const act = ref('')
const filters = ref([ const filters = ref([
{ text: '危化品车', key: 'dangerousCount', type: '0'}, { text: '危化品车', key: 'dangerousCount', type: '0' },
{ text: '货车', key: 'truckCount', type: '3' }, { text: '货车', key: 'truckCount', type: '3' },
{ text: '客车', key: 'passengerCount', type: '1'}, { text: '客车', key: 'passengerCount', type: '1' },
{ text: '轿车', key: 'sedanCount', type: '2' }, { text: '轿车', key: 'sedanCount', type: '2' },
{ text: '其他', key: 'otherCount', type: '4' } { text: '其他', key: 'otherCount', type: '4' }
]) ])
const DIRECTION_NAMES = computed(() => { 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) => { const goFilter = (e) => {
if (act.value === e.key) { if (act.value === e.key) {
...@@ -88,15 +87,13 @@ const goRemains = () => { ...@@ -88,15 +87,13 @@ const goRemains = () => {
}) })
} }
// 方向切换 // 新老洞切换
const directionChange = () => { const holeTypeChange = () => {
emits('direction-change') emits('hole-type-change')
} }
</script> </script>
<style lang='scss' scoped> <style lang="scss" scoped>
$base-url: '@/assets/images/home/'; $base-url: '@/assets/images/home/';
$car-url: '@/assets/images/cars/'; $car-url: '@/assets/images/cars/';
$common-url: '@/assets/images/common/'; $common-url: '@/assets/images/common/';
...@@ -107,7 +104,7 @@ $common-url: '@/assets/images/common/'; ...@@ -107,7 +104,7 @@ $common-url: '@/assets/images/common/';
height: 42px; height: 42px;
.title { .title {
color: white; color: white;
width: 360px; width: 460px;
height: 38px; height: 38px;
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -121,7 +118,7 @@ $common-url: '@/assets/images/common/'; ...@@ -121,7 +118,7 @@ $common-url: '@/assets/images/common/';
align-items: center; align-items: center;
.direction-change { .direction-change {
font-size: 16px; font-size: 16px;
color: #42E7FF; color: #42e7ff;
display: flex; display: flex;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
...@@ -193,9 +190,8 @@ $common-url: '@/assets/images/common/'; ...@@ -193,9 +190,8 @@ $common-url: '@/assets/images/common/';
&.act { &.act {
background: rgba(124, 213, 255, 0.15); background: rgba(124, 213, 255, 0.15);
box-sizing: border-box; box-sizing: border-box;
border: 1px solid #7CD5FF; border: 1px solid #7cd5ff;
} }
} }
} }
</style>
</style>
\ No newline at end of file
<template> <template>
<div class="traffic">
<div class="traffic"> <!-- <Scales
@plate-change="plateChange"
<Scales v-model:sceneMode="sceneMode"
@plate-change="plateChange" v-model:useMaterial="commons.useMaterial"
v-model:sceneMode="sceneMode" class="scale-ctrls"
v-model:useMaterial="commons.useMaterial" /> -->
class="scale-ctrls"
/> <FlowCarCtrl
@car-filter="carFilter"
<FlowCarCtrl @hole-type-change="handleHoleTypeChange"
@car-filter="carFilter" :infos="{ ...summary, direction: commons.direction, holeType: commons.holeType }"
@direction-change="handleDirectionChange" />
:infos="{ ...summary, direction: commons.direction }"
/> <!-- 新洞 -->
<XDirectionSectionRoad
<XDirectionSection ref="xDirectionSectionRoadRef"
v-bind="{...commons, traffic}" v-bind="{
v-model:sectionAct="commons.sectionAct" ...commons,
@car-socket="carSocket" traffic: newTraffic,
@section-change="sectionChange" section: false,
@go-track="goTrack" roadHeight: newTraffic?.road_section_w,
@camera-change="e => emits('camera-change', e)" act: commons.holeType === 'new'
/> }"
v-model:sectionAct="commons.sectionAct"
</div> @car-socket="carSocket"
@section-change="sectionChange"
@go-track="goTrack"
@camera-change="(e) => emits('camera-change', e)"
@update-space-axis="(scrollLeft) => updateSpaceAxis('new', scrollLeft)"
/>
<!-- 老洞 -->
<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> </template>
<script lang='ts' setup> <script lang="ts" setup>
import { ref, reactive, onBeforeMount } from 'vue' import { ref, reactive, onBeforeMount } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import FlowCarCtrl from '../FlowCarCtrls/index.vue' 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 TrafficEngine from '@/views/traffic/roads/Engine/index'
import SpaceAxis from '@/views/traffic/roads/XDirection/comps/SpaceAxis/index.vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
...@@ -56,6 +108,7 @@ const tunnelStore = useTunnelStoreWithOut() ...@@ -56,6 +108,7 @@ const tunnelStore = useTunnelStoreWithOut()
const commons = defineModel<any>() const commons = defineModel<any>()
const freshKey = ref(true) const freshKey = ref(true)
const traffic = ref(null) const traffic = ref(null)
const newTraffic = ref(null)
const summary = ref<any>({ const summary = ref<any>({
dangerousCount: 0, dangerousCount: 0,
...@@ -66,47 +119,47 @@ const summary = ref<any>({ ...@@ -66,47 +119,47 @@ const summary = ref<any>({
total: 0 total: 0
}) })
watch( // watch(
() => sceneMode.value, // () => sceneMode.value,
(e) => { // (e) => {
if (e === 'panorama') // if (e === 'panorama')
router.push({ // router.push({
path: 'home' // path: 'home'
}) // })
} // }
) // )
// 汇总同步 // 汇总同步
const carSocket = (e) => { const carSocket = (e) => {
const { dangerousCount, truckCount, passengerCount, sedanCount, otherCount } = e
const { dangerousCount, truckCount, passengerCount, sedanCount, otherCount } = e
summary.value = {
summary.value = { ...summary.value,
...summary.value, ...{
...{ dangerousCount,
dangerousCount, truckCount, passengerCount, sedanCount, otherCount, truckCount,
total: dangerousCount + dangerousCount + passengerCount + sedanCount + otherCount passengerCount,
sedanCount,
otherCount,
total: dangerousCount + dangerousCount + passengerCount + sedanCount + otherCount
}
} }
} }
}
// 车流筛选 // 车流筛选
const carFilter = ({ e }) => { const carFilter = ({ e }) => {
commons.value.filterType = e?.type || null commons.value.filterType = e?.type || null
} }
// 展示牌切换 // // 展示牌切换
const plateChange = (e) => { // const plateChange = (e) => {
commons.value.plateType = e // commons.value.plateType = e
} // }
// 方向切换 // 新/老洞切换
const handleDirectionChange = async () => { const handleHoleTypeChange = async () => {
// commons.value.direction = 1 - commons.value.direction
commons.value.direction = 1 - commons.value.direction commons.value.holeType = commons.value.holeType === 'old' ? 'new' : 'old'
summary.value.dangerousCount = 0 summary.value.dangerousCount = 0
summary.value.truckCount = 0 summary.value.truckCount = 0
...@@ -115,17 +168,26 @@ const handleDirectionChange = async () => { ...@@ -115,17 +168,26 @@ const handleDirectionChange = async () => {
summary.value.otherCount = 0 summary.value.otherCount = 0
summary.value.total = 0 summary.value.total = 0
traffic.value.cleanRoad() traffic.value?.cleanRoad()
newTraffic.value?.cleanRoad()
const configs = await getConfigs(commons.value.direction) const configs = await getConfigs(commons.value.direction)
if (configs) { if (configs) {
traffic.value = new TrafficEngine(sceneMode.value, configs) if (commons.value.holeType === 'old') {
traffic.value.startTraffic(res => { traffic.value = new TrafficEngine(sceneMode.value, configs)
carSocket(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 }) => { ...@@ -138,7 +200,8 @@ const goTrack = ({ id, direction }) => {
router.push({ router.push({
path: 'track', path: 'track',
query: { query: {
id, direction, id,
direction,
from: 'event' from: 'event'
} }
}) })
...@@ -146,12 +209,10 @@ const goTrack = ({ id, direction }) => { ...@@ -146,12 +209,10 @@ const goTrack = ({ id, direction }) => {
// 获取隧道数据 // 获取隧道数据
const getConfigs = (direct) => { const getConfigs = (direct) => {
let wait = null let wait = null
let stop = null let stop = null
return new Promise(resolve => { return new Promise((resolve) => {
wait = setTimeout(() => { wait = setTimeout(() => {
stop && stop() stop && stop()
resolve(false) resolve(false)
...@@ -165,41 +226,90 @@ const getConfigs = (direct) => { ...@@ -165,41 +226,90 @@ const getConfigs = (direct) => {
direction: direct, direction: direct,
start, start,
end, 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 () => { onBeforeMount(async () => {
const { x, direction, holeType } = route.query
const { x, direction } = route.query
if (isSomeNone([x, direction])) { if (isSomeNone([x, direction])) {
return ElMessage.error('地址错误!') return ElMessage.error('地址错误!')
} }
commons.value.direction = +(direction as string) commons.value.direction = +(direction as string)
commons.value.holeType = holeType as string
const configs = await getConfigs(+direction) const configs = await getConfigs(+direction)
if (configs) { if (configs) {
traffic.value = new TrafficEngine(sceneMode.value, configs) traffic.value = new TrafficEngine(sceneMode.value, {
traffic.value.startTraffic(res => { ...configs,
road_section_w: 134,
holeType: 'old'
})
traffic.value.startTraffic((res) => {
carSocket(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> </script>
<style lang='scss' scoped> <style lang="scss" scoped>
.traffic { .traffic {
position: relative; position: relative;
text-align: center; text-align: center;
...@@ -209,6 +319,23 @@ onBeforeMount(async () => { ...@@ -209,6 +319,23 @@ onBeforeMount(async () => {
right: 85px; right: 85px;
top: 15px; top: 15px;
} }
}
</style> .space-axis-block {
\ No newline at end of file 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> <template>
<div class="section">
<div class="section"> <div class="flow-switch">
<div class="inner">
<Cameras ref="videosRef" :commons /> <Scales
<Traffic @camera-change="cameraChange" v-model="commons" /> :isShowSceneMode="false"
@plate-change="plateChange"
</div> v-model:useMaterial="commons.useMaterial"
class="scale-ctrls"
<!-- <Video ref="videoRef" /> --> />
</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>
<!-- <Video ref="videoRef" /> -->
</template> </template>
<script lang='ts' setup> <script lang="ts" setup>
// import Cameras from './components/Cameras/index.vue'
import Cameras from './components/Cameras/index.vue'
import Traffic from './components/Traffic/index.vue' import Traffic from './components/Traffic/index.vue'
// import Video from '@/components/Video/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' import { ref } from 'vue'
// const videoRef = ref<any>(null) // const videoRef = ref<any>(null)
const videosRef = ref<any>(null) const videosRef = ref<any>(null)
const route = useRoute() const route = useRoute()
const router = useRouter()
// 公共参数 // 公共参数
const commons = reactive({ const commons = reactive({
...@@ -31,30 +43,102 @@ const commons = reactive({ ...@@ -31,30 +43,102 @@ const commons = reactive({
useMaterial: false, // 是否使用贴图 useMaterial: false, // 是否使用贴图
plateType: 1, // 铭牌展示类型 plateType: 1, // 铭牌展示类型
direction: 0, // 车流方向 direction: 0, // 车流方向
sectionAct: 1 // 当前断面 sectionAct: 1, // 当前断面
scrollLeft: null, // 滚动轴位置
holeType: 'old' // 洞类型
}) })
const cameraChange = (e) => { const cameraChange = (e) => {
videosRef.value.cameraSet(e) // videosRef.value.cameraSet(e)
}
// 展示牌切换
const plateChange = (e) => {
commons.plateType = e
}
// 返回全局视图
const backHome = () => {
router.push({
name: 'Home'
})
} }
onBeforeMount(() => { onBeforeMount(() => {
const querys = route.query const querys = route.query
commons.sectionAct = +querys.x commons.sectionAct = +querys.x
commons.direction = +querys.direction commons.direction = +querys.direction
commons.scrollLeft = querys.scrollLeft ?? null
commons.holeType = querys.holeType
}) })
// provide('openVideoPop', (e) => { // provide('openVideoPop', (e) => {
// videoRef.value?.diaOpen(e) // videoRef.value?.diaOpen(e)
// }) // })
</script> </script>
<style lang='scss' scoped> <style lang="scss" scoped>
$base-url: '@/assets/images/section/';
.section { .section {
text-align: center; text-align: center;
}
</style> .flow-switch {
\ No newline at end of file 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>
...@@ -35,13 +35,15 @@ ...@@ -35,13 +35,15 @@
</div> </div>
</el-popover> </el-popover>
<div @click="sceneChange('panorama')" :class="['x-btn', 'reduce', { 'disabled': sceneMode === 'panorama' }]"> <template v-if="isShowSceneMode">
<div class="inner"></div> <div @click="sceneChange('panorama')" :class="['x-btn', 'reduce', { 'disabled': sceneMode === 'panorama' }]">
</div> <div class="inner"></div>
<div @click="sceneChange('section')" :class="['x-btn', 'enlarge', { 'disabled': sceneMode === 'section' }]"> </div>
<div class="inner"></div> <div @click="sceneChange('section')" :class="['x-btn', 'enlarge', { 'disabled': sceneMode === 'section' }]">
</div> <div class="inner"></div>
</div>
</template>
<slot></slot> <slot></slot>
...@@ -53,6 +55,12 @@ ...@@ -53,6 +55,12 @@
import { ref, defineModel } from 'vue' import { ref, defineModel } from 'vue'
const props = withDefaults(defineProps<{
isShowSceneMode?: boolean
}>(), {
isShowSceneMode: true
})
const emits = defineEmits(['plate-change']) const emits = defineEmits(['plate-change'])
const carTip = ref(1) const carTip = ref(1)
......
...@@ -177,8 +177,10 @@ class TrafficEngine { ...@@ -177,8 +177,10 @@ class TrafficEngine {
ratex = 1 // 换算比 ratex = 1 // 换算比
miles = 1800 // 隧道总长 miles = 1800 // 隧道总长
road_w = 90 // 路面宽
// section = 300 // 段面长 // section = 300 // 段面长
section = 100 section = 100
road_section_w = 270 // 缎面模式路面宽
start = 0 start = 0
end = 1800 end = 1800
...@@ -218,12 +220,14 @@ class TrafficEngine { ...@@ -218,12 +220,14 @@ class TrafficEngine {
if (secne === 'panorama') { if (secne === 'panorama') {
this.ratex = (appStore.getDesign - 180) / this.miles 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') { if (secne === 'section') {
this.ratex = appStore.getDesign / this.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 = [] this.cars = []
...@@ -374,9 +378,11 @@ class TrafficEngine { ...@@ -374,9 +378,11 @@ class TrafficEngine {
// 设施换算 // 设施换算
facilitiesFormate (facilities) { facilitiesFormate (facilities) {
return facilities.map(({ start, end, type, direction }) => { return facilities.map(({ leftStart, rightStart, width, type, direction }) => {
return { return {
x: start * this.ratex, left_x: leftStart * this.ratex,
right_x: rightStart * this.ratex,
width: width * this.ratex,
type, type,
direction direction
} }
...@@ -387,8 +393,8 @@ class TrafficEngine { ...@@ -387,8 +393,8 @@ class TrafficEngine {
// 停车带换算 // 停车带换算
parkingFormate (packings) { parkingFormate (packings) {
return packings.map(({ start }) => { return packings.map(({ start, end }) => {
return { x: start * this.ratex } return { x: start * this.ratex, long: (end - start) * this.ratex }
}) })
} }
......
<template> <template>
<div :class="['facilities', act ? 'act' : 'disabled']">
<div :class="['facilities', act ? 'act' : 'disabled']"> <div
<div v-for="(item, x) in facilities"
v-for="(item, x) in facilities" :key="x"
:key="x" :class="['x-facility', item.type]"
:class="['x-facility', item.type]" :style="item.style"
:style="item.style" ></div>
></div> </div>
</div>
</template> </template>
<script lang='ts' setup> <script lang="ts" setup>
import { useTunnelStoreWithOut } from '@/store/modules/tunnel' import { useTunnelStoreWithOut } from '@/store/modules/tunnel'
const props = withDefaults(defineProps<{ const props = withDefaults(
act: boolean defineProps<{
}>(), { act: boolean
act: true traffic: any
}) direction: number
}>(),
{
act: true
}
)
const tunnelStore = useTunnelStoreWithOut() const tunnelStore = useTunnelStoreWithOut()
const tunnel = computed(() => {
return tunnelStore.getUpTunnel
})
const facilities = computed(() => { const facilities = computed(() => {
if (!props.traffic) return []
const rax = 1920 / +tunnel.value.long return props.traffic.facilitiesFormate(tunnelStore.getFacilities).map((item) => {
return tunnelStore.getFacilities.map(item => { const { left_x,right_x, width, type } = item
const { start, type } = item const style = {}
return { style.left = left_x + 'px'
style: { left: start * rax + 'px' }, // style.width = width + 'px'
type
}
})
}) const temp = { type, style }
return temp
})
})
</script> </script>
<style lang='scss' scoped> <style lang="scss" scoped>
$base-url: '@/assets/images/common/'; $base-url: '@/assets/images/common/';
.facilities { .facilities {
...@@ -79,8 +77,9 @@ $base-url: '@/assets/images/common/'; ...@@ -79,8 +77,9 @@ $base-url: '@/assets/images/common/';
width: 40px; width: 40px;
height: 100%; height: 100%;
&::after { &::after {
content: ""; content: '';
width: 40px; // width: 40px;
width: 100%;
height: 100%; height: 100%;
position: absolute; position: absolute;
left: 50%; left: 50%;
...@@ -110,8 +109,5 @@ $base-url: '@/assets/images/common/'; ...@@ -110,8 +109,5 @@ $base-url: '@/assets/images/common/';
// background-size: cover; // background-size: cover;
// } // }
} }
} }
</style>
</style>
\ No newline at end of file
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
act ? 'act' : 'disabled' act ? 'act' : 'disabled'
]"> ]">
<template v-if="!isDownCtrls"> <template v-if="!isDownCtrls && section">
<!-- 段面控制器 --> <!-- 段面控制器 -->
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
</template> </template>
<div class="main-road"> <div class="main-road" :style="roadStyle">
<!-- 停车带层 --> <!-- 停车带层 -->
<Parking v-bind="{ scene: 'panorama', direction, tunnel, _traffic }" /> <Parking v-bind="{ scene: 'panorama', direction, tunnel, _traffic }" />
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
</div> </div>
</div> </div>
<template v-if="isDownCtrls"> <template v-if="isDownCtrls && section">
<!-- 刻度线 --> <!-- 刻度线 -->
<Scale v-bind="{ data:_sections, isDownCtrls, direction, tunnel }" /> <Scale v-bind="{ data:_sections, isDownCtrls, direction, tunnel }" />
...@@ -121,16 +121,18 @@ const props = withDefaults( ...@@ -121,16 +121,18 @@ const props = withDefaults(
evtConfigs: any, // 事件详情 evtConfigs: any, // 事件详情
act: boolean, // 是否激活 act: boolean, // 是否激活
traffic?: any, // 车流引擎 traffic?: any, // 车流引擎
section?: boolean
filterType?: null | string, // 筛选类型 filterType?: null | string, // 筛选类型
isDownCtrls?: boolean, // 是否下方展示控制器 isDownCtrls?: boolean, // 是否下方展示控制器
trackers?: any trackers?: any
roadHeight?: number
}>(), }>(),
{ {
filterType: null, filterType: null,
isDownCtrls: false, isDownCtrls: false,
act: true, act: true,
trackers: {} trackers: {},
roadHeight: 120
} }
) )
...@@ -143,6 +145,11 @@ const _eventSelected= ref(null) // 选中车辆 ...@@ -143,6 +145,11 @@ const _eventSelected= ref(null) // 选中车辆
// const sectionAct = ref(null) // 当前段面 // const sectionAct = ref(null) // 当前段面
const start = ref(true) // 是否初次追踪 const start = ref(true) // 是否初次追踪
const _lines = ref(3) const _lines = ref(3)
const roadStyle = computed(() => {
return {
height: `${props.roadHeight}px`
}
})
const sectionAct = defineModel('sectionAct') // 选中段面 const sectionAct = defineModel('sectionAct') // 选中段面
...@@ -330,19 +337,16 @@ $base-common: '@/assets/images/common/'; ...@@ -330,19 +337,16 @@ $base-common: '@/assets/images/common/';
background: url($base-url + 'entry.png') no-repeat; background: url($base-url + 'entry.png') no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
position: absolute; position: absolute;
left: 0;
top: 0; top: 0;
z-index: 1; z-index: 1;
} }
.export { .export {
height: 100%; height: 100%;
width: 90px; width: 90px;
background: url($base-url + 'entry.png') no-repeat; background: url($base-url + 'export.png') no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
position: absolute; position: absolute;
right: 0;
top: 0; top: 0;
transform: rotateY(180deg);
z-index: 1; z-index: 1;
} }
.inner { .inner {
...@@ -433,6 +437,13 @@ $base-common: '@/assets/images/common/'; ...@@ -433,6 +437,13 @@ $base-common: '@/assets/images/common/';
margin-top: -60px; margin-top: -60px;
} }
&.in { &.in {
.entry {
right: 0;
transform: rotateY(180deg);
}
.export {
left: 0;
}
.running-car { .running-car {
right: -60px; right: -60px;
} }
...@@ -442,6 +453,13 @@ $base-common: '@/assets/images/common/'; ...@@ -442,6 +453,13 @@ $base-common: '@/assets/images/common/';
} }
&.out { &.out {
.entry {
left: 0;
}
.export {
right: 0;
transform: rotateY(180deg);
}
.running-car { .running-car {
left: -60px; left: -60px;
.core { .core {
......
...@@ -34,6 +34,7 @@ import { EVENT_TYPE } from '@/config/enum' ...@@ -34,6 +34,7 @@ import { EVENT_TYPE } from '@/config/enum'
import EVENT_IMAGE from '@/assets/images/event/event.png' import EVENT_IMAGE from '@/assets/images/event/event.png'
import TrafficSocket from '@/views/traffic/utils/socket' import TrafficSocket from '@/views/traffic/utils/socket'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useTrafficStoreOut } from '@/store/modules/traffic'
const props = defineProps<{ const props = defineProps<{
scene: string, scene: string,
...@@ -46,6 +47,7 @@ const props = defineProps<{ ...@@ -46,6 +47,7 @@ const props = defineProps<{
const events = defineModel<any[]>('events') // 实时事件 const events = defineModel<any[]>('events') // 实时事件
const selected = defineModel<any>('selected') // 选中事件 const selected = defineModel<any>('selected') // 选中事件
const socketEvent = ref<any>(null) // 事件流 const socketEvent = ref<any>(null) // 事件流
const trafficStore = useTrafficStoreOut()
const route = useRoute() const route = useRoute()
...@@ -109,7 +111,9 @@ const evtFlowStart = async () => { ...@@ -109,7 +111,9 @@ const evtFlowStart = async () => {
) )
socketEvent.value.start(res => { 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(() => { ...@@ -170,6 +174,11 @@ onMounted(() => {
}) })
onBeforeUnmount(() => {
// 清理事件实例的 WebSocket 连接
evtFlowClose()
})
</script> </script>
......
...@@ -37,6 +37,7 @@ const packings = computed(() => { ...@@ -37,6 +37,7 @@ const packings = computed(() => {
const style = {} const style = {}
style[props.direction ? 'left' : 'right'] = item.x + 'px' style[props.direction ? 'left' : 'right'] = item.x + 'px'
style.width = item.long + 'px'
const temp = { const temp = {
...item, ...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> <template>
<div
<div :class="[ :class="[
'x-direction', 'x-direction',
isDownCtrls ? 'down' : 'up', isDownCtrls ? 'down' : 'up',
+direction ? 'out' : 'in', +direction ? 'out' : 'in',
act ? 'act' : 'disabled' act ? 'act' : 'disabled'
]"> ]"
>
<template v-if="!isDownCtrls && section"> <template v-if="!isDownCtrls && section">
<!-- 段面控制器 --> <!-- 段面控制器 -->
<div class="section-box"> <div class="section-box">
<Section <Section
v-bind="{ data:_sections, scene: 'section', direction, trackers }" v-bind="{ data: _sections, scene: 'section', direction, trackers }"
v-model="sectionAct" v-model="sectionAct"
@go-section="goSection" @go-section="goSection"
/> />
...@@ -23,33 +21,33 @@ ...@@ -23,33 +21,33 @@
<!-- 刻度线 --> <!-- 刻度线 -->
<Scale class="scale-line" v-bind="{ data: _sections, isDownCtrls, direction, tunnel }" /> <Scale class="scale-line" v-bind="{ data: _sections, isDownCtrls, direction, tunnel }" />
</template> </template>
<div class="main-road"> <div class="main-road" :style="roadStyle">
<!-- 停车带层 --> <!-- 停车带层 -->
<Parking v-bind="{ scene: 'panorama', direction, tunnel, _traffic: traffic }" /> <Parking v-bind="{ scene: 'panorama', direction, tunnel, _traffic: traffic }" />
<!-- 路面层 --> <!-- 路面层 -->
<div class="level-road"> <div class="level-road">
<div class="entry"></div> <div class="entry"></div>
<div class="export"></div> <div class="export"></div>
<div class="inner"> <div class="inner">
<!-- 热力图层 --> <!-- 热力图层 -->
<HeatRoad class="dis-tag" :direction /> <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 }" /> <Line v-bind="{ direction, tunnel, _traffic: traffic }" />
<!-- 路标 --> <!-- 路标 -->
<Arrow v-bind="{ direction, tunnel, _traffic: traffic }" /> <Arrow v-bind="{ direction, tunnel, _traffic: traffic }" />
<!-- 车流层 --> <!-- 车流层 -->
<Cars <Cars
...@@ -57,12 +55,18 @@ ...@@ -57,12 +55,18 @@
v-model:cars="_cars" v-model:cars="_cars"
v-model:selected="_carSelected" v-model:selected="_carSelected"
v-bind="{ v-bind="{
scene: 'panorama', scene: 'panorama',
_traffic: traffic, _events, tunnel, filterType, _traffic: traffic,
direction, plateType, useMaterial, _events,
trackers, evtConfigs tunnel,
}" filterType,
@car-socket="e => emits('car-socket', e)" direction,
plateType,
useMaterial,
trackers,
evtConfigs
}"
@car-socket="(e) => emits('car-socket', e)"
/> />
<!-- 事件层 --> <!-- 事件层 -->
...@@ -71,52 +75,42 @@ ...@@ -71,52 +75,42 @@
v-model:selected="_eventSelected" v-model:selected="_eventSelected"
v-bind="{ v-bind="{
scene: 'panorama', scene: 'panorama',
_traffic: traffic, direction, evtConfigs, tunnel _traffic: traffic,
direction,
evtConfigs,
tunnel
}" }"
/> />
</div> </div>
</div> </div>
</div> </div>
<template v-if="isDownCtrls && section"> <template v-if="isDownCtrls && section">
<!-- 刻度线 --> <!-- 刻度线 -->
<Scale v-bind="{ data:_sections, isDownCtrls, direction, tunnel }" /> <Scale v-bind="{ data: _sections, isDownCtrls, direction, tunnel }" />
<!-- 段面控制器 --> <!-- 段面控制器 -->
<Section <Section
v-bind="{ data:_sections, isDownCtrls, scene: 'panorama', direction }" v-bind="{ data: _sections, isDownCtrls, scene: 'panorama', direction }"
@go-section="goSection" @go-section="goSection"
/> />
</template> </template>
</div> </div>
<CarCollect <CarCollect
v-model="_carSelected" v-model="_carSelected"
:cars="_cars" :cars="_cars"
ref="collectRef" ref="collectRef"
@go-track="e => emits('go-track', e)" @go-track="(e) => emits('go-track', e)"
/> />
<EventPop <EventPop :events="_events" v-model="_eventSelected" ref="eventRef" />
:events="_events"
v-model="_eventSelected"
ref="eventRef"
/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Section from './comps/Section/index.vue' import Section from './comps/Section/index.vue'
import Scale from './comps/Scale/index.vue' import Scale from './comps/Scale/index.vue'
import Parking from './comps/Parking/index.vue' import Parking from './comps/Parking/index.vue'
import Camera from './comps/Camera/index.vue' import Camera from './comps/Camera/index.vue'
import Line from './comps/Line/index.vue' import Line from './comps/Line/index.vue'
import Arrow from './comps/Arrow/index.vue' import Arrow from './comps/Arrow/index.vue'
...@@ -132,7 +126,14 @@ import tunnelConfigs from '@/config/tunnel' ...@@ -132,7 +126,14 @@ import tunnelConfigs from '@/config/tunnel'
import { useTunnelStoreWithOut } from '@/store/modules/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 tunnelStore = useTunnelStoreWithOut()
const collectRef = ref<any>(null) // 收藏弹窗实例 const collectRef = ref<any>(null) // 收藏弹窗实例
...@@ -140,46 +141,49 @@ const eventRef = ref<any>(null) // 事件弹窗实例 ...@@ -140,46 +141,49 @@ const eventRef = ref<any>(null) // 事件弹窗实例
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
direction: number, // 道路方向 direction: number // 道路方向
plateType: number, // 铭牌类型 plateType: number // 铭牌类型
useMaterial: boolean, // 是否使用动态车辆贴图 useMaterial: boolean // 是否使用动态车辆贴图
evtConfigs: any, // 事件详情 evtConfigs: any // 事件详情
act: boolean, // 是否激活 act: boolean // 是否激活
traffic?: any, // 车流引擎 traffic?: any // 车流引擎
section?: boolean, section?: boolean
filterType?: null | string, // 筛选类型 filterType?: null | string // 筛选类型
isDownCtrls?: boolean, // 是否下方展示控制器 isDownCtrls?: boolean // 是否下方展示控制器
trackers?: any trackers?: any
roadHeight?: number // 路面高度
}>(), }>(),
{ {
section: true, section: true,
filterType: null, filterType: null,
isDownCtrls: false, isDownCtrls: false,
act: true, act: true,
trackers: {} trackers: {},
roadHeight: 90
} }
) )
const _traffic = ref<any>(null) // 车流引擎 const _traffic = ref<any>(null) // 车流引擎
const _sections = ref<any[]>([]) // 段面控制 const _sections = ref<any[]>([]) // 段面控制
const _cars = ref<any[]>([]) // 车流 const _cars = ref<any[]>([]) // 车流
const _carSelected= ref(null) // 选中车辆 const _carSelected = ref(null) // 选中车辆
const _events = ref<any[]>([]) // 事件列表 const _events = ref<any[]>([]) // 事件列表
const _eventSelected= ref(null) // 选中车辆 const _eventSelected = ref(null) // 选中车辆
// const sectionAct = ref(null) // 当前段面 // const sectionAct = ref(null) // 当前段面
const start = ref(true) // 是否初次追踪 const start = ref(true) // 是否初次追踪
const _lines = ref(3) const _lines = ref(3)
const roadStyle = computed(() => {
return {
height: `${props.roadHeight}px`
}
})
const sectionAct = defineModel('sectionAct') // 选中段面 const sectionAct = defineModel('sectionAct') // 选中段面
const tunnel = computed(() => { const tunnel = computed(() => {
return [ return [tunnelStore.getUpTunnel, tunnelStore.getDownTunnel][props.direction]
tunnelStore.getUpTunnel,
tunnelStore.getDownTunnel
][props.direction]
}) })
watch( watch(
() => props.traffic, () => props.traffic,
(e) => { (e) => {
...@@ -206,10 +210,10 @@ watch( ...@@ -206,10 +210,10 @@ watch(
watch( watch(
() => props.trackers.car, () => props.trackers.car,
(xcar) => { (xcar) => {
if (xcar && props.trackers.tracking) { if (xcar && props.trackers.tracking) {
const distance = props.direction
const distance = props.direction ? xcar.originalCar.carMilestone - tunnel.value.start : tunnel.value.end - xcar.originalCar.carMilestone ? xcar.originalCar.carMilestone - tunnel.value.start
: tunnel.value.end - xcar.originalCar.carMilestone
// const left = distance % 300 // const left = distance % 300
// const sections = (distance - left) / 300 + (left > 0 ? 1 : 0) // const sections = (distance - left) / 300 + (left > 0 ? 1 : 0)
...@@ -234,9 +238,6 @@ const goSection = (x) => { ...@@ -234,9 +238,6 @@ const goSection = (x) => {
direction: props.direction direction: props.direction
}) })
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -265,8 +266,6 @@ $base-common: '@/assets/images/common/'; ...@@ -265,8 +266,6 @@ $base-common: '@/assets/images/common/';
} }
} }
.ctrls { .ctrls {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
...@@ -332,25 +331,23 @@ $base-common: '@/assets/images/common/'; ...@@ -332,25 +331,23 @@ $base-common: '@/assets/images/common/';
position: absolute; position: absolute;
background: url($base-url + 'road.png'); background: url($base-url + 'road.png');
background-size: auto 100%; background-size: auto 100%;
.entry { .entry {
height: 100%; height: 100%;
width: 90px; width: 90px;
background: url($base-url + 'entry.png') no-repeat; background: url($base-url + 'entry.png') no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
position: absolute; position: absolute;
left: 0;
top: 0; top: 0;
z-index: 1; z-index: 1;
} }
.export { .export {
height: 100%; height: 100%;
width: 90px; width: 90px;
background: url($base-url + 'entry.png') no-repeat; background: url($base-url + 'export.png') no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
position: absolute; position: absolute;
right: 0;
top: 0; top: 0;
transform: rotateY(180deg);
z-index: 1; z-index: 1;
} }
.inner { .inner {
...@@ -418,7 +415,6 @@ $base-common: '@/assets/images/common/'; ...@@ -418,7 +415,6 @@ $base-common: '@/assets/images/common/';
} }
.down { .down {
.stone-num { .stone-num {
bottom: 12px; bottom: 12px;
} }
...@@ -433,6 +429,13 @@ $base-common: '@/assets/images/common/'; ...@@ -433,6 +429,13 @@ $base-common: '@/assets/images/common/';
margin-top: -60px; margin-top: -60px;
} }
&.in { &.in {
.entry {
right: 0;
transform: rotateY(180deg);
}
.export {
left: 0;
}
.running-car { .running-car {
right: -60px; right: -60px;
} }
...@@ -442,6 +445,13 @@ $base-common: '@/assets/images/common/'; ...@@ -442,6 +445,13 @@ $base-common: '@/assets/images/common/';
} }
&.out { &.out {
.entry {
left: 0;
}
.export {
right: 0;
transform: rotateY(180deg);
}
.running-car { .running-car {
left: -60px; left: -60px;
.core { .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