Files
wenzi/frontend/components/MosquitoPosterCard.vue
Your Name 91a0b77f7a test(cache): 修复CacheConfigTest边界值测试
- 修改 shouldVerifyCacheManager_withMaximumIntegerTtl 为 shouldVerifyCacheManager_withMaximumAllowedTtl
- 使用正确的最大TTL值(10080分钟,7天)而不是 Integer.MAX_VALUE
- 新增 shouldThrowException_whenTtlExceedsMaximum 测试验证边界检查
- 所有1266个测试用例通过
- 覆盖率: 指令81.89%, 行88.48%, 分支51.55%

docs: 添加项目状态报告
- 生成 PROJECT_STATUS_REPORT.md 详细记录项目当前状态
- 包含质量指标、已完成功能、待办事项和技术债务
2026-03-02 13:31:54 +08:00

204 lines
4.9 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>