refactor(log): 替换任务日志组件为通用执行日志组件

- 移除 inspection 和 intelligenceTest 模块中的重复日志显示代码
- 引入统一的 TaskExecutionLog 组件处理日志渲染逻辑
- 新组件支持多媒体内容展示包括图片、视频和音频
- 实现媒体资源自动解析和预览功能
- 优化滚动到底部逻辑确保日志实时显示
- 采用响应式布局适配不同屏幕尺寸
This commit is contained in:
lixiaolong 2026-08-12 14:39:36 +08:00
parent e88a81049b
commit 82642db70f
3 changed files with 176 additions and 134 deletions

View File

@ -0,0 +1,162 @@
<template>
<div ref="messageContainer" class="task-log" :style="{ height: `${height}px` }">
<div v-for="(log, index) in list" :key="log.id || index" class="task-log__item">
<div class="task-log__time">{{ formatDate(log.logTime) }}</div>
<div class="task-log__body">
<div class="task-log__content">{{ log.logContent }}</div>
<div v-if="mediaOf(log).length" class="task-log__media">
<template v-for="media in mediaOf(log)" :key="`${media.type}:${media.url}`">
<el-image
v-if="media.type === 'image'"
class="task-log__image"
:src="media.url"
:preview-src-list="imageUrlsOf(log)"
preview-teleported
fit="cover"
/>
<video v-else-if="media.type === 'video'" class="task-log__video" :src="media.url" controls preload="metadata" />
<audio v-else class="task-log__audio" :src="media.url" controls preload="metadata" />
</template>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { nextTick, onMounted, ref, watch } from 'vue'
import { formatDate } from '@/utils/index'
const props = defineProps({
list: { type: Array, default: () => [] },
height: { type: Number, default: 150 },
})
const messageContainer = ref(null)
const mediaCache = new WeakMap()
const parseObject = (value) => {
if (!value) return {}
if (typeof value === 'object') return value
try {
const parsed = JSON.parse(value)
return parsed && typeof parsed === 'object' ? parsed : {}
} catch {
return {}
}
}
const addMedia = (result, seen, type, value) => {
const values = Array.isArray(value) ? value : [value]
values.forEach((item) => {
const url = typeof item === 'string' ? item.trim() : ''
const key = `${type}:${url}`
if (url && !seen.has(key)) {
seen.add(key)
result.push({ type, url })
}
})
}
const mediaOf = (log) => {
if (!log || typeof log !== 'object') return []
if (mediaCache.has(log)) return mediaCache.get(log)
const result = []
const seen = new Set()
const output = {
...parseObject(log.actualResult),
...parseObject(log.extraInfo),
}
addMedia(result, seen, 'image', output.imageUrl)
addMedia(result, seen, 'video', output.videoUrl)
addMedia(result, seen, 'audio', output.audioUrl)
addMedia(result, seen, 'image', log.screenshotUrl)
addMedia(result, seen, 'video', log.videoUrl)
const legacyType = Number(log.logType) === 2 ? 'image'
: Number(log.logType) === 3 ? 'video'
: Number(log.logType) === 4 ? 'audio' : ''
if (legacyType) addMedia(result, seen, legacyType, log.mediaUrl)
mediaCache.set(log, result)
return result
}
const imageUrlsOf = (log) => mediaOf(log)
.filter((item) => item.type === 'image')
.map((item) => item.url)
const scrollToBottom = () => {
nextTick(() => {
if (messageContainer.value) messageContainer.value.scrollTop = messageContainer.value.scrollHeight
})
}
watch(() => props.list, scrollToBottom, { deep: true })
onMounted(scrollToBottom)
</script>
<style scoped lang="scss">
.task-log {
margin-top: 12px;
padding: 12px;
overflow-y: auto;
background: #fff;
border: 1px solid #eee;
border-radius: 8px;
}
.task-log__item {
display: grid;
grid-template-columns: 145px minmax(0, 1fr);
gap: 12px;
align-items: start;
margin-bottom: 10px;
}
.task-log__time {
color: #999;
font-size: 13px;
white-space: nowrap;
}
.task-log__body {
min-width: 0;
}
.task-log__content {
color: #333;
font-size: 14px;
line-height: 20px;
}
.task-log__media {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.task-log__image {
width: 96px;
height: 72px;
border-radius: 4px;
}
.task-log__video {
width: min(320px, 100%);
max-height: 220px;
background: #111;
border-radius: 4px;
}
.task-log__audio {
width: min(340px, 100%);
height: 40px;
}
@media (max-width: 640px) {
.task-log__item {
grid-template-columns: 1fr;
gap: 3px;
}
}
</style>

View File

@ -1,73 +1,12 @@
<template>
<div class="log-container" :style="{ 'height': height + 'px' }" ref="messageContainer">
<div class="log" v-for="log, index in list">
<div class="time">{{ formatDate(log.logTime) }}</div>
<div class="context">{{ log.logContent }}</div>
</div>
</div>
<TaskExecutionLog :list="list" :height="height" />
</template>
<script setup>
import { formatDate } from "@/utils/index";
import { watch } from "vue";
const props = defineProps({
list: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 150
}
import TaskExecutionLog from '@/views/flow/components/TaskExecutionLog.vue'
defineProps({
list: { type: Array, default: () => [] },
height: { type: Number, default: 150 },
})
const messageContainer = ref(null)
const SCROLL_DELAY = 100
const scrollToBottom = () => {
console.log(123, messageContainer.value)
nextTick(() => {
setTimeout(() => {
if (messageContainer.value) {
console.log('messageContainer.value.scrollHeight', messageContainer.value.scrollHeight)
messageContainer.value.scrollTop = messageContainer.value.scrollHeight
}
}, SCROLL_DELAY)
})
}
watch(() => props.list, () => {
scrollToBottom()
}, { deep: true })
onMounted(() => {
scrollToBottom()
})
</script>
<style lang="scss" scoped>
.log-container {
margin-top: 12px;
overflow-y: auto;
background: #fff;
border: 1px solid #eee;
border-radius: 8px;
padding: 12px;
.log {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
.time {
color: #999;
font-size: 14px;
}
.context {
color: #333;
font-size: 14px;
}
}
}
</style>

View File

@ -1,71 +1,12 @@
<template>
<div class="log-container" :style="{ 'height': height + 'px' }" ref="messageContainer">
<div class="log" v-for="log, index in list">
<div class="time">{{ formatDate(log.logTime) }}</div>
<div class="context">{{ log.logContent }}</div>
</div>
</div>
<TaskExecutionLog :list="list" :height="height" />
</template>
<script setup>
import { formatDate } from "@/utils/index";
import { watch } from "vue";
const props = defineProps({
list: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 150
}
import TaskExecutionLog from '@/views/flow/components/TaskExecutionLog.vue'
defineProps({
list: { type: Array, default: () => [] },
height: { type: Number, default: 150 },
})
const messageContainer = ref(null)
const SCROLL_DELAY = 100
const scrollToBottom = () => {
nextTick(() => {
setTimeout(() => {
if (messageContainer.value) {
messageContainer.value.scrollTop = messageContainer.value.scrollHeight
}
}, SCROLL_DELAY)
})
}
watch(() => props.list, () => {
scrollToBottom()
}, { deep: true })
onMounted(() => {
scrollToBottom()
})
</script>
<style lang="scss" scoped>
.log-container {
margin-top: 12px;
overflow-y: auto;
background: #fff;
border: 1px solid #eee;
border-radius: 8px;
padding: 12px;
.log {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
.time {
color: #999;
font-size: 14px;
}
.context {
color: #333;
font-size: 14px;
}
}
}
</style>