Files
jiapuapp/pages/family/articles.vue
T

329 lines
9.8 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="article-list-page" :class="`article-list-state--${listState}`">
<ModulePageBackground module="family" />
<view class="article-list-header"
><PageHeader
title="谱文"
:action="hasValidContext ? '新建' : ''"
@action="createArticle"
/></view>
<view class="article-list-content">
<view v-if="articleCategoryError" class="article-category-error" role="alert">
<text>{{ articleCategoryError }}</text>
</view>
<view v-if="categoryOptions.length > 1" class="article-filter">
<text>文章分类</text>
<picker :range="categoryLabels" :value="categoryIndex" @change="selectCategory">
<view class="article-filter__value">{{ selectedCategoryLabel }} </view>
</picker>
</view>
<view v-if="listState === 'list'" class="article-list-items">
<view
v-for="item in filteredArticles"
:key="item.id"
class="article-card"
@click="openArticle(item)"
>
<image
v-if="item.coverFile?.accessUrl"
class="article-card__cover"
:src="item.coverFile.accessUrl"
mode="aspectFill"
aria-hidden="true"
/>
<text class="article-card__title">{{ item.title }}</text>
<text class="article-card__summary">{{
item.summary || item.content
}}</text>
<text class="article-card__meta"
>{{ item.category ? `${item.category} · ` : "" }}{{ item.author || "家族成员" }} ·
{{ item.time || "未标注时间" }}</text
>
</view>
<view v-if="!filteredArticles.length" class="article-filter-empty">
<text>这个分类下还没有谱文</text>
<text>可以切换其他分类查看</text>
</view>
</view>
<view v-else class="article-list-state-card">
<text>{{ stateCopy.title }}</text>
<text>{{ stateCopy.copy }}</text>
<AppButton
v-if="!hasValidContext || listState !== 'loading'"
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 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 { familyArticleApi } from "@/services/api/family-article-service.js";
import { goBack, openPage } from "@/utils/navigation/gateway.js";
const genealogyId = ref("");
const hasValidContext = ref(false);
const listState = ref("loading");
const articles = ref([]);
const categories = ref([]);
const articleCategoryError = ref("");
const selectedCategoryId = ref("");
const articleListRequestController = createRequestController();
const articleCategoryRequestController = createRequestController();
let pageActive = true;
let skipInitialShowRefresh = true;
const categoryOptions = computed(() => [
{ id: "", name: "全部分类" },
...categories.value.filter((item) => item.enabled),
]);
const categoryLabels = computed(() => categoryOptions.value.map((item) => item.name));
const categoryIndex = computed(() =>
Math.max(0, categoryOptions.value.findIndex((item) => item.id === selectedCategoryId.value)),
);
const selectedCategoryLabel = computed(() => categoryOptions.value[categoryIndex.value]?.name || "全部分类");
const filteredArticles = computed(() =>
selectedCategoryId.value
? articles.value.filter((item) => item.categoryId === selectedCategoryId.value)
: articles.value,
);
const stateCopy = computed(() =>
hasValidContext.value
? listState.value === "empty"
? {
title: "还没有谱文",
copy: "新建第一篇谱文,记录值得传承的故事。",
action: "新建谱文",
}
: listState.value === "error"
? {
title: "谱文暂时无法读取",
copy: "请检查网络后重新读取。",
action: "重新读取",
}
: {
title: "正在读取谱文",
copy: "请稍候。",
action: "重新读取",
}
: {
title: "暂时无法打开谱文",
copy: "未找到家谱信息,请返回后重新进入。",
action: "返回上一页",
},
);
onLoad((query) => {
genealogyId.value = String(query?.genealogyId || "");
hasValidContext.value = /^[1-9]\d*$/.test(genealogyId.value);
if (hasValidContext.value) void loadArticles();
else listState.value = "invalid";
});
onShow(() => {
if (skipInitialShowRefresh) {
skipInitialShowRefresh = false;
return;
}
if (hasValidContext.value) void loadArticles();
});
onUnload(() => {
pageActive = false;
articleListRequestController.abort();
articleCategoryRequestController.abort();
});
const loadArticleCategories = async () => {
articleCategoryError.value = "";
try {
return await familyArticleApi.getArticleCategories(genealogyId.value, {
requestController: articleCategoryRequestController,
});
} catch (error) {
if (isRequestCancelled(error)) throw error;
articleCategoryError.value = getRequestErrorMessage(
error,
"谱文分类暂时无法读取,当前仍可查看全部谱文。",
);
return null;
}
};
const loadArticles = async () => {
articleListRequestController.abort();
articleCategoryRequestController.abort();
listState.value = "loading";
try {
const [rows, categoryRows] = await Promise.all([
familyArticleApi.getArticles(genealogyId.value, {
requestController: articleListRequestController,
}),
loadArticleCategories(),
]);
if (!pageActive) return;
articles.value = rows;
if (categoryRows) categories.value = categoryRows;
if (!categoryOptions.value.some((item) => item.id === selectedCategoryId.value)) {
selectedCategoryId.value = "";
}
listState.value = articles.value.length ? "list" : "empty";
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
listState.value = "error";
}
};
const selectCategory = (event) => {
selectedCategoryId.value = categoryOptions.value[Number(event.detail.value)]?.id || "";
};
const createArticle = () =>
hasValidContext.value
? openPage("F06", { genealogyId: genealogyId.value, mode: "create" }, "F04")
: Promise.resolve(false);
const openArticle = (item) =>
openPage("F05", { genealogyId: genealogyId.value, articleId: item.id }, "F04");
const handleStateAction = () => {
if (!hasValidContext.value) return goBack();
if (listState.value === "error") return loadArticles();
if (listState.value === "empty") return createArticle();
return undefined;
};
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.article-list-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.article-list-header,
.article-list-content {
z-index: 1;
}
.article-list-content {
padding: 18rpx 24rpx calc(72rpx + env(safe-area-inset-bottom));
}
.article-list-items {
margin-top: 24rpx;
}
.article-category-error {
margin-top: 20rpx;
padding: 18rpx 22rpx;
border: 1rpx solid rgba(159, 35, 35, 0.22);
background: rgba(255, 247, 233, 0.94);
color: $brand-red;
font-size: clamp(13px, 22rpx, 16px);
line-height: 1.6;
}
.article-filter {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20rpx;
padding: 22rpx 26rpx;
border: 1rpx solid rgba(159, 35, 35, 0.18);
background: rgba(255, 252, 242, 0.92);
color: $ink;
font-size: clamp(14px, 24rpx, 17px);
min-height: 80rpx;
box-sizing: border-box;
}
.article-filter__value {
color: $brand-red;
}
.article-filter-empty {
padding: 64rpx 24rpx;
text-align: center;
}
.article-filter-empty text {
display: block;
color: $ink-muted;
font-size: clamp(14px, 24rpx, 17px);
line-height: 1.7;
}
.article-filter-empty text:first-child {
color: $ink;
font-size: clamp(17px, 30rpx, 21px);
font-weight: 700;
}
.article-card {
@include adaptive-family-content;
display: flex;
min-height: 150rpx;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
margin-bottom: 16rpx;
padding: 28rpx;
background-color: rgba($paper, 0.82);
}
.article-card__cover {
width: 100%;
height: 280rpx;
margin-bottom: 20rpx;
border-radius: 10rpx;
background: rgba(128, 89, 49, 0.12);
}
.article-card text {
display: block;
}
.article-card__title {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(17px, 31rpx, 22px);
font-weight: 700;
overflow-wrap: anywhere;
}
.article-card__summary {
display: -webkit-box;
margin-top: 9rpx;
overflow: hidden;
color: $ink-muted;
font-size: clamp(15px, 24rpx, 18px);
line-height: 1.45;
overflow-wrap: anywhere;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.article-card__meta {
margin-top: 10rpx;
color: $brand-red;
font-size: clamp(13px, 21rpx, 16px);
}
.article-list-state-card {
@include adaptive-family-content;
background-color: rgba($paper, 0.82);
width: 100%;
min-height: 340rpx;
margin-top: 30rpx;
padding: 78rpx 52rpx 50rpx;
text-align: center;
}
.article-list-state-card > text {
display: block;
}
.article-list-state-card > text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(19px, 35rpx, 24px);
font-weight: 700;
}
.article-list-state-card > text:nth-child(2) {
margin-top: 18rpx;
color: $ink-muted;
font-size: clamp(15px, 24rpx, 18px);
line-height: 1.65;
}
.article-list-state-card .app-button {
margin-top: 28rpx;
}
</style>