feat(home): 逐像素移植 vivid 旧版首页 + 悬浮胶囊导航

整页按 vivid commit 8b16666 的 HomeView 复刻:
- Hero 左文右图:模型在线徽章、「让想象/即刻成画」渐变标题、
  渐变描边魔法输入框、提示词胶囊、三项统计
- 右侧四格作品拼贴(带错位旋转/悬停浮层/shimmer 占位)
- 01·能力矩阵(bento 网格)、02·灵感作品墙(编号卡+引导卡)
- 紫色 CTA 横幅 + 分栏页脚(社交图标接 site.contact)
- 顶栏改 vivid 胶囊:999px 毛玻璃、滚动展开通栏、
  游客态单个渐变「登录/注册」按钮
- 页面局部覆盖 vivid 原版色板(--bg #fafafb、accent #6d4df2),
  隐藏 aurora 背景
- Icon 组件补 size prop 与缺失图标,修复 SVG 无尺寸撑爆布局

数据接本站 /managed-models(sellable) 与 /showcase(hero→拼贴,work→作品墙)

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 14:58:17 +08:00
co-authored by Devin
parent 4632b1ab04
commit 466507b388
3 changed files with 1105 additions and 804 deletions
+13 -1
View File
@@ -28,12 +28,24 @@ const PATHS = {
music: '<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>', music: '<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>',
play: '<path d="m6 3 14 9-14 9V3Z"/>', play: '<path d="m6 3 14 9-14 9V3Z"/>',
pause: '<rect x="14" y="4" width="4" height="16" rx="1"/><rect x="6" y="4" width="4" height="16" rx="1"/>', pause: '<rect x="14" y="4" width="4" height="16" rx="1"/><rect x="6" y="4" width="4" height="16" rx="1"/>',
arrow: '<path d="M5 12h14"/><path d="m13 6 6 6-6 6"/>',
image: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/>',
chat: '<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/>',
layers: '<path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.84Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/>',
key: '<path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l3-3a1 1 0 0 0 0-1.4l-2.3-2.3a1 1 0 0 0-1.4 0l-3 3a1 1 0 0 0 0 1.4z"/><path d="m15.5 7.5-8 8"/><circle cx="7.5" cy="15.5" r="5.5"/>',
mail: '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 5L2 7"/>',
team: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
wallet: '<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4Z"/>',
} }
defineProps({ name: { type: String, required: true } }) defineProps({
name: { type: String, required: true },
size: { type: [Number, String], default: null },
})
</script> </script>
<template> <template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
:width="size || undefined" :height="size || undefined"
stroke-linecap="round" stroke-linejoin="round" v-html="PATHS[name] || ''" /> stroke-linecap="round" stroke-linejoin="round" v-html="PATHS[name] || ''" />
</template> </template>
+77 -28
View File
@@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, ref, watch } from 'vue' import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { auth, isAuthed, isAdmin, openLogin, openRegister, logout } from '../auth' import { auth, isAuthed, isAdmin, openLogin, openRegister, logout } from '../auth'
import { isDark, toggleTheme } from '../theme' import { isDark, toggleTheme } from '../theme'
@@ -39,6 +39,12 @@ const menuOpen = ref(false) // avatar dropdown
const mobileOpen = ref(false) // hamburger panel const mobileOpen = ref(false) // hamburger panel
watch(() => route.fullPath, () => { menuOpen.value = false; mobileOpen.value = false }) watch(() => route.fullPath, () => { menuOpen.value = false; mobileOpen.value = false })
// 悬浮胶囊顶栏:静止时浮在页面上方(999px 圆角),滚动后展开为通栏。
const scrolled = ref(false)
function onScroll() { scrolled.value = window.scrollY > 4 }
onMounted(() => { onScroll(); window.addEventListener('scroll', onScroll, { passive: true }) })
onUnmounted(() => window.removeEventListener('scroll', onScroll))
// credits — the logged-in user's real server-side balance (auth.user.credits) // credits — the logged-in user's real server-side balance (auth.user.credits)
const credits = computed(() => Number(auth.user?.credits || 0)) const credits = computed(() => Number(auth.user?.credits || 0))
const creditsLabel = computed(() => pointsLabel(credits.value)) const creditsLabel = computed(() => pointsLabel(credits.value))
@@ -71,9 +77,9 @@ async function onLogout() {
<template> <template>
<div class="theme-x min-h-screen bg-[var(--app-bg)] text-[color:var(--fg-2)] selection:bg-orange-400/30"> <div class="theme-x min-h-screen bg-[var(--app-bg)] text-[color:var(--fg-2)] selection:bg-orange-400/30">
<!-- ===== Top bar ===== --> <!-- ===== Top bar:悬浮胶囊,滚动展开为通栏 ===== -->
<header class="fixed top-0 inset-x-0 z-40 backdrop-blur-xl top-bar"> <header class="top-nav fixed top-0 inset-x-0 z-40" :class="{ scrolled }">
<div class="mx-auto max-w-[1400px] px-4 md:px-8 h-[58px] flex items-center gap-3"> <div class="nav-inner mx-auto flex items-center gap-3">
<!-- brand --> <!-- brand -->
<router-link to="/" class="flex items-center gap-2.5 shrink-0 transition-transform hover:scale-[1.03]"> <router-link to="/" class="flex items-center gap-2.5 shrink-0 transition-transform hover:scale-[1.03]">
<img v-if="site.logo" :src="site.logo" :alt="site.title" class="w-8 h-8 object-contain" /> <img v-if="site.logo" :src="site.logo" :alt="site.title" class="w-8 h-8 object-contain" />
@@ -98,8 +104,8 @@ async function onLogout() {
余额 <span class="text-[color:var(--fg)] font-semibold">{{ creditsLabel }}</span> 余额 <span class="text-[color:var(--fg)] font-semibold">{{ creditsLabel }}</span>
</router-link> </router-link>
<!-- theme --> <!-- theme(登录后可见,游客态与 vivid 一致只留登录入口) -->
<button type="button" @click="toggleTheme" class="round-btn" <button v-if="isAuthed()" type="button" @click="toggleTheme" class="round-btn"
:title="isDark ? '切换到亮色' : '切换到暗色'"> :title="isDark ? '切换到亮色' : '切换到暗色'">
<svg v-if="isDark" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg> <svg v-if="isDark" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
<svg v-else class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg> <svg v-else class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
@@ -118,10 +124,7 @@ async function onLogout() {
<!-- guest actions --> <!-- guest actions -->
<template v-if="!isAuthed()"> <template v-if="!isAuthed()">
<button type="button" class="hidden sm:inline-flex top-link" @click="openLogin('/user')">登录</button> <button type="button" class="login-pill" @click="openLogin('/user')">登录 / 注册</button>
<button type="button" class="solid-btn" @click="openRegister('', '/user')">
免费开始 <span aria-hidden="true">→</span>
</button>
</template> </template>
<!-- signed-in: avatar menu --> <!-- signed-in: avatar menu -->
@@ -145,7 +148,7 @@ async function onLogout() {
</div> </div>
<!-- mobile panel --> <!-- mobile panel -->
<div v-if="mobileOpen" class="md:hidden border-t border-[color:var(--ink-soft)] px-3 py-3 bg-[var(--menu-bg)]"> <div v-if="mobileOpen" class="mobile-panel md:hidden px-3 py-3">
<router-link v-for="n in nav" :key="n.to" :to="n.to" <router-link v-for="n in nav" :key="n.to" :to="n.to"
:exact-active-class="n.to === '/' ? 'm-on' : ''" :exact-active-class="n.to === '/' ? 'm-on' : ''"
:active-class="n.to === '/' ? '' : 'm-on'" :active-class="n.to === '/' ? '' : 'm-on'"
@@ -167,8 +170,8 @@ async function onLogout() {
<!-- ===== Main column ===== --> <!-- ===== Main column ===== -->
<div class="relative"> <div class="relative">
<!-- aurora backdrop: drifting gradient orbs + faint grain --> <!-- aurora backdrop: drifting gradient orbs + faint grain (首页用平铺浅色底,不渲染) -->
<div aria-hidden="true" class="pointer-events-none absolute inset-0 overflow-hidden"> <div v-if="!isHome" aria-hidden="true" class="pointer-events-none absolute inset-0 overflow-hidden">
<div class="memphis-dots absolute inset-0 opacity-50"></div> <div class="memphis-dots absolute inset-0 opacity-50"></div>
<div class="absolute -top-40 left-1/4 w-[42rem] h-[42rem] rounded-full opacity-[0.5]" <div class="absolute -top-40 left-1/4 w-[42rem] h-[42rem] rounded-full opacity-[0.5]"
style="background: radial-gradient(circle, rgb(139 92 246 / 0.35), transparent 60%); filter: blur(100px)"></div> style="background: radial-gradient(circle, rgb(139 92 246 / 0.35), transparent 60%); filter: blur(100px)"></div>
@@ -180,12 +183,12 @@ async function onLogout() {
</div> </div>
<!-- Route label header — the home page brings its own hero instead. --> <!-- Route label header — the home page brings its own hero instead. -->
<header v-if="!isHome" class="relative z-10 mx-auto max-w-[1400px] px-4 md:px-8 pt-[86px] pb-4"> <header v-if="!isHome" class="relative z-10 mx-auto max-w-[1400px] px-4 md:px-8 pt-[104px] pb-4">
<span class="text-[22px] font-bold tracking-tight text-[color:var(--fg)]">{{ currentLabel }}</span> <span class="text-[22px] font-bold tracking-tight text-[color:var(--fg)]">{{ currentLabel }}</span>
</header> </header>
<main :class="['relative z-10 mx-auto max-w-[1400px] pb-24', <main :class="['relative z-10 mx-auto max-w-[1400px] pb-24',
isHome ? 'pt-[58px] px-0' : 'px-4 md:px-8 pt-2', isHome ? 'pt-0 px-0' : 'px-4 md:px-8 pt-2',
{ 'public-dark': isDark }]"> { 'public-dark': isDark }]">
<router-view v-slot="{ Component, route: viewRoute }"> <router-view v-slot="{ Component, route: viewRoute }">
<component :is="Component" v-if="viewRoute.path === '/canvas'" /> <component :is="Component" v-if="viewRoute.path === '/canvas'" />
@@ -199,30 +202,66 @@ async function onLogout() {
</template> </template>
<style scoped> <style scoped>
.top-bar { /* 悬浮胶囊导航:不压满宽黑线,毛玻璃胶囊浮在页面上方;滚动后展开为通栏 */
background: color-mix(in srgb, var(--app-bg) 62%, transparent); .top-nav {
padding-top: 10px;
background: transparent;
pointer-events: none;
transition: padding-top 0.22s ease;
}
.nav-inner {
pointer-events: auto;
height: 64px;
width: min(1200px, calc(100% - 40px));
padding: 0 14px 0 20px;
border-radius: 999px;
background: var(--menu-bg);
-webkit-backdrop-filter: blur(20px) saturate(1.7);
backdrop-filter: blur(20px) saturate(1.7);
border: 1px solid var(--ink-soft);
box-shadow: 0 8px 30px -14px rgb(15 23 42 / 0.18);
transition: width 0.25s ease, border-radius 0.25s ease, border-color 0.25s ease,
box-shadow 0.25s ease, background 0.25s ease;
}
.top-nav.scrolled { padding-top: 0; }
.top-nav.scrolled .nav-inner {
width: 100%;
border-radius: 0;
border-color: transparent;
border-bottom: 1px solid var(--ink-soft); border-bottom: 1px solid var(--ink-soft);
transition: border-color 0.2s ease; box-shadow: 0 10px 30px -12px rgb(15 23 42 / 0.12);
}
html.dark .nav-inner { box-shadow: 0 8px 30px -14px rgb(0 0 0 / 0.5); }
.mobile-panel {
pointer-events: auto;
width: min(1200px, calc(100% - 32px));
margin: 6px auto 0;
border-radius: 1rem;
background: var(--menu-bg);
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
border: 1px solid var(--ink-soft);
box-shadow: var(--glass-shadow-lg);
} }
.top-link { .top-link {
padding: 0.45rem 0.8rem; padding: 0.55rem 1.1rem;
border-radius: 0.6rem; border-radius: 999px;
font-size: 13.5px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--fg-2); color: var(--fg-2);
background: transparent; background: transparent;
border: 1px solid transparent; border: none;
cursor: pointer; cursor: pointer;
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; transition: color 0.2s ease, background 0.2s ease, transform 0.15s ease;
} }
.top-link:hover { color: var(--fg); background: var(--hover); border-color: rgb(124 92 252 / 0.3); } .top-link:active { transform: scale(0.95); }
.top-link:hover { color: var(--fg); background: var(--hover); }
.top-link.on { .top-link.on {
color: #fff; color: var(--m-orange);
font-weight: 700; font-weight: 700;
background: linear-gradient(120deg, #7c5cfc, #5f6cf8); background: rgb(124 92 252 / 0.12);
border-color: transparent;
box-shadow: 0 4px 14px var(--accent-glow);
} }
.brand-spark { .brand-spark {
@@ -272,6 +311,16 @@ async function onLogout() {
transition: color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease, border-color 0.15s ease; transition: color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease, border-color 0.15s ease;
} }
.round-btn:hover { color: var(--fg); background: var(--hover); border-color: rgb(124 92 252 / 0.4); box-shadow: 0 4px 14px rgb(124 92 252 / 0.16); transform: translateY(-1px); } .round-btn:hover { color: var(--fg); background: var(--hover); border-color: rgb(124 92 252 / 0.4); box-shadow: 0 4px 14px rgb(124 92 252 / 0.16); transform: translateY(-1px); }
/* 游客登录入口:vivid 同款渐变胶囊 */
.login-pill {
display: inline-flex; align-items: center;
padding: 8px 18px; border: none; border-radius: 999px;
background: linear-gradient(135deg, #8b5cf6 0%, #4f46e5 100%);
color: #fff; font-family: inherit; font-size: 13px; font-weight: 700;
cursor: pointer; box-shadow: 0 10px 26px -8px rgba(109, 77, 242, 0.55);
transition: transform 0.15s ease, filter 0.2s ease;
}
.login-pill:hover { transform: translateY(-1px); filter: brightness(1.08); }
.round-btn:active { transform: scale(0.95) translateY(1px); box-shadow: none; } .round-btn:active { transform: scale(0.95) translateY(1px); box-shadow: none; }
/* mobile-only: .round-btn's display:grid outranks Tailwind's md:hidden, so /* mobile-only: .round-btn's display:grid outranks Tailwind's md:hidden, so
File diff suppressed because it is too large Load Diff