- 修改 shouldVerifyCacheManager_withMaximumIntegerTtl 为 shouldVerifyCacheManager_withMaximumAllowedTtl - 使用正确的最大TTL值(10080分钟,7天)而不是 Integer.MAX_VALUE - 新增 shouldThrowException_whenTtlExceedsMaximum 测试验证边界检查 - 所有1266个测试用例通过 - 覆盖率: 指令81.89%, 行88.48%, 分支51.55% docs: 添加项目状态报告 - 生成 PROJECT_STATUS_REPORT.md 详细记录项目当前状态 - 包含质量指标、已完成功能、待办事项和技术债务
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>
|