163 lines
4.0 KiB
Vue
163 lines
4.0 KiB
Vue
|
|
<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>
|