This commit is contained in:
flowerstonezl
2026-05-19 21:05:31 +08:00
parent 4ae691bfb4
commit a65cb05d5d
15 changed files with 2443 additions and 202 deletions

View File

@@ -7,7 +7,14 @@ export const api = axios.create({
headers: { 'Content-Type': 'application/json' },
})
export type MemoryFeatureType = 'heart_window' | 'fissure_page' | 'standard_nepal'
export type MemoryFeatureType =
| 'heart_window'
| 'fissure_page'
| 'standard_nepal'
| 'first_meet'
| 'first_meet_ju'
| 'first_meet_rui'
| 'first_meet_train'
export type NepalArtStyle =
| 'none'

View File

@@ -0,0 +1,161 @@
<script setup lang="ts">
import { usePreferredReducedMotion } from '@vueuse/core'
import { computed } 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 waveHeights = [18, 34, 24, 42, 28, 36, 20, 31]
</script>
<template>
<div
class="relative mx-auto max-w-lg overflow-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-12 top-12 h-44 w-44 rounded-full bg-cyan-400/15 blur-[3rem]"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute -right-10 bottom-8 h-52 w-52 rounded-full bg-violet-400/14 blur-[3.5rem]"
aria-hidden="true"
/>
<div
class="relative z-[1] mx-auto mb-7 max-w-[min(100%,396px)] overflow-hidden rounded-[2rem] border border-slate-800/85 bg-[#080d18] shadow-[0_26px_60px_-18px_rgba(15,23,42,0.6)]"
>
<div
class="pointer-events-none absolute inset-0 opacity-85"
aria-hidden="true"
style="
background-image:
radial-gradient(circle at 14% 16%, rgb(255 255 255 / 0.75) 0 1px, transparent 1.5px),
radial-gradient(circle at 80% 18%, rgb(255 255 255 / 0.7) 0 1px, transparent 1.5px),
radial-gradient(circle at 68% 40%, rgb(56 189 248 / 0.12) 0 2px, transparent 3px);
background-size: 100px 100px, 120px 120px, 200px 200px;
"
/>
<div class="relative px-5 pb-5 pt-5 sm:px-6 sm:pb-6">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-[10px] font-semibold uppercase tracking-[0.32em] text-cyan-300/68">
First Call
</p>
<h3 class="mt-2 font-display-cn text-[1.9rem] leading-none text-white">
第一次通电话
</h3>
</div>
<div class="rounded-2xl border border-white/10 bg-white/6 px-3 py-2 text-right backdrop-blur-sm">
<p class="text-[10px] font-semibold tracking-[0.2em] text-slate-300/78">
通话时长
</p>
<p class="mt-1 font-mono text-2xl font-bold text-cyan-100">20:00</p>
</div>
</div>
<div class="mt-6 rounded-[1.8rem] border border-white/8 bg-gradient-to-b from-slate-900/95 to-slate-950 px-4 py-5 shadow-inner">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
<div class="flex h-14 w-14 items-center justify-center rounded-full bg-gradient-to-br from-cyan-400 to-blue-600 text-lg font-bold text-white shadow-[0_10px_20px_rgba(14,165,233,0.28)]">
</div>
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.26em] text-cyan-200/65">
Connecting
</p>
<p class="mt-1 text-lg font-medium text-white">很温柔很害羞</p>
</div>
</div>
<div class="flex h-11 w-11 items-center justify-center rounded-full border border-rose-300/25 bg-rose-400/15 text-rose-100 shadow-[0_8px_16px_rgba(244,63,94,0.16)]">
</div>
</div>
<div class="mt-6 rounded-[1.4rem] border border-cyan-400/12 bg-black/18 px-4 py-4">
<div class="flex items-end justify-center gap-2" aria-hidden="true">
<span
v-for="(h, i) in waveHeights"
:key="`${h}-${i}`"
class="call-wave block w-2 rounded-full bg-gradient-to-t from-cyan-500 via-sky-300 to-cyan-100"
:class="reduceMotion === 'reduce' ? '' : `call-wave--${(i % 4) + 1}`"
:style="{ height: `${h}px` }"
/>
</div>
<p class="mt-3 text-center text-xs leading-relaxed text-slate-300/80">
第一次听见你的声音原来文字之外还有更柔软的那一层
</p>
</div>
<div class="mt-5 grid gap-2 sm:grid-cols-3">
<div
v-for="tag in ['很温柔', '很害羞', '想帮到你']"
:key="tag"
class="rounded-2xl border border-white/8 bg-white/6 px-3 py-2 text-center text-[11px] font-medium text-slate-200/86"
>
{{ tag }}
</div>
</div>
</div>
</div>
</div>
<div class="relative z-[1] flex flex-col gap-6 sm:gap-8">
<PolaroidCard :memory="memory" :index="memory.story_order" compact />
<div
class="nepal-torn-panel mx-auto max-w-md border border-slate-200/70 bg-gradient-to-b from-[#fffdfa] to-slate-50/55 px-5 py-6 shadow-[0_16px_36px_-12px_rgba(15,23,42,0.2)] ring-1 ring-white/80 sm:px-6 sm:py-7"
>
<div class="mb-4 h-1 w-14 rounded-full bg-gradient-to-r from-cyan-400 to-violet-400" aria-hidden="true" />
<div
class="text-[15px] leading-[1.82] tracking-wide text-slate-800 [&_a]:font-bold [&_a]:text-cyan-700 [&_a]:underline [&_a]:underline-offset-4 [&_blockquote]:border-l-4 [&_blockquote]:border-violet-300/55 [&_blockquote]:bg-violet-50/45 [&_blockquote]:py-1 [&_blockquote]:pl-3 [&_blockquote]:text-slate-600 [&_code]:rounded [&_code]:bg-slate-100 [&_code]:px-1.5 [&_strong]:text-slate-900 [&_p+p]:mt-4"
v-html="bodyHtml"
/>
</div>
</div>
</div>
</template>
<style scoped>
.call-wave {
transform-origin: center bottom;
}
.call-wave--1 {
animation: call-wave-bounce 1.2s ease-in-out infinite;
}
.call-wave--2 {
animation: call-wave-bounce 1.2s ease-in-out 0.15s infinite;
}
.call-wave--3 {
animation: call-wave-bounce 1.2s ease-in-out 0.3s infinite;
}
.call-wave--4 {
animation: call-wave-bounce 1.2s ease-in-out 0.45s infinite;
}
@keyframes call-wave-bounce {
0%,
100% {
transform: scaleY(0.82);
opacity: 0.7;
}
50% {
transform: scaleY(1.12);
opacity: 1;
}
}
</style>

View File

@@ -0,0 +1,187 @@
<script setup lang="ts">
import { usePreferredReducedMotion } from '@vueuse/core'
import { computed } 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 morseGroups = ['-- .- -.--', '.. / .... . .- .-. -..', '.- .-.. .-.. / -. .. --. .... -']
</script>
<template>
<div
class="relative mx-auto max-w-lg overflow-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-16 top-12 h-48 w-48 rounded-full bg-cyan-400/15 blur-[3.5rem]"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute -right-12 bottom-10 h-52 w-52 rounded-full bg-fuchsia-400/16 blur-[3.5rem]"
aria-hidden="true"
/>
<p
class="relative z-[1] mb-4 flex items-center justify-center gap-3 text-[11px] font-bold tracking-[0.34em] text-slate-500/80"
>
<span class="h-px w-6 bg-gradient-to-r from-transparent to-cyan-400/60" aria-hidden="true" />
深夜漫游
<span class="h-px w-6 bg-gradient-to-l from-transparent to-fuchsia-400/60" aria-hidden="true" />
</p>
<div
class="relative z-[1] mx-auto mb-7 max-w-[min(100%,390px)] overflow-hidden rounded-[2rem] border border-slate-800/80 bg-[#08111f] shadow-[0_25px_60px_-18px_rgba(15,23,42,0.62)]"
>
<div
class="pointer-events-none absolute inset-0 opacity-90"
aria-hidden="true"
style="
background-image:
radial-gradient(circle at 15% 16%, rgb(255 255 255 / 0.8) 0 1.2px, transparent 1.6px),
radial-gradient(circle at 74% 22%, rgb(255 255 255 / 0.75) 0 1.1px, transparent 1.5px),
radial-gradient(circle at 42% 12%, rgb(255 255 255 / 0.7) 0 1px, transparent 1.4px);
background-size: 120px 120px, 150px 150px, 130px 130px;
"
/>
<div class="relative h-[320px] overflow-hidden px-4 pb-4 pt-5 sm:h-[340px] sm:px-5">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-[10px] font-semibold uppercase tracking-[0.32em] text-cyan-300/70">
04.22 / 02:00
</p>
<h3 class="mt-2 font-display-cn text-2xl text-white sm:text-[1.9rem]">
集电院的夜
</h3>
<p class="mt-2 max-w-[14rem] text-sm leading-relaxed text-slate-300/88">
从一张照片开始话题一路延伸到冷暴力摩斯密码还有凌晨两点还舍不得停下的聊天
</p>
</div>
<div
class="rounded-2xl border border-cyan-400/20 bg-slate-900/75 px-3 py-2 text-right shadow-[0_8px_24px_rgba(34,211,238,0.12)] backdrop-blur-md"
>
<p class="text-[9px] font-semibold uppercase tracking-[0.28em] text-cyan-300/70">
Campus Log
</p>
<p class="mt-1 text-lg font-brush text-cyan-50">JDY</p>
</div>
</div>
<div class="relative mt-8 h-[168px] overflow-hidden rounded-[1.6rem] border border-slate-700/70 bg-gradient-to-b from-slate-900 via-slate-950 to-[#02050b] shadow-inner">
<div class="pointer-events-none absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-cyan-400/10 to-transparent" aria-hidden="true" />
<div
class="pointer-events-none absolute left-1/2 top-0 h-full w-[1px] bg-gradient-to-b from-transparent via-cyan-300/16 to-transparent"
aria-hidden="true"
/>
<div class="absolute bottom-0 left-0 right-0 flex items-end justify-between px-5 pb-6">
<div class="relative h-20 w-20 rounded-t-[2rem] border border-cyan-200/18 bg-cyan-300/8">
<div class="absolute inset-x-2 bottom-0 top-4 rounded-t-[1.4rem] border border-cyan-100/12 bg-cyan-200/6" />
</div>
<div class="relative h-28 w-24 rounded-t-[2.2rem] border border-cyan-200/22 bg-cyan-300/10">
<div class="absolute inset-x-3 bottom-0 top-4 rounded-t-[1.7rem] border border-cyan-100/12 bg-cyan-200/6" />
<div class="absolute left-1/2 top-7 h-8 w-8 -translate-x-1/2 rounded-full border border-cyan-200/25 bg-cyan-200/8" />
</div>
<div class="relative h-24 w-20 rounded-t-[2rem] border border-cyan-200/18 bg-cyan-300/8">
<div class="absolute inset-x-2 bottom-0 top-5 rounded-t-[1.4rem] border border-cyan-100/12 bg-cyan-200/6" />
</div>
</div>
<div
class="absolute bottom-6 left-6 rounded-2xl border border-cyan-400/18 bg-slate-900/78 px-3 py-2.5 text-cyan-50 shadow-[0_10px_25px_rgba(8,145,178,0.16)] backdrop-blur-md"
>
<p class="text-[10px] font-semibold tracking-[0.18em] text-cyan-300/75">你拍给我的照片</p>
<p class="mt-1 font-brush text-xl">集电院</p>
</div>
<div
class="absolute right-4 top-4 max-w-[11rem] rounded-2xl rounded-tr-sm border border-fuchsia-300/16 bg-fuchsia-200/10 px-3 py-2 text-xs leading-relaxed text-slate-100 shadow-[0_8px_18px_rgba(168,85,247,0.16)] backdrop-blur-md"
:class="reduceMotion === 'reduce' ? '' : 'campus-bubble-rise'"
>
原来你最怕的是冷暴力
</div>
<div class="absolute inset-x-4 bottom-3 flex gap-2" aria-hidden="true">
<span
v-for="tag in ['冷暴力', '摩斯密码', '凌晨 2 点']"
:key="tag"
class="rounded-full border border-white/10 bg-white/8 px-2.5 py-1 text-[10px] font-medium text-slate-200/88"
>
{{ tag }}
</span>
</div>
</div>
<div class="mt-4 grid gap-2 sm:grid-cols-3">
<div
v-for="line in morseGroups"
:key="line"
class="rounded-2xl border border-white/10 bg-white/8 px-3 py-2 font-mono text-[10px] tracking-[0.22em] text-slate-300/85"
:class="reduceMotion === 'reduce' ? '' : 'campus-morse-glow'"
>
{{ line }}
</div>
</div>
</div>
</div>
<div class="relative z-[1] flex flex-col gap-6 sm:gap-8">
<PolaroidCard :memory="memory" :index="memory.story_order" compact />
<div
class="nepal-torn-panel mx-auto max-w-md border border-slate-200/70 bg-gradient-to-b from-[#fffdfa] to-slate-50/55 px-5 py-6 shadow-[0_16px_36px_-12px_rgba(15,23,42,0.18)] ring-1 ring-white/85 sm:px-6 sm:py-7"
>
<div class="mb-4 flex items-center gap-2">
<span class="h-1.5 w-1.5 rounded-full bg-cyan-400" />
<span class="h-1.5 w-8 rounded-full bg-gradient-to-r from-cyan-400 to-fuchsia-400" />
</div>
<div
class="text-[15px] leading-[1.82] tracking-wide text-slate-800 [&_a]:font-bold [&_a]:text-fuchsia-600 [&_a]:underline [&_a]:underline-offset-4 [&_blockquote]:border-l-4 [&_blockquote]:border-cyan-300/60 [&_blockquote]:bg-cyan-50/40 [&_blockquote]:py-1 [&_blockquote]:pl-3 [&_blockquote]:text-slate-600 [&_code]:rounded [&_code]:bg-slate-100 [&_code]:px-1.5 [&_strong]:text-slate-900 [&_p+p]:mt-4"
v-html="bodyHtml"
/>
</div>
</div>
</div>
</template>
<style scoped>
.campus-bubble-rise {
animation: campus-bubble-rise 0.65s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes campus-bubble-rise {
0% {
opacity: 0;
transform: translateY(18px) scale(0.92);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.campus-morse-glow {
animation: campus-morse-glow 4.2s ease-in-out infinite;
}
@keyframes campus-morse-glow {
0%,
100% {
box-shadow: 0 0 0 rgb(34 211 238 / 0);
}
50% {
box-shadow: 0 0 18px rgb(34 211 238 / 0.08);
}
}
</style>

View File

@@ -0,0 +1,342 @@
<script setup lang="ts">
import { usePreferredReducedMotion } from '@vueuse/core'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import type { Memory } from '../api/client'
import { getStandardNepalTheme } from '../constants/nepalThemes'
import PolaroidCard from './PolaroidCard.vue'
import { renderMarkdown } from '../utils/renderMarkdown'
/** 初见场景:对方发来的第一条抖音私信(文案可改此处) */
const DM_BODY =
'您好,刷到您的抖音,想问一下成电读博很辛苦吗?我男朋友博一,一天忙的只回不超过五条信息,课题组真的压力很大事情很多吗?谢谢[流泪]'
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()
/** typing → shown */
const revealPhase = ref<'typing' | 'shown'>('typing')
let revealTimer: ReturnType<typeof setTimeout> | undefined
function scheduleReveal() {
if (revealTimer) clearTimeout(revealTimer)
if (reduceMotion.value === 'reduce') {
revealPhase.value = 'shown'
return
}
revealPhase.value = 'typing'
revealTimer = setTimeout(() => {
revealPhase.value = 'shown'
revealTimer = undefined
}, 2200) // 稍微延长一点输入时间,增加期待感
}
watch(reduceMotion, scheduleReveal, { immediate: true })
onBeforeUnmount(() => {
if (revealTimer) clearTimeout(revealTimer)
})
const capMotionClass = computed(() =>
reduceMotion.value === 'reduce' ? '' : 'fm-cap-bob',
)
const tasselMotionClass = computed(() =>
reduceMotion.value === 'reduce' ? '' : 'fm-tassel-sway',
)
const gridMotionClass = computed(() =>
reduceMotion.value === 'reduce' ? '' : 'fm-uestc-grid',
)
/** 将 [流泪] 显示为表情,其余保持原文 */
const dmDisplayParts = computed(() => {
const s = DM_BODY
const idx = s.indexOf('[流泪]')
if (idx === -1) return [{ type: 'text' as const, text: s }]
return [
{ type: 'text' as const, text: s.slice(0, idx) },
{ type: 'tear' as const },
{ type: 'text' as const, text: s.slice(idx + '[流泪]'.length) },
]
})
</script>
<template>
<div
class="relative mx-auto max-w-lg overflow-x-hidden rounded-3xl bg-gradient-to-br px-3 pb-8 pt-5 sm:px-5 sm:pb-10 sm:pt-8"
:class="theme.gradient"
>
<div
class="pointer-events-none absolute -right-10 top-[15%] h-56 w-56 rounded-full bg-cyan-400/20 blur-[3rem]"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute -left-12 bottom-[25%] h-48 w-48 rounded-full bg-fuchsia-500/15 blur-[3rem]"
aria-hidden="true"
/>
<p
class="relative z-[1] mb-4 flex items-center justify-center gap-3 text-[11px] font-bold uppercase tracking-[0.35em] text-slate-500/80"
>
<span class="h-px w-6 bg-gradient-to-r from-transparent to-slate-400/50" aria-hidden="true" />
初见 · 屏幕那头
<span class="h-px w-6 bg-gradient-to-l from-transparent to-slate-400/50" aria-hidden="true" />
</p>
<div class="relative z-[1] mx-auto mb-8 max-w-[min(100%,370px)]">
<div
class="overflow-hidden rounded-[2.5rem] border-[6px] border-[#1e293b] bg-black p-[2px] shadow-[0_25px_50px_-12px_rgba(15,23,42,0.6),inset_0_2px_8px_rgba(255,255,255,0.15)] ring-1 ring-slate-800/80"
>
<div class="relative overflow-hidden rounded-[2.2rem] bg-slate-950">
<div
class="relative flex h-10 items-center justify-between px-5 text-[11px] font-semibold text-slate-100"
aria-hidden="true"
>
<span class="w-12 tracking-wider">9:41</span>
<div class="absolute left-1/2 top-2 h-5 w-24 -translate-x-1/2 rounded-full bg-black shadow-[inset_0_-1px_2px_rgba(255,255,255,0.15)]">
<div class="absolute right-2.5 top-1/2 h-2.5 w-2.5 -translate-y-1/2 rounded-full bg-[#0a0a0a] shadow-[inset_0_1px_1px_rgba(255,255,255,0.1)]"></div>
</div>
<div class="flex w-12 justify-end gap-1.5">
<svg class="h-3 w-4 opacity-90" viewBox="0 0 16 12" fill="currentColor"><path d="M1 9h2V5H1v4zm3 0h2V3H4v6zm3 0h2V1H7v8zm3 0h2V7h-2v2zm3-4v4h2V5h-2z"/></svg>
<svg class="h-3 w-5 opacity-90" viewBox="0 0 20 12" fill="currentColor"><rect x="1" y="2" width="16" height="8" rx="2" stroke="currentColor" fill="none"/><rect x="2" y="3" width="12" height="6" rx="1"/><path d="M18 5v2a1 1 0 000-2z"/></svg>
</div>
</div>
<header
class="flex items-center justify-between border-b border-slate-800/80 bg-slate-950/80 px-4 pb-3 pt-1 backdrop-blur-md"
>
<span class="flex h-6 w-6 items-center justify-center rounded-full text-lg text-slate-300 hover:bg-slate-800" aria-hidden="true"></span>
<span class="text-[15px] font-bold tracking-wide text-slate-100">私信</span>
<span class="flex h-6 w-6 items-center justify-center gap-[3px]" aria-hidden="true">
<span class="h-1.5 w-1.5 rounded-full bg-slate-400" />
<span class="h-1.5 w-1.5 rounded-full bg-slate-400" />
<span class="h-1.5 w-1.5 rounded-full bg-slate-400" />
</span>
</header>
<div
class="pointer-events-none absolute right-1 top-20 z-20 sm:right-3 sm:top-24"
:class="capMotionClass"
aria-hidden="true"
>
<svg
class="h-16 w-16 drop-shadow-[0_8px_16px_rgba(0,0,0,0.6)] sm:h-[4.5rem] sm:w-[4.5rem]"
viewBox="0 0 80 88"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M40 8 L72 22 L40 36 L8 22 Z" fill="#0f172a" stroke="#334155" stroke-width="1.5" />
<path d="M8 22 L40 36 L40 52 L8 38 Z" fill="#1e293b" stroke="#475569" stroke-width="1.2" />
<path d="M72 22 L40 36 L40 52 L72 38 Z" fill="#334155" stroke="#64748b" stroke-width="1.2" />
<circle cx="40" cy="22" r="3.5" fill="#f59e0b" />
<g :class="tasselMotionClass" style="transform-origin: 56px 28px">
<path
d="M56 28 Q62 48 58 68"
stroke="#fbbf24"
stroke-width="2.5"
stroke-linecap="round"
fill="none"
/>
<circle cx="58" cy="70" r="4.5" fill="#f59e0b" stroke="#b45309" stroke-width="1" />
</g>
</svg>
</div>
<div class="relative min-h-[300px] bg-gradient-to-b from-slate-900 to-[#080c14] px-4 pb-6 pt-4">
<div
class="relative mb-5 overflow-hidden rounded-2xl border border-cyan-500/30 bg-slate-900/90 px-3.5 py-3 shadow-[0_4px_12px_rgba(6,182,212,0.1),inset_0_1px_0_rgba(255,255,255,0.1)]"
>
<div
class="uestc-circuit-grid pointer-events-none absolute inset-0 opacity-30 mix-blend-screen"
:class="gridMotionClass"
aria-hidden="true"
/>
<div class="relative flex items-center gap-3">
<div
class="flex h-11 w-11 shrink-0 flex-col items-center justify-center rounded-xl bg-gradient-to-br from-cyan-500 via-cyan-600 to-blue-900 text-[10px] font-black leading-tight text-white shadow-lg ring-1 ring-cyan-300/50"
>
<span class="tracking-widest">UESTC</span>
<span class="mt-0.5 h-[2px] w-5 rounded-full bg-cyan-200/60"></span>
</div>
<div class="min-w-0">
<p class="font-brush text-[1.1rem] leading-tight text-cyan-50 drop-shadow-sm">电子科技大学</p>
<p class="mt-1 flex items-center gap-1.5 text-[9px] font-bold tracking-[0.15em] text-cyan-400/80">
<span class="h-1.5 w-1.5 rounded-full bg-cyan-400 animate-pulse"></span>
清水河校区
</p>
</div>
</div>
</div>
<div class="flex gap-3">
<div class="relative h-10 w-10 shrink-0">
<div class="absolute inset-0 animate-ping rounded-full bg-rose-500/30 opacity-75"></div>
<div
class="relative flex h-full w-full items-center justify-center rounded-full bg-gradient-to-br from-rose-400 to-fuchsia-600 text-[13px] font-bold text-white shadow-[0_2px_8px_rgba(225,29,72,0.4)] ring-2 ring-white/10"
aria-hidden="true"
>
</div>
</div>
<div class="min-w-0 flex-1 pt-0.5">
<p class="mb-1.5 pl-1 text-[11px] font-medium text-slate-400">陌生人消息</p>
<div
v-if="revealPhase === 'typing'"
class="relative inline-flex items-center gap-1.5 rounded-2xl rounded-tl-sm border border-slate-700/60 bg-slate-800/90 px-4 py-3 shadow-md backdrop-blur-sm"
aria-live="polite"
>
<span class="fm-dot h-1.5 w-1.5 rounded-full bg-slate-300" />
<span class="fm-dot fm-dot-d1 h-1.5 w-1.5 rounded-full bg-slate-300" />
<span class="fm-dot fm-dot-d2 h-1.5 w-1.5 rounded-full bg-slate-300" />
</div>
<div
v-else
class="relative rounded-2xl rounded-tl-sm border border-slate-600/40 bg-slate-800/95 px-4 py-3.5 text-[14px] leading-[1.7] text-slate-100 shadow-[0_4px_15px_rgba(0,0,0,0.3)]"
:class="reduceMotion === 'reduce' ? '' : 'fm-bubble-in'"
>
<template v-for="(part, i) in dmDisplayParts" :key="i">
<span v-if="part.type === 'text'" class="tracking-wide">{{ part.text }}</span>
<span v-else class="inline-block px-1 text-xl leading-none translate-y-0.5" title="流泪">😭</span>
</template>
</div>
<p class="mt-2.5 pl-1 text-[10px] text-slate-500/80 font-medium tracking-wide">
对方已通过视频推荐发起会话
</p>
</div>
</div>
</div>
<div class="absolute bottom-2 left-1/2 h-1 w-1/3 -translate-x-1/2 rounded-full bg-slate-600/50"></div>
</div>
</div>
</div>
<div class="relative z-[1] flex flex-col gap-6 sm:gap-8">
<PolaroidCard :memory="memory" :index="memory.story_order" compact />
<div
class="nepal-torn-panel mx-auto max-w-md border border-slate-200/70 bg-gradient-to-b from-[#fffdfa] to-slate-50/50 px-5 py-6 shadow-[0_15px_35px_-10px_rgba(30,41,59,0.15)] ring-1 ring-white/80 sm:px-6 sm:py-7"
>
<div
class="mb-4 h-1 w-12 rounded-full bg-gradient-to-r from-cyan-400 to-fuchsia-400"
aria-hidden="true"
/>
<div
class="text-[15px] leading-[1.8] tracking-wide text-slate-800 [&_a]:font-bold [&_a]:text-fuchsia-600 [&_a]:underline [&_a]:decoration-fuchsia-300 [&_a]:underline-offset-4 [&_blockquote]:border-l-4 [&_blockquote]:border-fuchsia-300/60 [&_blockquote]:bg-fuchsia-50/40 [&_blockquote]:py-1 [&_blockquote]:pl-3 [&_blockquote]:text-slate-600 [&_code]:rounded [&_code]:bg-slate-100 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_strong]:text-slate-900 [&_p+p]:mt-4"
v-html="bodyHtml"
/>
</div>
</div>
</div>
</template>
<style scoped>
.uestc-circuit-grid {
background-image:
linear-gradient(90deg, rgba(34, 211, 238, 0.4) 1px, transparent 1px),
linear-gradient(rgba(34, 211, 238, 0.4) 1px, transparent 1px),
radial-gradient(circle at center, rgba(34, 211, 238, 0.8) 1.5px, transparent 1.5px);
background-size: 14px 14px, 14px 14px, 28px 28px;
background-position: 0 0, 0 0, 7px 7px;
}
.fm-cap-bob {
animation: fm-cap-bob 4s ease-in-out infinite;
}
@keyframes fm-cap-bob {
0%,
100% {
transform: translateY(0) rotate(-3deg);
}
50% {
transform: translateY(-6px) rotate(3deg);
}
}
.fm-tassel-sway {
animation: fm-tassel-sway 2.2s ease-in-out infinite;
}
@keyframes fm-tassel-sway {
0%,
100% {
transform: rotate(-5deg);
}
50% {
transform: rotate(8deg);
}
}
.fm-uestc-grid {
animation: fm-uestc-grid 20s linear infinite;
}
@keyframes fm-uestc-grid {
0% {
background-position: 0 0, 0 0, 7px 7px;
}
100% {
background-position: 140px 140px, 140px 140px, 147px 147px;
}
}
.fm-dot {
animation: fm-dot 1.2s ease-in-out infinite;
}
.fm-dot-d1 {
animation-delay: 0.2s;
}
.fm-dot-d2 {
animation-delay: 0.4s;
}
@keyframes fm-dot {
0%,
80%,
100% {
transform: translateY(0);
opacity: 0.3;
}
40% {
transform: translateY(-4px);
opacity: 1;
}
}
.fm-bubble-in {
animation: fm-bubble-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
transform-origin: top left;
}
@keyframes fm-bubble-in {
0% {
opacity: 0;
transform: translateY(15px) scale(0.9);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* 撕纸边缘容器补充 */
.nepal-torn-panel {
border-radius: 4px 4px 16px 16px;
}
</style>

View File

@@ -0,0 +1,442 @@
<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>

View File

@@ -2,14 +2,31 @@
import { usePointerSwipe, usePreferredReducedMotion } from '@vueuse/core'
import { computed, ref } from 'vue'
import type { Memory } from '../api/client'
import PolaroidCard from './PolaroidCard.vue'
import { renderMarkdown } from '../utils/renderMarkdown'
defineProps<{
const props = defineProps<{
memory: Memory
}>()
const stageRef = ref<HTMLElement | null>(null)
const split = ref(0)
const reduceMotion = usePreferredReducedMotion()
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
const hurtFacts = [
'我把你和前任之间的事情告诉了别人',
'本该被保护的隐私,被我轻率地说了出去',
'于是别人开始对你评头论足',
]
const judgmentClouds = [
'那些不属于他们的评价',
'被误读、被讨论、被围观',
'我知道这件事最先伤到的是你',
]
const apologyTags = ['越界了', '在认错', '也在解释']
usePointerSwipe(stageRef, {
disableTextSelect: true,
@@ -23,90 +40,349 @@ usePointerSwipe(stageRef, {
},
})
const rippleShift = computed(() => `${split.value * 14}px`)
const isOpen = computed(() => split.value > 0.35)
function toggleSplit() {
split.value = split.value > 0.35 ? 0 : 0.85
split.value = isOpen.value ? 0 : 0.85
}
const splitHint = computed(() =>
isOpen.value
? '裂缝拉开后,露出那封迟来的长信。'
: '左右轻扫,把我没说完的道歉从裂缝里拉出来。',
)
const fracturePulseClass = computed(() => {
if (reduceMotion.value === 'reduce' || isOpen.value) return ''
return 'fissure-fracture-pulse'
})
</script>
<template>
<div class="mx-auto max-w-lg px-2 py-6 sm:px-4">
<div
class="relative mx-auto max-w-lg overflow-hidden rounded-3xl bg-gradient-to-br from-[#2b1118] via-[#5f2430] to-[#f1dfd5] px-3 py-7 text-white sm:px-5 sm:py-9"
>
<div
class="pointer-events-none absolute -left-20 top-0 h-56 w-56 rounded-full bg-rose-400/28 blur-3xl"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute -right-16 bottom-0 h-48 w-48 rounded-full bg-amber-200/22 blur-3xl"
aria-hidden="true"
/>
<header class="relative z-[1] mb-5 text-center">
<p
class="mb-2 text-[10px] font-semibold uppercase tracking-[0.32em] text-white/60"
>
裂缝 / 失言 / 道歉信
</p>
<h2
v-if="memory.event_title"
class="font-brush text-2xl text-white sm:text-[2rem]"
>
{{ memory.event_title }}
</h2>
<p class="mx-auto mt-3 max-w-sm text-sm leading-relaxed text-white/78">
这次裂开的不只是情绪而是我把不该说出去的事说给了别人听
</p>
</header>
<div
ref="stageRef"
class="relative mx-auto min-h-[320px] max-w-md touch-none select-none overflow-hidden rounded-2xl border-[3px] border-slate-900/80 bg-white shadow-[8px_8px_0_rgb(15,23,42,0.55)] ring-2 ring-fuchsia-200/40"
class="relative z-[1] mx-auto min-h-[390px] max-w-md touch-none select-none overflow-hidden rounded-[1.55rem] border border-white/15 bg-[#140c12] shadow-[0_24px_70px_-18px_rgba(0,0,0,0.58)] ring-1 ring-white/8 sm:min-h-[430px]"
style="touch-action: none"
role="application"
:aria-label="`吵架页裂缝左右滑动拉开当前${isOpen ? '已拉开' : '合上'}`"
>
<div class="absolute inset-0 flex">
<div
class="relative flex-1 overflow-hidden bg-lokta-cream/90"
:class="reduceMotion === 'reduce' ? '' : 'transition-transform duration-500 ease-out'"
:style="{ transform: `translateX(-${split * 28}%)` }"
<div class="pointer-events-none absolute inset-0 nepal-fiber opacity-[0.08] mix-blend-screen" />
<div
class="pointer-events-none absolute inset-0"
aria-hidden="true"
style="
background:
radial-gradient(circle at 16% 18%, rgb(255 255 255 / 0.07) 0 2px, transparent 3px),
radial-gradient(circle at 82% 14%, rgb(255 255 255 / 0.06) 0 2px, transparent 3px),
linear-gradient(180deg, rgb(255 255 255 / 0.04), transparent 26%);
background-size: 120px 120px, 140px 140px, 100% 100%;
"
/>
<div
class="absolute inset-[14px] overflow-hidden rounded-[1.25rem] border border-white/10 bg-[linear-gradient(180deg,#f6ebe3_0%,#f0ddd2_100%)]"
>
<article
class="absolute inset-x-[17%] bottom-5 top-5 z-[1] overflow-hidden rounded-[1.25rem] border border-[#d8c2b4] bg-[linear-gradient(180deg,#fffaf5_0%,#f8efe7_100%)] px-4 py-4 text-[#4c342c] shadow-[0_18px_32px_rgba(76,52,44,0.12)] sm:inset-x-[18%] sm:px-5 sm:py-5"
:class="reduceMotion === 'reduce' ? '' : 'fissure-letter-rise'"
>
<div
class="pointer-events-none absolute -right-1/4 inset-y-0 w-[140%] opacity-90"
style="
background: repeating-conic-gradient(
from 0deg at 60% 45%,
#fbcfe8 0deg 14deg,
#fce7f3 14deg 28deg,
#f9a8d4 28deg 42deg,
#fbcfe8 42deg 56deg
);
mask-image: radial-gradient(circle at 70% 40%, black 0%, transparent 68%);
"
/>
<div class="relative z-[1] flex h-full flex-col justify-center px-3 py-8 sm:px-5">
<p
class="text-right font-brush text-lg leading-relaxed text-lokta-deep sm:text-xl"
>
旁人皆三千江水
</p>
<div class="mb-4 flex items-center justify-between gap-3 border-b border-dashed border-[#d4b9aa] pb-3">
<div>
<p class="text-[10px] font-semibold uppercase tracking-[0.28em] text-[#9a6754]">
Long Apology
</p>
<h3 class="mt-1 font-brush text-2xl text-[#7a2f39]">
对不起
</h3>
</div>
<div class="text-right">
<p class="text-[10px] font-semibold tracking-[0.24em] text-[#9a6754]">
我写了很长的小作文
</p>
<div class="mt-2 flex gap-1.5">
<span
v-for="tag in apologyTags"
:key="tag"
class="rounded-full bg-[#f7e3d8] px-2 py-1 text-[10px] font-medium text-[#8d4d40]"
>
{{ tag }}
</span>
</div>
</div>
</div>
</div>
<div
class="relative z-10 w-px shrink-0 bg-gradient-to-b from-transparent via-ink/40 to-transparent shadow-[0_0_12px_rgba(61,52,40,0.35)]"
aria-hidden="true"
/>
<div
class="max-h-full overflow-hidden text-[13px] leading-[1.82] tracking-wide text-[#5c4339] [&_blockquote]:border-l-4 [&_blockquote]:border-[#d48d7e] [&_blockquote]:bg-[#fff2eb] [&_blockquote]:py-1 [&_blockquote]:pl-3 [&_blockquote]:text-[#7b5348] [&_strong]:text-[#732f39] [&_p+p]:mt-3"
v-html="bodyHtml"
/>
<div
class="pointer-events-none absolute -bottom-10 left-1/2 h-28 w-44 -translate-x-1/2 rounded-full bg-[#f5d7c7]/45 blur-2xl"
aria-hidden="true"
/>
</article>
<div class="absolute inset-0 flex">
<!-- 左半 · 说出去的事 -->
<div
class="relative flex-1 overflow-hidden bg-lokta-cream/90"
:class="reduceMotion === 'reduce' ? '' : 'transition-transform duration-500 ease-out'"
:style="{ transform: `translateX(${split * 28}%)` }"
class="relative z-[3] flex-1 overflow-hidden border-r border-white/6 bg-[linear-gradient(180deg,#4b1522_0%,#241017_100%)]"
:class="reduceMotion === 'reduce' ? '' : 'transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]'"
:style="{ transform: `translateX(-${split * 30}%)` }"
>
<div
class="pointer-events-none absolute -left-1/4 inset-y-0 w-[140%] opacity-90"
style="
background: repeating-conic-gradient(
from 180deg at 40% 45%,
#a5f3fc 0deg 16deg,
#cffafe 16deg 32deg,
#22d3ee 32deg 48deg,
#a5f3fc 48deg 64deg
);
mask-image: radial-gradient(circle at 30% 40%, black 0%, transparent 68%);
"
class="pointer-events-none absolute inset-0 opacity-[0.16]"
aria-hidden="true"
style="
background-image:
linear-gradient(130deg, transparent 0 44%, rgb(255 255 255 / 0.07) 44% 46%, transparent 46% 100%),
radial-gradient(circle at 24% 28%, rgb(255 255 255 / 0.08) 0 2px, transparent 3px);
background-size: 100% 100%, 120px 120px;
"
/>
<div class="relative z-[1] flex h-full flex-col justify-center px-3 py-8 sm:px-5">
<p class="font-brush text-lg leading-relaxed text-lokta-deep sm:text-xl">
<span>唯你是心上</span>
<span
class="relative z-20 inline-block -mx-1 font-brush text-2xl text-rose-deep sm:text-3xl"
:style="{ transform: `translateX(${rippleShift})` }"
>涟漪</span>
</p>
<div
class="pointer-events-none absolute inset-y-0 right-0 w-10 bg-gradient-to-l from-black/35 to-transparent"
/>
<div
class="relative z-[1] flex h-full min-h-[360px] flex-col justify-between px-4 py-5 sm:px-5 sm:py-6"
>
<div>
<p class="text-[10px] font-semibold uppercase tracking-[0.28em] text-rose-200/62">
The Mistake
</p>
<h3 class="mt-2 max-w-[10rem] font-display-cn text-[1.6rem] leading-tight text-white">
我说了
<span class="text-rose-200">不该说的事</span>
</h3>
</div>
<div class="space-y-2.5">
<div
v-for="fact in hurtFacts"
:key="fact"
class="rounded-[1.15rem] border border-white/10 bg-white/8 px-3 py-3 text-sm leading-relaxed text-white/88 shadow-[0_10px_20px_rgba(0,0,0,0.16)]"
>
{{ fact }}
</div>
</div>
<div class="rounded-[1.2rem] border border-rose-200/12 bg-black/18 px-3 py-3 text-[11px] leading-relaxed text-white/68">
那些原本只属于你的过去不该被我拿出去换成别人的一句两句评价
</div>
</div>
</div>
<!-- 裂缝光带 -->
<div
class="relative z-20 flex w-[8px] shrink-0 flex-col items-center justify-stretch py-5"
:class="fracturePulseClass"
aria-hidden="true"
>
<div
class="pointer-events-none absolute inset-y-4 -left-3 -right-3 rounded-full bg-gradient-to-b from-transparent via-rose-400/25 to-transparent blur-lg"
/>
<div
class="pointer-events-none absolute inset-y-6 -left-2 -right-2 rounded-full bg-gradient-to-b from-transparent via-orange-200/24 to-transparent blur-md"
/>
<div
class="relative h-full w-px flex-1 rounded-full bg-gradient-to-b from-transparent via-white/85 to-transparent shadow-[0_0_12px_rgba(255,255,255,0.3)]"
/>
</div>
<!-- 右半 · 别人的目光 -->
<div
class="relative z-[3] flex-1 overflow-hidden border-l border-white/6 bg-[linear-gradient(180deg,#2a1317_0%,#120d10_100%)]"
:class="reduceMotion === 'reduce' ? '' : 'transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]'"
:style="{ transform: `translateX(${split * 30}%)` }"
>
<div
class="pointer-events-none absolute inset-0 opacity-[0.16]"
aria-hidden="true"
style="
background-image:
linear-gradient(230deg, transparent 0 44%, rgb(255 255 255 / 0.07) 44% 46%, transparent 46% 100%),
radial-gradient(circle at 80% 30%, rgb(255 255 255 / 0.08) 0 2px, transparent 3px);
background-size: 100% 100%, 120px 120px;
"
/>
<div
class="pointer-events-none absolute inset-y-0 left-0 w-10 bg-gradient-to-r from-black/35 to-transparent"
/>
<div
class="relative z-[1] flex h-full min-h-[360px] flex-col justify-between px-4 py-5 sm:px-5 sm:py-6"
>
<div class="text-right">
<p class="text-[10px] font-semibold uppercase tracking-[0.28em] text-orange-100/60">
Their Voices
</p>
<h3 class="mt-2 ml-auto max-w-[10rem] font-display-cn text-[1.6rem] leading-tight text-white">
不是你的错
<span class="text-orange-100">却落在你身上</span>
</h3>
</div>
<div class="space-y-2.5">
<div
v-for="line in judgmentClouds"
:key="line"
class="rounded-[1.15rem] border border-white/10 bg-white/8 px-3 py-3 text-sm leading-relaxed text-white/82 shadow-[0_10px_20px_rgba(0,0,0,0.16)] backdrop-blur-sm"
:class="reduceMotion === 'reduce' ? '' : 'fissure-cloud-drift'"
>
{{ line }}
</div>
</div>
<div class="rounded-[1.2rem] border border-orange-100/12 bg-black/18 px-3 py-3 text-[11px] leading-relaxed text-white/68">
我后来才反应过来我最该做的是护着你而不是让你被别人看见议论定义
</div>
</div>
</div>
</div>
</div>
</div>
<button
type="button"
class="mx-auto mt-2 block rounded-full border border-paper-shadow bg-white/90 px-4 py-2 text-xs font-medium text-ink shadow-sm transition hover:bg-paper-deep"
@click="toggleSplit"
<section
class="relative z-[1] mt-6 grid items-center gap-4 md:grid-cols-[minmax(0,248px)_1fr]"
>
{{ split > 0.35 ? '让裂缝合上' : '轻轻拉开' }}
</button>
<PolaroidCard :memory="memory" :index="memory.story_order" compact />
<div
class="rounded-[1.45rem] border border-white/14 bg-white/8 px-4 py-4 text-white shadow-[0_14px_32px_rgba(0,0,0,0.18)] backdrop-blur-md sm:px-5"
>
<p class="text-[10px] font-semibold uppercase tracking-[0.3em] text-white/55">
Photo Evidence
</p>
<h3 class="mt-2 font-display-cn text-[1.35rem] leading-tight text-white">
把那天也留在这一页
</h3>
<p class="mt-3 text-sm leading-relaxed text-white/78">
这张照片不只是记录当时的场景也提醒我这一页里有真实发生过的伤害也有我认真写下的道歉
</p>
<div class="mt-4 flex flex-wrap gap-2">
<span
class="rounded-full border border-white/12 bg-black/14 px-3 py-1.5 text-[11px] font-medium text-white/78"
>
争执那天
</span>
<span
class="rounded-full border border-white/12 bg-black/14 px-3 py-1.5 text-[11px] font-medium text-white/78"
>
真实现场
</span>
<span
class="rounded-full border border-white/12 bg-black/14 px-3 py-1.5 text-[11px] font-medium text-white/78"
>
道歉也被记住
</span>
</div>
</div>
</section>
<div class="relative z-[1] mt-5 flex flex-col items-center gap-2 sm:mt-6">
<button
type="button"
class="rounded-full border border-white/18 bg-white/12 px-5 py-2.5 text-xs font-semibold text-white shadow-[0_12px_30px_rgba(0,0,0,0.2)] backdrop-blur-md transition hover:-translate-y-px hover:bg-white/16 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/40 focus-visible:ring-offset-2 focus-visible:ring-offset-[#5f2430]"
:aria-pressed="isOpen"
@click="toggleSplit"
>
{{ isOpen ? '先把裂缝合上' : '拉开那封道歉信' }}
</button>
<p class="max-w-xs text-center text-[11px] leading-relaxed text-white/70">
{{ splitHint }}
</p>
</div>
<div class="relative z-[1] mt-6 grid gap-3 sm:grid-cols-3">
<div
class="rounded-[1.3rem] border border-white/12 bg-white/8 px-4 py-3 text-center text-sm leading-relaxed text-white/82 backdrop-blur-sm"
>
隐私被说出去了
</div>
<div
class="rounded-[1.3rem] border border-white/12 bg-white/8 px-4 py-3 text-center text-sm leading-relaxed text-white/82 backdrop-blur-sm"
>
别人开始评头论足
</div>
<div
class="rounded-[1.3rem] border border-white/12 bg-white/8 px-4 py-3 text-center text-sm leading-relaxed text-white/82 backdrop-blur-sm"
>
我写了很长的解释和道歉
</div>
</div>
</div>
</template>
<style scoped>
.fissure-fracture-pulse {
animation: fissure-fracture-pulse 2.8s ease-in-out infinite;
}
@keyframes fissure-fracture-pulse {
0%,
100% {
opacity: 1;
filter: brightness(1);
}
50% {
opacity: 0.88;
filter: brightness(1.12);
}
}
.fissure-letter-rise {
animation: fissure-letter-rise 0.75s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes fissure-letter-rise {
0% {
opacity: 0.75;
transform: translateY(12px) scale(0.98);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.fissure-cloud-drift {
animation: fissure-cloud-drift 5.8s ease-in-out infinite;
}
@keyframes fissure-cloud-drift {
0%,
100% {
transform: translateX(0);
}
50% {
transform: translateX(-4px);
}
}
</style>

View File

@@ -1,25 +1,68 @@
<!-- 终章心窗背后柔色漫画夜空 + 零星花瓣线稿 -->
<template>
<div
class="pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-2xl bg-gradient-to-b from-violet-100/50 via-pink-50/40 to-lokta-cream/60"
class="pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-[1.25rem] bg-gradient-to-b from-[#f8dfe8] via-[#fef3ef] to-[#eef9f8]"
aria-hidden="true"
>
<svg class="absolute inset-0 h-full w-full opacity-70" viewBox="0 0 400 280" xmlns="http://www.w3.org/2000/svg">
<circle cx="300" cy="48" r="22" fill="#fef3c7" stroke="#b45309" stroke-width="1.2" />
<div class="absolute inset-x-0 top-0 h-16 bg-gradient-to-b from-white/35 to-transparent" />
<svg class="absolute inset-0 h-full w-full opacity-85" viewBox="0 0 400 280" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="hwMoon" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#fff8cf" />
<stop offset="100%" stop-color="#ffd6a5" />
</linearGradient>
<linearGradient id="hwHill" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#f2b7c7" />
<stop offset="100%" stop-color="#9fd7de" />
</linearGradient>
</defs>
<circle cx="298" cy="46" r="24" fill="url(#hwMoon)" stroke="#c97b5a" stroke-width="1.1" />
<circle cx="80" cy="40" r="2" fill="#ffffff" opacity="0.82" />
<circle cx="106" cy="62" r="1.8" fill="#ffffff" opacity="0.76" />
<circle cx="336" cy="82" r="1.9" fill="#ffffff" opacity="0.75" />
<circle cx="356" cy="48" r="1.5" fill="#ffffff" opacity="0.65" />
<circle cx="48" cy="86" r="1.5" fill="#ffffff" opacity="0.7" />
<path
d="M80 200 Q120 160 160 200 Q120 220 80 200"
fill="none"
stroke="#db2777"
stroke-width="1.2"
opacity="0.45"
d="M0 206 C52 176 110 180 162 204 C216 228 292 228 400 190 L400 280 L0 280 Z"
fill="url(#hwHill)"
opacity="0.85"
/>
<path
d="M260 220 Q300 180 340 220 Q300 240 260 220"
d="M0 224 C72 194 126 196 188 220 C248 244 316 240 400 210"
fill="none"
stroke="#ffffff"
stroke-width="1.1"
opacity="0.38"
/>
<path
d="M76 186 Q114 154 152 186 Q114 208 76 186"
fill="none"
stroke="#db2777"
stroke-width="1.1"
opacity="0.42"
/>
<path
d="M250 198 Q290 166 330 198 Q290 220 250 198"
fill="none"
stroke="#7c3aed"
stroke-width="1.1"
opacity="0.4"
/>
<path
d="M206 84 C214 68 236 68 244 84 C252 100 239 114 225 126 C211 114 198 100 206 84 Z"
fill="#f8a1b5"
opacity="0.55"
/>
<path d="M54 154 C62 146 76 146 84 154" fill="none" stroke="#ffffff" stroke-width="1.2" opacity="0.5" />
<path d="M318 148 C326 140 340 140 348 148" fill="none" stroke="#ffffff" stroke-width="1.2" opacity="0.48" />
</svg>
<div
class="absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-white/16 via-white/6 to-transparent"
aria-hidden="true"
/>
</div>
</template>

View File

@@ -3,6 +3,7 @@ import { usePointerSwipe, usePreferredReducedMotion } from '@vueuse/core'
import { computed, ref } from 'vue'
import type { Memory } from '../api/client'
import HeartMangaBackdrop from './HeartMangaBackdrop.vue'
import PolaroidCard from './PolaroidCard.vue'
import { renderMarkdown } from '../utils/renderMarkdown'
const props = defineProps<{
@@ -26,152 +27,229 @@ usePointerSwipe(stageRef, {
})
const openDeg = computed(() => openAmount.value * 78)
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
const isOpen = computed(() => openAmount.value > 0.4)
const statusText = computed(() =>
isOpen.value ? '心窗已经被推开,光会落进来。' : '如果不开心,就把这一页轻轻打开。',
)
const finaleTags = ['不开心时', '想说话时', '只想被陪着时']
function toggleDoors() {
openAmount.value = openAmount.value > 0.4 ? 0 : 0.92
openAmount.value = isOpen.value ? 0 : 0.92
}
</script>
<template>
<div class="mx-auto max-w-lg px-3 py-6 sm:px-4">
<p
class="text-center font-brush text-lg leading-snug text-lokta-deep sm:text-xl"
>
不开<span class="font-hand text-rose-deep"></span>就打开<span
class="text-lokta-teal"
>心墙</span>
</p>
<p
class="mt-2 text-center font-hand text-sm text-ink-muted sm:text-base"
>
我会在你身边 · <span class="font-[family-name:var(--font-hand)] text-rose-deep">open</span>
</p>
<div
class="relative mx-auto max-w-lg overflow-hidden rounded-[2rem] bg-gradient-to-br from-[#fff5ef] via-[#fde8ef] to-[#eef8f8] px-3 py-6 sm:px-4 sm:py-7"
>
<div
class="pointer-events-none absolute -left-16 top-4 h-44 w-44 rounded-full bg-rose-300/24 blur-[3rem]"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute -right-10 bottom-6 h-52 w-52 rounded-full bg-cyan-300/20 blur-[3.5rem]"
aria-hidden="true"
/>
<header class="relative z-[1] text-center">
<p class="text-[10px] font-semibold uppercase tracking-[0.34em] text-lokta-deep/48">
Finale / Open When You Need Me
</p>
<h2 class="mt-3 font-brush text-[2rem] leading-none text-lokta-deep sm:text-[2.35rem]">
心墙与窗 · 终章
</h2>
<p class="mx-auto mt-3 max-w-sm text-sm leading-relaxed text-ink-muted sm:text-[15px]">
这一页不再讲相遇或争执而是把我会在你身边做成一扇真的能被推开的窗
</p>
</header>
<div
ref="stageRef"
class="relative mx-auto mt-8 max-w-md touch-none select-none [perspective:1200px]"
class="relative z-[1] mx-auto mt-8 max-w-md touch-none select-none [perspective:1200px]"
style="touch-action: none"
>
<div
class="relative flex h-[min(52vw,280px)] max-h-[300px] min-h-[220px] items-stretch justify-center overflow-visible rounded-2xl border-[3px] border-white/90 shadow-[6px_6px_0_rgb(30,41,59,0.18)] ring-2 ring-rose-200/50"
class="mb-3 flex items-center justify-between gap-3 rounded-full border border-white/60 bg-white/72 px-4 py-2 text-[11px] text-ink-muted shadow-[0_10px_25px_rgba(30,53,80,0.08)] backdrop-blur-md"
>
<HeartMangaBackdrop />
<div
class="pointer-events-none relative z-[3] flex h-full w-1/2 origin-left rounded-l-2xl border border-rose-soft/40 bg-gradient-to-br from-white via-lokta-cream to-lokta-blush/30 shadow-inner will-change-transform"
:class="reduceMotion === 'reduce' ? '' : 'transition-transform duration-300 ease-out'"
:style="{
transform: `rotateY(${-openDeg}deg)`,
transformStyle: 'preserve-3d',
}"
/>
<div
class="pointer-events-none relative z-[3] flex h-full w-1/2 origin-right rounded-r-2xl border border-rose-soft/40 bg-gradient-to-bl from-white via-lokta-cream to-lokta-teal/25 shadow-inner will-change-transform"
:class="reduceMotion === 'reduce' ? '' : 'transition-transform duration-300 ease-out'"
:style="{
transform: `rotateY(${openDeg}deg)`,
transformStyle: 'preserve-3d',
}"
/>
<div
class="pointer-events-none absolute inset-2 z-[2] flex flex-col items-center justify-center rounded-xl bg-paper-deep/90 p-3 text-center text-sm text-ink-muted shadow-inner"
>
<div
class="max-w-none text-left text-xs leading-relaxed text-ink [&_p]:my-1 sm:text-sm"
v-html="bodyHtml"
/>
</div>
<span class="font-semibold tracking-[0.18em] text-lokta-deep/70">心窗机关</span>
<span>{{ statusText }}</span>
</div>
<button
type="button"
class="mx-auto mt-3 block rounded-full border border-paper-shadow bg-white/90 px-4 py-2 text-xs font-medium text-ink shadow-sm transition hover:bg-paper-deep"
@click="toggleDoors"
>
{{ openAmount > 0.4 ? '合上心窗' : '推开一点' }}
</button>
</div>
<div
class="relative mx-auto mt-10 max-w-md rounded-2xl border-4 border-transparent bg-origin-border p-[2px] shadow-[0_16px_40px_-12px_rgba(30,53,80,0.35)]"
style="
background-image: linear-gradient(#f4e8dc, #e8d4c4),
repeating-linear-gradient(
0deg,
#8f3a36,
#8f3a36 10px,
#b84a42 10px,
#b84a42 20px
);
background-origin: border-box;
background-clip: padding-box, border-box;
"
>
<div
class="rounded-xl bg-gradient-to-b from-lokta-cream to-paper-deep/90 p-4 nepal-fiber"
class="relative flex min-h-[285px] items-stretch justify-center overflow-visible rounded-[1.8rem] border border-[#b97d87]/26 bg-[linear-gradient(180deg,#e7cfc5_0%,#dcc0b4_100%)] p-3 shadow-[0_24px_60px_-20px_rgba(115,64,79,0.35)] ring-1 ring-white/70 sm:min-h-[320px]"
>
<p class="text-center text-xs font-medium tracking-widest text-ink-muted">
藏宝格 · 情书券插槽
</p>
<div
class="relative mx-auto mt-3 h-24 w-[88%] max-w-xs rounded-md border-2 border-dashed border-rose-soft/50 bg-black/5 shadow-inner"
class="pointer-events-none absolute inset-3 rounded-[1.35rem] bg-[linear-gradient(180deg,rgba(255,255,255,0.42),rgba(255,255,255,0)_22%,rgba(138,85,104,0.08)_100%)]"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute left-1/2 top-3 h-4 w-24 -translate-x-1/2 rounded-full bg-white/40 blur-md"
aria-hidden="true"
/>
<div
class="relative flex h-[260px] w-full items-stretch justify-center overflow-hidden rounded-[1.45rem] border-[3px] border-white/75 bg-[#f5e4d8] shadow-[inset_0_1px_0_rgba(255,255,255,0.8),0_8px_18px_rgba(115,64,79,0.12)] sm:h-[292px]"
>
<HeartMangaBackdrop />
<div
class="absolute inset-x-3 bottom-1 top-2 flex items-center justify-center overflow-hidden rounded-sm bg-gradient-to-br from-fuchsia-100 via-pink-200 to-rose-200 shadow-md ring-1 ring-fuchsia-400/40"
class="pointer-events-none absolute inset-[0.6rem] z-[2] rounded-[1.05rem] border border-white/40 bg-gradient-to-b from-white/55 via-transparent to-black/5"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute inset-x-[13%] bottom-[1.1rem] top-[1.1rem] z-[2] overflow-hidden rounded-[1.2rem] border border-[#dfc3b8] bg-[linear-gradient(180deg,#fffaf6_0%,#f7ede7_100%)] p-4 text-center shadow-[0_15px_35px_rgba(124,74,89,0.12)] sm:inset-x-[15%] sm:p-5"
>
<svg
class="h-full w-full opacity-95"
viewBox="0 0 320 140"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<defs>
<pattern
id="h0"
width="18"
height="18"
patternUnits="userSpaceOnUse"
>
<circle cx="3" cy="3" r="1.2" fill="#be185d" opacity="0.35" />
<circle cx="12" cy="11" r="1" fill="#9d174d" opacity="0.3" />
</pattern>
</defs>
<rect width="320" height="140" fill="url(#h0)" />
<text
x="160"
y="42"
text-anchor="middle"
fill="#831843"
font-size="16"
font-family="system-ui, sans-serif"
<div class="mx-auto mb-3 h-1 w-14 rounded-full bg-gradient-to-r from-rose-300 to-cyan-300" />
<p class="text-[10px] font-semibold uppercase tracking-[0.28em] text-[#9b6f67]">
Open When You Need Me
</p>
<div
class="mt-3 text-left text-[13px] leading-[1.82] text-[#5a4339] [&_blockquote]:border-l-4 [&_blockquote]:border-[#d28f86] [&_blockquote]:bg-[#fff2ec] [&_blockquote]:py-1 [&_blockquote]:pl-3 [&_blockquote]:text-[#7a5b50] [&_em]:text-[#9a3d4a] [&_strong]:text-[#7b3240] [&_p+p]:mt-3 sm:text-[14px]"
v-html="bodyHtml"
/>
</div>
<div
class="pointer-events-none absolute inset-y-[1.2rem] left-[50%] z-[2] w-px -translate-x-1/2 bg-gradient-to-b from-transparent via-white/65 to-transparent"
aria-hidden="true"
/>
<div
class="pointer-events-none relative z-[3] flex h-full w-1/2 origin-left flex-col justify-between rounded-l-[1.2rem] border border-[#b35f6f]/22 bg-[linear-gradient(135deg,rgba(255,255,255,0.94)_0%,rgba(247,231,225,0.96)_36%,rgba(241,188,207,0.46)_100%)] p-4 shadow-[inset_-10px_0_20px_rgba(154,61,74,0.08)] will-change-transform"
:class="reduceMotion === 'reduce' ? '' : 'transition-transform duration-300 ease-out'"
:style="{
transform: `rotateY(${-openDeg}deg)`,
transformStyle: 'preserve-3d',
}"
>
<div class="flex items-start justify-between">
<span class="rounded-full bg-white/72 px-2.5 py-1 text-[10px] font-semibold tracking-[0.22em] text-rose-deep/70">
心墙
</span>
<span class="text-lg text-rose-deep/55"></span>
</div>
<div class="flex flex-1 items-center justify-center">
<div
class="grid h-28 w-24 place-items-center rounded-t-[45%] rounded-b-[1rem] border border-white/75 bg-white/36 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.4)]"
>
Love 520 · 情书券非流通
</text>
<text
x="160"
y="78"
text-anchor="middle"
fill="#9f1239"
font-size="28"
font-weight="700"
font-family="Georgia, serif"
<div class="grid h-20 w-16 place-items-center rounded-t-[45%] rounded-b-[0.8rem] border border-rose-200/70 bg-gradient-to-b from-white/72 to-rose-50/55">
<span class="text-3xl text-rose-deep/70"></span>
</div>
</div>
</div>
<div class="flex justify-end">
<span class="rounded-full border border-white/70 bg-white/55 px-2.5 py-1 text-[10px] text-rose-deep/75">
推开一点
</span>
</div>
</div>
<div
class="pointer-events-none relative z-[3] flex h-full w-1/2 origin-right flex-col justify-between rounded-r-[1.2rem] border border-[#72b9c7]/22 bg-[linear-gradient(225deg,rgba(255,255,255,0.94)_0%,rgba(239,246,244,0.96)_36%,rgba(168,228,235,0.48)_100%)] p-4 shadow-[inset_10px_0_20px_rgba(78,196,212,0.08)] will-change-transform"
:class="reduceMotion === 'reduce' ? '' : 'transition-transform duration-300 ease-out'"
:style="{
transform: `rotateY(${openDeg}deg)`,
transformStyle: 'preserve-3d',
}"
>
<div class="flex items-start justify-between">
<span class="text-lg text-lokta-teal/65"></span>
<span class="rounded-full bg-white/72 px-2.5 py-1 text-[10px] font-semibold tracking-[0.22em] text-lokta-deep/65">
心窗
</span>
</div>
<div class="flex flex-1 items-center justify-center">
<div
class="grid h-28 w-24 place-items-center rounded-t-[45%] rounded-b-[1rem] border border-white/75 bg-white/36 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.4)]"
>
壹佰份喜欢
</text>
<text
x="160"
y="108"
text-anchor="middle"
fill="#881337"
font-size="11"
>
仅供纪念 · 人民银行不存在于本宇宙
</text>
</svg>
<div class="grid h-20 w-16 place-items-center rounded-t-[45%] rounded-b-[0.8rem] border border-cyan-200/70 bg-gradient-to-b from-white/72 to-cyan-50/55">
<span class="text-3xl text-lokta-teal/70"></span>
</div>
</div>
</div>
<div class="flex justify-start">
<span class="rounded-full border border-white/70 bg-white/55 px-2.5 py-1 text-[10px] text-lokta-deep/72">
我在这里
</span>
</div>
</div>
</div>
</div>
<div class="mt-4 flex flex-col items-center gap-2">
<button
type="button"
class="rounded-full border border-white/70 bg-white/86 px-5 py-2.5 text-xs font-medium text-lokta-deep shadow-[0_10px_25px_rgba(30,53,80,0.1)] transition hover:-translate-y-px hover:bg-white"
@click="toggleDoors"
>
{{ isOpen ? '轻轻合上心窗' : '把心窗推开一点' }}
</button>
<p class="text-center text-[11px] leading-relaxed text-ink-muted">
左右轻扫也可以开合这一扇窗
</p>
</div>
</div>
<section class="relative z-[1] mt-8 grid gap-5 md:grid-cols-[minmax(0,250px)_1fr] md:items-start">
<PolaroidCard :memory="memory" :index="memory.story_order" compact />
<div class="space-y-4">
<div
class="rounded-[1.5rem] border border-white/65 bg-white/82 px-4 py-4 text-ink shadow-[0_14px_32px_rgba(30,53,80,0.08)] backdrop-blur-md sm:px-5"
>
<p class="text-[10px] font-semibold uppercase tracking-[0.3em] text-lokta-deep/50">
Promise Window
</p>
<h3 class="mt-2 font-display-cn text-[1.45rem] leading-tight text-lokta-deep">
你需要我的时候
<span class="text-rose-deep">这一页永远能打开</span>
</h3>
<p class="mt-3 text-sm leading-relaxed text-ink-muted">
终章不是结束而是留下一种使用方式难过的时候想说话的时候什么都不想解释的时候都可以来敲这扇窗
</p>
<div class="mt-4 flex flex-wrap gap-2">
<span
v-for="tag in finaleTags"
:key="tag"
class="rounded-full border border-rose-100 bg-rose-50/72 px-3 py-1.5 text-[11px] font-medium text-rose-deep/88"
>
{{ tag }}
</span>
</div>
</div>
<div
class="relative rounded-[1.55rem] border border-[#cc9097]/30 bg-[linear-gradient(135deg,#fffdf9_0%,#fff4f0_46%,#f2fbfb_100%)] p-3 shadow-[0_18px_42px_-16px_rgba(154,61,74,0.24)]"
>
<div class="absolute left-1/2 top-2 h-5 w-24 -translate-x-1/2 rotate-[1.5deg] bg-rose-200/35 shadow-sm" aria-hidden="true" />
<div class="rounded-[1.2rem] border border-white/75 bg-white/76 p-4">
<p class="text-center text-[10px] font-semibold uppercase tracking-[0.3em] text-rose-deep/52">
Keepsake Ticket
</p>
<div
class="relative mx-auto mt-3 overflow-hidden rounded-[1.2rem] border border-rose-200/55 bg-gradient-to-br from-[#fff1f4] via-[#ffe0ea] to-[#fff5df] px-4 py-4 shadow-[0_12px_24px_rgba(190,80,120,0.12)]"
>
<div class="pointer-events-none absolute inset-y-0 left-3 w-px border-l-2 border-dashed border-rose-300/55" aria-hidden="true" />
<div class="pointer-events-none absolute -left-2 top-1/2 h-4 w-4 -translate-y-1/2 rounded-full bg-white shadow-[inset_1px_0_3px_rgba(0,0,0,0.08)]" 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-white shadow-[inset_-1px_0_3px_rgba(0,0,0,0.08)]" aria-hidden="true" />
<p class="pl-3 text-[10px] font-semibold uppercase tracking-[0.26em] text-rose-deep/58">
Love 520 · 非流通纪念券
</p>
<p class="mt-2 pl-3 font-brush text-[2rem] leading-none text-rose-deep sm:text-[2.2rem]">
壹佰份喜欢
</p>
<p class="mt-2 pl-3 text-sm leading-relaxed text-[#7d5d54]">
使用规则情绪低落时可无限次兑换陪伴拥抱安静地在身边
</p>
<p class="mt-3 pl-3 text-[11px] tracking-[0.16em] text-[#9d6b63]">
open when you need me
</p>
</div>
</div>
</div>
</div>
</section>
</div>
</template>

View File

@@ -0,0 +1,149 @@
<script setup lang="ts">
import { usePreferredReducedMotion } from '@vueuse/core'
import { computed } 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()
</script>
<template>
<div
class="relative mx-auto max-w-lg overflow-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-12 top-10 h-48 w-48 rounded-full bg-amber-300/24 blur-[3rem]"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute -right-10 bottom-8 h-56 w-56 rounded-full bg-orange-300/18 blur-[3.8rem]"
aria-hidden="true"
/>
<div
class="relative z-[1] mx-auto mb-7 max-w-[min(100%,398px)] overflow-hidden rounded-[2rem] border border-amber-200/65 bg-[linear-gradient(180deg,#fff9ef_0%,#fff0dc_100%)] shadow-[0_24px_56px_-18px_rgba(217,119,6,0.24)]"
>
<div class="pointer-events-none absolute inset-0 nepal-fiber opacity-[0.14] mix-blend-multiply" />
<div class="relative px-5 pb-5 pt-5 sm:px-6 sm:pb-6">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-[10px] font-semibold uppercase tracking-[0.3em] text-amber-700/68">
Tiny Home / 05.08
</p>
<h3 class="mt-2 font-display-cn text-[1.9rem] leading-none text-amber-950">
一起租了房
</h3>
</div>
<div
class="rounded-2xl border border-amber-200/70 bg-white/88 px-3 py-2 text-right shadow-[0_8px_18px_rgba(245,158,11,0.14)]"
>
<p class="text-[9px] font-semibold tracking-[0.22em] text-amber-700/70">
新家标签
</p>
<p class="mt-1 font-brush text-xl text-amber-950">小宇宙</p>
</div>
</div>
<div class="mt-6 rounded-[1.8rem] border border-amber-200/60 bg-white/72 p-4 shadow-inner">
<div class="relative h-[228px] overflow-hidden rounded-[1.5rem] bg-gradient-to-b from-[#fff6df] via-[#fff1d0] to-[#f4dcb5]">
<div class="absolute inset-y-0 right-0 w-[42%] bg-gradient-to-l from-amber-100/85 to-transparent" aria-hidden="true" />
<div
class="window-beam absolute right-[18%] top-0 h-full w-20 -skew-x-[18deg] bg-gradient-to-b from-white/70 via-amber-100/45 to-transparent blur-sm"
:class="reduceMotion === 'reduce' ? '' : 'window-beam--move'"
aria-hidden="true"
/>
<div class="absolute left-6 top-7 flex gap-4">
<div class="h-20 w-20 rounded-[1.4rem] border border-amber-300/55 bg-white/70 shadow-[0_10px_20px_rgba(180,83,9,0.08)]" />
<div class="mt-5 h-14 w-20 rounded-[1.2rem] border border-amber-300/55 bg-white/65 shadow-[0_10px_20px_rgba(180,83,9,0.08)]" />
</div>
<div class="absolute bottom-0 left-0 right-0 h-10 bg-gradient-to-t from-amber-300/30 to-transparent" aria-hidden="true" />
<div class="absolute bottom-4 left-5 right-5">
<div class="flex items-end justify-between gap-5">
<div class="rounded-[1.3rem] border border-amber-300/60 bg-white/78 px-4 py-4 shadow-[0_14px_26px_rgba(180,83,9,0.08)]">
<p class="text-[10px] font-semibold uppercase tracking-[0.24em] text-amber-700/65">
Balcony
</p>
<p class="mt-2 max-w-[9rem] text-sm leading-relaxed text-amber-950/88">
阳台晒进来的光像新生活按下了开始键
</p>
</div>
<div class="relative flex shrink-0 flex-col items-center">
<div class="h-9 w-1 rounded-full bg-amber-700/55" aria-hidden="true" />
<div class="relative flex h-20 w-20 items-center justify-center rounded-full bg-[#2d8cff] shadow-[0_12px_22px_rgba(45,140,255,0.25)] ring-4 ring-white/65">
<div class="absolute bottom-4 h-7 w-7 rounded-full bg-[#f7d34b] shadow-inner ring-2 ring-[#c99113]" />
<div class="absolute top-5 h-7 w-11 rounded-full bg-white" />
<div class="absolute top-8 h-7 w-12 rounded-b-[1rem] rounded-t-[0.5rem] bg-white" />
<div class="absolute left-5 top-[2.15rem] h-1.5 w-1.5 rounded-full bg-slate-900" />
<div class="absolute right-5 top-[2.15rem] h-1.5 w-1.5 rounded-full bg-slate-900" />
</div>
<p class="mt-2 text-[10px] font-medium tracking-[0.2em] text-blue-700/75">
钥匙圈
</p>
</div>
</div>
</div>
</div>
<div class="mt-4 flex flex-wrap justify-center gap-2">
<span
v-for="tag in ['钥匙圈', '阳台的光', '共同生活']"
:key="tag"
class="rounded-full border border-amber-200 bg-white/88 px-3 py-1.5 text-[11px] font-medium text-amber-900/80"
>
{{ tag }}
</span>
</div>
</div>
</div>
</div>
<div class="relative z-[1] flex flex-col gap-6 sm:gap-8">
<PolaroidCard :memory="memory" :index="memory.story_order" compact />
<div
class="nepal-torn-panel mx-auto max-w-md border border-amber-200/70 bg-gradient-to-b from-[#fffefa] to-amber-50/65 px-5 py-6 shadow-[0_16px_36px_-12px_rgba(217,119,6,0.18)] ring-1 ring-white/80 sm:px-6 sm:py-7"
>
<div class="mb-4 h-1 w-14 rounded-full bg-gradient-to-r from-amber-400 to-orange-400" aria-hidden="true" />
<div
class="text-[15px] leading-[1.82] tracking-wide text-amber-950/88 [&_a]:font-bold [&_a]:text-orange-700 [&_a]:underline [&_a]:underline-offset-4 [&_blockquote]:border-l-4 [&_blockquote]:border-amber-300/60 [&_blockquote]:bg-amber-50/55 [&_blockquote]:py-1 [&_blockquote]:pl-3 [&_blockquote]:text-amber-900/72 [&_code]:rounded [&_code]:bg-amber-100/50 [&_code]:px-1.5 [&_strong]:text-amber-950 [&_p+p]:mt-4"
v-html="bodyHtml"
/>
</div>
</div>
</div>
</template>
<style scoped>
.window-beam {
opacity: 0.8;
}
.window-beam--move {
animation: window-beam-move 6s ease-in-out infinite;
}
@keyframes window-beam-move {
0%,
100% {
transform: translateX(-8px) skewX(-18deg);
opacity: 0.55;
}
50% {
transform: translateX(10px) skewX(-18deg);
opacity: 0.88;
}
}
</style>

View File

@@ -0,0 +1,145 @@
<script setup lang="ts">
import { usePreferredReducedMotion } from '@vueuse/core'
import { computed } 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()
</script>
<template>
<div
class="relative mx-auto max-w-lg overflow-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-12 top-12 h-48 w-48 rounded-full bg-blue-300/20 blur-[3rem]"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute -right-10 bottom-8 h-56 w-56 rounded-full bg-amber-300/16 blur-[3.8rem]"
aria-hidden="true"
/>
<div
class="relative z-[1] mx-auto mb-7 max-w-[min(100%,398px)] overflow-hidden rounded-[2rem] border border-indigo-200/60 bg-[linear-gradient(180deg,#f7faff_0%,#eef4ff_100%)] shadow-[0_24px_56px_-18px_rgba(79,70,229,0.22)]"
>
<div class="pointer-events-none absolute inset-0 nepal-fiber opacity-[0.12] mix-blend-multiply" />
<div class="relative px-5 pb-5 pt-5 sm:px-6 sm:pb-6">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-[10px] font-semibold uppercase tracking-[0.3em] text-indigo-700/68">
Blue Box Date / 05.10
</p>
<h3 class="mt-2 font-display-cn text-[1.9rem] leading-none text-indigo-950">
逛宜家的小日子
</h3>
</div>
<div
class="rounded-2xl border border-indigo-200/70 bg-white/90 px-3 py-2 text-right shadow-[0_8px_20px_rgba(79,70,229,0.12)]"
>
<p class="text-[9px] font-semibold tracking-[0.22em] text-indigo-700/70">
Showroom Mood
</p>
<p class="mt-1 font-brush text-xl text-indigo-950">Blue + Yellow</p>
</div>
</div>
<div class="mt-6 rounded-[1.8rem] border border-indigo-200/60 bg-white/72 p-4 shadow-inner">
<div class="relative overflow-hidden rounded-[1.5rem] bg-gradient-to-b from-[#eaf3ff] via-[#ddeaff] to-[#cfe1ff] px-4 pb-5 pt-5">
<div class="absolute inset-x-0 top-0 h-12 bg-[#0c5adb]" aria-hidden="true" />
<div class="absolute right-5 top-4 rounded-full bg-[#f8d548] px-3 py-1 text-[10px] font-black tracking-[0.24em] text-[#0c4ca9]">
SALE
</div>
<div class="mt-10 grid gap-4 sm:grid-cols-[1fr_0.95fr] sm:items-end">
<div class="rounded-[1.4rem] border border-white/70 bg-white/80 p-4 shadow-[0_12px_24px_rgba(12,90,219,0.12)]">
<div class="flex items-center justify-between gap-4">
<div class="relative h-20 w-12 rounded-t-full bg-[#ffe083]">
<div class="absolute left-1/2 top-[-0.4rem] h-4 w-4 -translate-x-1/2 rounded-full border-2 border-[#0c4ca9] bg-white" />
<div class="absolute bottom-0 left-1/2 h-12 w-1.5 -translate-x-1/2 bg-[#0c4ca9]" />
<div class="absolute bottom-0 left-1/2 h-4 w-8 -translate-x-1/2 rounded-full bg-[#0c4ca9]" />
</div>
<div class="flex-1 rounded-[1.2rem] bg-[#f6f9ff] px-3 py-3">
<p class="text-[10px] font-semibold uppercase tracking-[0.24em] text-indigo-700/65">
Cart List
</p>
<div class="mt-2 flex flex-wrap gap-2 text-[11px] text-indigo-950/88">
<span class="rounded-full bg-[#dbeafe] px-2.5 py-1">台灯</span>
<span class="rounded-full bg-[#dbeafe] px-2.5 py-1">地毯</span>
<span class="rounded-full bg-[#dbeafe] px-2.5 py-1">热狗</span>
</div>
</div>
</div>
</div>
<div
class="relative mx-auto h-32 w-[180px]"
:class="reduceMotion === 'reduce' ? '' : 'ikea-cart-bob'"
>
<div class="absolute bottom-3 left-4 h-14 w-24 rounded-[1.2rem] border-[3px] border-[#0c4ca9] bg-white/90" />
<div class="absolute bottom-11 left-10 h-8 w-14 rotate-[-12deg] rounded-lg bg-[#f8d548] shadow-[0_8px_16px_rgba(248,213,72,0.3)]" />
<div class="absolute bottom-12 left-20 h-6 w-10 rounded-full bg-[#ff7a59] shadow-[0_8px_16px_rgba(255,122,89,0.22)]" />
<div class="absolute bottom-[4.6rem] right-8 h-6 w-8 rounded-full bg-[#f5f5f5] shadow-sm" />
<div class="absolute bottom-[3.2rem] right-7 h-10 w-10 rounded-b-full rounded-t-[0.7rem] bg-[#0c5adb]" />
<div class="absolute bottom-3 left-2 h-4 w-4 rounded-full border-[3px] border-[#0c4ca9] bg-white" />
<div class="absolute bottom-3 left-24 h-4 w-4 rounded-full border-[3px] border-[#0c4ca9] bg-white" />
<div class="absolute bottom-[4.35rem] left-[6.2rem] h-2 w-12 rounded-full bg-[#0c4ca9]" />
<div class="absolute bottom-[5.1rem] left-[7rem] h-8 w-1.5 rounded-full bg-[#0c4ca9]" />
</div>
</div>
</div>
<div class="mt-4 flex flex-wrap justify-center gap-2">
<span
v-for="tag in ['推车里的碎片', '未来会再提起', '一起逛生活']"
:key="tag"
class="rounded-full border border-indigo-200 bg-white/88 px-3 py-1.5 text-[11px] font-medium text-indigo-900/82"
>
{{ tag }}
</span>
</div>
</div>
</div>
</div>
<div class="relative z-[1] flex flex-col gap-6 sm:gap-8">
<PolaroidCard :memory="memory" :index="memory.story_order" compact />
<div
class="nepal-torn-panel mx-auto max-w-md border border-indigo-200/70 bg-gradient-to-b from-[#fffefa] to-indigo-50/55 px-5 py-6 shadow-[0_16px_36px_-12px_rgba(79,70,229,0.16)] ring-1 ring-white/80 sm:px-6 sm:py-7"
>
<div class="mb-4 h-1 w-14 rounded-full bg-gradient-to-r from-indigo-500 to-amber-400" aria-hidden="true" />
<div
class="text-[15px] leading-[1.82] tracking-wide text-indigo-950/88 [&_a]:font-bold [&_a]:text-indigo-700 [&_a]:underline [&_a]:underline-offset-4 [&_blockquote]:border-l-4 [&_blockquote]:border-indigo-300/60 [&_blockquote]:bg-indigo-50/55 [&_blockquote]:py-1 [&_blockquote]:pl-3 [&_blockquote]:text-indigo-900/72 [&_code]:rounded [&_code]:bg-indigo-100/55 [&_code]:px-1.5 [&_strong]:text-indigo-950 [&_p+p]:mt-4"
v-html="bodyHtml"
/>
</div>
</div>
</div>
</template>
<style scoped>
.ikea-cart-bob {
animation: ikea-cart-bob 4.6s ease-in-out infinite;
}
@keyframes ikea-cart-bob {
0%,
100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-5px) rotate(-1deg);
}
}
</style>

View File

@@ -0,0 +1,182 @@
<script setup lang="ts">
import { usePreferredReducedMotion } from '@vueuse/core'
import { computed } 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()
</script>
<template>
<div
class="relative mx-auto max-w-lg overflow-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-10 top-16 h-44 w-44 rounded-full bg-orange-300/20 blur-[3rem]"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute -right-10 bottom-8 h-52 w-52 rounded-full bg-blue-300/16 blur-[3.5rem]"
aria-hidden="true"
/>
<div
class="relative z-[1] mx-auto mb-7 max-w-[min(100%,395px)] overflow-hidden rounded-[2rem] border border-slate-200/65 bg-[linear-gradient(180deg,#fffaf3_0%,#fff3dc_100%)] shadow-[0_24px_55px_-18px_rgba(180,83,9,0.22)]"
>
<div class="pointer-events-none absolute inset-0 nepal-fiber opacity-[0.14] mix-blend-multiply" />
<div class="relative px-5 pb-5 pt-5 sm:px-6 sm:pt-6">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-[10px] font-semibold uppercase tracking-[0.3em] text-orange-700/65">
Orange C / Desk Light
</p>
<h3 class="mt-2 font-display-cn text-[1.9rem] leading-none text-orange-950">
瑞幸咖啡
</h3>
</div>
<div
class="rounded-2xl border border-orange-200/80 bg-white/88 px-3 py-2 text-right shadow-[0_8px_18px_rgba(249,115,22,0.14)]"
>
<p class="text-[9px] font-semibold tracking-[0.22em] text-orange-700/70">
你记得
</p>
<p class="mt-1 font-brush text-xl text-orange-950"> C</p>
</div>
</div>
<div class="mt-6 grid items-end gap-4 sm:grid-cols-[1.08fr_0.92fr]">
<div class="relative h-[230px] rounded-[1.8rem] bg-gradient-to-b from-[#f7efe0] via-[#f2dcc2] to-[#ecd0ad] px-4 pb-4 pt-5 shadow-inner">
<div
class="absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-orange-200/45 to-transparent"
aria-hidden="true"
/>
<div class="absolute left-5 top-3 h-4 w-24 rounded-full bg-white/65 blur-sm" aria-hidden="true" />
<div class="relative mx-auto mt-2 flex w-[128px] flex-col items-center">
<div
class="coffee-steam absolute -top-7 left-4 h-12 w-10 rounded-full bg-white/55 blur-md"
:class="reduceMotion === 'reduce' ? '' : 'coffee-steam--1'"
aria-hidden="true"
/>
<div
class="coffee-steam absolute -top-9 right-4 h-14 w-10 rounded-full bg-white/45 blur-md"
:class="reduceMotion === 'reduce' ? '' : 'coffee-steam--2'"
aria-hidden="true"
/>
<div class="relative h-12 w-[108px] rounded-t-[1.2rem] bg-[#0d6efe]" />
<div class="relative -mt-1 h-[124px] w-[96px] overflow-hidden rounded-b-[1.4rem] rounded-t-[0.7rem] bg-white shadow-[0_10px_18px_rgba(15,23,42,0.12)] ring-1 ring-black/5">
<div class="absolute inset-x-0 top-0 h-8 bg-[#0d6efe]" />
<div class="absolute inset-x-0 top-8 flex justify-center">
<div class="flex h-14 w-14 items-center justify-center rounded-full bg-[#f8fafc] ring-1 ring-slate-200">
<div class="relative h-8 w-8 rounded-full bg-[#0d6efe]">
<div class="absolute right-0 top-1 h-3 w-4 rounded-full bg-orange-400" />
</div>
</div>
</div>
<p class="absolute bottom-5 left-1/2 -translate-x-1/2 font-mono text-[11px] font-bold tracking-[0.28em] text-slate-400">
ORANGE C
</p>
</div>
</div>
<div
class="absolute bottom-5 left-1/2 w-[85%] -translate-x-1/2 rounded-[1.2rem] border border-white/70 bg-white/72 px-3 py-2 shadow-[0_10px_25px_rgba(255,255,255,0.45)] backdrop-blur-sm"
>
<div class="flex flex-wrap justify-center gap-2 text-[10px] font-semibold text-orange-800/80">
<span class="rounded-full bg-orange-100 px-2.5 py-1">咖啡</span>
<span class="rounded-full bg-orange-100 px-2.5 py-1">橙C</span>
<span class="rounded-full bg-orange-100 px-2.5 py-1">灵笼</span>
</div>
</div>
</div>
<div class="space-y-4">
<div
class="rounded-[1.6rem] border border-orange-200/70 bg-white/90 p-4 shadow-[0_12px_26px_rgba(249,115,22,0.12)]"
>
<p class="text-[10px] font-semibold uppercase tracking-[0.26em] text-orange-700/60">
Receipt
</p>
<div class="mt-3 space-y-2.5 text-sm text-slate-700">
<div class="flex items-center justify-between border-b border-dashed border-orange-200 pb-2">
<span>生椰拿铁</span>
<span class="font-mono text-slate-500">x 1</span>
</div>
<div class="flex items-center justify-between border-b border-dashed border-orange-200 pb-2">
<span> C 美式</span>
<span class="font-mono text-slate-500">x 1</span>
</div>
<div class="flex items-center justify-between">
<span>记住你的喜好</span>
<span class="font-brush text-xl text-orange-950">1 </span>
</div>
</div>
</div>
<div
class="relative overflow-hidden rounded-[1.6rem] border border-slate-200/70 bg-slate-900 px-4 py-4 text-slate-100 shadow-[0_15px_30px_rgba(15,23,42,0.18)]"
>
<div class="absolute -right-8 -top-8 h-24 w-24 rounded-full bg-blue-400/20 blur-2xl" aria-hidden="true" />
<p class="text-[10px] font-semibold uppercase tracking-[0.28em] text-blue-200/65">
Desk Corner
</p>
<p class="mt-2 text-sm leading-relaxed text-slate-200/88">
你连桌角的小小灵笼都看见了喜欢有时候不是一句话是被认真注意到
</p>
</div>
</div>
</div>
</div>
</div>
<div class="relative z-[1] flex flex-col gap-6 sm:gap-8">
<PolaroidCard :memory="memory" :index="memory.story_order" compact />
<div
class="nepal-torn-panel mx-auto max-w-md border border-orange-200/70 bg-gradient-to-b from-[#fffefa] to-orange-50/65 px-5 py-6 shadow-[0_16px_36px_-12px_rgba(180,83,9,0.18)] ring-1 ring-white/80 sm:px-6 sm:py-7"
>
<div class="mb-4 h-1 w-14 rounded-full bg-gradient-to-r from-orange-400 to-sky-400" aria-hidden="true" />
<div
class="text-[15px] leading-[1.82] tracking-wide text-orange-950/90 [&_a]:font-bold [&_a]:text-blue-700 [&_a]:underline [&_a]:underline-offset-4 [&_blockquote]:border-l-4 [&_blockquote]:border-orange-300/60 [&_blockquote]:bg-orange-50/55 [&_blockquote]:py-1 [&_blockquote]:pl-3 [&_blockquote]:text-orange-900/70 [&_code]:rounded [&_code]:bg-orange-100/50 [&_code]:px-1.5 [&_strong]:text-orange-950 [&_p+p]:mt-4"
v-html="bodyHtml"
/>
</div>
</div>
</div>
</template>
<style scoped>
.coffee-steam {
opacity: 0.7;
}
.coffee-steam--1 {
animation: coffee-steam-rise 3.6s ease-in-out infinite;
}
.coffee-steam--2 {
animation: coffee-steam-rise 3.6s ease-in-out 0.9s infinite;
}
@keyframes coffee-steam-rise {
0%,
100% {
transform: translateY(0) scale(0.88);
opacity: 0.35;
}
50% {
transform: translateY(-8px) scale(1.06);
opacity: 0.72;
}
}
</style>

View File

@@ -1,14 +1,22 @@
<script setup lang="ts">
import { usePreferredReducedMotion, useSessionStorage } from '@vueuse/core'
import { computed, watch } from 'vue'
import { computed, type Component, watch } from 'vue'
import type { Memory, StatusPayload } from '../api/client'
import {
getStoryNavAccentClass,
getStoryShellClasses,
getStoryTransitionName,
} from '../utils/storyPageTransition'
import FirstMeetDmPage from './FirstMeetDmPage.vue'
import FirstMeetCampusPage from './FirstMeetCampusPage.vue'
import FirstCallPage from './FirstCallPage.vue'
import FirstMeetTrainPage from './FirstMeetTrainPage.vue'
import FissurePage from './FissurePage.vue'
import HeartWindowPage from './HeartWindowPage.vue'
import HomeRentalPage from './HomeRentalPage.vue'
import IkeaDatePage from './IkeaDatePage.vue'
import LuckinCoffeePage from './LuckinCoffeePage.vue'
import ReconcilePage from './ReconcilePage.vue'
import StandardNepalPage from './StandardNepalPage.vue'
const props = defineProps<{
@@ -32,9 +40,34 @@ watch(
const current = computed(() => props.memories[storyIndex.value] ?? null)
const pageComponent = computed(() => {
const t = current.value?.unique_feature_type
const memory = current.value
if (!memory) return StandardNepalPage
const storyPageMap: Record<number, Component> = {
20260420: FirstMeetDmPage,
20260422: FirstMeetCampusPage,
20260423: LuckinCoffeePage,
20260424: FirstCallPage,
20260430: FirstMeetTrainPage,
20260503: FissurePage,
20260504: ReconcilePage,
20260508: HomeRentalPage,
20260510: IkeaDatePage,
20260520: HeartWindowPage,
}
const exactPage = storyPageMap[memory.story_order]
if (exactPage) return exactPage
const t = memory.unique_feature_type
if (t === 'heart_window') return HeartWindowPage
if (t === 'fissure_page') return FissurePage
if (t === 'first_meet') return FirstMeetDmPage
if (t === 'first_meet_ju') return FirstMeetCampusPage
if (t === 'first_meet_rui') {
return memory.event_title.includes('电话') ? FirstCallPage : LuckinCoffeePage
}
if (t === 'first_meet_train') return FirstMeetTrainPage
return StandardNepalPage
})

View File

@@ -7,6 +7,8 @@ import WashiTape from './WashiTape.vue'
const props = defineProps<{
memory: Memory
index: number
/** 插画页用:缩小照片区、隐藏正文摘要,避免与长文抢版面 */
compact?: boolean
}>()
const root = ref<HTMLElement | null>(null)
@@ -136,12 +138,13 @@ onBeforeUnmount(() => {
<template>
<article
ref="root"
class="relative mx-auto max-w-[min(100%,320px)] transition-all duration-700 ease-out"
:class="
class="relative mx-auto transition-all duration-700 ease-out"
:class="[
compact ? 'max-w-[min(100%,260px)]' : 'max-w-[min(100%,320px)]',
visible
? `translate-y-0 opacity-100 ${rotateClass}`
: 'translate-y-6 opacity-0 rotate-0'
"
: 'translate-y-6 opacity-0 rotate-0',
]"
>
<WashiTape
class="-left-2 top-3"
@@ -150,13 +153,21 @@ onBeforeUnmount(() => {
width-class="w-20"
/>
<div
class="nepal-deckle-frame relative bg-white p-3 pb-16 shadow-[0_14px_44px_-10px_rgba(30,53,80,0.28)] ring-1 ring-lokta-deep/10"
class="nepal-deckle-frame relative bg-white shadow-[0_14px_44px_-10px_rgba(30,53,80,0.28)] ring-1 ring-lokta-deep/10"
:class="compact ? 'p-2.5 pb-9' : 'p-3 pb-16'"
>
<div
class="pointer-events-none absolute inset-2 nepal-fiber opacity-[0.18] mix-blend-multiply"
aria-hidden="true"
/>
<div class="relative aspect-[4/5] overflow-visible rounded-sm bg-paper-deep">
<div
class="relative overflow-visible rounded-sm bg-paper-deep"
:class="
compact
? 'mx-auto h-[min(40vw,188px)] w-[min(32vw,150px)] sm:h-[210px] sm:w-[168px]'
: 'aspect-[4/5]'
"
>
<template v-if="imageUrls.length">
<div
v-if="!loadedSet[activeIndex] && imageUrls[activeIndex]"
@@ -188,18 +199,20 @@ onBeforeUnmount(() => {
</template>
<div
v-else
class="flex h-full min-h-[200px] items-center justify-center text-ink-muted text-sm"
class="flex h-full items-center justify-center text-ink-muted text-sm"
:class="compact ? 'min-h-[120px]' : 'min-h-[200px]'"
>
待上传照片
</div>
</div>
<p
class="font-hand text-2xl leading-tight text-ink mt-3 px-1 text-center sm:text-3xl"
class="font-hand leading-tight text-ink px-1 text-center"
:class="compact ? 'mt-2 text-lg sm:text-xl' : 'mt-3 text-2xl sm:text-3xl'"
>
{{ memory.event_title }}
</p>
<p
v-if="excerpt"
v-if="excerpt && !compact"
class="mt-3 px-1 text-center text-sm leading-relaxed text-ink-muted"
>
{{ excerpt }}

View File

@@ -0,0 +1,171 @@
<script setup lang="ts">
import { usePreferredReducedMotion } from '@vueuse/core'
import { computed } 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()
</script>
<template>
<div
class="relative mx-auto max-w-lg overflow-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-10 top-12 h-44 w-44 rounded-full bg-emerald-300/22 blur-[3rem]"
aria-hidden="true"
/>
<div
class="pointer-events-none absolute -right-10 bottom-10 h-52 w-52 rounded-full bg-cyan-300/18 blur-[3.5rem]"
aria-hidden="true"
/>
<div
class="relative z-[1] mx-auto mb-7 max-w-[min(100%,396px)] overflow-hidden rounded-[2rem] border border-emerald-200/65 bg-[linear-gradient(180deg,#f7fffb_0%,#eefcf7_100%)] shadow-[0_24px_56px_-18px_rgba(16,185,129,0.22)]"
>
<div class="pointer-events-none absolute inset-0 nepal-fiber opacity-[0.14] mix-blend-multiply" />
<div class="relative px-5 pb-5 pt-5 sm:px-6 sm:pb-6">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-[10px] font-semibold uppercase tracking-[0.32em] text-emerald-700/65">
Make Up / 05.04
</p>
<h3 class="mt-2 font-display-cn text-[1.9rem] leading-none text-emerald-950">
和好了在一起吧
</h3>
</div>
<div
class="rounded-2xl border border-emerald-200/70 bg-white/88 px-3 py-2 text-right shadow-[0_8px_20px_rgba(16,185,129,0.12)]"
>
<p class="text-[9px] font-semibold tracking-[0.22em] text-emerald-700/70">
关系状态
</p>
<p class="mt-1 font-brush text-xl text-emerald-950">已和好</p>
</div>
</div>
<div class="mt-6 rounded-[1.8rem] border border-emerald-200/60 bg-white/78 p-4 shadow-inner">
<div class="grid gap-3 sm:grid-cols-[0.92fr_0.16fr_0.92fr] sm:items-center">
<div
class="rounded-[1.5rem] border border-rose-200/65 bg-rose-50/88 px-4 py-4 shadow-[0_10px_20px_rgba(244,114,182,0.08)]"
:class="reduceMotion === 'reduce' ? '' : 'reconcile-card-left'"
>
<p class="text-[10px] font-semibold uppercase tracking-[0.25em] text-rose-700/65">
Before
</p>
<p class="mt-2 text-lg font-medium text-rose-950">别扭沉默想解释</p>
</div>
<div class="relative flex items-center justify-center py-2">
<div
class="flex h-16 w-16 items-center justify-center rounded-full bg-gradient-to-br from-emerald-300 to-cyan-300 text-2xl text-white shadow-[0_14px_28px_rgba(16,185,129,0.22)]"
:class="reduceMotion === 'reduce' ? '' : 'reconcile-heartbeat'"
>
</div>
<div class="absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-gradient-to-r from-rose-200 via-emerald-300 to-cyan-200" aria-hidden="true" />
</div>
<div
class="rounded-[1.5rem] border border-cyan-200/70 bg-cyan-50/88 px-4 py-4 shadow-[0_10px_20px_rgba(34,211,238,0.08)]"
:class="reduceMotion === 'reduce' ? '' : 'reconcile-card-right'"
>
<p class="text-[10px] font-semibold uppercase tracking-[0.25em] text-cyan-700/65">
After
</p>
<p class="mt-2 text-lg font-medium text-cyan-950">说开了靠近了在一起了</p>
</div>
</div>
<div class="mt-4 rounded-[1.4rem] border border-emerald-200/75 bg-emerald-50/85 px-4 py-4">
<div class="flex flex-wrap justify-center gap-2">
<span
v-for="tag in ['把话说完', '空气变甜', '正式在一起']"
:key="tag"
class="rounded-full border border-emerald-200 bg-white/85 px-3 py-1.5 text-[11px] font-medium text-emerald-800/85"
>
{{ tag }}
</span>
</div>
</div>
</div>
</div>
</div>
<div class="relative z-[1] flex flex-col gap-6 sm:gap-8">
<PolaroidCard :memory="memory" :index="memory.story_order" compact />
<div
class="nepal-torn-panel mx-auto max-w-md border border-emerald-200/70 bg-gradient-to-b from-[#fffefa] to-emerald-50/65 px-5 py-6 shadow-[0_16px_36px_-12px_rgba(16,185,129,0.18)] ring-1 ring-white/80 sm:px-6 sm:py-7"
>
<div class="mb-4 h-1 w-14 rounded-full bg-gradient-to-r from-emerald-400 to-cyan-400" aria-hidden="true" />
<div
class="text-[15px] leading-[1.82] tracking-wide text-emerald-950/88 [&_a]:font-bold [&_a]:text-cyan-700 [&_a]:underline [&_a]:underline-offset-4 [&_blockquote]:border-l-4 [&_blockquote]:border-emerald-300/60 [&_blockquote]:bg-emerald-50/55 [&_blockquote]:py-1 [&_blockquote]:pl-3 [&_blockquote]:text-emerald-900/70 [&_code]:rounded [&_code]:bg-emerald-100/50 [&_code]:px-1.5 [&_strong]:text-emerald-950 [&_p+p]:mt-4"
v-html="bodyHtml"
/>
</div>
</div>
</div>
</template>
<style scoped>
.reconcile-heartbeat {
animation: reconcile-heartbeat 2.4s ease-in-out infinite;
}
@keyframes reconcile-heartbeat {
0%,
100% {
transform: scale(1);
}
20% {
transform: scale(1.08);
}
36% {
transform: scale(0.98);
}
56% {
transform: scale(1.05);
}
}
.reconcile-card-left {
animation: reconcile-card-left 0.65s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.reconcile-card-right {
animation: reconcile-card-right 0.65s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes reconcile-card-left {
0% {
opacity: 0;
transform: translateX(-16px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
@keyframes reconcile-card-right {
0% {
opacity: 0;
transform: translateX(16px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
</style>

View File

@@ -42,6 +42,8 @@ export function getStoryTransitionName(m: Memory | null): StoryTransitionName {
const t = m.unique_feature_type as MemoryFeatureType
if (t === 'heart_window') return 'story-heart'
if (t === 'fissure_page') return 'story-fissure'
if (t === 'first_meet' || t === 'first_meet_ju' || t === 'first_meet_rui') return 'story-night'
if (t === 'first_meet_train') return 'story-dawn'
return transitionForStandardStyle(m.nepal_art_style)
}
@@ -57,6 +59,12 @@ export function getStoryShellClasses(m: Memory | null): string {
if (t === 'fissure_page') {
return 'border-fuchsia-300/30 shadow-[0_22px_65px_-24px_rgba(120,60,160,0.28)] ring-fuchsia-200/20'
}
if (t === 'first_meet' || t === 'first_meet_ju' || t === 'first_meet_rui') {
return 'border-slate-400/35 shadow-[0_24px_70px_-22px_rgba(30,58,138,0.28)] ring-cyan-900/15'
}
if (t === 'first_meet_train') {
return 'border-amber-200/50 shadow-[0_24px_72px_-22px_rgba(234,88,12,0.24)] ring-amber-100/35'
}
const shells: Partial<Record<NepalArtStyle, string>> = {
dawn_meet:
'border-amber-200/45 shadow-[0_22px_68px_-22px_rgba(217,119,6,0.22)] ring-amber-100/30',
@@ -87,6 +95,10 @@ export function getStoryNavAccentClass(m: Memory | null): string {
const t = m.unique_feature_type
if (t === 'heart_window') return 'hover:border-rose-300/50 hover:bg-rose-50/80'
if (t === 'fissure_page') return 'hover:border-fuchsia-300/45 hover:bg-fuchsia-50/70'
if (t === 'first_meet' || t === 'first_meet_ju' || t === 'first_meet_rui') {
return 'hover:border-slate-400/45 hover:bg-slate-100/85'
}
if (t === 'first_meet_train') return 'hover:border-amber-300/50 hover:bg-amber-50/80'
const accents: Partial<Record<NepalArtStyle, string>> = {
dawn_meet: 'hover:border-amber-300/50 hover:bg-amber-50/80',
blush_argue: 'hover:border-rose-300/45 hover:bg-rose-50/75',