141 lines
4.4 KiB
Vue
141 lines
4.4 KiB
Vue
<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>
|