127 lines
5.7 KiB
Vue
127 lines
5.7 KiB
Vue
<!-- 页面编号:F-05;用途:谱文详情。 -->
|
||
<template>
|
||
<view class="article-detail-page" :class="`article-state--${articleState}`">
|
||
<ModulePageBackground module="family" />
|
||
<view class="article-detail-header"><PageHeader title="谱文详情" custom-back @back="backToArticles" /></view>
|
||
|
||
<view class="article-detail-content">
|
||
<AppLoading v-if="articleState === 'loading'" text="正在读取谱文" description="请稍候,正在同步谱文正文。" />
|
||
<view v-else-if="articleState === 'ready'" class="article-card">
|
||
<text v-if="article.category" class="article-card__category">{{ article.category }}</text>
|
||
<text class="article-card__title">{{ article.title }}</text>
|
||
<text class="article-card__meta">{{ article.author }} · {{ article.time || '未标注时间' }}</text>
|
||
<text v-if="article.summary" class="article-card__summary">{{ article.summary }}</text>
|
||
<view class="article-card__divider" />
|
||
<text class="article-card__content">{{ article.content || '作者暂未填写正文。' }}</text>
|
||
<text class="article-card__views">阅读 {{ article.viewCount }} 次</text>
|
||
</view>
|
||
<view v-else class="article-state-card">
|
||
<text>{{ stateCopy.title }}</text>
|
||
<text>{{ stateCopy.copy }}</text>
|
||
<AppButton block :label="stateCopy.action" @click="handleStateAction" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref } from "vue";
|
||
import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
||
import AppButton from "@/components/AppButton.vue";
|
||
import AppLoading from "@/components/AppLoading.vue";
|
||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||
import PageHeader from "@/components/PageHeader.vue";
|
||
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||
import { goBack, returnTo } from "@/utils/navigation.js";
|
||
|
||
const genealogyId = ref("");
|
||
const articleId = ref("");
|
||
const articleState = ref("loading");
|
||
const article = ref(null);
|
||
const controller = createRequestController();
|
||
let pageActive = true;
|
||
|
||
const hasValidContext = computed(() =>
|
||
/^[1-9]\d*$/.test(genealogyId.value) && /^[1-9]\d*$/.test(articleId.value),
|
||
);
|
||
const stateCopy = computed(() => {
|
||
if (!hasValidContext.value) {
|
||
return {
|
||
title: "谱文入口无效",
|
||
copy: "没有取得有效家谱或文章标识,页面不会展示其他谱文。",
|
||
action: "返回上一页",
|
||
};
|
||
}
|
||
if (articleState.value === "missing") {
|
||
return {
|
||
title: "该谱文已不存在",
|
||
copy: "它可能已被作者删除,或当前账号已不再拥有查看权限。",
|
||
action: "返回谱文列表",
|
||
};
|
||
}
|
||
return {
|
||
title: "谱文暂时无法读取",
|
||
copy: "请检查网络后重新读取;本页不会替换为其他谱文。",
|
||
action: "重新读取",
|
||
};
|
||
});
|
||
|
||
const loadArticle = async () => {
|
||
if (!hasValidContext.value) {
|
||
articleState.value = "invalid";
|
||
return;
|
||
}
|
||
articleState.value = "loading";
|
||
try {
|
||
const current = await appApi.getArticleDetail(genealogyId.value, articleId.value, { requestController: controller });
|
||
if (!pageActive) return;
|
||
article.value = current;
|
||
articleState.value = "ready";
|
||
} catch (error) {
|
||
if (!pageActive || isRequestCancelled(error)) return;
|
||
articleState.value = error?.code === "HTTP_ERROR" && error?.httpStatus === 404 ? "missing" : "error";
|
||
}
|
||
};
|
||
|
||
onLoad((query) => {
|
||
genealogyId.value = String(query?.genealogyId || "");
|
||
articleId.value = String(query?.articleId || "");
|
||
void loadArticle();
|
||
});
|
||
onShow(() => {
|
||
if (hasValidContext.value) void loadArticle();
|
||
});
|
||
onUnload(() => {
|
||
pageActive = false;
|
||
controller.abort();
|
||
});
|
||
|
||
const backToArticles = () => hasValidContext.value
|
||
? returnTo("F04", { genealogyId: genealogyId.value })
|
||
: goBack();
|
||
const handleStateAction = () => articleState.value === "error" ? loadArticle() : backToArticles();
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||
|
||
.article-detail-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||
.article-detail-header, .article-detail-content { z-index: 1; }
|
||
.article-detail-content { flex: 1; padding: 18rpx 24rpx 72rpx; }
|
||
.article-card, .article-state-card { @include adaptive.adaptive-family-content; box-sizing: border-box; }
|
||
.article-card { margin-top: 18rpx; padding: 34rpx 30rpx; }
|
||
.article-card__category, .article-card__title, .article-card__meta, .article-card__summary, .article-card__content, .article-card__views { display: block; }
|
||
.article-card__category { color: $brand-red; font-size: 23rpx; font-weight: 700; }
|
||
.article-card__title { margin-top: 14rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 40rpx; font-weight: 700; line-height: 1.32; }
|
||
.article-card__meta { margin-top: 16rpx; color: $ink-muted; font-size: 22rpx; }
|
||
.article-card__summary { margin-top: 22rpx; color: $ink-muted; font-size: 25rpx; line-height: 1.6; }
|
||
.article-card__divider { height: 1rpx; margin: 26rpx 0; background: rgba(128, 89, 49, .22); }
|
||
.article-card__content { color: $ink; font-size: 28rpx; line-height: 1.85; white-space: pre-wrap; }
|
||
.article-card__views { margin-top: 30rpx; color: $ink-muted; font-size: 21rpx; text-align: right; }
|
||
.article-state-card { min-height: 350rpx; margin-top: 36rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
|
||
.article-state-card > text { display: block; }
|
||
.article-state-card > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
|
||
.article-state-card > text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
|
||
.article-state-card .app-button { margin-top: 28rpx; }
|
||
</style>
|