Skip to content

Commit 4f35466

Browse files
添加视频封面以及gif封面 (#74)
* [add]添加可以上传视频封面以及gif封面的能力 * auto prettier format code * 生图接口添加Authorization --------- Co-authored-by: linjm8780860 <11494038+linjm8780860@users.noreply.github.com> Co-authored-by: dfhfg123 <3247895009@qq.com>
1 parent f3f2c62 commit 4f35466

9 files changed

Lines changed: 294 additions & 69 deletions

File tree

src/components/assistant/util.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,9 @@ export async function generateImage(options: {
397397
const response = await fetch('/bizyair/model/images', {
398398
method: 'POST',
399399
headers: {
400-
'Content-Type': 'application/json'
400+
'Content-Type': 'application/json',
401+
Authorization: Cookies.get('bizy_token') || '',
402+
...(options as any)?.headers
401403
},
402404
body: JSON.stringify({
403405
prompt: prompt,

src/components/community/detail/Index.vue

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@
5858
5959
const showAllTags = ref(false)
6060
61+
// 添加视频检测函数
62+
const isVideoUrl = (url: string) => {
63+
if (!url) return false
64+
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv']
65+
const lowercaseUrl = url.toLowerCase()
66+
return videoExtensions.some(ext => lowercaseUrl.includes(ext))
67+
}
68+
6169
const fetchModelDetail = async () => {
6270
try {
6371
const res = await model_detail({
@@ -821,15 +829,30 @@
821829
class="flex flex-col gap-4 items-start justify-start relative min-w-[620px] w-[65%] overflow-hidden"
822830
>
823831
<div class="w-full">
824-
<NImageGroup v-if="currentVersion?.cover_urls && currentVersion?.cover_urls.length > 0">
825-
<NImage
826-
v-for="(cover, index) in currentVersion?.cover_urls"
827-
:key="index"
828-
:src="cover"
829-
:preview-src="cover"
830-
height="512px"
831-
/>
832-
</NImageGroup>
832+
<div
833+
v-if="currentVersion?.cover_urls && currentVersion?.cover_urls.length > 0"
834+
class="space-y-4"
835+
>
836+
<div v-for="(cover, index) in currentVersion?.cover_urls" :key="index" class="w-full">
837+
<!-- 视频显示 -->
838+
<video
839+
v-if="isVideoUrl(cover)"
840+
:src="cover"
841+
controls
842+
class="w-full h-auto max-h-[512px] object-contain rounded-lg"
843+
preload="metadata"
844+
/>
845+
<!-- 图片显示 -->
846+
<NImageGroup v-else>
847+
<NImage
848+
:src="cover"
849+
:preview-src="cover"
850+
height="512px"
851+
class="w-full object-contain"
852+
/>
853+
</NImageGroup>
854+
</div>
855+
</div>
833856
<MdPreview
834857
v-if="currentVersion?.intro"
835858
id="previewRef"

src/components/community/modules/ModelCard.vue

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { Model } from '@/types/model'
2+
// import { Model } from '@/types/model'
33
import vDefaultPic from '@/components/modules/vDefaultPic.vue'
44
import vTooltips from '@/components/modules/v-tooltip.vue'
55
import { sliceString, formatNumber } from '@/utils/tool'
@@ -18,24 +18,16 @@
1818
const showDialog = ref(false)
1919
const imgSrc = ref('')
2020
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+
}>()
3526
3627
const emit = defineEmits(['action', 'image-load', 'image-error'])
3728
38-
// 计算工作流或节点的提示文本
29+
const isHovering = ref(false)
30+
3931
const actionTooltipText = computed(() => {
4032
return props.model?.type === 'Workflow'
4133
? t('community.modelCard.tooltips.loadWorkflow')
@@ -94,6 +86,41 @@
9486
imgSrc.value = url.includes('?') ? `${url}&t=${timestamp}` : `${url}?t=${timestamp}`
9587
}
9688
})
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+
}
97124
</script>
98125

99126
<template>
@@ -139,17 +166,39 @@
139166
<div
140167
class="relative aspect-[2/3] md:aspect-[3/4] lg:aspect-[2/3] overflow-hidden"
141168
@click.prevent="handleDetail(Number(model?.id), Number(model?.versions?.[0]?.id))"
169+
@mouseenter="handleMouseEnter"
170+
@mouseleave="handleMouseLeave"
142171
>
143172
<div
144173
class="absolute inset-0 bg-gradient-to-br from-[#2a2a2a] to-[#1a1a1a]"
145174
:class="{ 'opacity-0': props.imageLoaded }"
146175
></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+
147194
<img
148-
v-if="imgSrc"
149-
:src="imgSrc"
195+
v-else-if="currentMediaSrc"
196+
:src="currentMediaSrc"
150197
:alt="model.versions?.[0]?.version || model.name"
151198
:crossorigin="
152-
typeof imgSrc === 'string' && imgSrc.startsWith('blob:') ? 'anonymous' : undefined
199+
typeof currentMediaSrc === 'string' && currentMediaSrc.startsWith('blob:')
200+
? 'anonymous'
201+
: undefined
153202
"
154203
class="absolute inset-0 w-full h-full object-cover transition-all duration-300"
155204
:class="{
@@ -159,6 +208,7 @@
159208
@load="handleImageLoad"
160209
@error="handleImageError"
161210
/>
211+
162212
<div v-if="!props.imageLoaded" class="absolute inset-0 flex items-center justify-center">
163213
<vDefaultPic />
164214
</div>

src/components/model-select/detail/Index.vue

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@
4646
const isLoading = ref(false)
4747
const activeTab = ref<number>()
4848
const showAllTags = ref(false)
49+
50+
// 添加视频检测函数
51+
const isVideoUrl = (url: string) => {
52+
if (!url) return false
53+
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv']
54+
const lowercaseUrl = url.toLowerCase()
55+
return videoExtensions.some(ext => lowercaseUrl.includes(ext))
56+
}
57+
4958
const fetchModelDetail = async () => {
5059
try {
5160
const res = await model_detail({
@@ -688,15 +697,30 @@
688697
class="flex flex-col gap-4 items-start justify-start relative min-w-[620px] w-[65%] overflow-hidden"
689698
>
690699
<div class="w-full">
691-
<NImageGroup v-if="currentVersion?.cover_urls && currentVersion?.cover_urls.length > 0">
692-
<NImage
693-
v-for="(cover, index) in currentVersion?.cover_urls"
694-
:key="index"
695-
:src="cover"
696-
:preview-src="cover"
697-
height="512px"
698-
/>
699-
</NImageGroup>
700+
<div
701+
v-if="currentVersion?.cover_urls && currentVersion?.cover_urls.length > 0"
702+
class="space-y-4"
703+
>
704+
<div v-for="(cover, index) in currentVersion?.cover_urls" :key="index" class="w-full">
705+
<!-- 视频显示 -->
706+
<video
707+
v-if="isVideoUrl(cover)"
708+
:src="cover"
709+
controls
710+
class="w-full h-auto max-h-[512px] object-contain rounded-lg"
711+
preload="metadata"
712+
/>
713+
<!-- 图片显示 -->
714+
<NImageGroup v-else>
715+
<NImage
716+
:src="cover"
717+
:preview-src="cover"
718+
height="512px"
719+
class="w-full object-contain"
720+
/>
721+
</NImageGroup>
722+
</div>
723+
</div>
700724
<MdPreview
701725
v-if="currentVersion?.intro"
702726
id="previewRef"
@@ -813,7 +837,7 @@
813837
>
814838
{{ t('community.detail.baseModel') }}
815839
</div>
816-
<div className="flex-1 p-4 border-b border-[rgba(78,78,78,0.50)]">
840+
<div className="flex-1 p-4 border-b border-b-[rgba(78,78,78,0.50)]">
817841
{{ currentVersion?.base_model }}
818842
</div>
819843
</div>

src/components/model-select/modules/ModelCard.vue

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
import { sliceString, formatNumber } from '@/utils/tool'
88
import { useModelSelectStore } from '@/stores/modelSelectStore'
9-
import { ref, watch, onMounted } from 'vue'
9+
import { ref, watch, onMounted, computed } from 'vue'
1010
import { useDictStore } from '@/stores/dictStore'
1111
1212
const { t } = useI18n()
@@ -18,13 +18,13 @@
1818
const modelSelectStore = useModelSelectStore()
1919
const imgSrc = ref('')
2020
const tagsStore = useDictStore()
21+
const isHovering = ref(false)
2122
2223
const props = defineProps({
2324
model: {
2425
type: Object as () => Model | null,
2526
default: null
2627
},
27-
2828
loading: {
2929
type: Boolean,
3030
default: false
@@ -37,6 +37,48 @@
3737
3838
const emit = defineEmits(['action', 'image-load', 'image-error'])
3939
40+
// 添加计算属性判断是否为视频
41+
const isVideo = computed(() => {
42+
if (!imgSrc.value) return false
43+
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv']
44+
const url = imgSrc.value.toLowerCase()
45+
return videoExtensions.some(ext => url.includes(ext))
46+
})
47+
48+
// 生成视频缩略图URL
49+
const getVideoThumbnail = (videoUrl: string) => {
50+
// 如果URL已经包含OSS处理参数,直接返回
51+
if (videoUrl.includes('x-oss-process=video/snapshot')) {
52+
return videoUrl
53+
}
54+
// 添加OSS视频截图处理参数
55+
const separator = videoUrl.includes('?') ? '&' : '?'
56+
return `${videoUrl}${separator}x-oss-process=video/snapshot,t_000,f_jpg,w_300,h_600`
57+
}
58+
59+
// 当前显示的媒体源
60+
const currentMediaSrc = computed(() => {
61+
if (!imgSrc.value) return ''
62+
if (isVideo.value) {
63+
// 如果是视频且鼠标悬停,返回视频URL,否则返回缩略图
64+
return isHovering.value ? imgSrc.value : getVideoThumbnail(imgSrc.value)
65+
}
66+
return imgSrc.value
67+
})
68+
69+
// 鼠标悬停处理
70+
const handleMouseEnter = () => {
71+
if (isVideo.value) {
72+
isHovering.value = true
73+
}
74+
}
75+
76+
const handleMouseLeave = () => {
77+
if (isVideo.value) {
78+
isHovering.value = false
79+
}
80+
}
81+
4082
const handleImageLoad = (e: Event) => {
4183
emit('image-load', e)
4284
}
@@ -114,17 +156,40 @@
114156
<div
115157
class="relative aspect-[2/3] md:aspect-[3/4] lg:aspect-[2/3] overflow-hidden"
116158
@click.prevent="handleDetail(Number(model?.id), Number(model?.versions?.[0]?.id))"
159+
@mouseenter="handleMouseEnter"
160+
@mouseleave="handleMouseLeave"
117161
>
118162
<div
119163
class="absolute inset-0 bg-gradient-to-br from-[#2a2a2a] to-[#1a1a1a]"
120164
:class="{ 'opacity-0': props.imageLoaded }"
121165
></div>
166+
167+
<!-- 视频显示(悬停时) -->
168+
<video
169+
v-if="isVideo && isHovering && currentMediaSrc"
170+
:src="currentMediaSrc"
171+
class="absolute inset-0 w-full h-full object-cover transition-all duration-300"
172+
:class="{
173+
'opacity-0': !props.imageLoaded,
174+
'opacity-100 group-hover:scale-105': props.imageLoaded
175+
}"
176+
muted
177+
autoplay
178+
loop
179+
preload="metadata"
180+
@loadeddata="handleImageLoad"
181+
@error="handleImageError"
182+
/>
183+
184+
<!-- 图片显示(包括视频缩略图) -->
122185
<img
123-
v-if="imgSrc"
124-
:src="imgSrc"
186+
v-else-if="currentMediaSrc"
187+
:src="currentMediaSrc"
125188
:alt="model.versions?.[0]?.version || model.name"
126189
:crossorigin="
127-
typeof imgSrc === 'string' && imgSrc.startsWith('blob:') ? 'anonymous' : undefined
190+
typeof currentMediaSrc === 'string' && currentMediaSrc.startsWith('blob:')
191+
? 'anonymous'
192+
: undefined
128193
"
129194
class="absolute inset-0 w-full h-full object-cover transition-all duration-300"
130195
:class="{
@@ -134,6 +199,7 @@
134199
@load="handleImageLoad"
135200
@error="handleImageError"
136201
/>
202+
137203
<div v-if="!props.imageLoaded" class="absolute inset-0 flex items-center justify-center">
138204
<vDefaultPic />
139205
</div>

0 commit comments

Comments
 (0)