You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
customAgent/src/components/common/DynamicImage.vue

275 lines
7.8 KiB

<template>
<div ref="rootRef" :class="['dynamic-image', { loading: isLoading, error: hasError, clickable }]">
<img :src="currentSrc" :alt="alt" :class="imageClass" :style="imageStyle" @load="handleLoad" @error="handleError"
@click="handleClick" v-bind="attrs" />
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick, useAttrs } from 'vue'
import imageService from '@/utils/imageService'
import errorImg from '@/assets/images/error2.png'
import loadingImg from '@/assets/images/loading2.gif'
// ── inheritAttrs ────────────────────────────────────
defineOptions({ inheritAttrs: false })
// ── Props ───────────────────────────────────────────
const props = defineProps({
imageKey: { type: String, default: '' },
fallback: { type: String, default: '' },
alt: { type: String, default: '' },
useCache: { type: Boolean, default: true },
lazy: { type: Boolean, default: false },
placeholder: { type: String, default: '' },
loading: { type: String, default: '' },
preload: { type: Boolean, default: false },
timeout: { type: Number, default: 10000 },
retryCount: { type: Number, default: 1 },
imageClass: { type: [String, Array, Object], default: '' },
imageStyle: { type: [String, Object], default: '' },
clickable: { type: Boolean, default: false },
})
// ── Emits ───────────────────────────────────────────
const emit = defineEmits(['load', 'error', 'click'])
// ── Attrs ───────────────────────────────────────────
const attrs = useAttrs()
// ── 模板引用 ────────────────────────────────────────
const rootRef = ref(null)
// ── 响应式状态 ──────────────────────────────────────
const currentSrc = ref(getInitialSrc())
const isLoading = ref(true)
const hasError = ref(false)
const isIntersecting = ref(false)
// IntersectionObserver 实例
let observer = null
// ── 计算属性 ────────────────────────────────────────
const loadOptions = computed(() => ({
useCache: props.useCache,
fallback: props.fallback || errorImg,
timeout: props.timeout,
retryCount: props.retryCount,
}))
// ── 辅助函数 ────────────────────────────────────────
function getInitialSrc() {
if (props.lazy) {
return props.placeholder || props.loading || loadingImg
}
return props.loading || loadingImg
}
// ── 懒加载 ──────────────────────────────────────────
function setupLazyLoading() {
if (!window.IntersectionObserver) {
loadImage()
return
}
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
isIntersecting.value = true
loadImage()
observer.unobserve(rootRef.value)
}
})
},
{ rootMargin: '50rem' },
)
nextTick(() => {
if (rootRef.value) {
observer.observe(rootRef.value)
}
})
}
// ── 图片加载 ────────────────────────────────────────
async function loadImage() {
if (!props.imageKey) {
handleError(new Error('No image key provided'))
return
}
isLoading.value = true
hasError.value = false
try {
const imageUrl = await imageService.getImageByKey(props.imageKey, loadOptions.value)
if (
imageUrl &&
(imageUrl.startsWith('data:') ||
imageUrl.startsWith('blob:') ||
imageUrl.startsWith('http'))
) {
currentSrc.value = imageUrl
isLoading.value = false
emit('load', { src: imageUrl, key: props.imageKey })
} else {
throw new Error('Invalid image URL format')
}
} catch (error) {
console.error('DynamicImage: 加载图片失败:', error)
handleError(error)
}
}
// ── 事件处理 ────────────────────────────────────────
function handleLoad(event) {
isLoading.value = false
hasError.value = false
emit('load', { event, src: currentSrc.value, key: props.imageKey })
}
function handleError(error) {
isLoading.value = false
hasError.value = true
const fallbackSrc = props.fallback || errorImg
currentSrc.value = fallbackSrc
emit('error', { error, key: props.imageKey, fallback: fallbackSrc })
console.warn('DynamicImage: Load failed', {
key: props.imageKey,
error: error.message || error,
})
}
function handleClick(event) {
if (props.clickable) {
emit('click', { event, src: currentSrc.value, key: props.imageKey })
}
}
// ── 重置 & 重新加载 ─────────────────────────────────
function reset() {
currentSrc.value = getInitialSrc()
isLoading.value = true
hasError.value = false
isIntersecting.value = false
}
function reload() {
reset()
loadImage()
}
// ── 清理 ────────────────────────────────────────────
function cleanup() {
if (observer) {
observer.disconnect()
observer = null
}
imageService.revokeImageUrl(currentSrc.value)
}
// ── 生命周期 ────────────────────────────────────────
onMounted(() => {
if (props.lazy) {
setupLazyLoading()
} else {
loadImage()
}
if (props.preload && props.imageKey) {
imageService.preloadImages([props.imageKey], loadOptions.value)
}
})
onBeforeUnmount(() => {
cleanup()
})
// ── 监听 imageKey 变化 ──────────────────────────────
watch(
() => props.imageKey,
(newKey, oldKey) => {
if (newKey !== oldKey) {
reset()
if (props.lazy) {
if (isIntersecting.value) {
loadImage()
}
} else {
loadImage()
}
}
},
)
// ── 调试日志(currentSrc 变化时)─────────────────────
watch(currentSrc, (newSrc, oldSrc) => {
// console.log('DynamicImage: currentSrc changed', {
// imageKey: props.imageKey,
// oldSrc: oldSrc?.substring(0, 50),
// newSrc: newSrc?.substring(0, 50),
// srcLength: newSrc?.length,
// })
})
// ── 暴露方法给父组件 ────────────────────────────────
defineExpose({ reload, reset })
</script>
<style scoped>
.dynamic-image {
position: relative;
display: inline-block;
max-width: 100%;
max-height: 100%;
}
.dynamic-image img {
display: block;
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
border-radius: inherit;
}
.dynamic-image.loading {
opacity: 0.7;
}
.dynamic-image.error {
opacity: 0.5;
}
.dynamic-image.clickable {
cursor: pointer;
}
.dynamic-image.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20rem;
height: 20rem;
margin: -10rem 0 0 -10rem;
border: 2rem solid #f3f3f3;
border-top: 2rem solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>