完善进度条

This commit is contained in:
yueliuli 2026-04-21 18:00:30 +08:00
parent f04b1aed41
commit ce29a7c38d
4 changed files with 527 additions and 159 deletions

View File

@ -28,7 +28,7 @@ function handleItemClick(item: string, index: number) {
<!-- 标题栏 --> <!-- 标题栏 -->
<el-row class="px-3 pb-1 pt-3"> <el-row class="px-3 pb-1 pt-3">
<el-text type="info" size="small"> <el-text type="info" size="small">
{{ props.title }} {{ props.title }} ( {{ props.data.length }} )
</el-text> </el-text>
</el-row> </el-row>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { ElButton, ElScrollbar } from 'element-plus' import { ElScrollbar } from 'element-plus'
import { computed, onMounted, ref, watch } from 'vue' import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
const props = defineProps({ const props = defineProps({
currentFrame: { currentFrame: {
@ -11,167 +11,331 @@ const props = defineProps({
type: Number, type: Number,
required: true, required: true,
}, },
objects: { hazardRanges: {
type: Array, type: Object,
default: () => [], default: () => ({}),
}, },
}) })
const emit = defineEmits(['zoom', 'frameChange']) const emit = defineEmits(['hazardClick', 'frameChange'])
const FPS = 30
const pxPerFrame = ref(2) const pxPerFrame = ref(2)
const minPxPerFrame = 0.5
const maxPxPerFrame = 10
const timelineContainer = ref(null) const timelineContainer = ref(null)
const isDragging = ref(false)
const isPlayheadDragging = ref(false)
const isRulerDragging = ref(false)
const dragStartX = ref(0)
const scrollStartLeft = ref(0)
const localCurrentFrame = ref(0)
//
const playheadPos = computed(() => {
return 100 + props.currentFrame * pxPerFrame.value
})
//
const trackWidth = computed(() => { const trackWidth = computed(() => {
return `${props.totalFrames * pxPerFrame.value}px` return `${props.totalFrames * pxPerFrame.value + 50}px`
}) })
// const totalSeconds = computed(() => {
function handleZoom() { return Math.ceil(props.totalFrames / FPS)
emit('zoom') })
function updatePxPerFrame() {
if (!timelineContainer.value)
return
const containerWidth = timelineContainer.value.clientWidth - 20
const calculatedPxPerFrame = containerWidth / props.totalFrames
pxPerFrame.value = Math.max(minPxPerFrame, Math.min(calculatedPxPerFrame, maxPxPerFrame))
} }
// function zoomIn() {
function updatePlayhead(e) { pxPerFrame.value = Math.min(pxPerFrame.value * 1.5, maxPxPerFrame)
}
function zoomOut() {
pxPerFrame.value = Math.max(pxPerFrame.value / 1.5, minPxPerFrame)
}
function handleZoomIn() {
zoomIn()
}
function handleZoomOut() {
zoomOut()
}
const timeTicks = computed(() => {
const ticks = []
for (let s = 0; s <= totalSeconds.value; s += 10) {
ticks.push({
second: s,
position: s * FPS * pxPerFrame.value,
label: formatTime(s),
})
}
return ticks
})
function formatTime(seconds) {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
if (mins > 0) {
return `${mins}:${secs.toString().padStart(2, '0')}`
}
return `${secs}s`
}
const playheadPos = computed(() => {
return props.currentFrame * pxPerFrame.value
})
const trackRows = computed(() => {
const ranges = props.hazardRanges
const sortedHazards = Object.entries(ranges)
.map(([id, frames]) => ({
id,
ranges: frames.reduce((acc, _, i) => {
if (i % 2 === 0) {
acc.push([frames[i], frames[i + 1]])
}
return acc
}, []),
}))
.sort((a, b) => {
const aStart = a.ranges[0]?.[0] || 0
const bStart = b.ranges[0]?.[0] || 0
return aStart - bStart
})
const rows = []
sortedHazards.forEach((hazard) => {
let assigned = false
for (let i = 0; i < rows.length; i++) {
const canPlace = hazard.ranges.every(([start, end]) => {
return rows[i].every((existing) => {
return end <= existing.start || start >= existing.end
})
})
if (canPlace) {
rows[i].push({
id: hazard.id,
start: hazard.ranges[0][0],
end: hazard.ranges[0][1],
ranges: hazard.ranges,
})
assigned = true
break
}
}
if (!assigned) {
rows.push([{
id: hazard.id,
start: hazard.ranges[0][0],
end: hazard.ranges[0][1],
ranges: hazard.ranges,
}])
}
})
return rows
})
function getHazardStyle(start, end) {
const left = start * pxPerFrame.value
const width = (end - start) * pxPerFrame.value
return {
left: `${left}px`,
width: `${width}px`,
}
}
function handleHazardClick(hazardId) {
emit('hazardClick', Number(hazardId))
}
function handleWheel(e) {
if (!timelineContainer.value)
return
e.preventDefault()
const container = timelineContainer.value
const delta = e.deltaY || e.deltaX
container.scrollLeft += delta
}
function handleMouseDown(e) {
if (e.button !== 0)
return
const target = e.target
const playhead = document.getElementById('playhead')
if (playhead && (playhead.contains(target) || target === playhead))
return
const ruler = timelineContainer.value?.querySelector('.timeline-ruler')
if (ruler && (ruler.contains(target) || target === ruler)) {
isRulerDragging.value = true
}
isDragging.value = true
dragStartX.value = e.clientX
if (timelineContainer.value)
scrollStartLeft.value = timelineContainer.value.scrollLeft
document.body.style.cursor = 'grabbing'
document.body.style.userSelect = 'none'
}
function handleMouseMove(e) {
if (!isDragging.value || !timelineContainer.value)
return
const delta = dragStartX.value - e.clientX
timelineContainer.value.scrollLeft = scrollStartLeft.value + delta
}
function handleMouseUp() {
isDragging.value = false
isPlayheadDragging.value = false
isRulerDragging.value = false
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
function handlePlayheadMouseDown(e) {
e.stopPropagation()
isPlayheadDragging.value = true
dragStartX.value = e.clientX
document.body.style.cursor = 'ew-resize'
document.body.style.userSelect = 'none'
}
function handleRulerClick(e) {
if (isRulerDragging.value)
return
if (!timelineContainer.value) if (!timelineContainer.value)
return return
const rect = timelineContainer.value.getBoundingClientRect() const rect = timelineContainer.value.getBoundingClientRect()
const x = Math.max(0, Math.min(e.clientX - rect.left - 100, props.totalFrames * pxPerFrame.value)) const scrollLeft = timelineContainer.value.scrollLeft
const x = e.clientX - rect.left + scrollLeft
const frame = Math.floor(x / pxPerFrame.value) const frame = Math.floor(x / pxPerFrame.value)
emit('frameChange', frame) const clampedFrame = Math.max(0, Math.min(frame, props.totalFrames - 1))
localCurrentFrame.value = clampedFrame
emit('frameChange', clampedFrame)
}
function handlePlayheadDrag(e) {
if (!isPlayheadDragging.value || !timelineContainer.value)
return
const rect = timelineContainer.value.getBoundingClientRect()
const scrollLeft = timelineContainer.value.scrollLeft
const x = e.clientX - rect.left + scrollLeft
const frame = Math.floor(x / pxPerFrame.value)
const clampedFrame = Math.max(0, Math.min(frame, props.totalFrames - 1))
localCurrentFrame.value = clampedFrame
emit('frameChange', clampedFrame)
} }
//
watch(() => props.currentFrame, () => { watch(() => props.currentFrame, () => {
if (!isPlayheadDragging.value) {
localCurrentFrame.value = props.currentFrame
}
nextTick(() => {
if (!timelineContainer.value) if (!timelineContainer.value)
return return
const container = timelineContainer.value const container = timelineContainer.value
const playhead = document.getElementById('playhead') const playhead = document.getElementById('playhead')
if (playhead) { if (!playhead)
container.scrollLeft = playhead.offsetLeft - container.clientWidth / 2 return
const playheadLeft = playhead.offsetLeft
const containerWidth = container.clientWidth
const containerScrollLeft = container.scrollLeft
const playheadVisible = playheadLeft >= containerScrollLeft && playheadLeft <= containerScrollLeft + containerWidth - 50
if (!playheadVisible) {
container.scrollLeft = playheadLeft - containerWidth / 2
} }
}) })
})
//
onMounted(() => { onMounted(() => {
const progressContainer = timelineContainer.value const progressContainer = timelineContainer.value
if (!progressContainer) if (!progressContainer)
return return
let isDragging = false
progressContainer.addEventListener('mousedown', (e) => { updatePxPerFrame()
isDragging = true
updatePlayhead(e) progressContainer.addEventListener('wheel', handleWheel, { passive: false })
progressContainer.addEventListener('mousedown', handleMouseDown)
window.addEventListener('mousemove', handlePlayheadDrag)
}) })
window.addEventListener('mousemove', (e) => { watch(() => props.totalFrames, () => {
if (isDragging) nextTick(() => {
updatePlayhead(e) updatePxPerFrame()
})
}) })
window.addEventListener('mouseup', () => { onUnmounted(() => {
isDragging = false window.removeEventListener('mousemove', handleMouseMove)
}) window.removeEventListener('mousemove', handlePlayheadDrag)
window.removeEventListener('mouseup', handleMouseUp)
}) })
</script> </script>
<template> <template>
<div class="timeline-wrapper h-[33.333%] flex flex-col border-t border-slate-700 bg-slate-900"> <div class="timeline-wrapper">
<!-- 工具栏 --> <div class="timeline-toolbar">
<div class="h-8 flex items-center gap-2 border-b border-slate-800 bg-slate-800/50 px-2"> <span class="timeline-title">隐患时间线 ( {{ Object.keys(hazardRanges).length }} 个隐患)</span>
<ElButton <div class="timeline-zoom-controls">
text <button class="zoom-btn" title="缩小" @click="handleZoomOut">
class="!h-auto !p-1 !text-slate-400 hover:!bg-slate-700 hover:!text-white"
title="缩放" </button>
@click="handleZoom" <button class="zoom-btn" title="放大" @click="handleZoomIn">
> +
<svg </button>
class="h-4 w-4" </div>
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"
/>
</svg>
</ElButton>
<div class="mx-1 h-4 w-px bg-slate-700" />
<span class="text-xs text-slate-500">轨道视图</span>
</div> </div>
<!-- 时间轴主体横向滚动容器 -->
<div <div
ref="timelineContainer" ref="timelineContainer"
class="custom-scrollbar relative flex-1 overflow-x-auto bg-slate-900" class="timeline-container"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseUp"
> >
<!-- 播放头固定垂直不随轨道上下滚 -->
<div <div
id="playhead" id="playhead"
class="pointer-events-none absolute bottom-0 top-0 z-20 w-px bg-red-500" class="playhead"
:style="{ left: `${playheadPos}px` }" :style="{ left: `${playheadPos}px` }"
@mousedown="handlePlayheadMouseDown"
> >
<div <div class="playhead-indicator" />
class="absolute top-0 h-3 w-3 rotate-45 bg-red-500 -mt-1.5 -translate-x-1/2"
/>
</div> </div>
<!-- 网格背景固定不动 --> <div class="timeline-content" :style="{ width: trackWidth }">
<div class="timeline-ruler" @click="handleRulerClick">
<div <div
class="pointer-events-none absolute inset-0 opacity-10" v-for="tick in timeTicks"
style=" :key="tick.second"
background-image: linear-gradient(to right, #fff 1px, transparent 1px); class="time-tick"
background-size: 50px 100%; :style="{ left: `${tick.position}px` }"
"
/>
<!-- 内容层宽度撑开横向滚动 -->
<div class="relative" :style="{ width: trackWidth }">
<!-- 左侧标签占位 -->
<div class="absolute bottom-0 left-0 top-0 z-10 w-20 bg-slate-900" />
<!-- 轨道列表仅这里垂直滚动 -->
<ElScrollbar class="ml-20" :style="{ height: '100%' }">
<div id="tracks-list" class="pb-10 pt-4">
<div
v-for="obj in objects"
:key="obj.id"
class="relative h-12 flex items-center border-b border-slate-800/50 hover:bg-slate-800/30"
> >
<!-- 轨道标签固定左侧不横向滚动 --> <div class="tick-mark" />
<div <span class="tick-label">{{ tick.label }}</span>
class="absolute left-2 z-10 w-20 truncate text-[10px] text-slate-400" </div>
:style="{ transform: 'translateX(-100%)' }"
>
{{ obj.name }}
</div> </div>
<!-- 轨道进度条 --> <ElScrollbar :style="{ height: '100%' }">
<div id="tracks-list" class="tracks-list">
<div <div
class="absolute h-6 cursor-pointer border border-opacity-50 rounded bg-opacity-20 hover:bg-opacity-30" v-for="(row, rowIndex) in trackRows"
:style="{ :key="rowIndex"
left: '0px', class="track-row"
width: trackWidth,
backgroundColor: obj.color,
borderColor: obj.color,
top: '12px',
}"
> >
<div <div
v-for="i in Math.floor(props.totalFrames / 30)" v-for="hazard in row"
:key="i" :key="hazard.id"
class="absolute top-1/2 h-1 w-1 rounded-full bg-white opacity-50 -translate-y-1/2" class="hazard-block"
:style="{ left: `${i * 30 * pxPerFrame}px` }" :style="getHazardStyle(hazard.start, hazard.end)"
/> @click="handleHazardClick(hazard.id)"
>
{{ parseInt(hazard.id) + 1 }}
</div> </div>
</div> </div>
</div> </div>
@ -182,29 +346,189 @@ onMounted(() => {
</template> </template>
<style scoped> <style scoped>
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #1e293b;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
.timeline-wrapper { .timeline-wrapper {
height: 33.333%;
display: flex;
flex-direction: column;
border-top: 1px solid var(--ep-border-color);
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
} }
/* 让 ElScrollbar 填满高度 */ .timeline-toolbar {
height: 32px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--ep-border-color-light);
padding: 0 8px;
}
.timeline-title {
font-size: 12px;
color: var(--ep-text-color-secondary);
}
.timeline-zoom-controls {
display: flex;
gap: 4px;
}
.zoom-btn {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--ep-border-color);
border-radius: 4px;
background-color: var(--ep-bg-color);
color: var(--ep-text-color-primary);
cursor: pointer;
font-size: 14px;
line-height: 1;
}
.zoom-btn:hover {
background-color: var(--ep-fill-color-light);
}
.timeline-container {
flex: 1;
overflow-x: auto;
overflow-y: hidden;
position: relative;
background-color: var(--ep-bg-color);
cursor: grab;
/* padding-top: 28px; */
}
.timeline-container:active {
cursor: grabbing;
}
.time-tick {
position: absolute;
top: 0;
height: 100%;
display: flex;
align-items: flex-start;
pointer-events: none;
}
.tick-mark {
width: 1px;
height: 8px;
background-color: var(--ep-color-info);
margin-top: 0;
}
.tick-label {
font-size: 10px;
color: var(--ep-text-color-secondary);
margin-left: 4px;
margin-top: 0;
white-space: nowrap;
}
.playhead {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background-color: #ef4444;
z-index: 25;
cursor: ew-resize;
}
.playhead-indicator {
position: absolute;
top: 0;
left: -4px;
width: 10px;
height: 10px;
background-color: #ef4444;
border-radius: 50%;
}
.timeline-content {
position: relative;
}
.timeline-ruler {
background-color: var(--ep-fill-color-light);
border-bottom: 1px solid var(--ep-border-color);
height: 24px;
cursor: pointer;
z-index: 5;
}
.time-tick-inner {
position: absolute;
top: 0;
height: 100%;
}
.tick-line {
width: 1px;
height: 100%;
background-color: var(--ep-border-color-lighter);
opacity: 0.5;
}
.time-tick-inner.is-major .tick-line {
background-color: var(--ep-color-info);
opacity: 0.8;
}
.track-row {
position: relative;
height: 32px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--ep-border-color-lighter);
transition: background-color 0.2s;
}
.track-row:hover {
background-color: var(--ep-fill-color-light);
}
.hazard-block {
position: absolute;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
background-color: var(--ep-color-primary);
color: #fff;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s;
}
.hazard-block:hover {
background-color: color-mix(in srgb, var(--ep-color-primary) 80%, transparent);
}
.timeline-container::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.timeline-container::-webkit-scrollbar-thumb {
background: var(--ep-color-info);
border-radius: 3px;
}
.timeline-container::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--ep-color-info) 60%, transparent);
}
:deep(.el-scrollbar) { :deep(.el-scrollbar) {
height: 100% !important; height: 100% !important;
} }
:deep(.el-scrollbar__wrap) { :deep(.el-scrollbar__wrap) {
height: 100% !important; height: 100% !important;
} }

View File

@ -17,7 +17,7 @@ interface HazardItem {
interface DataFormat { interface DataFormat {
隐患列表: string[] 隐患列表: string[]
// : string[] 隐患范围字典: Record<string, number[]>
隐患数据: HazardItem[] 隐患数据: HazardItem[]
} }
@ -29,6 +29,7 @@ interface ResultObject {
class_id: number class_id: number
level: number level: number
start_frame: number start_frame: number
end_frame: number
start_sec: number start_sec: number
location: string location: string
} }
@ -54,7 +55,7 @@ const resultData = ref<ResultData>({
const videoRef = ref<HTMLVideoElement>() const videoRef = ref<HTMLVideoElement>()
const data = ref<DataFormat>({ const data = ref<DataFormat>({
隐患列表: [], 隐患列表: [],
// : [], 隐患范围字典: {},
隐患数据: [], 隐患数据: [],
}) })
// const data = ref({ // const data = ref({
@ -80,24 +81,47 @@ const data = ref<DataFormat>({
// ], // ],
// }) // })
const objects = ref([
{
id: 'obj_001',
name: '行人 (Person A)',
type: 'person',
color: '#3b82f6',
frames: [...Array.from({ length: 300 }).keys()],
},
{
id: 'obj_001',
name: '行人 (Person A)',
type: 'person',
color: '#3b82f6',
frames: [...Array.from({ length: 300 }).keys()],
},
])
const selectedHazard = ref(-1) const selectedHazard = ref(-1)
const currentFrame = ref(0)
const videoDuration = ref(0)
const FPS = 30
const totalFrames = computed(() => {
if (videoDuration.value > 0) {
return Math.floor(videoDuration.value * FPS)
}
const objects = resultData.value.objects
if (!objects || objects.length === 0)
return 300
const maxEndFrame = Math.max(...objects.map(obj => obj.end_frame || obj.start_frame))
return maxEndFrame + 100
})
function handleTimelineHazardClick(hazardId: string) {
selectedHazard.value = Number(hazardId)
handleJumpToTimePoint(Number(hazardId))
}
function handleFrameChange(frame: number) {
const videoEl = videoRef.value
if (!videoEl)
return
const seconds = frame / FPS
videoEl.currentTime = Math.min(seconds, videoEl.duration)
}
function updateCurrentFrame() {
const videoEl = videoRef.value
if (!videoEl)
return
currentFrame.value = Math.floor(videoEl.currentTime * 30)
}
function handleVideoLoadedMetadata() {
const videoEl = videoRef.value
if (!videoEl)
return
videoDuration.value = videoEl.duration
}
function getData() { function getData() {
const { tag, base, objects } = resultData.value const { tag, base, objects } = resultData.value
@ -106,19 +130,27 @@ function getData() {
return `(${resultData.value?.class_list?.[obj.class_id] || ''}) ${tag?.[obj.tag_id] || ''}` return `(${resultData.value?.class_list?.[obj.class_id] || ''}) ${tag?.[obj.tag_id] || ''}`
}) })
data.value.隐患范围字典 = {}
;(objects || []).forEach((obj: any) => {
const 编号 = String(obj.hazard_track_id)
if (!data.value.隐患范围字典[编号])
data.value.隐患范围字典[编号] = []
data.value.隐患范围字典[编号].push(obj.start_frame, obj.end_frame)
})
// data.value. = (objects || []).map((_: any, i: number) => `${i + 1}`) // data.value. = (objects || []).map((_: any, i: number) => `${i + 1}`)
data.value.隐患数据 = (objects || []).map((obj: any) => { data.value.隐患数据 = (objects || []).map((obj: any) => {
const totalSeconds = obj.start_sec || 0 const jumpPoint = obj.start_sec || 0
const hh = String(Math.floor(totalSeconds / 3600)).padStart(2, '0') const hh = String(Math.floor(jumpPoint / 3600)).padStart(2, '0')
const mm = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0') const mm = String(Math.floor((jumpPoint % 3600) / 60)).padStart(2, '0')
const ss = String(Math.floor(totalSeconds % 60)).padStart(2, '0') const ss = String(Math.floor(jumpPoint % 60)).padStart(2, '0')
return { return {
隐患编号: obj.hazard_track_id || '', 隐患编号: obj.hazard_track_id || '',
物体类型: resultData.value.class_list?.[obj.class_id] || '', 物体类型: resultData.value.class_list?.[obj.class_id] || '',
隐患名称: tag?.[obj.tag_id] || '', 隐患名称: tag?.[obj.tag_id] || '',
时间点: `${hh}:${mm}:${ss}`, 时间点: `${hh}:${mm}:${ss}`,
跳转时间点: totalSeconds, 跳转时间点: jumpPoint,
隐患描述: obj.location || '', 隐患描述: obj.location || '',
依据: base?.[obj.base_id] || '', 依据: base?.[obj.base_id] || '',
整改建议: '', 整改建议: '',
@ -241,6 +273,7 @@ onMounted(() => {
class_id: 0, class_id: 0,
level: 1, level: 1,
start_frame: 551, start_frame: 551,
end_frame: 581,
start_sec: 18.4, start_sec: 18.4,
location: '画面中央偏右的墙壁上', location: '画面中央偏右的墙壁上',
}, },
@ -252,6 +285,7 @@ onMounted(() => {
class_id: 1, class_id: 1,
level: 2, level: 2,
start_frame: 3618, start_frame: 3618,
end_frame: 3648,
start_sec: 120.6, start_sec: 120.6,
location: '画面右侧墙壁上的白色插座面板', location: '画面右侧墙壁上的白色插座面板',
}, },
@ -263,6 +297,7 @@ onMounted(() => {
class_id: 2, class_id: 2,
level: 1, level: 1,
start_frame: 3733, start_frame: 3733,
end_frame: 3763,
start_sec: 124.4, start_sec: 124.4,
location: '画面中央偏左墙壁上的配电箱正下方紧贴放置有一台不锈钢设备导致配电箱前1米内被占用。', location: '画面中央偏左墙壁上的配电箱正下方紧贴放置有一台不锈钢设备导致配电箱前1米内被占用。',
}, },
@ -274,6 +309,7 @@ onMounted(() => {
class_id: 1, class_id: 1,
level: 1, level: 1,
start_frame: 3767, start_frame: 3767,
end_frame: 3900,
start_sec: 125.6, start_sec: 125.6,
location: '画面左侧墙壁上,操作台上方区域', location: '画面左侧墙壁上,操作台上方区域',
}, },
@ -285,6 +321,7 @@ onMounted(() => {
class_id: 1, class_id: 1,
level: 1, level: 1,
start_frame: 3812, start_frame: 3812,
end_frame: 3842,
start_sec: 127.1, start_sec: 127.1,
location: '画面中央偏右墙壁上,配电箱下方区域', location: '画面中央偏右墙壁上,配电箱下方区域',
}, },
@ -296,6 +333,7 @@ onMounted(() => {
class_id: 2, class_id: 2,
level: 1, level: 1,
start_frame: 5199, start_frame: 5199,
end_frame: 5229,
start_sec: 173.3, start_sec: 173.3,
location: '画面中央偏右的蓝色配电箱正下方及周围区域', location: '画面中央偏右的蓝色配电箱正下方及周围区域',
}, },
@ -307,6 +345,7 @@ onMounted(() => {
class_id: 2, class_id: 2,
level: 1, level: 1,
start_frame: 5409, start_frame: 5409,
end_frame: 5439,
start_sec: 180.3, start_sec: 180.3,
location: '画面左侧墙壁上的灰色配电箱箱门缺失,内部元器件裸露', location: '画面左侧墙壁上的灰色配电箱箱门缺失,内部元器件裸露',
}, },
@ -318,6 +357,7 @@ onMounted(() => {
class_id: 0, class_id: 0,
level: 1, level: 1,
start_frame: 5654, start_frame: 5654,
end_frame: 5684,
start_sec: 188.5, start_sec: 188.5,
location: '画面右侧的蓝色消火栓箱内部', location: '画面右侧的蓝色消火栓箱内部',
}, },
@ -337,7 +377,7 @@ onMounted(() => {
<template> <template>
<!-- 最外层容器占满整个视口 --> <!-- 最外层容器占满整个视口 -->
<el-container style="margin: 0; height: 100%; flex-direction: column;"> <el-container style="margin: 0; height: 100%; flex-direction: column;">
<el-header style="height: 60px; display: flex; align-items: center; border-bottom: 1px solid var(--ep-border-color);"> <el-header style="height: 30px; display: flex; align-items: center; border-bottom: 1px solid var(--ep-border-color);">
<el-breadcrumb separator="/"> <el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }"> <el-breadcrumb-item :to="{ path: '/' }">
主页 主页
@ -368,15 +408,19 @@ onMounted(() => {
:src="vidUrl" :src="vidUrl"
controls controls
class="video" class="video"
@timeupdate="updateCurrentFrame"
@loadedmetadata="handleVideoLoadedMetadata"
/> />
</div> </div>
</el-main> </el-main>
<el-footer style="height: 200px; padding: 0px;"> <el-footer style="height: 200px; padding: 0px;">
<Timeline <Timeline
style="height: 100%;" style="height: 100%;"
:current-frame="0" :current-frame="currentFrame"
:total-frames="300" :total-frames="totalFrames"
:objects="objects" :hazard-ranges="data.隐患范围字典"
@hazard-click="handleTimelineHazardClick"
@frame-change="handleFrameChange"
/> />
</el-footer> </el-footer>
</el-container> </el-container>

View File

@ -91,7 +91,7 @@ async function goToResult(formEl: FormInstance | undefined) {
<template> <template>
<!-- 最外层容器占满整个视口 --> <!-- 最外层容器占满整个视口 -->
<el-container style="margin: 0; height: 100%; flex-direction: column;"> <el-container style="margin: 0; height: 100%; flex-direction: column;">
<el-header style="height: 60px; display: flex; align-items: center; border-bottom: 1px solid var(--ep-border-color);"> <el-header style="height: 30px; display: flex; align-items: center; border-bottom: 1px solid var(--ep-border-color);">
<el-breadcrumb separator="/"> <el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }"> <el-breadcrumb-item :to="{ path: '/' }">
主页 主页