修改完成待测试

This commit is contained in:
2026-09-13 17:45:47 +08:00
parent 9ad572907b
commit bbc024f7f2
80 changed files with 3844 additions and 1672 deletions
+354
View File
@@ -0,0 +1,354 @@
<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>