Files
mwy/frontend/src/components/FirstMeetTrainPage.vue
flowerstonezl a65cb05d5d mwy
2026-05-19 21:05:31 +08:00

442 lines
18 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 lang="ts">
import { usePreferredReducedMotion } from '@vueuse/core'
import { computed, ref } from 'vue'
import type { Memory } from '../api/client'
import { getStandardNepalTheme } from '../constants/nepalThemes'
import PolaroidCard from './PolaroidCard.vue'
import { renderMarkdown } from '../utils/renderMarkdown'
const props = defineProps<{
memory: Memory
}>()
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
const reduceMotion = usePreferredReducedMotion()
const phase = ref<'idle' | 'running' | 'met'>('idle')
const ticketMotionClass = computed(() => {
if (reduceMotion.value === 'reduce' || phase.value !== 'idle') return ''
return 'fm-ticket-float'
})
function startJourney() {
if (phase.value !== 'idle') return
if (reduceMotion.value === 'reduce') {
phase.value = 'met'
return
}
phase.value = 'running'
}
function onTrainAnimationEnd(ev: AnimationEvent) {
if (ev.animationName !== 'fm-train-xa-cd') return
if (phase.value === 'running') {
phase.value = 'met'
}
}
</script>
<template>
<div
class="relative mx-auto max-w-lg overflow-x-visible overflow-y-hidden rounded-3xl bg-gradient-to-br px-3 pb-8 pt-6 sm:px-5 sm:pb-10 sm:pt-8"
:class="theme.gradient"
>
<div
class="pointer-events-none absolute -left-1/4 -top-1/4 h-1/2 w-[70%] rounded-full bg-gradient-to-br from-amber-300/40 via-orange-200/20 to-transparent blur-3xl"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute -bottom-8 right-0 h-40 w-40 rounded-full bg-gradient-to-tl from-rose-300/30 to-transparent blur-3xl"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute inset-2 rounded-3xl opacity-[0.18] mix-blend-multiply"
aria-hidden="true"
style="
background-image: radial-gradient(circle at 18% 22%, rgb(251 191 36 / 0.5) 0 2px, transparent 3px),
radial-gradient(circle at 78% 28%, rgb(253 186 116 / 0.4) 0 2px, transparent 3px);
background-size: 26px 26px, 30px 30px;
"
/>
<p
class="relative z-[2] mb-3 flex items-center justify-center gap-3 text-[11px] font-bold tracking-[0.3em] text-amber-900/45"
>
<span class="h-px w-8 bg-gradient-to-r from-transparent to-amber-400/60" aria-hidden="true" />
<span class="uppercase">第一章</span>
<span class="h-px w-8 bg-gradient-to-l from-transparent to-amber-400/60" aria-hidden="true" />
</p>
<div
class="relative z-[1] mx-auto mb-8 min-h-[min(68vw,300px)] max-w-[min(100%,400px)] overflow-hidden rounded-[1.5rem] border-[6px] border-white/90 bg-gradient-to-b from-amber-50/80 via-amber-100/40 to-orange-50/40 px-2 pb-3 pt-3 shadow-[inset_0_10px_25px_rgba(120,53,15,0.2),inset_0_2px_4px_rgba(0,0,0,0.05),0_15px_40px_-15px_rgba(180,83,9,0.35)] sm:min-h-[320px] sm:px-3 sm:pb-4 sm:pt-4"
>
<div
class="pointer-events-none absolute -right-1 top-2 z-[2] sm:-right-2 sm:top-3"
:class="phase === 'idle' && !reduceMotion ? 'fm-sun-glow' : ''"
aria-hidden="true"
>
<svg
class="h-40 w-36 drop-shadow-[0_8px_16px_rgba(180,83,9,0.25)] sm:h-48 sm:w-44"
viewBox="0 0 140 160"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient id="fm-petal" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#fcd34d" />
<stop offset="100%" stop-color="#f59e0b" />
</linearGradient>
</defs>
<ellipse cx="88" cy="132" rx="11" ry="38" fill="#15803d" stroke="#14532d" stroke-width="1.3" />
<g transform="translate(88 58)">
<ellipse
v-for="p in 12"
:key="p"
cx="0"
cy="-30"
rx="12"
ry="24"
:transform="`rotate(${(p - 1) * 30})`"
fill="url(#fm-petal)"
stroke="#b45309"
stroke-width="1.05"
/>
</g>
<circle cx="88" cy="58" r="16" fill="#92400e" stroke="#451a03" stroke-width="1.2" />
<circle cx="88" cy="58" r="7" fill="#451a03" />
<circle cx="92" cy="54" r="2.5" fill="#fffbeb" opacity="0.5" />
</svg>
<svg
class="pointer-events-none absolute bottom-8 left-0 h-24 w-20 opacity-90 drop-shadow-lg sm:bottom-10"
viewBox="0 0 80 96"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<ellipse cx="44" cy="82" rx="7" ry="22" fill="#166534" stroke="#14532d" stroke-width="1" />
<g transform="translate(44 44)">
<ellipse
v-for="q in 10"
:key="q"
cx="0"
cy="-18"
rx="7"
ry="15"
:transform="`rotate(${(q - 1) * 36})`"
fill="#fde68a"
stroke="#ca8a04"
stroke-width="0.9"
/>
</g>
<circle cx="44" cy="44" r="9" fill="#a16207" stroke="#713f12" stroke-width="1" />
</svg>
</div>
<div
class="pointer-events-none absolute left-3 top-[42%] z-[3] -translate-y-1/2 sm:left-5"
aria-hidden="true"
>
<div
class="rounded-md border border-amber-900/40 bg-gradient-to-b from-amber-50 to-amber-100 px-2.5 py-1.5 text-center shadow-[2px_4px_0_rgb(120,53,15,0.3)] ring-1 ring-white"
>
<span class="mb-0.5 block text-[8px] font-bold text-amber-800/70">始发</span>
<p class="font-brush text-xl leading-none text-amber-950">西安</p>
</div>
<div class="mx-auto mt-0.5 h-6 w-1.5 rounded-b-sm bg-gradient-to-r from-amber-800 to-amber-900 shadow-sm" />
</div>
<div
class="pointer-events-none absolute right-3 top-[42%] z-[3] -translate-y-1/2 sm:right-5"
aria-hidden="true"
>
<div
class="rounded-md border border-rose-900/40 bg-gradient-to-b from-rose-50 to-rose-100 px-2.5 py-1.5 text-center shadow-[2px_4px_0_rgb(157,23,77,0.25)] ring-1 ring-white"
>
<span class="mb-0.5 block text-[8px] font-bold text-rose-800/70">终到</span>
<p class="font-brush text-xl leading-none text-rose-950">成都</p>
</div>
<div class="mx-auto mt-0.5 h-6 w-1.5 rounded-b-sm bg-gradient-to-r from-rose-800 to-rose-900 shadow-sm" />
</div>
<div
class="pointer-events-none absolute bottom-[32%] left-[10%] right-[10%] z-0 h-10 rounded-t-[100%] bg-gradient-to-t from-amber-300/30 to-transparent opacity-80 blur-[2px]"
aria-hidden="true"
/>
<div class="pointer-events-none absolute bottom-[17%] left-[-5%] right-[-5%] z-[1]" aria-hidden="true">
<div class="h-1.5 rounded-full bg-gradient-to-r from-slate-500/30 via-slate-700/60 to-slate-500/30 shadow-sm" />
<div class="mt-1 flex justify-between px-2 opacity-60">
<span v-for="n in 35" :key="n" class="h-1.5 w-[3px] rounded-full bg-slate-700" />
</div>
<div class="mt-0.5 h-0.5 rounded-full bg-slate-500/40" />
</div>
<div
class="fm-train-wrap absolute bottom-[12%] left-0 z-[4] w-[46%] max-w-[210px] will-change-[left] sm:bottom-[13%]"
:class="{
'fm-train--run': phase === 'running',
'fm-train--end': phase === 'met',
}"
@animationend.self="onTrainAnimationEnd($event)"
>
<div :class="phase === 'running' ? 'fm-train-bump' : ''">
<svg
class="w-full drop-shadow-[0_6px_2px_rgb(15,23,42,0.15),0_8px_24px_rgba(14,116,144,0.2)]"
viewBox="0 0 220 80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<defs>
<linearGradient id="fm-train-body" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#f0f9ff" />
<stop offset="50%" stop-color="#bae6fd" />
<stop offset="100%" stop-color="#7dd3fc" />
</linearGradient>
</defs>
<path
d="M6 42 L58 30 L182 30 L202 38 L212 48 L212 62 L6 62 Z"
fill="url(#fm-train-body)"
stroke="#0c4a6e"
stroke-width="1.7"
/>
<path d="M182 30 L202 38 L202 56 L182 56 Z" fill="#38bdf8" stroke="#0c4a6e" stroke-width="1.35" />
<rect x="26" y="40" width="30" height="16" rx="1.5" fill="#0ea5e9" stroke="#075985" stroke-width="1" />
<rect x="62" y="40" width="30" height="16" rx="1.5" fill="#0ea5e9" stroke="#075985" stroke-width="1" />
<rect x="98" y="40" width="30" height="16" rx="1.5" fill="#0ea5e9" stroke="#075985" stroke-width="1" />
<path d="M34 44h18M70 44h18M106 44h18" stroke="#e0f2fe" stroke-width="1.2" opacity="0.55" />
<circle cx="198" cy="46" r="3" fill="#fef08a" stroke="#ca8a04" stroke-width="0.8" />
<path d="M188 48 L206 48" stroke="#0c4a6e" stroke-width="1.3" stroke-linecap="round" />
</svg>
</div>
</div>
<button
type="button"
class="group absolute bottom-1.5 left-2 z-[6] max-w-[min(230px,60vw)] overflow-hidden rounded-lg border border-rose-900/30 bg-gradient-to-br from-[#fffdfa] via-rose-50 to-amber-50 py-2.5 pl-4 pr-3 text-left shadow-[5px_6px_0_rgb(136,19,55,0.18),0_0_0_1px_rgba(255,255,255,0.8)_inset] ring-1 ring-rose-200/50 transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-400 focus-visible:ring-offset-2 disabled:cursor-default sm:bottom-3 sm:left-4"
:class="[
ticketMotionClass,
phase === 'idle'
? 'rotate-[-3deg] hover:-translate-y-1 hover:rotate-[-1deg] hover:shadow-[8px_10px_0_rgb(136,19,55,0.22)] active:translate-y-0'
: 'rotate-[-2deg]',
]"
:disabled="phase !== 'idle'"
:aria-label="phase === 'idle' ? '点击发车西安开往成都' : phase === 'running' ? '列车行驶中' : '已相遇'"
@click="startJourney"
>
<div class="pointer-events-none absolute -left-2 top-1/2 h-4 w-4 -translate-y-1/2 rounded-full bg-amber-100 shadow-[inset_1px_0_3px_rgba(0,0,0,0.15)]" aria-hidden="true" />
<div class="pointer-events-none absolute -right-2 top-1/2 h-4 w-4 -translate-y-1/2 rounded-full bg-amber-100 shadow-[inset_-1px_0_3px_rgba(0,0,0,0.15)]" aria-hidden="true" />
<div class="pointer-events-none absolute bottom-0 left-2 top-0 w-px border-l-2 border-dashed border-rose-200/60" aria-hidden="true" />
<div class="relative pl-1.5">
<div class="flex items-end justify-between gap-3 border-b border-rose-200/80 pb-2">
<div class="min-w-0 flex-1">
<p class="font-brush text-[1.4rem] leading-tight text-rose-950 sm:text-2xl">
西安
<span class="mx-1 inline-block -translate-y-1 text-sm text-amber-600/80"></span>
成都
</p>
<p class="mt-1 font-mono text-[9px] font-bold tracking-widest text-rose-800/70">G 心动直达 · D1915车</p>
</div>
</div>
<div class="mt-2 flex items-center justify-between gap-2">
<span class="font-mono text-[13px] font-bold text-slate-500/90">04.30 </span>
<span class="font-mono text-[10px] font-bold text-slate-500/90">0850-1211</span>
<div class="flex h-3 flex-1 items-end justify-end gap-[1.5px] opacity-40 mix-blend-multiply">
<div class="h-full w-1 bg-rose-900" />
<div class="h-[70%] w-[1px] bg-rose-900" />
<div class="h-full w-[2px] bg-rose-900" />
<div class="h-[90%] w-[1.5px] bg-rose-900" />
<div class="h-full w-[1px] bg-rose-900" />
<div class="h-[60%] w-[2.5px] bg-rose-900" />
<div class="h-full w-[1px] bg-rose-900" />
</div>
</div>
<p
v-if="phase === 'idle'"
class="mt-2.5 text-center text-[10px] font-bold tracking-wider text-rose-600 group-hover:text-rose-700"
>
<span class="rounded bg-rose-500/10 px-2 py-0.5">检票发车</span>
</p>
<p v-else-if="phase === 'running'" class="mt-2.5 text-center text-[10px] font-bold tracking-wider text-amber-700">
<span class="inline-block animate-pulse">行驶中...</span>
</p>
<p v-else class="mt-2.5 text-center text-[10px] font-bold tracking-wider text-emerald-700">
已到站
</p>
</div>
</button>
<div
class="pointer-events-none absolute bottom-[28%] left-1/2 z-[5] flex -translate-x-1/2 items-end gap-1 transition-opacity duration-500 sm:gap-2"
:class="phase === 'met' ? 'opacity-100' : 'opacity-0'"
aria-hidden="true"
>
<div
class="relative flex flex-col items-center transition-all duration-700 ease-out"
:class="phase === 'met' ? 'translate-x-0 opacity-100' : '-translate-x-12 opacity-0'"
>
<div class="absolute -bottom-1 left-1/2 h-2 w-12 -translate-x-1/2 rounded-full bg-slate-900/20 blur-[2px]" />
<div class="z-10 h-7 w-7 rounded-full border-[1.5px] border-slate-700 bg-[#fde0d0] shadow-sm" />
<div class="absolute top-[26px] z-20 h-2 w-8 rounded-full border border-slate-700 bg-slate-300 shadow-sm" />
<div class="mt-[-2px] h-12 w-10 rounded-b-xl rounded-t-md border-[1.5px] border-slate-700 bg-gradient-to-b from-sky-200 to-sky-400 shadow-sm" />
</div>
<div
v-show="phase === 'met'"
class="relative z-30 animate-[fmHeart_0.65s_cubic-bezier(0.34,1.56,0.64,1)_both] pb-8 text-3xl drop-shadow-[0_2px_8px_rgba(225,29,72,0.4)] sm:text-4xl"
>
<span class="bg-gradient-to-br from-rose-400 via-rose-500 to-pink-600 bg-clip-text text-transparent"></span>
<span class="fm-sparkle absolute -right-2 -top-2 text-amber-300 opacity-90"></span>
<span class="fm-sparkle-delay absolute -left-3 top-1 text-rose-200 opacity-80"></span>
</div>
<div
class="relative flex flex-col items-center transition-all duration-700 ease-out"
:class="phase === 'met' ? 'translate-x-0 opacity-100' : 'translate-x-12 opacity-0'"
>
<div class="absolute -bottom-1 left-1/2 h-2 w-12 -translate-x-1/2 rounded-full bg-slate-900/20 blur-[2px]" />
<div class="z-10 h-6 w-6 rounded-full border-[1.5px] border-slate-700 bg-[#fde0d0] shadow-sm" />
<div class="absolute top-[22px] z-20 h-2.5 w-9 rounded-full border border-slate-700 bg-amber-200 shadow-sm" />
<div class="mt-[-2px] h-11 w-9 rounded-b-xl rounded-t-md border-[1.5px] border-slate-700 bg-gradient-to-b from-rose-200 to-rose-400 shadow-sm" />
</div>
</div>
<div
class="pointer-events-none absolute inset-x-0 bottom-0 z-0 h-20 bg-gradient-to-t from-amber-200/50 to-transparent"
aria-hidden="true"
/>
</div>
<div class="relative z-[2] flex flex-col items-stretch gap-6 sm:gap-8">
<PolaroidCard :memory="memory" :index="memory.story_order" compact />
<div
class="nepal-torn-panel relative mx-auto max-w-md border border-amber-200/60 bg-gradient-to-b from-[#fffdfa] to-amber-50/40 px-5 py-6 text-left shadow-[0_12px_30px_-15px_rgba(180,83,9,0.2)] sm:px-6 sm:py-7"
>
<div class="absolute -top-3 left-1/2 h-6 w-20 -translate-x-1/2 rotate-[2deg] bg-rose-200/40 shadow-[0_1px_2px_rgba(0,0,0,0.05)] backdrop-blur-sm" />
<div
class="mb-4 h-1 w-12 rounded-full bg-gradient-to-r from-amber-400 to-rose-400/80"
aria-hidden="true"
/>
<div
class="text-[15px] leading-[1.8] tracking-wide text-amber-950/90 [&_a]:font-bold [&_a]:text-rose-600 [&_a]:underline [&_a]:decoration-rose-300 [&_a]:underline-offset-4 [&_blockquote]:border-l-4 [&_blockquote]:border-rose-300/60 [&_blockquote]:bg-rose-50/30 [&_blockquote]:py-1 [&_blockquote]:pl-3 [&_blockquote]:text-rose-900/70 [&_code]:rounded [&_code]:bg-amber-100/50 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_strong]:text-rose-900 [&_p+p]:mt-4"
v-html="bodyHtml"
/>
</div>
</div>
</div>
</template>
<style scoped>
.fm-train-wrap {
left: -40%;
}
.fm-train--run {
animation-name: fm-train-xa-cd;
animation-duration: 2.85s;
animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
animation-fill-mode: forwards;
}
.fm-train--end {
left: 58%;
}
@keyframes fm-train-xa-cd {
0% {
left: -40%;
}
100% {
left: 58%;
}
}
/* 动车行驶中的颠簸感 */
.fm-train-bump {
animation: train-bump 0.15s ease-in-out infinite alternate;
}
@keyframes train-bump {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-1.5px);
}
}
@keyframes fmHeart {
0% {
transform: scale(0) rotate(-12deg) translateY(10px);
opacity: 0;
}
50% {
transform: scale(1.25) rotate(5deg) translateY(-5px);
opacity: 1;
}
100% {
transform: scale(1) rotate(0deg) translateY(0);
opacity: 1;
}
}
.fm-ticket-float {
animation: fm-ticket-bob 4.5s ease-in-out infinite;
}
@keyframes fm-ticket-bob {
0%,
100% {
transform: rotate(-3deg) translateY(0);
}
50% {
transform: rotate(-2deg) translateY(-5px);
}
}
.fm-sun-glow {
animation: fm-sun-sway 6s ease-in-out infinite;
}
@keyframes fm-sun-sway {
0%,
100% {
transform: rotate(0deg);
}
50% {
transform: rotate(2deg);
}
}
.fm-sparkle {
animation: fm-spark 1.2s ease-out 0.4s both;
}
.fm-sparkle-delay {
animation: fm-spark 1s ease-out 0.65s both;
}
@keyframes fm-spark {
0% {
transform: scale(0) rotate(-20deg);
opacity: 0;
}
100% {
transform: scale(1) rotate(0);
opacity: 1;
}
}
/* 撕纸边缘容器样式补充 */
.nepal-torn-panel {
border-radius: 4px 4px 12px 12px;
}
</style>