204 lines
4.9 KiB
Vue
204 lines
4.9 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="mosquito-poster-card" :style="{ width, height }">
|
|||
|
|
<div
|
|||
|
|
v-if="loading"
|
|||
|
|
class="loading-placeholder"
|
|||
|
|
:style="{ width, height }"
|
|||
|
|
>
|
|||
|
|
<div class="loading-skeleton"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div
|
|||
|
|
v-else-if="error"
|
|||
|
|
class="error-placeholder"
|
|||
|
|
:style="{ width, height }"
|
|||
|
|
@click="retryLoad"
|
|||
|
|
>
|
|||
|
|
<div class="error-content">
|
|||
|
|
<svg class="w-8 h-8 text-red-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|||
|
|
</svg>
|
|||
|
|
<p class="text-sm text-gray-600">加载失败</p>
|
|||
|
|
<p class="text-xs text-gray-500 mt-1">{{ error.message }}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<img
|
|||
|
|
v-else
|
|||
|
|
:src="posterUrl"
|
|||
|
|
alt="分享海报"
|
|||
|
|
:style="{ width, height }"
|
|||
|
|
class="poster-image"
|
|||
|
|
@load="onImageLoad"
|
|||
|
|
@error="onImageError"
|
|||
|
|
@click="$emit('click')"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- 加载指示器 -->
|
|||
|
|
<div v-if="loading" class="loading-indicator">
|
|||
|
|
<svg class="animate-spin h-6 w-6 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|||
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|||
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|||
|
|
</svg>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 重试按钮 -->
|
|||
|
|
<button
|
|||
|
|
v-if="showRetry"
|
|||
|
|
class="retry-button"
|
|||
|
|
@click="retryLoad"
|
|||
|
|
>
|
|||
|
|
重试
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, computed, watch } from 'vue'
|
|||
|
|
import { useMosquito } from '../index'
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
activityId: number
|
|||
|
|
userId: number
|
|||
|
|
template?: string
|
|||
|
|
width?: string
|
|||
|
|
height?: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = withDefaults(defineProps<Props>(), {
|
|||
|
|
template: 'default',
|
|||
|
|
width: '300px',
|
|||
|
|
height: '400px'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
click: []
|
|||
|
|
error: [error: Error]
|
|||
|
|
loaded: []
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
const { getPosterImage, config } = useMosquito()
|
|||
|
|
const loading = ref(false)
|
|||
|
|
const error = ref<Error | null>(null)
|
|||
|
|
const posterUrl = ref('')
|
|||
|
|
const retryCount = ref(0)
|
|||
|
|
const showRetry = ref(false)
|
|||
|
|
|
|||
|
|
// 生成海报URL
|
|||
|
|
const generatePosterUrl = () => {
|
|||
|
|
const timestamp = Date.now()
|
|||
|
|
return `${config.baseUrl}/api/v1/me/poster/image?activityId=${props.activityId}&userId=${props.userId}&template=${props.template}&t=${timestamp}`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载海报
|
|||
|
|
const loadPoster = async () => {
|
|||
|
|
if (loading.value) return
|
|||
|
|
|
|||
|
|
loading.value = true
|
|||
|
|
error.value = null
|
|||
|
|
showRetry.value = false
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 尝试使用API获取海报
|
|||
|
|
const imageBlob = await getPosterImage(props.activityId, props.userId, props.template)
|
|||
|
|
|
|||
|
|
// 创建本地URL
|
|||
|
|
const url = URL.createObjectURL(imageBlob)
|
|||
|
|
posterUrl.value = url
|
|||
|
|
retryCount.value = 0
|
|||
|
|
emit('loaded')
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('加载海报失败:', err)
|
|||
|
|
error.value = err as Error
|
|||
|
|
emit('error', error.value)
|
|||
|
|
|
|||
|
|
// 如果API失败,使用备用URL
|
|||
|
|
if (retryCount.value < 3) {
|
|||
|
|
retryCount.value++
|
|||
|
|
setTimeout(() => {
|
|||
|
|
posterUrl.value = generatePosterUrl()
|
|||
|
|
}, 1000 * retryCount.value)
|
|||
|
|
} else {
|
|||
|
|
showRetry.value = true
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 图片加载成功
|
|||
|
|
const onImageLoad = () => {
|
|||
|
|
showRetry.value = false
|
|||
|
|
retryCount.value = 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 图片加载失败
|
|||
|
|
const onImageError = () => {
|
|||
|
|
if (!error.value) {
|
|||
|
|
error.value = new Error('海报图片加载失败')
|
|||
|
|
emit('error', error.value)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 重试加载
|
|||
|
|
const retryLoad = () => {
|
|||
|
|
posterUrl.value = generatePosterUrl()
|
|||
|
|
loadPoster()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 组件挂载时加载海报
|
|||
|
|
loadPoster()
|
|||
|
|
|
|||
|
|
// 监听参数变化重新加载
|
|||
|
|
watch(() => [props.activityId, props.userId, props.template], () => {
|
|||
|
|
loadPoster()
|
|||
|
|
}, { deep: true })
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.mosquito-poster-card {
|
|||
|
|
@apply relative overflow-hidden rounded-lg shadow-md cursor-pointer transition-shadow hover:shadow-lg;
|
|||
|
|
background-color: var(--mosquito-bg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-placeholder {
|
|||
|
|
@apply flex items-center justify-center;
|
|||
|
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|||
|
|
background-size: 200% 100%;
|
|||
|
|
animation: loading 1.5s infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-skeleton {
|
|||
|
|
@apply w-16 h-16 bg-gray-300 rounded;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error-placeholder {
|
|||
|
|
@apply flex items-center justify-center bg-mosquito-bg;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error-content {
|
|||
|
|
@apply text-center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.poster-image {
|
|||
|
|
@apply object-cover;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-indicator {
|
|||
|
|
@apply absolute inset-0 flex items-center justify-center bg-black bg-opacity-50;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.retry-button {
|
|||
|
|
@apply absolute bottom-2 left-1/2 transform -translate-x-1/2 px-4 py-2 bg-black bg-opacity-70 text-white text-sm rounded-md hover:bg-opacity-80 transition-opacity;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes loading {
|
|||
|
|
0% {
|
|||
|
|
background-position: 200% 0;
|
|||
|
|
}
|
|||
|
|
100% {
|
|||
|
|
background-position: -200% 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|