refactor(log): 替换任务日志组件为通用执行日志组件
- 移除 inspection 和 intelligenceTest 模块中的重复日志显示代码 - 引入统一的 TaskExecutionLog 组件处理日志渲染逻辑 - 新组件支持多媒体内容展示包括图片、视频和音频 - 实现媒体资源自动解析和预览功能 - 优化滚动到底部逻辑确保日志实时显示 - 采用响应式布局适配不同屏幕尺寸
This commit is contained in:
parent
e88a81049b
commit
82642db70f
162
src/views/flow/components/TaskExecutionLog.vue
Normal file
162
src/views/flow/components/TaskExecutionLog.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user