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:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+1015
-775
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user