新增:对话列表新增关键词高亮

- 新增:对话列表新增关键词高亮
- 新增:对话列表中含有关键词的对话使用淡红色背景
- 修改:优化次级信息配色
- 修改:优化隐患结果页右侧边栏滚动条效果
- 修改:优化隐患结果页左侧边栏滚动条效果
- 修改:视频背景改为黑色
This commit is contained in:
yueliuli 2026-05-08 11:59:55 +08:00
parent 14bc8ffbf7
commit b477db4bd2
4 changed files with 133 additions and 76 deletions

2
public/data/keyword.csv Normal file
View File

@ -0,0 +1,2 @@
拍一下,
现场整改,
1 拍一下
2 现场整改

1
src/components.d.ts vendored
View File

@ -41,6 +41,7 @@ declare module 'vue' {
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']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSegmented: typeof import('element-plus/es')['ElSegmented'] ElSegmented: typeof import('element-plus/es')['ElSegmented']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSelectV2: typeof import('element-plus/es')['ElSelectV2'] ElSelectV2: typeof import('element-plus/es')['ElSelectV2']

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { CaretRight, Location } from '@element-plus/icons-vue' import { CaretRight, Location } from '@element-plus/icons-vue'
import { ref, shallowRef, watch } from 'vue' import { computed, ref, shallowRef, watch } from 'vue'
const props = defineProps({ const props = defineProps({
title: { title: {
@ -15,6 +15,10 @@ const props = defineProps({
type: Number, type: Number,
default: 0, default: 0,
}, },
keywords: {
type: Array,
default: () => [],
},
}) })
const emit = defineEmits(['click', 'play']) const emit = defineEmits(['click', 'play'])
@ -28,6 +32,22 @@ function handlePlayClick(e: Event, item: any[], index: number) {
emit('play', item, index) emit('play', item, index)
} }
const keywordList = computed(() => props.keywords as string[])
function highlightText(text: string): string {
const kws = keywordList.value
if (!kws.length || !text)
return text
let result = text
for (const kw of kws) {
if (!kw)
continue
const regex = new RegExp(`(${kw})`, 'gi')
result = result.replace(regex, '<span class="keyword-highlight">$1</span>')
}
return result
}
const dataCache = shallowRef<[number, string, string, string][]>([]) const dataCache = shallowRef<[number, string, string, string][]>([])
const indexMap = ref(new Map<number, number[]>()) const indexMap = ref(new Map<number, number[]>())
const sortedTimes = ref<number[]>([]) const sortedTimes = ref<number[]>([])
@ -37,6 +57,7 @@ watch(() => props.data, (newData) => {
dataCache.value = arr dataCache.value = arr
const map = new Map<number, number[]>() const map = new Map<number, number[]>()
const times: number[] = [] const times: number[] = []
for (let i = 0; i < arr.length; i++) { for (let i = 0; i < arr.length; i++) {
const t = (arr[i][0] * 10) | 0 const t = (arr[i][0] * 10) | 0
if (!map.has(t)) { if (!map.has(t)) {
@ -51,6 +72,30 @@ watch(() => props.data, (newData) => {
}, { immediate: true }) }, { immediate: true })
const currentHighlight = ref(-1) const currentHighlight = ref(-1)
const keywordItemSet = ref(new Set<number>())
watch(keywordList, () => {
const arr = dataCache.value
const kws = keywordList.value
keywordItemSet.value = new Set<number>()
if (!kws.length || !arr.length)
return
for (let i = 0; i < arr.length; i++) {
if (arr[i][3]) {
for (const kw of kws) {
if (!kw)
continue
const regex = new RegExp(kw, 'i')
if (regex.test(arr[i][3])) {
keywordItemSet.value.add(i)
break
}
}
}
}
}, { immediate: true })
watch(() => props.currentTime, (t) => { watch(() => props.currentTime, (t) => {
const time = (t * 10) | 0 const time = (t * 10) | 0
@ -85,20 +130,24 @@ watch(() => props.currentTime, (t) => {
<!-- 滚动列表区域 --> <!-- 滚动列表区域 -->
<el-row style="flex: 1; overflow-y: auto;"> <el-row style="flex: 1; overflow-y: auto;">
<el-col> <el-col>
<el-row v-for="(item, index) in props.data" :key="index" class="message-item" :class="{ highlighted: currentHighlight === index }"> <el-row v-for="(item, index) in props.data" :key="index" class="message-item" :class="{ 'highlighted': currentHighlight === index, 'keyword-highlighted': keywordItemSet.has(index) }">
<div class="message-content" @click="handleItemClick(item as any[], index)"> <div class="message-content" @click="handleItemClick(item as any[], index)">
<div class="flex flex-col items-start"> <div class="flex flex-col items-start">
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<el-text class="item-text" type="info" size="small"> <el-text class="item-text info-text" type="info" size="small">
{{ (item as any[])[1] }} {{ (item as any[])[1] }}
</el-text> </el-text>
<el-text class="item-text" type="info" size="small"> <el-text class="item-text info-text" type="info" size="small">
{{ (item as any[])[2] }} {{ (item as any[])[2] }}
</el-text> </el-text>
</div> </div>
<el-text class="item-text"> <div class="item-text">
{{ (item as any[])[3] }} <el-text>
</el-text> <template #default>
<span v-html="highlightText((item as any[])[3])" />
</template>
</el-text>
</div>
</div> </div>
</div> </div>
<div class="hover-actions"> <div class="hover-actions">
@ -150,6 +199,10 @@ watch(() => props.currentTime, (t) => {
background-color: var(--ep-color-primary-light-8) !important; background-color: var(--ep-color-primary-light-8) !important;
} }
.message-item.keyword-highlighted {
background-color: var(--ep-color-danger-light-9);
}
.message-content { .message-content {
flex: 1; flex: 1;
cursor: pointer; cursor: pointer;
@ -171,31 +224,16 @@ watch(() => props.currentTime, (t) => {
display: flex; display: flex;
} }
.message-btn { :deep(.keyword-highlight) {
width: 100%; color: #f56c6c;
padding: 4px 0.75rem; font-weight: bold;
display: flex;
justify-content: flex-start;
height: auto;
min-height: 28px;
line-height: normal;
} }
.item-text { .item-text {
word-break: break-word;
white-space: normal;
text-align: left; text-align: left;
} }
.message-title { .info-text {
display: flex; color: var(--ep-color-info-light-3) !important;
justify-content: flex-start;
align-items: flex-start;
gap: 4px;
}
.item-text.ep-text {
align-self: start;
line-height: 1.5;
} }
</style> </style>

View File

@ -82,6 +82,7 @@ const resultData = ref<ResultData>({
}) })
const videoRef = ref<HTMLVideoElement>() const videoRef = ref<HTMLVideoElement>()
const keywords = ref<string[]>([])
const data = ref<DataFormat>({ const data = ref<DataFormat>({
隐患列表: [], 隐患列表: [],
隐患范围字典: {}, 隐患范围字典: {},
@ -322,7 +323,16 @@ function getFieldValue(field: typeof hazardFields[0]) {
return field.transform ? field.transform(value) : (value || '无') return field.transform ? field.transform(value) : (value || '无')
} }
onMounted(() => { onMounted(async () => {
try {
const res = await fetch('/data/keyword.csv')
const text = await res.text()
keywords.value = text.split('\n').map(k => k.trim().replace(/,$/, '')).filter(k => k)
}
catch (e) {
console.error('Failed to load keywords:', e)
}
// //
const vidFile = router.currentRoute.value.query.vid_file as string const vidFile = router.currentRoute.value.query.vid_file as string
if (vidFile) { if (vidFile) {
@ -394,9 +404,11 @@ onMounted(() => {
<el-container style="flex: 1; min-height: 0;"> <el-container style="flex: 1; min-height: 0;">
<el-aside width="200px" style="border-right: 1px solid var(--ep-border-color);"> <el-aside width="200px" style="border-right: 1px solid var(--ep-border-color);">
<!-- <el-row class="flex flex-col" style="border-bottom: 1px solid var(--ep-border-color);"> --> <!-- <el-row class="flex flex-col" style="border-bottom: 1px solid var(--ep-border-color);"> -->
<el-row class="flex flex-col"> <el-scrollbar>
<ItemList title="隐患" :data="data.隐患列表" @click="handleHazardClick" /> <el-row class="flex flex-col">
</el-row> <ItemList title="隐患" :data="data.隐患列表" @click="handleHazardClick" />
</el-row>
</el-scrollbar>
<!-- <el-row style="flex: 1; overflow-y: auto;"> <!-- <el-row style="flex: 1; overflow-y: auto;">
<ItemList title="物体列表" :data="data.物体列表" /> <ItemList title="物体列表" :data="data.物体列表" />
</el-row> --> </el-row> -->
@ -427,57 +439,60 @@ onMounted(() => {
</el-container> </el-container>
<el-aside style="width: 300px;"> <el-aside style="width: 300px;">
<!-- <el-row style="border-bottom: 1px solid var(--ep-border-color);"> --> <!-- <el-row style="border-bottom: 1px solid var(--ep-border-color);"> -->
<el-row class="px-3 py-2"> <el-scrollbar>
<el-col v-if="selectedHazard >= 0"> <el-row class="px-3 py-2">
<template v-for="group in groupedFields" :key="group[0].group"> <el-col v-if="selectedHazard >= 0">
<el-row :gutter="12"> <template v-for="group in groupedFields" :key="group[0].group">
<el-col v-for="field in group" :key="field.key" :span="24 / group.length"> <el-row :gutter="12">
<el-row class="result-title"> <el-col v-for="field in group" :key="field.key" :span="24 / group.length">
<el-text type="info" size="small"> <el-row class="result-title">
{{ field.label }} <el-text type="info" size="small">
</el-text> {{ field.label }}
</el-row> </el-text>
<el-row class="result-content"> </el-row>
<el-text v-if="field.key === '隐患等级' && getFieldValue(field) === '重大隐患'" type="danger"> <el-row class="result-content">
{{ getFieldValue(field) }} <el-text v-if="field.key === '隐患等级' && getFieldValue(field) === '重大隐患'" type="danger">
</el-text> {{ getFieldValue(field) }}
<el-text v-else-if="field.key === '置信度' && getFieldValue(field) === '疑似'" type="warning"> </el-text>
{{ getFieldValue(field) }} <el-text v-else-if="field.key === '置信度' && getFieldValue(field) === '疑似'" type="warning">
</el-text> {{ getFieldValue(field) }}
<el-text v-else> </el-text>
{{ getFieldValue(field) }} <el-text v-else>
</el-text> {{ getFieldValue(field) }}
</el-row> </el-text>
</el-col> </el-row>
</el-col>
</el-row>
<div class="py-1" />
</template>
<el-row>
<el-button style="width: 100%;" @click="handleJumpToHazard(selectedHazard)">
跳转到隐患时间点
</el-button>
</el-row> </el-row>
<div class="py-1" /> </el-col>
</template> <el-col v-else>
<el-row> <el-row class="px-1 py-1">
<el-button style="width: 100%;" @click="handleJumpToHazard(selectedHazard)"> 无选中隐患
跳转到隐患时间点 </el-row>
</el-button> </el-col>
</el-row> </el-row>
</el-col> <el-row v-if="data.对话列表.length > 0" style="height: calc(100% - 400px); flex: auto; border-top: 1px solid var(--ep-border-color);">
<el-col v-else> <SubtitleList
<el-row class="px-1 py-1"> title="对话"
无选中隐患 :data="data.对话列表"
</el-row> :current-time="videoCurrentTime"
</el-col> :keywords="keywords"
</el-row> @click="(item: any[]) => handleJumpToTimePoint(Number(item[0]))"
<el-row v-if="data.对话列表.length > 0" style="flex: 1; overflow-y: auto; border-top: 1px solid var(--ep-border-color);"> @play="(item: any[]) => handlePlayAndSeek(item)"
<SubtitleList />
title="对话" </el-row>
:data="data.对话列表"
:current-time="videoCurrentTime"
@click="(item: any[]) => handleJumpToTimePoint(Number(item[0]))"
@play="(item: any[]) => handlePlayAndSeek(item)"
/>
</el-row>
<!-- <el-row class="px-1 py-1"> <!-- <el-row class="px-1 py-1">
<el-button> <el-button>
查看报告 查看报告
</el-button> </el-button>
</el-row> --> </el-row> -->
</el-scrollbar>
</el-aside> </el-aside>
</el-container> </el-container>
</el-container> </el-container>
@ -488,6 +503,7 @@ onMounted(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
background-color: var(--ep-color-black);
} }
.vid_box video { .vid_box video {
width: 100%; width: 100%;