mwy quanji
This commit is contained in:
@@ -75,6 +75,42 @@ class Command(BaseCommand):
|
|||||||
nepal_art_style=Memory.NepalArtStyle.DAWN_MEET,
|
nepal_art_style=Memory.NepalArtStyle.DAWN_MEET,
|
||||||
is_finale=False,
|
is_finale=False,
|
||||||
)
|
)
|
||||||
|
Memory.objects.create(
|
||||||
|
story_order=20260431,
|
||||||
|
date=date(2026, 4, 30),
|
||||||
|
event_title="出租牵手",
|
||||||
|
main_text=(
|
||||||
|
"**2026 年 4 月 30 日**,我们在车上开始牵手。\n\n"
|
||||||
|
"心跳开始加速、见面的花倒成了我们之间的障碍。"
|
||||||
|
),
|
||||||
|
unique_feature_type=Memory.FeatureType.FIRST_MEET_TAIX,
|
||||||
|
nepal_art_style=Memory.NepalArtStyle.DAWN_MEET,
|
||||||
|
is_finale=False,
|
||||||
|
)
|
||||||
|
Memory.objects.create(
|
||||||
|
story_order=20260432,
|
||||||
|
date=date(2026, 4, 30),
|
||||||
|
event_title="全季M爱",
|
||||||
|
main_text=(
|
||||||
|
"**2026 年 4 月 30 日**,下次见面还是全季。\n\n"
|
||||||
|
"所以爱的开始就在全季,那么我们下次可以去四季嘛。"
|
||||||
|
),
|
||||||
|
unique_feature_type=Memory.FeatureType.FIRST_MEET_QUANJI,
|
||||||
|
nepal_art_style=Memory.NepalArtStyle.DAWN_MEET,
|
||||||
|
is_finale=False,
|
||||||
|
)
|
||||||
|
Memory.objects.create(
|
||||||
|
story_order=20260501,
|
||||||
|
date=date(2026, 5, 1),
|
||||||
|
event_title="劳动节“劳动”",
|
||||||
|
main_text=(
|
||||||
|
"**2026 年 5 月 1 日**,劳动节我们倒是很累嘛,所以我身体真的很好吧。\n\n"
|
||||||
|
"从那天开始,我们就开始了劳动节的“劳动”,各种姿势,我们缠绵。"
|
||||||
|
),
|
||||||
|
unique_feature_type=Memory.FeatureType.MAKE_LOVE,
|
||||||
|
nepal_art_style=Memory.NepalArtStyle.DAWN_MEET,
|
||||||
|
is_finale=False,
|
||||||
|
)
|
||||||
Memory.objects.create(
|
Memory.objects.create(
|
||||||
story_order=20260503,
|
story_order=20260503,
|
||||||
date=date(2026, 5, 3),
|
date=date(2026, 5, 3),
|
||||||
@@ -121,7 +157,7 @@ class Command(BaseCommand):
|
|||||||
date=date(2026, 5, 10),
|
date=date(2026, 5, 10),
|
||||||
event_title="逛宜家的小日子",
|
event_title="逛宜家的小日子",
|
||||||
main_text=(
|
main_text=(
|
||||||
"推车里的台灯、地毯和一杯热狗,"
|
"推车里的热狗、桌子、盘子和杯子,"
|
||||||
"都是以后会说「还记得吗」的碎片。"
|
"都是以后会说「还记得吗」的碎片。"
|
||||||
),
|
),
|
||||||
unique_feature_type=Memory.FeatureType.STANDARD_NEPAL,
|
unique_feature_type=Memory.FeatureType.STANDARD_NEPAL,
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ class Memory(models.Model):
|
|||||||
FISSURE_PAGE = "fissure_page", "裂缝对开页"
|
FISSURE_PAGE = "fissure_page", "裂缝对开页"
|
||||||
STANDARD_NEPAL = "standard_nepal", "标准尼泊尔页"
|
STANDARD_NEPAL = "standard_nepal", "标准尼泊尔页"
|
||||||
FIRST_MEET_TRAIN = "first_meet_train", "初见·高铁遇见"
|
FIRST_MEET_TRAIN = "first_meet_train", "初见·高铁遇见"
|
||||||
|
FIRST_MEET_TAIX = "first_meet_taix", "初见·出租牵手"
|
||||||
|
FIRST_MEET_QUANJI = "first_meet_quanji", "初见·全季M爱"
|
||||||
|
MAKE_LOVE = "make_love", "爱爱页"
|
||||||
|
|
||||||
|
|
||||||
class NepalArtStyle(models.TextChoices):
|
class NepalArtStyle(models.TextChoices):
|
||||||
"""每页可选不同尼泊尔视觉皮肤(备用可继续加枚举)。"""
|
"""每页可选不同尼泊尔视觉皮肤(备用可继续加枚举)。"""
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export type MemoryFeatureType =
|
|||||||
| 'first_meet_ju'
|
| 'first_meet_ju'
|
||||||
| 'first_meet_rui'
|
| 'first_meet_rui'
|
||||||
| 'first_meet_train'
|
| 'first_meet_train'
|
||||||
|
| 'first_meet_taix'
|
||||||
|
| 'first_meet_quanji'
|
||||||
|
| 'make_love'
|
||||||
|
|
||||||
export type NepalArtStyle =
|
export type NepalArtStyle =
|
||||||
| 'none'
|
| 'none'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePreferredReducedMotion } from '@vueuse/core'
|
import { usePreferredReducedMotion } from '@vueuse/core'
|
||||||
import { computed } from 'vue'
|
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||||
import type { Memory } from '../api/client'
|
import type { Memory } from '../api/client'
|
||||||
import { getStandardNepalTheme } from '../constants/nepalThemes'
|
import { getStandardNepalTheme } from '../constants/nepalThemes'
|
||||||
import PolaroidCard from './PolaroidCard.vue'
|
import PolaroidCard from './PolaroidCard.vue'
|
||||||
@@ -13,8 +13,37 @@ const props = defineProps<{
|
|||||||
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
||||||
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
||||||
const reduceMotion = usePreferredReducedMotion()
|
const reduceMotion = usePreferredReducedMotion()
|
||||||
|
const callPhase = ref<'idle' | 'dialing' | 'connected'>('idle')
|
||||||
|
let connectTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
const waveHeights = [18, 34, 24, 42, 28, 36, 20, 31]
|
const waveHeights = [18, 34, 24, 42, 28, 36, 20, 31]
|
||||||
|
|
||||||
|
const callTimeLabel = computed(() => {
|
||||||
|
if (callPhase.value === 'idle') return '00:00'
|
||||||
|
if (callPhase.value === 'dialing') return '00:07'
|
||||||
|
return '20:00'
|
||||||
|
})
|
||||||
|
|
||||||
|
function startCall() {
|
||||||
|
if (callPhase.value === 'connected') {
|
||||||
|
callPhase.value = 'idle'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (connectTimer) clearTimeout(connectTimer)
|
||||||
|
if (reduceMotion.value === 'reduce') {
|
||||||
|
callPhase.value = 'connected'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callPhase.value = 'dialing'
|
||||||
|
connectTimer = setTimeout(() => {
|
||||||
|
callPhase.value = 'connected'
|
||||||
|
connectTimer = undefined
|
||||||
|
}, 900)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (connectTimer) clearTimeout(connectTimer)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -47,7 +76,7 @@ const waveHeights = [18, 34, 24, 42, 28, 36, 20, 31]
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="relative px-5 pb-5 pt-5 sm:px-6 sm:pb-6">
|
<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 class="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-[0.32em] text-cyan-300/68">
|
<p class="text-[10px] font-semibold uppercase tracking-[0.32em] text-cyan-300/68">
|
||||||
First Call
|
First Call
|
||||||
@@ -60,7 +89,7 @@ const waveHeights = [18, 34, 24, 42, 28, 36, 20, 31]
|
|||||||
<p class="text-[10px] font-semibold tracking-[0.2em] text-slate-300/78">
|
<p class="text-[10px] font-semibold tracking-[0.2em] text-slate-300/78">
|
||||||
通话时长
|
通话时长
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1 font-mono text-2xl font-bold text-cyan-100">20:00</p>
|
<p class="mt-1 font-mono text-2xl font-bold text-cyan-100">{{ callTimeLabel }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -88,12 +117,22 @@ const waveHeights = [18, 34, 24, 42, 28, 36, 20, 31]
|
|||||||
v-for="(h, i) in waveHeights"
|
v-for="(h, i) in waveHeights"
|
||||||
:key="`${h}-${i}`"
|
: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="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}`"
|
:class="[
|
||||||
|
reduceMotion === 'reduce' ? '' : `call-wave--${(i % 4) + 1}`,
|
||||||
|
callPhase === 'idle' ? 'opacity-45' : '',
|
||||||
|
callPhase === 'connected' ? 'brightness-110' : '',
|
||||||
|
]"
|
||||||
:style="{ height: `${h}px` }"
|
:style="{ height: `${h}px` }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-center text-xs leading-relaxed text-slate-300/80">
|
<p class="mt-3 text-center text-xs leading-relaxed text-slate-300/80">
|
||||||
第一次听见你的声音,原来文字之外,还有更柔软的那一层。
|
{{
|
||||||
|
callPhase === 'idle'
|
||||||
|
? '点一下拨通,第一次电话会从文字走进声音里。'
|
||||||
|
: callPhase === 'dialing'
|
||||||
|
? '正在接通,心跳也跟着一起变快。'
|
||||||
|
: '第一次听见你的声音,原来文字之外,还有更柔软的那一层。'
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -106,6 +145,22 @@ const waveHeights = [18, 34, 24, 42, 28, 36, 20, 31]
|
|||||||
{{ tag }}
|
{{ tag }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border border-cyan-300/24 bg-cyan-400/10 px-4 py-2 text-xs font-semibold tracking-[0.18em] text-cyan-100 transition hover:bg-cyan-400/18"
|
||||||
|
@click="startCall"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
callPhase === 'idle'
|
||||||
|
? '拨通这通电话'
|
||||||
|
: callPhase === 'dialing'
|
||||||
|
? '接通中…'
|
||||||
|
: '再听一遍'
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePreferredReducedMotion } from '@vueuse/core'
|
import { usePreferredReducedMotion } from '@vueuse/core'
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import type { Memory } from '../api/client'
|
import type { Memory } from '../api/client'
|
||||||
import { getStandardNepalTheme } from '../constants/nepalThemes'
|
import { getStandardNepalTheme } from '../constants/nepalThemes'
|
||||||
import PolaroidCard from './PolaroidCard.vue'
|
import PolaroidCard from './PolaroidCard.vue'
|
||||||
@@ -14,7 +14,12 @@ const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
|||||||
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
||||||
const reduceMotion = usePreferredReducedMotion()
|
const reduceMotion = usePreferredReducedMotion()
|
||||||
|
|
||||||
const morseGroups = ['-- .- -.--', '.. / .... . .- .-. -..', '.- .-.. .-.. / -. .. --. .... -']
|
const morseEntries = [
|
||||||
|
{ code: '-- .- -.--', text: '别怕,我在听' },
|
||||||
|
{ code: '.. / .... . .- .-. -..', text: '我听见你的害怕了' },
|
||||||
|
{ code: '.- .-.. .-.. / -. .. --. .... -', text: '聊到很晚也没关系' },
|
||||||
|
]
|
||||||
|
const activeMorse = ref(0)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -124,14 +129,28 @@ const morseGroups = ['-- .- -.--', '.. / .... . .- .-. -..', '.- .-.. .-.. / -.
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 grid gap-2 sm:grid-cols-3">
|
<div class="mt-4 grid gap-2 sm:grid-cols-3">
|
||||||
<div
|
<button
|
||||||
v-for="line in morseGroups"
|
v-for="(entry, index) in morseEntries"
|
||||||
:key="line"
|
:key="entry.code"
|
||||||
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"
|
type="button"
|
||||||
:class="reduceMotion === 'reduce' ? '' : 'campus-morse-glow'"
|
class="rounded-2xl border px-3 py-2 text-left font-mono text-[10px] tracking-[0.22em] transition"
|
||||||
|
:class="[
|
||||||
|
activeMorse === index
|
||||||
|
? 'border-cyan-300/60 bg-cyan-300/16 text-cyan-100 shadow-[0_10px_22px_rgba(34,211,238,0.14)]'
|
||||||
|
: 'border-white/10 bg-white/8 text-slate-300/85 hover:bg-white/12',
|
||||||
|
reduceMotion === 'reduce' ? '' : 'campus-morse-glow',
|
||||||
|
]"
|
||||||
|
@click="activeMorse = index"
|
||||||
>
|
>
|
||||||
{{ line }}
|
{{ entry.code }}
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 rounded-2xl border border-cyan-300/18 bg-cyan-300/10 px-4 py-3 text-sm leading-relaxed text-cyan-50 shadow-[0_10px_24px_rgba(34,211,238,0.1)]">
|
||||||
|
<p class="text-[10px] font-semibold uppercase tracking-[0.24em] text-cyan-200/70">
|
||||||
|
点一下,解码一下
|
||||||
|
</p>
|
||||||
|
<p class="mt-2">{{ morseEntries[activeMorse].text }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const props = defineProps<{
|
|||||||
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
||||||
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
||||||
const reduceMotion = usePreferredReducedMotion()
|
const reduceMotion = usePreferredReducedMotion()
|
||||||
|
const replySent = ref(false)
|
||||||
|
|
||||||
/** typing → shown */
|
/** typing → shown */
|
||||||
const revealPhase = ref<'typing' | 'shown'>('typing')
|
const revealPhase = ref<'typing' | 'shown'>('typing')
|
||||||
@@ -64,6 +65,11 @@ const dmDisplayParts = computed(() => {
|
|||||||
{ type: 'text' as const, text: s.slice(idx + '[流泪]'.length) },
|
{ type: 'text' as const, text: s.slice(idx + '[流泪]'.length) },
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function toggleReply() {
|
||||||
|
if (revealPhase.value === 'typing') return
|
||||||
|
replySent.value = !replySent.value
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -215,6 +221,32 @@ const dmDisplayParts = computed(() => {
|
|||||||
<p class="mt-2.5 pl-1 text-[10px] text-slate-500/80 font-medium tracking-wide">
|
<p class="mt-2.5 pl-1 text-[10px] text-slate-500/80 font-medium tracking-wide">
|
||||||
对方已通过「视频推荐」发起会话
|
对方已通过「视频推荐」发起会话
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-3 flex items-start justify-between gap-3 pl-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border border-cyan-400/25 bg-cyan-400/10 px-3 py-1.5 text-[10px] font-semibold tracking-[0.18em] text-cyan-200 transition hover:bg-cyan-400/18 disabled:cursor-default disabled:opacity-45"
|
||||||
|
:disabled="revealPhase === 'typing'"
|
||||||
|
@click="toggleReply"
|
||||||
|
>
|
||||||
|
{{ replySent ? '撤回心动回复' : '回一句试试' }}
|
||||||
|
</button>
|
||||||
|
<span class="text-[10px] text-slate-500/78">
|
||||||
|
{{ revealPhase === 'typing' ? '正在输入中…' : '轻点按钮,故事就会继续一点。' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="replySent"
|
||||||
|
class="mt-3 flex justify-end"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="max-w-[84%] rounded-2xl rounded-tr-sm border border-cyan-300/25 bg-gradient-to-br from-cyan-500/18 to-blue-500/16 px-4 py-3 text-right text-[13px] leading-[1.72] text-cyan-50 shadow-[0_10px_22px_rgba(6,182,212,0.12)]"
|
||||||
|
:class="reduceMotion === 'reduce' ? '' : 'fm-reply-pop'"
|
||||||
|
>
|
||||||
|
读博会辛苦,但你这样认真来问的人,也会被认真回答。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,8 +367,24 @@ const dmDisplayParts = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fm-reply-pop {
|
||||||
|
animation: fm-reply-pop 0.42s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
transform-origin: top right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fm-reply-pop {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(12px) scale(0.92);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 撕纸边缘容器补充 */
|
/* 撕纸边缘容器补充 */
|
||||||
.nepal-torn-panel {
|
.nepal-torn-panel {
|
||||||
border-radius: 4px 4px 16px 16px;
|
border-radius: 4px 4px 16px 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const stageRef = ref<HTMLElement | null>(null)
|
|||||||
const split = ref(0)
|
const split = ref(0)
|
||||||
const reduceMotion = usePreferredReducedMotion()
|
const reduceMotion = usePreferredReducedMotion()
|
||||||
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
||||||
|
const panelShift = computed(() => `${split.value * 72}%`)
|
||||||
|
|
||||||
const hurtFacts = [
|
const hurtFacts = [
|
||||||
'我把你和前任之间的事情告诉了别人',
|
'我把你和前任之间的事情告诉了别人',
|
||||||
@@ -43,7 +44,7 @@ usePointerSwipe(stageRef, {
|
|||||||
const isOpen = computed(() => split.value > 0.35)
|
const isOpen = computed(() => split.value > 0.35)
|
||||||
|
|
||||||
function toggleSplit() {
|
function toggleSplit() {
|
||||||
split.value = isOpen.value ? 0 : 0.85
|
split.value = isOpen.value ? 0 : 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const splitHint = computed(() =>
|
const splitHint = computed(() =>
|
||||||
@@ -156,7 +157,7 @@ const fracturePulseClass = computed(() => {
|
|||||||
<div
|
<div
|
||||||
class="relative z-[3] flex-1 overflow-hidden border-r border-white/6 bg-[linear-gradient(180deg,#4b1522_0%,#241017_100%)]"
|
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)]'"
|
:class="reduceMotion === 'reduce' ? '' : 'transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]'"
|
||||||
:style="{ transform: `translateX(-${split * 30}%)` }"
|
:style="{ transform: `translateX(-${panelShift})` }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute inset-0 opacity-[0.16]"
|
class="pointer-events-none absolute inset-0 opacity-[0.16]"
|
||||||
@@ -221,7 +222,7 @@ const fracturePulseClass = computed(() => {
|
|||||||
<div
|
<div
|
||||||
class="relative z-[3] flex-1 overflow-hidden border-l border-white/6 bg-[linear-gradient(180deg,#2a1317_0%,#120d10_100%)]"
|
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)]'"
|
:class="reduceMotion === 'reduce' ? '' : 'transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]'"
|
||||||
:style="{ transform: `translateX(${split * 30}%)` }"
|
:style="{ transform: `translateX(${panelShift})` }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute inset-0 opacity-[0.16]"
|
class="pointer-events-none absolute inset-0 opacity-[0.16]"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePreferredReducedMotion } from '@vueuse/core'
|
import { usePreferredReducedMotion } from '@vueuse/core'
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import type { Memory } from '../api/client'
|
import type { Memory } from '../api/client'
|
||||||
import { getStandardNepalTheme } from '../constants/nepalThemes'
|
import { getStandardNepalTheme } from '../constants/nepalThemes'
|
||||||
import PolaroidCard from './PolaroidCard.vue'
|
import PolaroidCard from './PolaroidCard.vue'
|
||||||
@@ -13,6 +13,7 @@ const props = defineProps<{
|
|||||||
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
||||||
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
||||||
const reduceMotion = usePreferredReducedMotion()
|
const reduceMotion = usePreferredReducedMotion()
|
||||||
|
const homeUnlocked = ref(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -59,7 +60,10 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
<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="absolute inset-y-0 right-0 w-[42%] bg-gradient-to-l from-amber-100/85 to-transparent" aria-hidden="true" />
|
||||||
<div
|
<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="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'"
|
:class="[
|
||||||
|
reduceMotion === 'reduce' ? '' : 'window-beam--move',
|
||||||
|
homeUnlocked ? 'opacity-100' : '',
|
||||||
|
]"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -80,9 +84,13 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative flex shrink-0 flex-col items-center">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="relative flex shrink-0 flex-col items-center rounded-[1.4rem] px-1 py-1 transition hover:-translate-y-0.5"
|
||||||
|
@click="homeUnlocked = !homeUnlocked"
|
||||||
|
>
|
||||||
<div class="h-9 w-1 rounded-full bg-amber-700/55" aria-hidden="true" />
|
<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="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" :class="homeUnlocked ? 'scale-[1.03]' : ''">
|
||||||
<div class="absolute bottom-4 h-7 w-7 rounded-full bg-[#f7d34b] shadow-inner ring-2 ring-[#c99113]" />
|
<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-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 top-8 h-7 w-12 rounded-b-[1rem] rounded-t-[0.5rem] bg-white" />
|
||||||
@@ -90,9 +98,9 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
<div class="absolute right-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>
|
</div>
|
||||||
<p class="mt-2 text-[10px] font-medium tracking-[0.2em] text-blue-700/75">
|
<p class="mt-2 text-[10px] font-medium tracking-[0.2em] text-blue-700/75">
|
||||||
钥匙圈
|
{{ homeUnlocked ? '门开啦' : '钥匙圈' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,6 +114,10 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
{{ tag }}
|
{{ tag }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 rounded-[1.25rem] border border-amber-200/70 bg-white/85 px-4 py-3 text-center text-sm leading-relaxed text-amber-950/82">
|
||||||
|
{{ homeUnlocked ? '钥匙轻轻一转,这个“共同生活”的按钮就真的按下去了。' : '点一下钥匙圈,试着把这间小屋先打开。' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePreferredReducedMotion } from '@vueuse/core'
|
import { usePreferredReducedMotion } from '@vueuse/core'
|
||||||
import { computed } from 'vue'
|
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||||
import type { Memory } from '../api/client'
|
import type { Memory } from '../api/client'
|
||||||
import { getStandardNepalTheme } from '../constants/nepalThemes'
|
import { getStandardNepalTheme } from '../constants/nepalThemes'
|
||||||
import PolaroidCard from './PolaroidCard.vue'
|
import PolaroidCard from './PolaroidCard.vue'
|
||||||
@@ -13,6 +13,61 @@ const props = defineProps<{
|
|||||||
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
||||||
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
||||||
const reduceMotion = usePreferredReducedMotion()
|
const reduceMotion = usePreferredReducedMotion()
|
||||||
|
const ikeaItems = ['热狗', '桌子', '盘子', '杯子']
|
||||||
|
const cartStage = ref(0)
|
||||||
|
const flyingItem = ref<string | null>(null)
|
||||||
|
const flightKey = ref(0)
|
||||||
|
const newestItem = ref<string | null>(null)
|
||||||
|
let flightTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
|
const activeItems = computed(() => ikeaItems.slice(0, cartStage.value))
|
||||||
|
const cartVisualItems = computed(() =>
|
||||||
|
activeItems.value.map((item, index) => ({
|
||||||
|
item,
|
||||||
|
style: {
|
||||||
|
left: `${14 + index * 14}%`,
|
||||||
|
bottom: `${40 + (index % 2) * 12}px`,
|
||||||
|
transform: `rotate(${index % 2 === 0 ? -8 : 7}deg)`,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
function triggerFlight(item: string) {
|
||||||
|
newestItem.value = item
|
||||||
|
if (reduceMotion.value === 'reduce') return
|
||||||
|
if (flightTimer) clearTimeout(flightTimer)
|
||||||
|
flightKey.value += 1
|
||||||
|
flyingItem.value = item
|
||||||
|
flightTimer = setTimeout(() => {
|
||||||
|
flyingItem.value = null
|
||||||
|
flightTimer = undefined
|
||||||
|
}, 820)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCartStage(nextStage: number, item?: string) {
|
||||||
|
cartStage.value = nextStage
|
||||||
|
if (item) triggerFlight(item)
|
||||||
|
else if (nextStage === 0) newestItem.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function advanceCart() {
|
||||||
|
if (cartStage.value >= ikeaItems.length) {
|
||||||
|
setCartStage(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const item = ikeaItems[cartStage.value]
|
||||||
|
setCartStage(cartStage.value + 1, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseItem(index: number) {
|
||||||
|
const nextStage = index + 1
|
||||||
|
const item = ikeaItems[index]
|
||||||
|
setCartStage(nextStage, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (flightTimer) clearTimeout(flightTimer)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -60,6 +115,15 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
<div class="absolute right-5 top-4 rounded-full bg-[#f8d548] px-3 py-1 text-[10px] font-black tracking-[0.24em] text-[#0c4ca9]">
|
<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
|
SALE
|
||||||
</div>
|
</div>
|
||||||
|
<Transition name="ikea-fly" mode="out-in">
|
||||||
|
<div
|
||||||
|
v-if="flyingItem"
|
||||||
|
:key="`${flyingItem}-${flightKey}`"
|
||||||
|
class="pointer-events-none absolute left-8 top-[5.8rem] z-30 rounded-full border border-indigo-200/80 bg-white/96 px-3 py-1.5 text-[11px] font-semibold text-indigo-950 shadow-[0_12px_22px_rgba(79,70,229,0.18)]"
|
||||||
|
>
|
||||||
|
{{ flyingItem }}
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<div class="mt-10 grid gap-4 sm:grid-cols-[1fr_0.95fr] sm:items-end">
|
<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="rounded-[1.4rem] border border-white/70 bg-white/80 p-4 shadow-[0_12px_24px_rgba(12,90,219,0.12)]">
|
||||||
@@ -74,9 +138,16 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
Cart List
|
Cart List
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-2 flex flex-wrap gap-2 text-[11px] text-indigo-950/88">
|
<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>
|
<button
|
||||||
<span class="rounded-full bg-[#dbeafe] px-2.5 py-1">地毯</span>
|
v-for="item in ikeaItems"
|
||||||
<span class="rounded-full bg-[#dbeafe] px-2.5 py-1">热狗</span>
|
:key="item"
|
||||||
|
type="button"
|
||||||
|
class="rounded-full px-2.5 py-1 transition"
|
||||||
|
:class="activeItems.includes(item) ? 'bg-[#93c5fd] text-indigo-950' : 'bg-[#dbeafe] hover:bg-[#c5dcff]'"
|
||||||
|
@click="chooseItem(ikeaItems.indexOf(item))"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,6 +157,9 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
class="relative mx-auto h-32 w-[180px]"
|
class="relative mx-auto h-32 w-[180px]"
|
||||||
:class="reduceMotion === 'reduce' ? '' : 'ikea-cart-bob'"
|
:class="reduceMotion === 'reduce' ? '' : 'ikea-cart-bob'"
|
||||||
>
|
>
|
||||||
|
<div class="absolute right-0 top-0 rounded-full bg-[#0c5adb] px-2.5 py-1 text-[10px] font-bold text-white shadow-lg">
|
||||||
|
{{ activeItems.length }}
|
||||||
|
</div>
|
||||||
<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-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-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-12 left-20 h-6 w-10 rounded-full bg-[#ff7a59] shadow-[0_8px_16px_rgba(255,122,89,0.22)]" />
|
||||||
@@ -95,19 +169,49 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
<div class="absolute bottom-3 left-24 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-[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 class="absolute bottom-[5.1rem] left-[7rem] h-8 w-1.5 rounded-full bg-[#0c4ca9]" />
|
||||||
|
<div
|
||||||
|
v-for="entry in cartVisualItems"
|
||||||
|
:key="entry.item"
|
||||||
|
class="absolute rounded-full border border-white/75 bg-[#fff7cf] px-2.5 py-1 text-[10px] font-semibold text-[#0c4ca9] shadow-[0_8px_16px_rgba(12,90,219,0.16)] transition-all"
|
||||||
|
:class="newestItem === entry.item ? 'ring-2 ring-[#f8d548]/80' : ''"
|
||||||
|
:style="entry.style"
|
||||||
|
>
|
||||||
|
{{ entry.item }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex flex-wrap justify-center gap-2">
|
<div class="mt-4 flex flex-wrap justify-center gap-2">
|
||||||
<span
|
<span
|
||||||
v-for="tag in ['推车里的碎片', '未来会再提起', '一起逛生活']"
|
v-for="tag in ['热狗和餐具', '一起挑桌子', '一起逛生活']"
|
||||||
:key="tag"
|
: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"
|
class="rounded-full border border-indigo-200 bg-white/88 px-3 py-1.5 text-[11px] font-medium text-indigo-900/82"
|
||||||
>
|
>
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex flex-col items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border border-indigo-200 bg-white/92 px-4 py-2 text-xs font-semibold tracking-[0.18em] text-indigo-950 transition hover:bg-indigo-50"
|
||||||
|
@click="advanceCart"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
cartStage >= ikeaItems.length
|
||||||
|
? '再逛一圈'
|
||||||
|
: `把${ikeaItems[cartStage]}放进推车`
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
<p class="text-center text-[11px] leading-relaxed text-indigo-900/70">
|
||||||
|
{{
|
||||||
|
activeItems.length
|
||||||
|
? `推车里现在有:${activeItems.join('、')}`
|
||||||
|
: '轻点一下,把今天想带走的小碎片先放进去。'
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,6 +237,32 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
animation: ikea-cart-bob 4.6s ease-in-out infinite;
|
animation: ikea-cart-bob 4.6s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ikea-fly-enter-active {
|
||||||
|
animation: ikea-fly-into-cart 0.8s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ikea-fly-leave-active {
|
||||||
|
transition: opacity 0.14s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ikea-fly-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ikea-fly-into-cart {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(0, 0) scale(0.88) rotate(-6deg);
|
||||||
|
}
|
||||||
|
16% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(168px, 92px) scale(0.74) rotate(11deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes ikea-cart-bob {
|
@keyframes ikea-cart-bob {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePreferredReducedMotion } from '@vueuse/core'
|
import { usePreferredReducedMotion } from '@vueuse/core'
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import type { Memory } from '../api/client'
|
import type { Memory } from '../api/client'
|
||||||
import { getStandardNepalTheme } from '../constants/nepalThemes'
|
import { getStandardNepalTheme } from '../constants/nepalThemes'
|
||||||
import PolaroidCard from './PolaroidCard.vue'
|
import PolaroidCard from './PolaroidCard.vue'
|
||||||
@@ -13,6 +13,7 @@ const props = defineProps<{
|
|||||||
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
||||||
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
||||||
const reduceMotion = usePreferredReducedMotion()
|
const reduceMotion = usePreferredReducedMotion()
|
||||||
|
const focusDetail = ref<'orange' | 'figure'>('orange')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -93,9 +94,23 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
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"
|
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">
|
<div class="flex flex-wrap justify-center gap-2 text-[10px] font-semibold text-orange-800/80">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full px-2.5 py-1 transition"
|
||||||
|
:class="focusDetail === 'orange' ? 'bg-orange-200 text-orange-950' : 'bg-orange-100 hover:bg-orange-150'"
|
||||||
|
@click="focusDetail = 'orange'"
|
||||||
|
>
|
||||||
|
橙C
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full px-2.5 py-1 transition"
|
||||||
|
:class="focusDetail === 'figure' ? 'bg-sky-200 text-sky-950' : 'bg-orange-100 hover:bg-orange-150'"
|
||||||
|
@click="focusDetail = 'figure'"
|
||||||
|
>
|
||||||
|
灵笼
|
||||||
|
</button>
|
||||||
<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">咖啡</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>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,14 +123,24 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
Receipt
|
Receipt
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-3 space-y-2.5 text-sm text-slate-700">
|
<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">
|
<button
|
||||||
<span>生椰拿铁</span>
|
type="button"
|
||||||
<span class="font-mono text-slate-500">x 1</span>
|
class="flex w-full items-center justify-between rounded-xl border-b border-dashed pb-2 text-left transition"
|
||||||
</div>
|
:class="focusDetail === 'figure' ? 'border-sky-200 bg-sky-50/70 px-2 pt-1 text-sky-900' : 'border-orange-200'"
|
||||||
<div class="flex items-center justify-between border-b border-dashed border-orange-200 pb-2">
|
@click="focusDetail = 'figure'"
|
||||||
|
>
|
||||||
|
<span>桌角灵笼</span>
|
||||||
|
<span class="font-mono text-slate-500">noticed</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex w-full items-center justify-between rounded-xl border-b border-dashed pb-2 text-left transition"
|
||||||
|
:class="focusDetail === 'orange' ? 'border-orange-200 bg-orange-50/70 px-2 pt-1 text-orange-950' : 'border-orange-200'"
|
||||||
|
@click="focusDetail = 'orange'"
|
||||||
|
>
|
||||||
<span>橙 C 美式</span>
|
<span>橙 C 美式</span>
|
||||||
<span class="font-mono text-slate-500">x 1</span>
|
<span class="font-mono text-slate-500">remembered</span>
|
||||||
</div>
|
</button>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span>记住你的喜好</span>
|
<span>记住你的喜好</span>
|
||||||
<span class="font-brush text-xl text-orange-950">1 次</span>
|
<span class="font-brush text-xl text-orange-950">1 次</span>
|
||||||
@@ -124,14 +149,19 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
</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)]"
|
class="relative overflow-hidden rounded-[1.6rem] border border-slate-200/70 px-4 py-4 shadow-[0_15px_30px_rgba(15,23,42,0.18)]"
|
||||||
|
:class="focusDetail === 'orange' ? 'bg-slate-900 text-slate-100' : 'bg-sky-950 text-slate-100'"
|
||||||
>
|
>
|
||||||
<div class="absolute -right-8 -top-8 h-24 w-24 rounded-full bg-blue-400/20 blur-2xl" aria-hidden="true" />
|
<div class="absolute -right-8 -top-8 h-24 w-24 rounded-full blur-2xl" :class="focusDetail === 'orange' ? 'bg-blue-400/20' : 'bg-sky-300/20'" aria-hidden="true" />
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-[0.28em] text-blue-200/65">
|
<p class="text-[10px] font-semibold uppercase tracking-[0.28em]" :class="focusDetail === 'orange' ? 'text-blue-200/65' : 'text-sky-200/70'">
|
||||||
Desk Corner
|
{{ focusDetail === 'orange' ? 'Orange C' : 'Desk Corner' }}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-sm leading-relaxed text-slate-200/88">
|
<p class="mt-2 text-sm leading-relaxed text-slate-200/88">
|
||||||
你连桌角的小小“灵笼”都看见了。喜欢有时候不是一句话,是被认真注意到。
|
{{
|
||||||
|
focusDetail === 'orange'
|
||||||
|
? '你记住了我会点橙C。喜欢有时候不是轰轰烈烈,是被记住这一点点。'
|
||||||
|
: '你连桌角的小小“灵笼”都看见了。喜欢有时候不是一句话,是被认真注意到。'
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ import FissurePage from './FissurePage.vue'
|
|||||||
import HeartWindowPage from './HeartWindowPage.vue'
|
import HeartWindowPage from './HeartWindowPage.vue'
|
||||||
import HomeRentalPage from './HomeRentalPage.vue'
|
import HomeRentalPage from './HomeRentalPage.vue'
|
||||||
import IkeaDatePage from './IkeaDatePage.vue'
|
import IkeaDatePage from './IkeaDatePage.vue'
|
||||||
|
import LaborDayPage from './LaborDayPage.vue'
|
||||||
import LuckinCoffeePage from './LuckinCoffeePage.vue'
|
import LuckinCoffeePage from './LuckinCoffeePage.vue'
|
||||||
|
import QuanjiPage from './QuanjiPage.vue'
|
||||||
import ReconcilePage from './ReconcilePage.vue'
|
import ReconcilePage from './ReconcilePage.vue'
|
||||||
import StandardNepalPage from './StandardNepalPage.vue'
|
import StandardNepalPage from './StandardNepalPage.vue'
|
||||||
|
import TaxiHandHoldPage from './TaxiHandHoldPage.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
memories: Memory[]
|
memories: Memory[]
|
||||||
@@ -49,6 +52,9 @@ const pageComponent = computed(() => {
|
|||||||
20260423: LuckinCoffeePage,
|
20260423: LuckinCoffeePage,
|
||||||
20260424: FirstCallPage,
|
20260424: FirstCallPage,
|
||||||
20260430: FirstMeetTrainPage,
|
20260430: FirstMeetTrainPage,
|
||||||
|
20260431: TaxiHandHoldPage,
|
||||||
|
20260432: QuanjiPage,
|
||||||
|
20260501: LaborDayPage,
|
||||||
20260503: FissurePage,
|
20260503: FissurePage,
|
||||||
20260504: ReconcilePage,
|
20260504: ReconcilePage,
|
||||||
20260508: HomeRentalPage,
|
20260508: HomeRentalPage,
|
||||||
@@ -68,6 +74,9 @@ const pageComponent = computed(() => {
|
|||||||
return memory.event_title.includes('电话') ? FirstCallPage : LuckinCoffeePage
|
return memory.event_title.includes('电话') ? FirstCallPage : LuckinCoffeePage
|
||||||
}
|
}
|
||||||
if (t === 'first_meet_train') return FirstMeetTrainPage
|
if (t === 'first_meet_train') return FirstMeetTrainPage
|
||||||
|
if (t === 'first_meet_taix') return TaxiHandHoldPage
|
||||||
|
if (t === 'first_meet_quanji') return QuanjiPage
|
||||||
|
if (t === 'make_love') return LaborDayPage
|
||||||
return StandardNepalPage
|
return StandardNepalPage
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -180,6 +189,8 @@ function next() {
|
|||||||
.story-fissure-leave-active,
|
.story-fissure-leave-active,
|
||||||
.story-dawn-enter-active,
|
.story-dawn-enter-active,
|
||||||
.story-dawn-leave-active,
|
.story-dawn-leave-active,
|
||||||
|
.story-silk-enter-active,
|
||||||
|
.story-silk-leave-active,
|
||||||
.story-blush-enter-active,
|
.story-blush-enter-active,
|
||||||
.story-blush-leave-active,
|
.story-blush-leave-active,
|
||||||
.story-mint-enter-active,
|
.story-mint-enter-active,
|
||||||
@@ -245,6 +256,18 @@ function next() {
|
|||||||
filter: brightness(0.92);
|
filter: brightness(0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 亲密页 · 被单与呼吸感 */
|
||||||
|
.story-silk-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.96);
|
||||||
|
filter: blur(2px) saturate(1.08);
|
||||||
|
}
|
||||||
|
.story-silk-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.03);
|
||||||
|
filter: blur(1px) brightness(1.04);
|
||||||
|
}
|
||||||
|
|
||||||
/* 别扭 · 粉雾 */
|
/* 别扭 · 粉雾 */
|
||||||
.story-blush-enter-from {
|
.story-blush-enter-from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePreferredReducedMotion } from '@vueuse/core'
|
import { usePreferredReducedMotion } from '@vueuse/core'
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import type { Memory } from '../api/client'
|
import type { Memory } from '../api/client'
|
||||||
import { getStandardNepalTheme } from '../constants/nepalThemes'
|
import { getStandardNepalTheme } from '../constants/nepalThemes'
|
||||||
import PolaroidCard from './PolaroidCard.vue'
|
import PolaroidCard from './PolaroidCard.vue'
|
||||||
@@ -13,6 +13,7 @@ const props = defineProps<{
|
|||||||
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
const bodyHtml = computed(() => renderMarkdown(props.memory.main_text))
|
||||||
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
const theme = computed(() => getStandardNepalTheme(props.memory.nepal_art_style))
|
||||||
const reduceMotion = usePreferredReducedMotion()
|
const reduceMotion = usePreferredReducedMotion()
|
||||||
|
const talkedItOut = ref(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -58,7 +59,10 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
<div class="grid gap-3 sm:grid-cols-[0.92fr_0.16fr_0.92fr] sm:items-center">
|
<div class="grid gap-3 sm:grid-cols-[0.92fr_0.16fr_0.92fr] sm:items-center">
|
||||||
<div
|
<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="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'"
|
:class="[
|
||||||
|
reduceMotion === 'reduce' ? '' : 'reconcile-card-left',
|
||||||
|
talkedItOut ? 'sm:translate-x-2' : '',
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-[0.25em] text-rose-700/65">
|
<p class="text-[10px] font-semibold uppercase tracking-[0.25em] text-rose-700/65">
|
||||||
Before
|
Before
|
||||||
@@ -69,7 +73,13 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
<div class="relative flex items-center justify-center py-2">
|
<div class="relative flex items-center justify-center py-2">
|
||||||
<div
|
<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="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'"
|
:class="[
|
||||||
|
reduceMotion === 'reduce' ? '' : 'reconcile-heartbeat',
|
||||||
|
talkedItOut ? 'scale-110' : '',
|
||||||
|
]"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="talkedItOut = !talkedItOut"
|
||||||
>
|
>
|
||||||
♡
|
♡
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +88,10 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
|
|
||||||
<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="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'"
|
:class="[
|
||||||
|
reduceMotion === 'reduce' ? '' : 'reconcile-card-right',
|
||||||
|
talkedItOut ? 'sm:-translate-x-2' : '',
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-[0.25em] text-cyan-700/65">
|
<p class="text-[10px] font-semibold uppercase tracking-[0.25em] text-cyan-700/65">
|
||||||
After
|
After
|
||||||
@@ -98,6 +111,19 @@ const reduceMotion = usePreferredReducedMotion()
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex flex-col items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border border-emerald-200 bg-white/92 px-4 py-2 text-xs font-semibold tracking-[0.18em] text-emerald-900 transition hover:bg-emerald-50"
|
||||||
|
@click="talkedItOut = !talkedItOut"
|
||||||
|
>
|
||||||
|
{{ talkedItOut ? '先把话收回一点' : '把话说开' }}
|
||||||
|
</button>
|
||||||
|
<p class="text-center text-[11px] leading-relaxed text-emerald-900/70">
|
||||||
|
{{ talkedItOut ? '说开以后,卡在中间的那口气终于顺了。' : '轻点一下,让“Before”和“After”靠近一点。' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type StoryTransitionName =
|
|||||||
| 'story-heart'
|
| 'story-heart'
|
||||||
| 'story-fissure'
|
| 'story-fissure'
|
||||||
| 'story-dawn'
|
| 'story-dawn'
|
||||||
|
| 'story-silk'
|
||||||
| 'story-blush'
|
| 'story-blush'
|
||||||
| 'story-mint'
|
| 'story-mint'
|
||||||
| 'story-saffron'
|
| 'story-saffron'
|
||||||
@@ -43,7 +44,10 @@ export function getStoryTransitionName(m: Memory | null): StoryTransitionName {
|
|||||||
if (t === 'heart_window') return 'story-heart'
|
if (t === 'heart_window') return 'story-heart'
|
||||||
if (t === 'fissure_page') return 'story-fissure'
|
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' || t === 'first_meet_ju' || t === 'first_meet_rui') return 'story-night'
|
||||||
if (t === 'first_meet_train') return 'story-dawn'
|
if (t === 'first_meet_train' || t === 'first_meet_taix' || t === 'first_meet_quanji') {
|
||||||
|
return 'story-dawn'
|
||||||
|
}
|
||||||
|
if (t === 'make_love') return 'story-silk'
|
||||||
return transitionForStandardStyle(m.nepal_art_style)
|
return transitionForStandardStyle(m.nepal_art_style)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,9 +66,12 @@ export function getStoryShellClasses(m: Memory | null): string {
|
|||||||
if (t === 'first_meet' || t === 'first_meet_ju' || t === 'first_meet_rui') {
|
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'
|
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') {
|
if (t === 'first_meet_train' || t === 'first_meet_taix' || t === 'first_meet_quanji') {
|
||||||
return 'border-amber-200/50 shadow-[0_24px_72px_-22px_rgba(234,88,12,0.24)] ring-amber-100/35'
|
return 'border-amber-200/50 shadow-[0_24px_72px_-22px_rgba(234,88,12,0.24)] ring-amber-100/35'
|
||||||
}
|
}
|
||||||
|
if (t === 'make_love') {
|
||||||
|
return 'border-rose-200/45 shadow-[0_24px_72px_-22px_rgba(190,80,120,0.28)] ring-rose-100/25'
|
||||||
|
}
|
||||||
const shells: Partial<Record<NepalArtStyle, string>> = {
|
const shells: Partial<Record<NepalArtStyle, string>> = {
|
||||||
dawn_meet:
|
dawn_meet:
|
||||||
'border-amber-200/45 shadow-[0_22px_68px_-22px_rgba(217,119,6,0.22)] ring-amber-100/30',
|
'border-amber-200/45 shadow-[0_22px_68px_-22px_rgba(217,119,6,0.22)] ring-amber-100/30',
|
||||||
@@ -98,7 +105,10 @@ export function getStoryNavAccentClass(m: Memory | null): string {
|
|||||||
if (t === 'first_meet' || t === 'first_meet_ju' || t === 'first_meet_rui') {
|
if (t === 'first_meet' || t === 'first_meet_ju' || t === 'first_meet_rui') {
|
||||||
return 'hover:border-slate-400/45 hover:bg-slate-100/85'
|
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'
|
if (t === 'first_meet_train' || t === 'first_meet_taix' || t === 'first_meet_quanji') {
|
||||||
|
return 'hover:border-amber-300/50 hover:bg-amber-50/80'
|
||||||
|
}
|
||||||
|
if (t === 'make_love') return 'hover:border-rose-300/50 hover:bg-rose-50/80'
|
||||||
const accents: Partial<Record<NepalArtStyle, string>> = {
|
const accents: Partial<Record<NepalArtStyle, string>> = {
|
||||||
dawn_meet: 'hover:border-amber-300/50 hover:bg-amber-50/80',
|
dawn_meet: 'hover:border-amber-300/50 hover:bg-amber-50/80',
|
||||||
blush_argue: 'hover:border-rose-300/45 hover:bg-rose-50/75',
|
blush_argue: 'hover:border-rose-300/45 hover:bg-rose-50/75',
|
||||||
|
|||||||
Reference in New Issue
Block a user