Files
mwy/frontend/src/views/OurStory.vue
flower_linux 6cc3fdf411 mwy2.0
2026-05-20 16:31:07 +08:00

141 lines
4.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { RefreshCw } from 'lucide-vue-next'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { fetchMemories, fetchStatus, type Memory, type StatusPayload } from '../api/client'
import MessageForm from '../components/MessageForm.vue'
import NepalChapterBand from '../components/NepalChapterBand.vue'
import NepalPageContainer from '../components/NepalPageContainer.vue'
import TogetherCounter from '../components/TogetherCounter.vue'
const status = ref<StatusPayload | null>(null)
const memories = ref<Memory[]>([])
const loadError = ref<string | null>(null)
const memoriesError = ref<string | null>(null)
const busy = ref(true)
const memoriesBusy = ref(false)
const serverOffsetMs = ref(0)
let statusPoll: ReturnType<typeof setInterval> | undefined
const siteTitle = computed(() => status.value?.site_title ?? 'Nepal Journey: Our Story')
const showAppChrome = computed(() => !busy.value && status.value !== null)
function applyServerClock(s: StatusPayload) {
serverOffsetMs.value = Date.parse(s.server_time) - Date.now()
status.value = s
}
async function loadStatus() {
try {
loadError.value = null
const s = await fetchStatus()
applyServerClock(s)
} catch {
loadError.value = '无法连接服务器,请确认后端已启动。'
} finally {
busy.value = false
}
}
async function loadMemories() {
memoriesBusy.value = true
memoriesError.value = null
try {
memories.value = await fetchMemories()
} catch {
memoriesError.value = '回忆加载失败,请稍后重试。'
} finally {
memoriesBusy.value = false
}
}
onMounted(async () => {
await loadStatus()
if (!loadError.value) await loadMemories()
statusPoll = setInterval(() => {
void loadStatus()
}, 60_000)
})
onBeforeUnmount(() => {
if (statusPoll) clearInterval(statusPoll)
})
/** 顶栏计时器与章节纪念日期轮播(按 story_order */
const counterMilestones = computed(() =>
[...memories.value]
.sort((a, b) => a.story_order - b.story_order)
.map((m) => ({ title: m.event_title, date: m.date })),
)
</script>
<template>
<div class="relative min-h-dvh pb-8">
<div
v-if="busy"
class="flex min-h-dvh items-center justify-center text-ink-muted text-sm"
>
正在打开手帐
</div>
<template v-else>
<header
v-if="showAppChrome && status"
class="sticky top-0 z-40 border-b border-lokta-deep/10 bg-gradient-to-r from-lokta-cream/90 via-paper/90 to-lokta-teal/15 backdrop-blur-md"
>
<div class="mx-auto max-w-3xl px-4 py-3 sm:px-6">
<div class="min-w-0">
<p class="truncate font-brush text-2xl text-lokta-deep sm:text-3xl">
{{ siteTitle }}
</p>
<TogetherCounter
:together-since-iso="status.together_since"
:server-offset-ms="serverOffsetMs"
:milestones="counterMilestones"
/>
</div>
</div>
</header>
<div v-if="loadError" class="mx-auto max-w-md px-6 py-10 text-center">
<p class="text-sm text-rose-deep">
{{ loadError }}
</p>
<button
type="button"
class="mt-4 inline-flex items-center gap-2 rounded-full border border-paper-shadow bg-white px-4 py-2 text-sm text-ink shadow-sm transition hover:bg-paper-deep"
@click="loadStatus"
>
<RefreshCw class="h-4 w-4" />
重试
</button>
</div>
<template v-else-if="status">
<div v-if="memoriesBusy" class="py-16 text-center text-sm text-ink-muted">
正在载入故事页
</div>
<p v-else-if="memoriesError" class="py-8 text-center text-sm text-rose-deep">
{{ memoriesError }}
</p>
<p
v-else-if="!memories.length"
class="mx-auto max-w-md px-6 py-16 text-center text-sm text-ink-muted"
>
还没有任何故事页请在项目目录运行
<code class="mt-2 block rounded bg-paper-deep px-2 py-1 text-xs text-ink">python manage.py seed_nepal_story</code>
</p>
<template v-else>
<NepalPageContainer :memories="memories" :status="status" />
<div class="mx-auto max-w-3xl px-2">
<NepalChapterBand label="最后一页 · 回信" />
<MessageForm />
</div>
</template>
</template>
</template>
</div>
</template>