|
1 | 1 | <script setup lang="ts"> |
2 | | - import { Model } from '@/types/model' |
| 2 | + // import { Model } from '@/types/model' |
3 | 3 | import vDefaultPic from '@/components/modules/vDefaultPic.vue' |
4 | 4 | import vTooltips from '@/components/modules/v-tooltip.vue' |
5 | 5 | import { sliceString, formatNumber } from '@/utils/tool' |
|
18 | 18 | const showDialog = ref(false) |
19 | 19 | const imgSrc = ref('') |
20 | 20 | const tagsStore = useDictStore() |
21 | | - const props = defineProps({ |
22 | | - model: { |
23 | | - type: Object as () => Model | null, |
24 | | - default: null |
25 | | - }, |
26 | | - loading: { |
27 | | - type: Boolean, |
28 | | - default: false |
29 | | - }, |
30 | | - imageLoaded: { |
31 | | - type: Boolean, |
32 | | - default: false |
33 | | - } |
34 | | - }) |
| 21 | + const props = defineProps<{ |
| 22 | + model?: any |
| 23 | + imageLoaded?: boolean |
| 24 | + loading?: boolean |
| 25 | + }>() |
35 | 26 |
|
36 | 27 | const emit = defineEmits(['action', 'image-load', 'image-error']) |
37 | 28 |
|
38 | | - // 计算工作流或节点的提示文本 |
| 29 | + const isHovering = ref(false) |
| 30 | +
|
39 | 31 | const actionTooltipText = computed(() => { |
40 | 32 | return props.model?.type === 'Workflow' |
41 | 33 | ? t('community.modelCard.tooltips.loadWorkflow') |
|
94 | 86 | imgSrc.value = url.includes('?') ? `${url}&t=${timestamp}` : `${url}?t=${timestamp}` |
95 | 87 | } |
96 | 88 | }) |
| 89 | +
|
| 90 | + const isVideo = computed(() => { |
| 91 | + if (!imgSrc.value) return false |
| 92 | + const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv'] |
| 93 | + const url = imgSrc.value.toLowerCase() |
| 94 | + return videoExtensions.some(ext => url.includes(ext)) |
| 95 | + }) |
| 96 | +
|
| 97 | + const getVideoThumbnail = (videoUrl: string) => { |
| 98 | + if (videoUrl.includes('x-oss-process=video/snapshot')) { |
| 99 | + return videoUrl |
| 100 | + } |
| 101 | + const separator = videoUrl.includes('?') ? '&' : '?' |
| 102 | + return `${videoUrl}${separator}x-oss-process=video/snapshot,t_0000,f_jpg,w_300,h_600` |
| 103 | + } |
| 104 | +
|
| 105 | + const currentMediaSrc = computed(() => { |
| 106 | + if (!imgSrc.value) return '' |
| 107 | + if (isVideo.value) { |
| 108 | + return isHovering.value ? imgSrc.value : getVideoThumbnail(imgSrc.value) |
| 109 | + } |
| 110 | + return imgSrc.value |
| 111 | + }) |
| 112 | +
|
| 113 | + const handleMouseEnter = () => { |
| 114 | + if (isVideo.value) { |
| 115 | + isHovering.value = true |
| 116 | + } |
| 117 | + } |
| 118 | +
|
| 119 | + const handleMouseLeave = () => { |
| 120 | + if (isVideo.value) { |
| 121 | + isHovering.value = false |
| 122 | + } |
| 123 | + } |
97 | 124 | </script> |
98 | 125 |
|
99 | 126 | <template> |
|
139 | 166 | <div |
140 | 167 | class="relative aspect-[2/3] md:aspect-[3/4] lg:aspect-[2/3] overflow-hidden" |
141 | 168 | @click.prevent="handleDetail(Number(model?.id), Number(model?.versions?.[0]?.id))" |
| 169 | + @mouseenter="handleMouseEnter" |
| 170 | + @mouseleave="handleMouseLeave" |
142 | 171 | > |
143 | 172 | <div |
144 | 173 | class="absolute inset-0 bg-gradient-to-br from-[#2a2a2a] to-[#1a1a1a]" |
145 | 174 | :class="{ 'opacity-0': props.imageLoaded }" |
146 | 175 | ></div> |
| 176 | + |
| 177 | + <!-- 视频显示(悬停时) --> |
| 178 | + <video |
| 179 | + v-if="isVideo && isHovering && currentMediaSrc" |
| 180 | + :src="currentMediaSrc" |
| 181 | + class="absolute inset-0 w-full h-full object-cover transition-all duration-300" |
| 182 | + :class="{ |
| 183 | + 'opacity-0': !props.imageLoaded, |
| 184 | + 'opacity-100 group-hover:scale-105': props.imageLoaded |
| 185 | + }" |
| 186 | + muted |
| 187 | + autoplay |
| 188 | + loop |
| 189 | + preload="metadata" |
| 190 | + @loadeddata="handleImageLoad" |
| 191 | + @error="handleImageError" |
| 192 | + /> |
| 193 | + |
147 | 194 | <img |
148 | | - v-if="imgSrc" |
149 | | - :src="imgSrc" |
| 195 | + v-else-if="currentMediaSrc" |
| 196 | + :src="currentMediaSrc" |
150 | 197 | :alt="model.versions?.[0]?.version || model.name" |
151 | 198 | :crossorigin=" |
152 | | - typeof imgSrc === 'string' && imgSrc.startsWith('blob:') ? 'anonymous' : undefined |
| 199 | + typeof currentMediaSrc === 'string' && currentMediaSrc.startsWith('blob:') |
| 200 | + ? 'anonymous' |
| 201 | + : undefined |
153 | 202 | " |
154 | 203 | class="absolute inset-0 w-full h-full object-cover transition-all duration-300" |
155 | 204 | :class="{ |
|
159 | 208 | @load="handleImageLoad" |
160 | 209 | @error="handleImageError" |
161 | 210 | /> |
| 211 | + |
162 | 212 | <div v-if="!props.imageLoaded" class="absolute inset-0 flex items-center justify-center"> |
163 | 213 | <vDefaultPic /> |
164 | 214 | </div> |
|
0 commit comments