qaunji
This commit is contained in:
157
frontend/src/components/LaborDayPage.vue
Normal file
157
frontend/src/components/LaborDayPage.vue
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { usePointerSwipe, 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 stageRef = ref<HTMLElement | null>(null)
|
||||||
|
const openAmount = ref(0)
|
||||||
|
const reduceMotion = usePreferredReducedMotion()
|
||||||
|
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
||||||
|
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
||||||
|
|
||||||
|
usePointerSwipe(stageRef, {
|
||||||
|
disableTextSelect: true,
|
||||||
|
onSwipeEnd(_e, direction) {
|
||||||
|
if (reduceMotion.value === 'reduce') return
|
||||||
|
if (direction === 'left') openAmount.value = Math.min(1, openAmount.value + 0.34)
|
||||||
|
if (direction === 'right') openAmount.value = Math.max(0, openAmount.value - 0.34)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const isOpen = computed(() => openAmount.value > 0.45)
|
||||||
|
const curtainShift = computed(() => `${openAmount.value * 86}%`)
|
||||||
|
const intimacyTags = computed(() =>
|
||||||
|
isOpen.value ? ['缠绵', '很累', '靠近得更彻底'] : ['暖灯', '床褶', '呼吸感'],
|
||||||
|
)
|
||||||
|
const stageCopy = computed(() => {
|
||||||
|
if (isOpen.value) return '幕布拉开以后,呼吸、灯影、褶皱全都开始说话。'
|
||||||
|
return '把幕布拉开一点,这一页才会真正进入劳动节的“劳动”。'
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleCurtain() {
|
||||||
|
openAmount.value = isOpen.value ? 0 : 1
|
||||||
|
}
|
||||||
|
</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-10 h-48 w-48 rounded-full bg-rose-300/28 blur-[3rem]" aria-hidden="true" />
|
||||||
|
<div class="pointer-events-none absolute -right-10 bottom-8 h-56 w-56 rounded-full bg-amber-300/20 blur-[3.8rem]" aria-hidden="true" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative z-[1] mx-auto mb-7 max-w-[min(100%,400px)] overflow-hidden rounded-[2rem] border border-rose-200/65 bg-[linear-gradient(180deg,#fff6f4_0%,#ffece2_100%)] shadow-[0_24px_60px_-18px_rgba(190,80,120,0.24)]"
|
||||||
|
>
|
||||||
|
<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-rose-700/66">
|
||||||
|
Labor Day / 05.01
|
||||||
|
</p>
|
||||||
|
<h3 class="mt-2 font-display-cn text-[1.9rem] leading-none text-rose-950">
|
||||||
|
劳动节“劳动”
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-rose-200/70 bg-white/88 px-3 py-2 text-right shadow-[0_8px_18px_rgba(190,80,120,0.12)]">
|
||||||
|
<p class="text-[9px] font-semibold tracking-[0.22em] text-rose-700/70">
|
||||||
|
Intimacy Meter
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 font-brush text-xl text-rose-950">{{ isOpen ? '很累嘛' : '快拉开' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 rounded-[1.8rem] border border-rose-200/60 bg-white/74 p-4 shadow-inner">
|
||||||
|
<div
|
||||||
|
ref="stageRef"
|
||||||
|
class="relative overflow-hidden rounded-[1.6rem] bg-gradient-to-b from-[#7f3b4f] via-[#b46878] to-[#f6cdb0] px-4 pb-5 pt-5"
|
||||||
|
style="touch-action: none"
|
||||||
|
>
|
||||||
|
<div class="absolute inset-x-0 top-0 h-12 bg-gradient-to-b from-white/15 to-transparent" aria-hidden="true" />
|
||||||
|
<div class="absolute right-7 top-5 h-12 w-12 rounded-full bg-[#ffd89c] shadow-[0_0_30px_rgba(255,216,156,0.35)]" aria-hidden="true" />
|
||||||
|
|
||||||
|
<div class="relative h-[230px] overflow-hidden rounded-[1.35rem] border border-white/18 bg-[#4a2730]">
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-b from-[#6d3244] via-[#8f4d62] to-[#352028]" />
|
||||||
|
<div class="absolute left-1/2 top-4 h-3 w-24 -translate-x-1/2 rounded-full bg-white/10 blur-md" aria-hidden="true" />
|
||||||
|
|
||||||
|
<div class="absolute bottom-0 left-0 right-0 h-24 rounded-t-[2rem] bg-gradient-to-b from-[#f8e6d8] to-[#ebc7b0]" />
|
||||||
|
<div class="absolute bottom-7 left-[18%] h-16 w-[64%] rounded-[2rem] bg-[#fff3ea]/70 shadow-[0_18px_24px_rgba(80,35,45,0.16)]" />
|
||||||
|
<div class="absolute bottom-11 left-[28%] h-12 w-16 rounded-full bg-[#d89a87]/70 blur-[1px]" />
|
||||||
|
<div class="absolute bottom-10 right-[28%] h-12 w-16 rounded-full bg-[#d89a87]/70 blur-[1px]" />
|
||||||
|
<div class="absolute bottom-[5.2rem] left-[36%] h-10 w-7 rounded-full bg-[#f4ccb7]/85" />
|
||||||
|
<div class="absolute bottom-[5.2rem] right-[36%] h-10 w-7 rounded-full bg-[#f4ccb7]/85" />
|
||||||
|
<div
|
||||||
|
class="absolute left-1/2 top-[5.6rem] z-[2] -translate-x-1/2 text-3xl transition-all duration-500"
|
||||||
|
:class="isOpen ? 'scale-100 opacity-100' : 'scale-75 opacity-0'"
|
||||||
|
>
|
||||||
|
♡
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute inset-y-0 left-0 z-[3] w-[52%] bg-[linear-gradient(180deg,rgba(255,245,244,0.97)_0%,rgba(244,196,203,0.92)_100%)] shadow-[14px_0_30px_rgba(127,59,79,0.18)] transition-transform duration-700 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
||||||
|
:style="{ transform: `translateX(-${curtainShift})` }"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute inset-y-0 right-0 z-[3] w-[52%] bg-[linear-gradient(180deg,rgba(255,245,244,0.97)_0%,rgba(244,196,203,0.92)_100%)] shadow-[-14px_0_30px_rgba(127,59,79,0.18)] transition-transform duration-700 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
||||||
|
:style="{ transform: `translateX(${curtainShift})` }"
|
||||||
|
/>
|
||||||
|
<div class="absolute left-[calc(50%-2px)] top-0 z-[4] h-full w-1 rounded-full bg-white/22" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 rounded-[1.25rem] border border-white/55 bg-white/78 px-4 py-3 text-center shadow-[0_10px_18px_rgba(120,53,15,0.08)]">
|
||||||
|
<p class="text-[10px] font-semibold uppercase tracking-[0.24em] text-rose-700/65">
|
||||||
|
Pull The Curtain
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm leading-relaxed text-rose-950/88">
|
||||||
|
{{ stageCopy }}
|
||||||
|
</p>
|
||||||
|
<div class="mt-3 flex flex-wrap justify-center gap-2">
|
||||||
|
<span
|
||||||
|
v-for="tag in intimacyTags"
|
||||||
|
:key="tag"
|
||||||
|
class="rounded-full border border-rose-200 bg-rose-50/90 px-2.5 py-1 text-[10px] font-medium text-rose-900/85"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border border-rose-300/70 bg-white/88 px-4 py-2 text-xs font-semibold tracking-[0.18em] text-rose-950 transition hover:-translate-y-0.5 hover:bg-white"
|
||||||
|
@click="toggleCurtain"
|
||||||
|
>
|
||||||
|
{{ isOpen ? '把幕布合回去' : '拉开被单 / 幕布' }}
|
||||||
|
</button>
|
||||||
|
</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-rose-200/60 bg-gradient-to-b from-[#fffdfa] to-rose-50/35 px-5 py-6 text-left shadow-[0_12px_30px_-15px_rgba(190,80,120,0.18)] sm:px-6 sm:py-7"
|
||||||
|
>
|
||||||
|
<div class="mb-4 h-1 w-12 rounded-full bg-gradient-to-r from-rose-400 to-amber-400/80" aria-hidden="true" />
|
||||||
|
<div
|
||||||
|
class="text-[15px] leading-[1.8] tracking-wide text-rose-950/88 [&_a]:font-bold [&_a]:text-rose-700 [&_a]:underline [&_a]:decoration-rose-300 [&_a]:underline-offset-4 [&_blockquote]:border-l-4 [&_blockquote]:border-rose-300/60 [&_blockquote]:bg-rose-50/50 [&_blockquote]:py-1 [&_blockquote]:pl-3 [&_blockquote]:text-rose-900/72 [&_code]:rounded [&_code]:bg-rose-100/55 [&_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>
|
||||||
168
frontend/src/components/QuanjiPage.vue
Normal file
168
frontend/src/components/QuanjiPage.vue
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { usePreferredReducedMotion } from '@vueuse/core'
|
||||||
|
import { computed, onBeforeUnmount, 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' | 'opening' | 'opened'>('idle')
|
||||||
|
let doorTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
|
const roomText = computed(() => {
|
||||||
|
if (phase.value === 'opened') return '门打开以后,故事就从“见面”走向了“爱的开始”。'
|
||||||
|
if (phase.value === 'opening') return '房卡刷过的一瞬间,灯光和情绪一起变暖。'
|
||||||
|
return '点一下房卡,把门打开,把这一页真正带进去。'
|
||||||
|
})
|
||||||
|
|
||||||
|
function openRoom() {
|
||||||
|
if (phase.value === 'opened') {
|
||||||
|
phase.value = 'idle'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (doorTimer) clearTimeout(doorTimer)
|
||||||
|
if (reduceMotion.value === 'reduce') {
|
||||||
|
phase.value = 'opened'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
phase.value = 'opening'
|
||||||
|
doorTimer = setTimeout(() => {
|
||||||
|
phase.value = 'opened'
|
||||||
|
doorTimer = undefined
|
||||||
|
}, 950)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (doorTimer) clearTimeout(doorTimer)
|
||||||
|
})
|
||||||
|
</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-10 h-44 w-44 rounded-full bg-amber-300/24 blur-[3rem]" aria-hidden="true" />
|
||||||
|
<div class="pointer-events-none absolute -right-12 bottom-8 h-52 w-52 rounded-full bg-rose-300/18 blur-[3.4rem]" 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,#fff8f2_0%,#ffefdf_100%)] shadow-[0_24px_56px_-18px_rgba(180,83,9,0.24)]"
|
||||||
|
>
|
||||||
|
<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-amber-700/68">
|
||||||
|
Hotel Night / 04.30
|
||||||
|
</p>
|
||||||
|
<h3 class="mt-2 font-display-cn text-[1.9rem] leading-none text-amber-950">
|
||||||
|
全季M爱
|
||||||
|
</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">
|
||||||
|
Room Mood
|
||||||
|
</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/74 p-4 shadow-inner">
|
||||||
|
<div class="relative overflow-hidden rounded-[1.55rem] bg-gradient-to-b from-[#efe7dd] via-[#e4d3c4] to-[#d1b8a1] px-4 pb-5 pt-5">
|
||||||
|
<div class="absolute inset-y-0 left-[51%] z-[1] w-[44%] bg-gradient-to-l from-amber-200/38 to-transparent opacity-0 transition-opacity duration-700" :class="phase !== 'idle' ? 'opacity-100' : ''" aria-hidden="true" />
|
||||||
|
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="rounded-2xl border border-white/70 bg-white/78 px-3 py-2 shadow-[0_10px_18px_rgba(120,53,15,0.08)]">
|
||||||
|
<p class="text-[10px] font-semibold uppercase tracking-[0.24em] text-amber-700/65">
|
||||||
|
季 Hotel
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 font-mono text-sm font-bold text-amber-950">04 · 30 / 2117</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-[1.2rem] border border-amber-200/80 bg-white/90 px-4 py-3 text-left shadow-[0_10px_20px_rgba(120,53,15,0.1)] transition hover:-translate-y-0.5 hover:bg-white"
|
||||||
|
@click="openRoom"
|
||||||
|
>
|
||||||
|
<p class="text-[10px] font-semibold uppercase tracking-[0.24em] text-amber-700/65">
|
||||||
|
房卡
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 font-brush text-xl text-amber-950">刷开门</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative mt-5 h-[220px] overflow-hidden rounded-[1.45rem] border border-[#9b765e]/30 bg-[#3c2f2c] shadow-[inset_0_2px_4px_rgba(255,255,255,0.12)]">
|
||||||
|
<div
|
||||||
|
class="absolute inset-y-0 left-0 w-1/2 border-r border-[#6f5547]/45 bg-[linear-gradient(180deg,#6c5347_0%,#4e3a31_100%)] transition-transform duration-700 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
||||||
|
:class="phase === 'idle' ? '' : 'translate-x-[-76%]'"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute inset-y-0 right-0 w-1/2 border-l border-[#6f5547]/45 bg-[linear-gradient(180deg,#6c5347_0%,#4e3a31_100%)] transition-transform duration-700 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
||||||
|
:class="phase === 'idle' ? '' : 'translate-x-[76%]'"
|
||||||
|
/>
|
||||||
|
<div class="absolute inset-x-[19%] top-4 h-3 rounded-full bg-white/12 blur-sm" aria-hidden="true" />
|
||||||
|
|
||||||
|
<div class="absolute inset-0 z-0 bg-gradient-to-b from-[#fff0d2]/18 via-[#f7dba7]/16 to-[#5a3c2d]/0 transition-opacity duration-700" :class="phase === 'idle' ? 'opacity-0' : 'opacity-100'" />
|
||||||
|
|
||||||
|
<div class="absolute inset-0 z-0 px-6 pb-5 pt-6">
|
||||||
|
<div class="mx-auto h-16 w-10 rounded-t-full bg-[#f4cb73] shadow-[0_0_28px_rgba(251,191,36,0.3)]" />
|
||||||
|
<div class="mx-auto h-12 w-2 bg-[#6b4f3a]" />
|
||||||
|
|
||||||
|
<div class="mx-auto mt-4 flex max-w-[11rem] items-end justify-center gap-3">
|
||||||
|
<div class="h-16 w-10 rounded-t-[1rem] bg-[#fff4ea]/78 shadow-sm" />
|
||||||
|
<div class="h-20 w-24 rounded-[1.4rem] bg-[#f3d6c3]/72 shadow-[0_10px_20px_rgba(80,40,20,0.14)]" />
|
||||||
|
<div class="h-12 w-12 rounded-t-[1rem] bg-[#fff4ea]/68 shadow-sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mx-auto mt-5 max-w-[12rem] rounded-[1.2rem] border border-white/45 bg-white/72 px-4 py-3 text-center shadow-[0_10px_22px_rgba(120,53,15,0.1)] transition-all duration-500"
|
||||||
|
:class="phase === 'opened' ? 'translate-y-0 opacity-100' : 'translate-y-3 opacity-70'"
|
||||||
|
>
|
||||||
|
<p class="text-[10px] font-semibold uppercase tracking-[0.24em] text-amber-700/65">
|
||||||
|
Door Status
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm leading-relaxed text-amber-950/88">
|
||||||
|
{{ roomText }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border border-amber-300/70 bg-white/88 px-4 py-2 text-xs font-semibold tracking-[0.18em] text-amber-950 transition hover:-translate-y-0.5 hover:bg-white"
|
||||||
|
@click="openRoom"
|
||||||
|
>
|
||||||
|
{{ phase === 'opened' ? '把门轻轻合上' : phase === 'opening' ? '门正在打开…' : '用房卡开门' }}
|
||||||
|
</button>
|
||||||
|
</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-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="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>
|
||||||
200
frontend/src/components/TaxiHandHoldPage.vue
Normal file
200
frontend/src/components/TaxiHandHoldPage.vue
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { usePreferredReducedMotion } from '@vueuse/core'
|
||||||
|
import { computed, onBeforeUnmount, 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' | 'approaching' | 'held'>('idle')
|
||||||
|
let holdTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
|
const subtitle = computed(() => {
|
||||||
|
if (phase.value === 'held') return '牵住了,连空气都跟着变暖。'
|
||||||
|
if (phase.value === 'approaching') return '那一秒,心跳比出租车拐弯还快。'
|
||||||
|
return '先让两只手慢慢靠近,再真正牵住。'
|
||||||
|
})
|
||||||
|
|
||||||
|
function bringHandsTogether() {
|
||||||
|
if (phase.value === 'held') {
|
||||||
|
phase.value = 'idle'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (holdTimer) clearTimeout(holdTimer)
|
||||||
|
if (reduceMotion.value === 'reduce') {
|
||||||
|
phase.value = 'held'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
phase.value = 'approaching'
|
||||||
|
holdTimer = setTimeout(() => {
|
||||||
|
phase.value = 'held'
|
||||||
|
holdTimer = undefined
|
||||||
|
}, 900)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (holdTimer) clearTimeout(holdTimer)
|
||||||
|
})
|
||||||
|
</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-8 h-48 w-48 rounded-full bg-amber-300/30 blur-[3rem]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute -right-10 bottom-6 h-52 w-52 rounded-full bg-rose-300/22 blur-[3.4rem]"
|
||||||
|
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,#fff7ef_0%,#fff0df_100%)] shadow-[0_24px_56px_-18px_rgba(180,83,9,0.24)]"
|
||||||
|
>
|
||||||
|
<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-amber-700/68">
|
||||||
|
Taxi Ride / 04.30
|
||||||
|
</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">
|
||||||
|
After Meeting
|
||||||
|
</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/74 p-4 shadow-inner">
|
||||||
|
<div class="relative overflow-hidden rounded-[1.55rem] bg-gradient-to-b from-[#ffe8cf] via-[#fed7aa] to-[#f5b27f] px-4 pb-5 pt-4">
|
||||||
|
<div class="absolute inset-x-0 top-0 h-12 bg-gradient-to-b from-white/20 to-transparent" aria-hidden="true" />
|
||||||
|
<div class="absolute inset-y-4 left-3 w-5 rounded-full bg-[#2f2f35]/65" aria-hidden="true" />
|
||||||
|
<div class="absolute inset-y-4 right-3 w-5 rounded-full bg-[#2f2f35]/65" aria-hidden="true" />
|
||||||
|
|
||||||
|
<div class="absolute left-8 top-8 h-24 w-[38%] overflow-hidden rounded-[1rem] border border-white/50 bg-[#12263a]" aria-hidden="true">
|
||||||
|
<div class="taxi-window-lights h-full w-[180%]" :class="phase === 'held' && !reduceMotion ? 'taxi-window-fast' : ''" />
|
||||||
|
</div>
|
||||||
|
<div class="absolute right-8 top-8 h-24 w-[38%] overflow-hidden rounded-[1rem] border border-white/50 bg-[#12263a]" aria-hidden="true">
|
||||||
|
<div class="taxi-window-lights h-full w-[180%]" :class="phase === 'held' && !reduceMotion ? 'taxi-window-fast' : ''" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute left-1/2 top-[5.7rem] z-[2] -translate-x-1/2 rotate-[-12deg]" aria-hidden="true">
|
||||||
|
<div class="rounded-full border border-rose-200/70 bg-white/86 px-3 py-1 text-[11px] font-semibold text-rose-900 shadow-[0_10px_18px_rgba(190,24,93,0.12)]">
|
||||||
|
花束卡住了一点点
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative mt-32 h-32">
|
||||||
|
<div
|
||||||
|
class="absolute bottom-3 left-5 h-20 w-24 rounded-[2rem] border border-[#734231]/55 bg-gradient-to-br from-[#f5c5b3] to-[#d9937b] shadow-[0_10px_20px_rgba(115,66,49,0.18)] transition-transform duration-700 ease-out"
|
||||||
|
:class="phase === 'idle' ? 'rotate-[8deg]' : phase === 'approaching' ? 'translate-x-10 rotate-[4deg]' : 'translate-x-15 rotate-[2deg]'"
|
||||||
|
>
|
||||||
|
<div class="absolute right-2 top-4 h-5 w-5 rounded-full bg-[#f8e0d6]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute bottom-3 right-5 h-20 w-24 rounded-[2rem] border border-[#734231]/55 bg-gradient-to-bl from-[#f5c5b3] to-[#d9937b] shadow-[0_10px_20px_rgba(115,66,49,0.18)] transition-transform duration-700 ease-out"
|
||||||
|
:class="phase === 'idle' ? 'rotate-[-8deg]' : phase === 'approaching' ? '-translate-x-10 rotate-[-4deg]' : '-translate-x-15 rotate-[-2deg]'"
|
||||||
|
>
|
||||||
|
<div class="absolute left-2 top-4 h-5 w-5 rounded-full bg-[#f8e0d6]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute left-1/2 top-7 z-[3] -translate-x-1/2 text-3xl drop-shadow-[0_4px_10px_rgba(225,29,72,0.25)] transition-all duration-500"
|
||||||
|
:class="phase === 'held' ? 'scale-100 opacity-100' : 'scale-75 opacity-0'"
|
||||||
|
>
|
||||||
|
♡
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 rounded-[1.2rem] border border-white/65 bg-white/74 px-4 py-3 text-center shadow-[0_10px_18px_rgba(180,83,9,0.08)]">
|
||||||
|
<p class="text-[10px] font-semibold uppercase tracking-[0.24em] text-amber-700/65">
|
||||||
|
Hold Hands
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm leading-relaxed text-amber-950/88">
|
||||||
|
{{ subtitle }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border border-amber-300/70 bg-white/88 px-4 py-2 text-xs font-semibold tracking-[0.18em] text-amber-950 transition hover:-translate-y-0.5 hover:bg-white"
|
||||||
|
@click="bringHandsTogether"
|
||||||
|
>
|
||||||
|
{{ phase === 'held' ? '再心动一次' : phase === 'approaching' ? '快要牵住了…' : '让双手靠近' }}
|
||||||
|
</button>
|
||||||
|
</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-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="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>
|
||||||
|
.taxi-window-lights {
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0 4%,
|
||||||
|
rgb(252 211 77 / 0.85) 4% 9%,
|
||||||
|
transparent 9% 16%,
|
||||||
|
rgb(244 114 182 / 0.6) 16% 20%,
|
||||||
|
transparent 20% 28%,
|
||||||
|
rgb(96 165 250 / 0.75) 28% 34%,
|
||||||
|
transparent 34% 42%,
|
||||||
|
rgb(251 146 60 / 0.8) 42% 47%,
|
||||||
|
transparent 47% 56%,
|
||||||
|
rgb(253 224 71 / 0.82) 56% 60%,
|
||||||
|
transparent 60% 100%
|
||||||
|
);
|
||||||
|
animation: taxi-window-lights 7s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taxi-window-fast {
|
||||||
|
animation-duration: 2.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes taxi-window-lights {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-40%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user