Files
jiapuapp/pages/family/site-home.vue
T
2026-09-13 17:45:52 +08:00

355 lines
9.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.
<template>
<view class="site-home-page">
<ModulePageBackground module="family" />
<PageHeader :root="!subpageMode" :custom-back="subpageMode" :title="headerTitle" />
<scroll-view class="site-home-scroll" scroll-y>
<view class="site-home-content" :class="{ 'site-home-content--root': !subpageMode }">
<view class="site-home-heading">
<text>{{ pageTitle }}</text>
<text>{{ pageDescription }}</text>
</view>
<AppLoading
v-if="articleState === 'loading'"
text="正在整理资讯"
description="请稍候,正在读取最新内容。"
/>
<view v-else-if="articleState === 'list'" class="site-article-list">
<button
v-for="article in articles"
:key="article.id"
class="site-article-card"
:aria-label="`${article.title},查看文章`"
@click="openArticle(article)"
>
<view class="site-article-card__meta">
<text>{{ article.typeLabel }}</text>
<text v-if="article.publishTime">{{ formatArticleDate(article.publishTime) }}</text>
</view>
<text class="site-article-card__title">{{ article.title }}</text>
<text v-if="article.summary" class="site-article-card__summary">
{{ article.summary }}
</text>
<text class="site-article-card__action">
{{ article.externalUrl ? "前往阅读" : "阅读全文" }}
</text>
</button>
</view>
<view v-else class="site-home-state">
<text>{{ articleState === "empty" ? "暂时没有资讯" : "资讯暂时无法读取" }}</text>
<text>{{ articleState === "empty" ? "新内容发布后会在这里展示。" : articleError }}</text>
<button v-if="articleState === 'error'" @click="loadArticles">重新加载</button>
</view>
<text v-if="openError" class="site-home-error" role="alert">{{ openError }}</text>
<AppPromotionStrip placement="home_bottom" title="更多内容" />
</view>
</scroll-view>
<AppDialog
:visible="Boolean(selectedArticle)"
:eyebrow="selectedArticle?.typeLabel || '传承资讯'"
:title="selectedArticle?.title || '文章详情'"
:message="selectedArticleContent"
confirm-text="关闭"
@confirm="closeArticle"
@close="closeArticle"
/>
<AppTabbar v-if="!subpageMode" active="family" />
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import AppDialog from "@/components/AppDialog.vue";
import AppLoading from "@/components/AppLoading.vue";
import AppPromotionStrip from "@/components/AppPromotionStrip.vue";
import AppTabbar from "@/components/AppTabbar.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
createRequestController,
isRequestCancelled,
} from "@/services/api/request-controller.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { siteContentApi } from "@/services/api/site-content-service.js";
import { openSiteContentTarget } from "@/utils/navigation/gateway.js";
const props = defineProps({
subpageMode: { type: Boolean, default: false },
});
const subpageMode = computed(() => props.subpageMode);
const articleState = ref("loading");
const articleType = ref("");
const pageTitle = ref("传承资讯");
const articleError = ref("");
const openError = ref("");
const articles = ref([]);
const selectedArticle = ref(null);
const articleRequestController = createRequestController();
let pageActive = true;
const decodeRouteText = (value) => {
const text = String(value || "").trim();
if (!text) return "";
try {
return decodeURIComponent(text);
} catch {
return text;
}
};
const headerTitle = computed(() => (subpageMode.value ? pageTitle.value : "代代相传"));
const pageDescription = computed(() =>
articleType.value === "notice"
? "平台发布的网站公告与服务通知"
: articleType.value === "news"
? "家谱文化、平台动态与最新资讯"
: "家谱文化、平台动态与实用文章",
);
const decodeArticleEntities = (value) =>
value
.replace(/&nbsp;/gi, " ")
.replace(/&amp;/gi, "&")
.replace(/&lt;/gi, "<")
.replace(/&gt;/gi, ">")
.replace(/&quot;/gi, '"')
.replace(/&#39;/gi, "'");
const articlePlainText = (value) =>
decodeArticleEntities(
String(value || "")
.replace(/<br\s*\/?>/gi, "\n")
.replace(/<\/(?:p|div|li|h[1-6])>/gi, "\n")
.replace(/<[^>]*>/g, ""),
)
.replace(/\n{3,}/g, "\n\n")
.trim();
const selectedArticleContent = computed(() =>
articlePlainText(selectedArticle.value?.content || selectedArticle.value?.summary) ||
"这篇文章暂时没有正文。",
);
const formatArticleDate = (value) => {
const text = String(value || "").trim();
return text.length >= 10 ? text.slice(0, 10) : text;
};
const loadArticles = async () => {
articleRequestController.abort();
articleState.value = "loading";
articleError.value = "";
try {
const rows = await siteContentApi.getSiteArticles({
limit: 20,
...(articleType.value ? { articleType: articleType.value } : {}),
requestController: articleRequestController,
});
if (!pageActive) return;
articles.value = rows;
articleState.value = rows.length ? "list" : "empty";
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
articleError.value = getRequestErrorMessage(error, "请稍后重新读取。");
articleState.value = "error";
}
};
const openArticle = async (article) => {
openError.value = "";
if (!article.externalUrl) {
selectedArticle.value = article;
return;
}
try {
await openSiteContentTarget(article.externalUrl, () => {
if (pageActive) openError.value = "外部文章暂时打不开,请稍后再试。";
});
} catch {
if (pageActive) openError.value = "外部文章暂时打不开,请稍后再试。";
}
};
const closeArticle = () => {
selectedArticle.value = null;
};
onLoad((query) => {
articleType.value = subpageMode.value && ["news", "notice"].includes(String(query?.articleType || ""))
? String(query.articleType)
: "";
pageTitle.value = subpageMode.value
? decodeRouteText(query?.title) ||
(articleType.value === "notice" ? "网站公告" : articleType.value === "news" ? "家谱新闻" : "文化传承文章")
: "传承资讯";
void loadArticles();
});
onUnload(() => {
pageActive = false;
articleRequestController.abort();
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.site-home-page {
display: flex;
height: 100vh;
min-height: 0;
flex-direction: column;
overflow: hidden;
background: $paper;
}
.site-home-scroll {
z-index: 1;
width: 100%;
height: 0;
min-height: 0;
flex: 1;
}
.site-home-content {
padding: 22rpx 24rpx calc(72rpx + env(safe-area-inset-bottom));
}
.site-home-content--root {
padding-bottom: calc(166rpx + env(safe-area-inset-bottom));
}
.site-home-heading text,
.site-article-card text,
.site-home-state text {
display: block;
}
.site-home-heading text:first-child {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(20px, 36rpx, 25px);
font-weight: 700;
}
.site-home-heading text:last-child {
margin-top: 6rpx;
color: $ink-muted;
font-size: clamp(14px, 22rpx, 17px);
}
.site-article-list {
margin-top: 22rpx;
}
.site-article-card {
@include adaptive.adaptive-family-letter;
display: block;
width: 100%;
min-height: 210rpx;
margin: 0 0 18rpx;
padding: 28rpx 30rpx;
border: 0;
background-color: rgba($paper, 0.86);
box-sizing: border-box;
color: $ink;
line-height: 1.5;
text-align: left;
}
.site-article-card::after {
border: 0;
}
.site-article-card__meta {
display: flex;
justify-content: space-between;
gap: 20rpx;
color: #946c48;
font-size: clamp(12px, 20rpx, 15px);
}
.site-article-card__title {
margin-top: 10rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(18px, 31rpx, 22px);
font-weight: 700;
}
.site-article-card__summary {
display: -webkit-box;
margin-top: 9rpx;
overflow: hidden;
color: $ink-muted;
font-size: clamp(14px, 22rpx, 17px);
line-height: 1.55;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.site-article-card__action {
margin-top: 14rpx;
color: $brand-red;
font-size: clamp(13px, 21rpx, 16px);
font-weight: 700;
text-align: right;
}
.site-home-state {
display: flex;
min-height: 420rpx;
flex-direction: column;
align-items: center;
justify-content: center;
color: $ink-muted;
text-align: center;
}
.site-home-state text:first-child {
color: $ink;
font-size: clamp(18px, 32rpx, 23px);
font-weight: 700;
}
.site-home-state text + text {
margin-top: 12rpx;
}
.site-home-state button {
min-height: 72rpx;
margin-top: 24rpx;
padding: 0 30rpx;
border: 1rpx solid rgba(159, 23, 15, 0.42);
border-radius: 36rpx;
background: rgba(255, 250, 240, 0.8);
color: $brand-red;
font-size: clamp(14px, 22rpx, 17px);
}
.site-home-state button::after {
border: 0;
}
.site-home-error {
display: block;
margin-top: 18rpx;
color: $brand-red;
font-size: clamp(13px, 21rpx, 16px);
text-align: center;
}
.site-home-content :deep(.promotion-strip) {
margin-right: 0;
margin-left: 0;
}
</style>