Files
my-page/src/pages/lab/PremiumShowcase.vue
2026-08-12 19:18:35 +08:00

485 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { onBeforeUnmount, onMounted, ref } from 'vue'
/* 鼠标视差 */
const px = ref(0)
const py = ref(0)
function onMove(e) {
px.value = e.clientX / window.innerWidth - 0.5
py.value = e.clientY / window.innerHeight - 0.5
}
/* 磁性按钮 */
const mag = ref({ x: 0, y: 0 })
function onMag(e) {
const r = e.currentTarget.getBoundingClientRect()
mag.value = {
x: (e.clientX - r.left - r.width / 2) * 0.35,
y: (e.clientY - r.top - r.height / 2) * 0.35,
}
}
function offMag() {
mag.value = { x: 0, y: 0 }
}
/* 滚动渐入 */
let ob = null
onMounted(() => {
window.addEventListener('pointermove', onMove, { passive: true })
ob = new IntersectionObserver(
(es) => es.forEach((e) => e.isIntersecting && (e.target.classList.add('in'), ob.unobserve(e.target))),
{ threshold: 0.16 },
)
document.querySelectorAll('.ps-rv').forEach((el) => ob.observe(el))
})
onBeforeUnmount(() => {
window.removeEventListener('pointermove', onMove)
ob?.disconnect()
})
const title = 'OBSIDIAN'
const craft = [
{ no: 'Ⅰ', t: '整块黑曜石表盘', d: '取自冰岛火山岩芯,每一枚纹理都独一无二,切割损耗率 92%。' },
{ no: 'Ⅱ', t: '手工鎏金时标', d: '0.2mm 金丝嵌入盘面,匠人一天只能完成三枚。' },
{ no: 'Ⅲ', t: '蓝宝石穹顶镜面', d: '双面防眩镀膜,像一层不存在的空气。' },
{ no: 'Ⅳ', t: '自研 OBS-01 机芯', d: '72 小时动储,日误差 -1/+2 秒。' },
]
const specs = [
['表径', '39.5 mm'],
['厚度', '9.8 mm'],
['机芯', 'OBS-01 自动上链'],
['动储', '72 小时'],
['防水', '5 ATM'],
['限量', '99 枚 / 年'],
]
</script>
<template>
<div class="ps">
<!-- 噪点覆层 -->
<i class="ps-noise" aria-hidden="true"></i>
<nav class="ps-nav">
<router-link to="/lab" class="ps-back">← 实验室</router-link>
<b class="ps-logo">OBSIDIAN</b>
<span class="ps-nav-r">EST. MMXXVI</span>
</nav>
<!-- Hero -->
<header class="ps-hero">
<div
class="ps-watch"
:style="{ transform: `translate(${px * -26}px, ${py * -18}px)` }"
aria-hidden="true"
>
<svg viewBox="0 0 320 320">
<defs>
<radialGradient id="ps-dial" cx="0.38" cy="0.32" r="0.9">
<stop offset="0" stop-color="#1c1c22" />
<stop offset="1" stop-color="#060608" />
</radialGradient>
<linearGradient id="ps-gold" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#e8dcc0" />
<stop offset="0.5" stop-color="#b8a06a" />
<stop offset="1" stop-color="#7a6636" />
</linearGradient>
</defs>
<circle cx="160" cy="160" r="148" fill="none" stroke="url(#ps-gold)" stroke-width="3" />
<circle cx="160" cy="160" r="140" fill="url(#ps-dial)" stroke="#2a2a32" />
<!-- 时标 -->
<g stroke="url(#ps-gold)" stroke-width="2.5">
<line v-for="n in 12" :key="n" x1="160" y1="30" x2="160" y2="44" :transform="`rotate(${n * 30} 160 160)`" />
</g>
<circle cx="160" cy="160" r="118" fill="none" stroke="rgba(184,160,106,.2)" stroke-dasharray="1 5" />
<text x="160" y="104" text-anchor="middle" class="ps-watch-brand">OBSIDIAN</text>
<!-- 指针 -->
<g class="ps-hand hour">
<line x1="160" y1="160" x2="160" y2="98" stroke="url(#ps-gold)" stroke-width="5" stroke-linecap="round" />
</g>
<g class="ps-hand min">
<line x1="160" y1="160" x2="160" y2="70" stroke="#e8dcc0" stroke-width="3" stroke-linecap="round" />
</g>
<g class="ps-hand sec">
<line x1="160" y1="172" x2="160" y2="58" stroke="#b8a06a" stroke-width="1.4" />
</g>
<circle cx="160" cy="160" r="6" fill="url(#ps-gold)" />
<!-- 镜面眩光 -->
<ellipse cx="118" cy="104" rx="64" ry="30" fill="rgba(255,255,255,.05)" transform="rotate(-28 118 104)" />
</svg>
<i class="ps-watch-glow" :style="{ transform: `translate(${px * 40}px, ${py * 30}px)` }"></i>
</div>
<p class="ps-kicker">HAUTE HORLOGERIE · 高级制表</p>
<h1 class="ps-title" aria-label="OBSIDIAN">
<span v-for="(ch, i) in title" :key="i" :style="{ animationDelay: 0.35 + i * 0.07 + 's' }">{{ ch }}</span>
</h1>
<p class="ps-sub">一块来自火山深处的时间</p>
<button
class="ps-cta"
:style="{ transform: `translate(${mag.x}px, ${mag.y}px)` }"
@mousemove="onMag"
@mouseleave="offMag"
>
<em>预约鉴赏</em>
</button>
</header>
<!-- 跑马灯 -->
<div class="ps-marquee" aria-hidden="true">
<div class="ps-marquee-track">
<span v-for="n in 2" :key="n">
BORN OF VOLCANIC GLASS · 限量 99 枚 · HAND-GILDED INDICES · 冰岛黑曜石 · 72H POWER RESERVE ·
</span>
</div>
</div>
<!-- 工艺 -->
<section class="ps-sec">
<h2 class="ps-h2 ps-rv">四重工艺</h2>
<div class="ps-craft">
<article v-for="(c, i) in craft" :key="c.no" class="ps-craft-item ps-rv" :style="{ transitionDelay: i * 0.08 + 's' }">
<span>{{ c.no }}</span>
<b>{{ c.t }}</b>
<p>{{ c.d }}</p>
</article>
</div>
</section>
<!-- 参数 -->
<section class="ps-sec">
<h2 class="ps-h2 ps-rv">精密参数</h2>
<dl class="ps-specs ps-rv">
<div v-for="[k, v] in specs" :key="k">
<dt>{{ k }}</dt>
<dd>{{ v }}</dd>
</div>
</dl>
</section>
<!-- 结尾 CTA -->
<section class="ps-final ps-rv">
<p class="ps-final-line">99 枚,每一枚都有自己的火山纹</p>
<button class="ps-cta ghost" :style="{ transform: `translate(${mag.x}px, ${mag.y}px)` }" @mousemove="onMag" @mouseleave="offMag">
<em>成为第 100 位等待者</em>
</button>
<p class="ps-foot">虚构品牌 · 高质感落地页样板 · <router-link to="/lab">返回实验室</router-link></p>
</section>
</div>
</template>
<style scoped>
.ps {
position: relative;
min-height: 100vh;
background: #08080a;
color: #e8e4da;
font-family: 'Noto Serif SC', serif;
overflow-x: clip;
}
.ps-noise {
position: fixed;
inset: 0;
z-index: 60;
pointer-events: none;
opacity: 0.05;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)'/%3E%3C/svg%3E");
}
.ps-nav {
position: sticky;
top: 0;
z-index: 40;
display: flex;
align-items: center;
padding: 16px clamp(16px, 5vw, 56px);
background: rgba(8, 8, 10, 0.72);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(184, 160, 106, 0.22);
}
.ps-back {
color: #8a8578;
text-decoration: none;
font-size: 12.5px;
letter-spacing: 0.1em;
}
.ps-back:hover {
color: #e8dcc0;
}
.ps-logo {
margin: 0 auto;
font-family: 'Playfair Display', serif;
font-size: 15px;
letter-spacing: 0.5em;
color: #e8dcc0;
transform: translateX(0.25em);
}
.ps-nav-r {
font-size: 10.5px;
letter-spacing: 0.24em;
color: #6b6656;
}
.ps-hero {
position: relative;
text-align: center;
padding: clamp(70px, 10vw, 110px) 20px 60px;
overflow: clip;
}
.ps-watch {
position: absolute;
left: 50%;
top: 46%;
width: min(560px, 92vw);
margin-left: min(-280px, -46vw);
margin-top: -180px;
opacity: 0.5;
pointer-events: none;
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
}
.ps-watch svg {
width: 100%;
height: auto;
}
.ps-watch-brand {
font-family: 'Playfair Display', serif;
font-size: 10px;
letter-spacing: 0.5em;
fill: rgba(232, 220, 192, 0.6);
}
.ps-hand {
transform-origin: 160px 160px;
}
.ps-hand.hour { animation: ps-rot 90s linear infinite; }
.ps-hand.min { animation: ps-rot 24s linear infinite; }
.ps-hand.sec { animation: ps-rot 6s linear infinite; }
@keyframes ps-rot {
to { transform: rotate(360deg); }
}
.ps-watch-glow {
position: absolute;
left: 20%;
top: 12%;
width: 60%;
height: 60%;
border-radius: 50%;
background: radial-gradient(circle, rgba(184, 160, 106, 0.14), transparent 65%);
filter: blur(24px);
}
.ps-kicker {
position: relative;
margin: 0 0 22px;
font-size: 11px;
letter-spacing: 0.5em;
color: #8a8578;
animation: ps-up 0.9s ease both;
}
.ps-title {
position: relative;
margin: 0 0 14px;
font-family: 'Playfair Display', serif;
font-size: clamp(52px, 12vw, 128px);
font-weight: 700;
letter-spacing: 0.12em;
line-height: 1;
}
.ps-title span {
display: inline-block;
background: linear-gradient(175deg, #f4ecd8 30%, #b8a06a 75%, #7a6636);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
opacity: 0;
transform: translateY(30px) rotate(4deg);
animation: ps-char 0.7s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes ps-char {
to { opacity: 1; transform: none; }
}
.ps-sub {
position: relative;
margin: 0 0 40px;
font-size: 15px;
letter-spacing: 0.3em;
color: #a89f8a;
animation: ps-up 0.9s ease 1s both;
}
@keyframes ps-up {
from { opacity: 0; transform: translateY(18px); }
}
.ps-cta {
position: relative;
border: 1px solid rgba(184, 160, 106, 0.6);
background: none;
color: #e8dcc0;
font-family: inherit;
font-size: 13.5px;
letter-spacing: 0.3em;
padding: 16px 46px;
border-radius: 999px;
cursor: pointer;
overflow: hidden;
transition: transform 0.18s cubic-bezier(0.22, 1, 0.36, 1), background 0.25s ease, color 0.25s ease;
animation: ps-up 0.9s ease 1.2s both;
}
.ps-cta em {
position: relative;
font-style: normal;
z-index: 1;
}
.ps-cta::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(120deg, transparent 30%, rgba(232, 220, 192, 0.25) 50%, transparent 70%);
transform: translateX(-130%);
transition: transform 0.6s ease;
}
.ps-cta:hover {
background: rgba(184, 160, 106, 0.12);
}
.ps-cta:hover::before {
transform: translateX(130%);
}
.ps-marquee {
border-top: 1px solid rgba(184, 160, 106, 0.22);
border-bottom: 1px solid rgba(184, 160, 106, 0.22);
overflow: hidden;
padding: 13px 0;
}
.ps-marquee-track {
display: flex;
white-space: nowrap;
animation: ps-marquee 30s linear infinite;
}
.ps-marquee-track span {
font-family: 'Playfair Display', 'Noto Serif SC', serif;
font-style: italic;
font-size: 13px;
letter-spacing: 0.22em;
color: #6b6656;
padding-right: 20px;
}
@keyframes ps-marquee {
to { transform: translateX(-50%); }
}
.ps-sec {
max-width: 940px;
margin: 0 auto;
padding: clamp(44px, 7vw, 76px) clamp(16px, 4vw, 26px) 8px;
}
.ps-h2 {
text-align: center;
font-size: clamp(22px, 3.6vw, 30px);
font-weight: 600;
letter-spacing: 0.3em;
margin: 0 0 34px;
color: #f4ecd8;
}
.ps-h2::after {
content: '';
display: block;
width: 44px;
height: 1px;
margin: 16px auto 0;
background: linear-gradient(90deg, transparent, #b8a06a, transparent);
}
.ps-craft {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1px;
background: rgba(184, 160, 106, 0.18);
border: 1px solid rgba(184, 160, 106, 0.18);
}
.ps-craft-item {
background: #0a0a0d;
padding: 30px 24px 28px;
transition: background 0.3s ease;
}
.ps-craft-item:hover {
background: #101014;
}
.ps-craft-item span {
font-family: 'Playfair Display', serif;
font-size: 24px;
color: #b8a06a;
}
.ps-craft-item b {
display: block;
margin: 14px 0 10px;
font-size: 15.5px;
color: #f4ecd8;
letter-spacing: 0.08em;
}
.ps-craft-item p {
margin: 0;
font-size: 12.5px;
line-height: 2;
color: #8a8578;
}
.ps-specs {
margin: 0;
border-top: 1px solid rgba(184, 160, 106, 0.3);
}
.ps-specs div {
display: grid;
grid-template-columns: 130px 1fr;
gap: 14px;
padding: 15px 4px;
border-bottom: 1px solid rgba(184, 160, 106, 0.14);
font-size: 13.5px;
}
.ps-specs dt {
color: #8a8578;
letter-spacing: 0.24em;
}
.ps-specs dd {
margin: 0;
color: #e8dcc0;
font-variant-numeric: tabular-nums;
letter-spacing: 0.06em;
}
.ps-final {
text-align: center;
padding: clamp(50px, 8vw, 90px) 20px 60px;
}
.ps-final-line {
margin: 0 0 30px;
font-size: clamp(17px, 2.6vw, 24px);
letter-spacing: 0.2em;
color: #d8cfb6;
}
.ps-cta.ghost {
animation: none;
}
.ps-foot {
margin: 34px 0 0;
font-size: 11.5px;
color: #55503f;
}
.ps-foot a {
color: #b8a06a;
text-decoration: none;
}
.ps-foot a:hover {
text-decoration: underline;
}
/* 滚动渐入 */
.ps-rv {
opacity: 0;
transform: translateY(26px);
transition: opacity 0.8s ease, transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
}
.ps-rv.in {
opacity: 1;
transform: none;
}
</style>