233 lines
6.1 KiB
Vue
233 lines
6.1 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">
|
||
@import "../../styles/adaptive-frame-profiles.scss";
|
||
|
||
.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-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: clamp(14px, 23rpx, 17px);
|
||
font-weight: 700;
|
||
}
|
||
.article-card__title {
|
||
margin-top: 14rpx;
|
||
color: $ink;
|
||
font-family: STKaiti, KaiTi, serif;
|
||
font-size: clamp(20px, 40rpx, 26px);
|
||
font-weight: 700;
|
||
line-height: 1.32;
|
||
}
|
||
.article-card__meta {
|
||
margin-top: 16rpx;
|
||
color: $ink-muted;
|
||
font-size: clamp(14px, 22rpx, 17px);
|
||
}
|
||
.article-card__summary {
|
||
margin-top: 22rpx;
|
||
color: $ink-muted;
|
||
font-size: clamp(15px, 25rpx, 18px);
|
||
line-height: 1.6;
|
||
}
|
||
.article-card__divider {
|
||
height: 1rpx;
|
||
margin: 26rpx 0;
|
||
background: rgba(128, 89, 49, 0.22);
|
||
}
|
||
.article-card__content {
|
||
color: $ink;
|
||
font-size: clamp(16px, 28rpx, 20px);
|
||
line-height: 1.85;
|
||
white-space: pre-wrap;
|
||
}
|
||
.article-card__views {
|
||
margin-top: 30rpx;
|
||
color: $ink-muted;
|
||
font-size: clamp(13px, 21rpx, 16px);
|
||
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: clamp(19px, 35rpx, 24px);
|
||
font-weight: 700;
|
||
}
|
||
.article-state-card > text:nth-child(2) {
|
||
margin-top: 18rpx;
|
||
color: $ink-muted;
|
||
font-size: clamp(15px, 24rpx, 18px);
|
||
line-height: 1.65;
|
||
}
|
||
.article-state-card .app-button {
|
||
margin-top: 28rpx;
|
||
}
|
||
</style>
|