优化时间线组件,优化主页

时间线组件
- 默认缩放值为1
主页
- 开始检查后显示进度条和检查用时
- 优化布局
This commit is contained in:
yueliuli 2026-04-24 10:03:28 +08:00
parent ec6508c262
commit ee208d5b16
4 changed files with 120 additions and 104 deletions

1
src/components.d.ts vendored
View File

@ -37,6 +37,7 @@ declare module 'vue' {
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup'] ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
ElOption: typeof import('element-plus/es')['ElOption'] ElOption: typeof import('element-plus/es')['ElOption']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow']

View File

@ -1,8 +1,16 @@
import type { Action } from 'element-plus'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessageBox } from 'element-plus'
import { computed, nextTick, ref, watch } from 'vue' import { computed, nextTick, ref, watch } from 'vue'
function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
let timeoutId: ReturnType<typeof setTimeout> | null = null
return ((...args: any[]) => {
if (timeoutId)
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn(...args), delay)
}) as T
}
export interface HazardData { export interface HazardData {
ranges: number[] ranges: number[]
level: number level: number
@ -48,13 +56,14 @@ export function useTimeline(
emit: TimelineEmits, emit: TimelineEmits,
) { ) {
const FPS = 30 const FPS = 30
const pxPerFrame = ref(2) const pxPerFrame = ref(1)
const minPxPerFrame = ref(0.5) const minPxPerFrame = ref(0.5)
const maxPxPerFrame = ref(3) const maxPxPerFrame = ref(3)
const timelineContainer: Ref<HTMLElement | null> = ref(null) const timelineContainer: Ref<HTMLElement | null> = ref(null)
const isDragging = ref(false) const isDragging = ref(false)
const isPlayheadDragging = ref(false) const isPlayheadDragging = ref(false)
const isRulerDragging = ref(false) const isRulerDragging = ref(false)
const isMounted = ref(false)
const dragStartX = ref(0) const dragStartX = ref(0)
const scrollStartLeft = ref(0) const scrollStartLeft = ref(0)
const localCurrentFrame = ref(0) const localCurrentFrame = ref(0)
@ -67,19 +76,27 @@ export function useTimeline(
return Math.ceil(props.totalFrames / FPS) return Math.ceil(props.totalFrames / FPS)
}) })
/**
*
* 线
*/
function updatePxPerFrame(): void { function updatePxPerFrame(): void {
if (!timelineContainer.value || props.totalFrames === 0) if (!timelineContainer.value || props.totalFrames === 0)
return return
const containerWidth = timelineContainer.value.clientWidth - 20
const calculatedPxPerFrame = containerWidth / props.totalFrames const container = timelineContainer.value
// 计算最小缩放倍数,确保能看到视频全长 if (!container.isConnected)
return
const containerWidth = container.clientWidth - 20
if (containerWidth <= 0)
return
const newMinPxPerFrame = Math.max(0.1, containerWidth / props.totalFrames) const newMinPxPerFrame = Math.max(0.1, containerWidth / props.totalFrames)
// 只在必要时更新,避免无限循环
if (Math.abs(newMinPxPerFrame - minPxPerFrame.value) > 0.01) { if (Math.abs(newMinPxPerFrame - minPxPerFrame.value) > 0.01) {
minPxPerFrame.value = newMinPxPerFrame minPxPerFrame.value = newMinPxPerFrame
} }
// 最大值固定为3不修改
pxPerFrame.value = Math.max(minPxPerFrame.value, Math.min(calculatedPxPerFrame, maxPxPerFrame.value))
} }
function setZoom(value: number): void { function setZoom(value: number): void {
@ -336,16 +353,17 @@ export function useTimeline(
}) })
}) })
/**
*
* 线
*/
function initMounted(): void { function initMounted(): void {
isMounted.value = true
const progressContainer = timelineContainer.value const progressContainer = timelineContainer.value
if (!progressContainer) if (!progressContainer)
return return
// 使用 nextTick 确保容器已渲染
nextTick(() => {
updatePxPerFrame()
})
progressContainer.addEventListener('wheel', handleWheel, { passive: false }) progressContainer.addEventListener('wheel', handleWheel, { passive: false })
progressContainer.addEventListener('mousedown', handleMouseDown) progressContainer.addEventListener('mousedown', handleMouseDown)
window.addEventListener('mousemove', handleMouseMove) window.addEventListener('mousemove', handleMouseMove)
@ -353,34 +371,18 @@ export function useTimeline(
window.addEventListener('mouseup', handleMouseUp) window.addEventListener('mouseup', handleMouseUp)
} }
const debouncedUpdatePxPerFrame = debounce(() => {
updatePxPerFrame()
}, 150)
watch(() => props.totalFrames, () => { watch(() => props.totalFrames, () => {
// 使用 setTimeout 避免频繁更新 if (!isMounted.value)
setTimeout(() => { return
updatePxPerFrame() debouncedUpdatePxPerFrame()
}, 100)
}) })
// 移除 resize 监听,避免无限循环
// 监听容器宽度变化
// let resizeObserver: ResizeObserver | null = null
// function handleResize(): void {
// updatePxPerFrame()
// }
// watch(() => timelineContainer.value, (newVal, oldVal) => {
// if (newVal && !oldVal) {
// // 使用 ResizeObserver 代替 window resize 监听
// resizeObserver = new ResizeObserver(handleResize)
// resizeObserver.observe(newVal)
// }
// else if (!newVal && oldVal && resizeObserver) {
// resizeObserver.disconnect()
// resizeObserver = null
// }
// })
function initUnmounted(): void { function initUnmounted(): void {
isMounted.value = false
window.removeEventListener('mousemove', handleMouseMove) window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mousemove', handlePlayheadDrag) window.removeEventListener('mousemove', handlePlayheadDrag)
window.removeEventListener('mouseup', handleMouseUp) window.removeEventListener('mouseup', handleMouseUp)
@ -391,12 +393,6 @@ export function useTimeline(
// if you want to disable its autofocus // if you want to disable its autofocus
// autofocus: false, // autofocus: false,
confirmButtonText: '确认', confirmButtonText: '确认',
// callback: (action: Action) => {
// ElMessage({
// type: 'info',
// message: `action: ${action}`,
// })
// },
}) })
} }

View File

@ -138,21 +138,6 @@ onUnmounted(() => {
</div> </div>
</el-tooltip> </el-tooltip>
</div> </div>
<!-- <div
v-for="hazard in row"
:key="hazard.id"
>
<el-tooltip :content="hazard.tip" placement="top">
<div
class="hazard-block"
:class="{ primary: hazard.level === 0, danger: hazard.level === 1 }"
:style="getHazardStyle(hazard.start, hazard.end)"
@click="handleHazardClick(hazard.id)"
>
{{ parseInt(hazard.id) + 1 }}
</div>
</el-tooltip>
</div> -->
</div> </div>
</div> </div>
</ElScrollbar> </ElScrollbar>

View File

@ -26,6 +26,28 @@ const ruleForm = reactive<RuleForm>({
], ],
}) })
const timerDisplay = ref('00:00:00')
let timerInterval: ReturnType<typeof setInterval> | null = null
let startTime: number = 0
function startTimer() {
startTime = Date.now()
timerInterval = setInterval(() => {
const elapsed = Date.now() - startTime
const hours = Math.floor(elapsed / 3600000)
const minutes = Math.floor((elapsed % 3600000) / 60000)
const seconds = Math.floor((elapsed % 60000) / 1000)
timerDisplay.value = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}, 1000)
}
function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval)
timerInterval = null
}
}
const rules = reactive<FormRules<RuleForm>>({ const rules = reactive<FormRules<RuleForm>>({
vidPath: [ vidPath: [
{ {
@ -50,6 +72,7 @@ async function runCheck(formEl: FormInstance | undefined) {
}) })
if (valid) { if (valid) {
isRunningCheck.value = true isRunningCheck.value = true
startTimer()
await runApi('/run', { await runApi('/run', {
vid_file: vidPaths.value[vidPaths.value.indexOf(ruleForm.vidPath)], vid_file: vidPaths.value[vidPaths.value.indexOf(ruleForm.vidPath)],
run_sam3: ruleForm.function.includes('runSam3'), run_sam3: ruleForm.function.includes('runSam3'),
@ -57,6 +80,7 @@ async function runCheck(formEl: FormInstance | undefined) {
gen_report: ruleForm.function.includes('runGenerateReport'), gen_report: ruleForm.function.includes('runGenerateReport'),
}).then((res) => { }).then((res) => {
isRunningCheck.value = false isRunningCheck.value = false
stopTimer()
// //
if (res !== 'error') { if (res !== 'error') {
router.push({ router.push({
@ -109,51 +133,61 @@ onMounted(() => {
<el-container style="margin: 0; height: 100%; flex-direction: column;"> <el-container style="margin: 0; height: 100%; flex-direction: column;">
<!-- 内层容器自动填充剩余高度 --> <!-- 内层容器自动填充剩余高度 -->
<el-main class="main" style="flex: 1; min-height: 0;"> <el-main class="main" style="flex: 1; min-height: 0;">
<el-form <div class="flex flex-col items-center justify-center">
ref="ruleFormRef" <el-form
style="max-width: 600px" ref="ruleFormRef"
:model="ruleForm" style="max-width: 600px"
:rules="rules" :model="ruleForm"
label-width="auto" :rules="rules"
> label-width="auto"
<el-form-item label="视频" prop="vidPath"> >
<el-select v-model="ruleForm.vidPath" placeholder="视频"> <el-form-item label="视频" prop="vidPath">
<el-option v-for="item in vidPaths" :key="item" :label="item" :value="item" /> <el-select v-model="ruleForm.vidPath" placeholder="视频">
</el-select> <el-option v-for="item in vidPaths" :key="item" :label="item" :value="item" />
</el-form-item> </el-select>
<el-form-item label="功能选择" prop="function"> </el-form-item>
<el-checkbox-group v-model="ruleForm.function" class="checkbox-group"> <el-form-item label="功能选择" prop="function">
<el-checkbox value="runSam3" name="function"> <el-checkbox-group v-model="ruleForm.function" class="checkbox-group">
物体识别 <el-checkbox value="runSam3" name="function">
</el-checkbox> 物体识别
<el-checkbox value="runHazardCheck" name="function"> </el-checkbox>
隐患检查 <el-checkbox value="runHazardCheck" name="function">
</el-checkbox> 隐患检查
<el-checkbox value="runGenerateReport" name="function"> </el-checkbox>
生成报告 <el-checkbox value="runGenerateReport" name="function">
</el-checkbox> 生成报告
<el-checkbox value="runAudioRecognition" name="function"> </el-checkbox>
音频识别 <el-checkbox value="runAudioRecognition" name="function">
</el-checkbox> 音频识别
</el-checkbox-group> </el-checkbox>
</el-form-item> </el-checkbox-group>
<el-form-item> </el-form-item>
<el-button @click="resetForm(ruleFormRef)"> <el-form-item v-if="!isRunningCheck">
重置表单 <el-button @click="resetForm(ruleFormRef)">
</el-button> 重置表单
<el-button @click="goToResult(ruleFormRef)"> </el-button>
查看结果 <el-button @click="goToResult(ruleFormRef)">
</el-button> 查看结果
<el-button </el-button>
ref="runCheckBtnRef" <el-button
type="primary" ref="runCheckBtnRef"
:loading="isRunningCheck" type="primary"
@click="runCheck(ruleFormRef)" :loading="isRunningCheck"
> @click="runCheck(ruleFormRef)"
运行检查 >
</el-button> 运行检查
</el-form-item> </el-button>
</el-form> </el-form-item>
</el-form>
<el-progress
v-if="isRunningCheck"
:percentage="50"
:indeterminate="true"
style="width: 100%;"
>
<el-text>{{ timerDisplay }}</el-text>
</el-progress>
</div>
</el-main> </el-main>
</el-container> </el-container>
</template> </template>