Files
mwy/frontend/src/views/OurStory.vue

141 lines
4.4 KiB
Vue
Raw Normal View History

2026-05-11 19:34:35 +08:00
<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"
>
2026-05-20 16:31:07 +08:00
<div class="mx-auto max-w-3xl px-4 py-3 sm:px-6">
2026-05-11 19:34:35 +08:00
<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>