feat(home): 首页展示位空数据时渲染内置渐变图兜底

/showcase 返回空时 hero 拼贴与作品墙不再留白——
用 public/showcase 下 11 张内置 SVG 渐变图填充;
接口有数据时仍优先使用后台配置的展示图。

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-10-27 15:08:07 +08:00
co-authored by Devin
parent 466507b388
commit 8fd49db8c2
12 changed files with 166 additions and 5 deletions
+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="800" viewBox="0 0 600 800">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#7c5cf5"/><stop offset="55%" stop-color="#ec4899"/><stop offset="100%" stop-color="#f59e0b"/></linearGradient>
<radialGradient id="b1" cx="0.3" cy="0.25" r="0.7"><stop offset="0%" stop-color="#ffffff" stop-opacity="0.5"/><stop offset="100%" stop-color="#ffffff" stop-opacity="0"/></radialGradient>
<radialGradient id="b2" cx="0.75" cy="0.75" r="0.6"><stop offset="0%" stop-color="#ffd1e8" stop-opacity="0.28"/><stop offset="100%" stop-color="#ffd1e8" stop-opacity="0"/></radialGradient>
</defs>
<rect width="600" height="800" fill="url(#g)"/>
<rect width="600" height="800" fill="url(#b1)"/>
<rect width="600" height="800" fill="url(#b2)"/>
<circle cx="432" cy="240" r="54" fill="rgba(255,255,255,0.5)"/>
<path d="M0 496 Q 180 400 330 496 T 600 440.00000000000006 V 800 H0 Z" fill="rgba(255,255,255,0.12)"/>
<path d="M0 576 Q 210 480 360 576 T 600 528 V 800 H0 Z" fill="rgba(255,255,255,0.1)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="800" viewBox="0 0 600 800">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#0ea5e9"/><stop offset="60%" stop-color="#6366f1"/><stop offset="100%" stop-color="#1e1b4b"/></linearGradient>
<radialGradient id="b1" cx="0.3" cy="0.25" r="0.7"><stop offset="0%" stop-color="#bae6fd" stop-opacity="0.5"/><stop offset="100%" stop-color="#bae6fd" stop-opacity="0"/></radialGradient>
<radialGradient id="b2" cx="0.75" cy="0.75" r="0.6"><stop offset="0%" stop-color="#ffffff" stop-opacity="0.28"/><stop offset="100%" stop-color="#ffffff" stop-opacity="0"/></radialGradient>
</defs>
<rect width="600" height="800" fill="url(#g)"/>
<rect width="600" height="800" fill="url(#b1)"/>
<rect width="600" height="800" fill="url(#b2)"/>
<circle cx="432" cy="240" r="54" fill="rgba(255,255,255,0.5)"/>
<path d="M0 496 Q 180 400 330 496 T 600 440.00000000000006 V 800 H0 Z" fill="rgba(255,255,255,0.12)"/>
<path d="M0 576 Q 210 480 360 576 T 600 528 V 800 H0 Z" fill="rgba(255,255,255,0.1)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" viewBox="0 0 600 600">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#f59e0b"/><stop offset="50%" stop-color="#ef4444"/><stop offset="100%" stop-color="#7c2d12"/></linearGradient>
<radialGradient id="b1" cx="0.3" cy="0.25" r="0.7"><stop offset="0%" stop-color="#fde68a" stop-opacity="0.5"/><stop offset="100%" stop-color="#fde68a" stop-opacity="0"/></radialGradient>
<radialGradient id="b2" cx="0.75" cy="0.75" r="0.6"><stop offset="0%" stop-color="#ffffff" stop-opacity="0.28"/><stop offset="100%" stop-color="#ffffff" stop-opacity="0"/></radialGradient>
</defs>
<rect width="600" height="600" fill="url(#g)"/>
<rect width="600" height="600" fill="url(#b1)"/>
<rect width="600" height="600" fill="url(#b2)"/>
<circle cx="432" cy="180" r="54" fill="rgba(255,255,255,0.5)"/>
<path d="M0 372 Q 180 300 330 372 T 600 330 V 600 H0 Z" fill="rgba(255,255,255,0.12)"/>
<path d="M0 432 Q 210 360 360 432 T 600 396 V 600 H0 Z" fill="rgba(255,255,255,0.1)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="800" viewBox="0 0 600 800">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#10b981"/><stop offset="60%" stop-color="#0ea5e9"/><stop offset="100%" stop-color="#0f172a"/></linearGradient>
<radialGradient id="b1" cx="0.3" cy="0.25" r="0.7"><stop offset="0%" stop-color="#a7f3d0" stop-opacity="0.5"/><stop offset="100%" stop-color="#a7f3d0" stop-opacity="0"/></radialGradient>
<radialGradient id="b2" cx="0.75" cy="0.75" r="0.6"><stop offset="0%" stop-color="#ffffff" stop-opacity="0.28"/><stop offset="100%" stop-color="#ffffff" stop-opacity="0"/></radialGradient>
</defs>
<rect width="600" height="800" fill="url(#g)"/>
<rect width="600" height="800" fill="url(#b1)"/>
<rect width="600" height="800" fill="url(#b2)"/>
<circle cx="432" cy="240" r="54" fill="rgba(255,255,255,0.5)"/>
<path d="M0 496 Q 180 400 330 496 T 600 440.00000000000006 V 800 H0 Z" fill="rgba(255,255,255,0.12)"/>
<path d="M0 576 Q 210 480 360 576 T 600 528 V 800 H0 Z" fill="rgba(255,255,255,0.1)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="534" viewBox="0 0 800 534">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#8b5cf6"/><stop offset="100%" stop-color="#4f46e5"/></linearGradient>
<radialGradient id="b1" cx="0.3" cy="0.25" r="0.7"><stop offset="0%" stop-color="#c4b5fd" stop-opacity="0.5"/><stop offset="100%" stop-color="#c4b5fd" stop-opacity="0"/></radialGradient>
<radialGradient id="b2" cx="0.75" cy="0.75" r="0.6"><stop offset="0%" stop-color="#ffffff" stop-opacity="0.28"/><stop offset="100%" stop-color="#ffffff" stop-opacity="0"/></radialGradient>
</defs>
<rect width="800" height="534" fill="url(#g)"/>
<rect width="800" height="534" fill="url(#b1)"/>
<rect width="800" height="534" fill="url(#b2)"/>
<circle cx="576" cy="160.2" r="72" fill="rgba(255,255,255,0.5)"/>
<path d="M0 331.08 Q 240 267 440.00000000000006 331.08 T 800 293.70000000000005 V 534 H0 Z" fill="rgba(255,255,255,0.12)"/>
<path d="M0 384.47999999999996 Q 280 320.4 480 384.47999999999996 T 800 352.44 V 534 H0 Z" fill="rgba(255,255,255,0.1)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="534" viewBox="0 0 800 534">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#ec4899"/><stop offset="100%" stop-color="#8b5cf6"/></linearGradient>
<radialGradient id="b1" cx="0.3" cy="0.25" r="0.7"><stop offset="0%" stop-color="#fbcfe8" stop-opacity="0.5"/><stop offset="100%" stop-color="#fbcfe8" stop-opacity="0"/></radialGradient>
<radialGradient id="b2" cx="0.75" cy="0.75" r="0.6"><stop offset="0%" stop-color="#ffffff" stop-opacity="0.28"/><stop offset="100%" stop-color="#ffffff" stop-opacity="0"/></radialGradient>
</defs>
<rect width="800" height="534" fill="url(#g)"/>
<rect width="800" height="534" fill="url(#b1)"/>
<rect width="800" height="534" fill="url(#b2)"/>
<circle cx="576" cy="160.2" r="72" fill="rgba(255,255,255,0.5)"/>
<path d="M0 331.08 Q 240 267 440.00000000000006 331.08 T 800 293.70000000000005 V 534 H0 Z" fill="rgba(255,255,255,0.12)"/>
<path d="M0 384.47999999999996 Q 280 320.4 480 384.47999999999996 T 800 352.44 V 534 H0 Z" fill="rgba(255,255,255,0.1)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="534" viewBox="0 0 800 534">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#22d3ee"/><stop offset="100%" stop-color="#3b82f6"/></linearGradient>
<radialGradient id="b1" cx="0.3" cy="0.25" r="0.7"><stop offset="0%" stop-color="#a5f3fc" stop-opacity="0.5"/><stop offset="100%" stop-color="#a5f3fc" stop-opacity="0"/></radialGradient>
<radialGradient id="b2" cx="0.75" cy="0.75" r="0.6"><stop offset="0%" stop-color="#ffffff" stop-opacity="0.28"/><stop offset="100%" stop-color="#ffffff" stop-opacity="0"/></radialGradient>
</defs>
<rect width="800" height="534" fill="url(#g)"/>
<rect width="800" height="534" fill="url(#b1)"/>
<rect width="800" height="534" fill="url(#b2)"/>
<circle cx="576" cy="160.2" r="72" fill="rgba(255,255,255,0.5)"/>
<path d="M0 331.08 Q 240 267 440.00000000000006 331.08 T 800 293.70000000000005 V 534 H0 Z" fill="rgba(255,255,255,0.12)"/>
<path d="M0 384.47999999999996 Q 280 320.4 480 384.47999999999996 T 800 352.44 V 534 H0 Z" fill="rgba(255,255,255,0.1)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="534" viewBox="0 0 800 534">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#f97316"/><stop offset="100%" stop-color="#ec4899"/></linearGradient>
<radialGradient id="b1" cx="0.3" cy="0.25" r="0.7"><stop offset="0%" stop-color="#fed7aa" stop-opacity="0.5"/><stop offset="100%" stop-color="#fed7aa" stop-opacity="0"/></radialGradient>
<radialGradient id="b2" cx="0.75" cy="0.75" r="0.6"><stop offset="0%" stop-color="#ffffff" stop-opacity="0.28"/><stop offset="100%" stop-color="#ffffff" stop-opacity="0"/></radialGradient>
</defs>
<rect width="800" height="534" fill="url(#g)"/>
<rect width="800" height="534" fill="url(#b1)"/>
<rect width="800" height="534" fill="url(#b2)"/>
<circle cx="576" cy="160.2" r="72" fill="rgba(255,255,255,0.5)"/>
<path d="M0 331.08 Q 240 267 440.00000000000006 331.08 T 800 293.70000000000005 V 534 H0 Z" fill="rgba(255,255,255,0.12)"/>
<path d="M0 384.47999999999996 Q 280 320.4 480 384.47999999999996 T 800 352.44 V 534 H0 Z" fill="rgba(255,255,255,0.1)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="534" viewBox="0 0 800 534">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#22d3ee"/></linearGradient>
<radialGradient id="b1" cx="0.3" cy="0.25" r="0.7"><stop offset="0%" stop-color="#c7d2fe" stop-opacity="0.5"/><stop offset="100%" stop-color="#c7d2fe" stop-opacity="0"/></radialGradient>
<radialGradient id="b2" cx="0.75" cy="0.75" r="0.6"><stop offset="0%" stop-color="#ffffff" stop-opacity="0.28"/><stop offset="100%" stop-color="#ffffff" stop-opacity="0"/></radialGradient>
</defs>
<rect width="800" height="534" fill="url(#g)"/>
<rect width="800" height="534" fill="url(#b1)"/>
<rect width="800" height="534" fill="url(#b2)"/>
<circle cx="576" cy="160.2" r="72" fill="rgba(255,255,255,0.5)"/>
<path d="M0 331.08 Q 240 267 440.00000000000006 331.08 T 800 293.70000000000005 V 534 H0 Z" fill="rgba(255,255,255,0.12)"/>
<path d="M0 384.47999999999996 Q 280 320.4 480 384.47999999999996 T 800 352.44 V 534 H0 Z" fill="rgba(255,255,255,0.1)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="534" viewBox="0 0 800 534">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#14b8a6"/><stop offset="100%" stop-color="#84cc16"/></linearGradient>
<radialGradient id="b1" cx="0.3" cy="0.25" r="0.7"><stop offset="0%" stop-color="#99f6e4" stop-opacity="0.5"/><stop offset="100%" stop-color="#99f6e4" stop-opacity="0"/></radialGradient>
<radialGradient id="b2" cx="0.75" cy="0.75" r="0.6"><stop offset="0%" stop-color="#ffffff" stop-opacity="0.28"/><stop offset="100%" stop-color="#ffffff" stop-opacity="0"/></radialGradient>
</defs>
<rect width="800" height="534" fill="url(#g)"/>
<rect width="800" height="534" fill="url(#b1)"/>
<rect width="800" height="534" fill="url(#b2)"/>
<circle cx="576" cy="160.2" r="72" fill="rgba(255,255,255,0.5)"/>
<path d="M0 331.08 Q 240 267 440.00000000000006 331.08 T 800 293.70000000000005 V 534 H0 Z" fill="rgba(255,255,255,0.12)"/>
<path d="M0 384.47999999999996 Q 280 320.4 480 384.47999999999996 T 800 352.44 V 534 H0 Z" fill="rgba(255,255,255,0.1)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="534" viewBox="0 0 800 534">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#a855f7"/><stop offset="100%" stop-color="#6366f1"/></linearGradient>
<radialGradient id="b1" cx="0.3" cy="0.25" r="0.7"><stop offset="0%" stop-color="#e9d5ff" stop-opacity="0.5"/><stop offset="100%" stop-color="#e9d5ff" stop-opacity="0"/></radialGradient>
<radialGradient id="b2" cx="0.75" cy="0.75" r="0.6"><stop offset="0%" stop-color="#ffffff" stop-opacity="0.28"/><stop offset="100%" stop-color="#ffffff" stop-opacity="0"/></radialGradient>
</defs>
<rect width="800" height="534" fill="url(#g)"/>
<rect width="800" height="534" fill="url(#b1)"/>
<rect width="800" height="534" fill="url(#b2)"/>
<circle cx="576" cy="160.2" r="72" fill="rgba(255,255,255,0.5)"/>
<path d="M0 331.08 Q 240 267 440.00000000000006 331.08 T 800 293.70000000000005 V 534 H0 Z" fill="rgba(255,255,255,0.12)"/>
<path d="M0 384.47999999999996 Q 280 320.4 480 384.47999999999996 T 800 352.44 V 534 H0 Z" fill="rgba(255,255,255,0.1)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+23 -5
View File
@@ -207,6 +207,16 @@ const PH_SLOTS = [
]
const heroSlots = computed(() => (carouselLoading.value && !carouselItems.value.length) ? PH_SLOTS : carouselItems.value.slice(0, 4))
const inspireItems = ref([])
// 后台未配置展示位时的内置兜底图(/public/showcase 下的本地渐变图)
const FALLBACK_HERO = [
{ id: 'fb-h1', title: '霓虹都市', prompt: '雨夜霓虹街道,赛博朋克光影', image: '/showcase/hero-1.svg' },
{ id: 'fb-h2', title: '深海蓝调', prompt: '深海蓝调梦境,光斑浮动', image: '/showcase/hero-2.svg' },
{ id: 'fb-h3', title: '熔金落日', prompt: '熔金色落日余晖', image: '/showcase/hero-3.svg' },
{ id: 'fb-h4', title: '翡翠星野', prompt: '翡翠色星野与极光', image: '/showcase/hero-4.svg' },
]
const FALLBACK_WORKS = [1, 2, 3, 4, 5, 6, 7].map(i => ({
id: 'fb-w' + i, title: '灵感作品 ' + i, prompt: '', image: `/showcase/work-${i}.svg`,
}))
// Hero 拼贴固定 4 格;后端已限制轮播最多 4 张,此处 slice 兜底
const heroArts = computed(() => carouselItems.value.slice(0, 4))
@@ -221,11 +231,17 @@ onMounted(async () => {
])
onlineModels.value = (m.data?.data || []).filter(model => model.sellable)
const data = sc.data?.data || {}
// hero 拼贴优先取 hero 位,不足 4 张用 bento 补齐
carouselItems.value = [...(data.hero || []), ...(data.bento || [])].filter(i => i.image).slice(0, 4)
inspireItems.value = data.work || []
// hero 拼贴按 hero → bento → 作品墙兜底取图,保证首屏有图可看
const pool = [...(data.hero || []), ...(data.bento || []), ...(data.work || [])]
carouselItems.value = pool.filter(i => i.image).slice(0, 4)
if (!carouselItems.value.length) carouselItems.value = FALLBACK_HERO
inspireItems.value = (data.work || []).length ? data.work : FALLBACK_WORKS
carouselLoading.value = false
} catch { carouselLoading.value = false /* 展示位加载失败不阻塞首页 */ }
} catch {
carouselItems.value = FALLBACK_HERO
inspireItems.value = FALLBACK_WORKS
carouselLoading.value = false /* 展示位加载失败不阻塞首页 */
}
await nextTick()
if (typeof IntersectionObserver === 'undefined') return
revealObs = new IntersectionObserver((entries) => {
@@ -278,7 +294,9 @@ const features = computed(() => {
// (like "user/abc.png") are served from /images by the backend.
function imgSrc(image) {
if (!image) return ''
return /^https?:\/\//i.test(image) ? image : generatedUrl(image)
if (/^https?:\/\//i.test(image)) return image
if (image.startsWith('/')) return image // 内置静态资源(public/ 下)
return generatedUrl(image)
}
async function copyQqGroup() {