355 lines
9.4 KiB
Vue
355 lines
9.4 KiB
Vue
<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(/ /gi, " ")
|
||
.replace(/&/gi, "&")
|
||
.replace(/</gi, "<")
|
||
.replace(/>/gi, ">")
|
||
.replace(/"/gi, '"')
|
||
.replace(/'/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>
|