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

399 lines
12 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">
<BatchManagementBar
v-if="deletableArticles.length"
resource-name="谱文"
:active="articleBatch.selectionMode.value"
:selected-count="articleBatch.selectedCount.value"
:all-selected="articleBatch.allSelected.value"
:busy="articleBatch.deleting.value"
@start="articleBatch.enterSelectionMode"
@finish="articleBatch.exitSelectionMode"
@toggle-all="articleBatch.toggleAll"
@delete="articleBatch.requestDelete"
/>
<text v-if="articleBatch.notice.value" class="article-batch-notice" role="status">{{ articleBatch.notice.value }}</text>
<text v-if="articleBatch.error.value" class="article-batch-error" role="alert">{{ articleBatch.error.value }}</text>
<view
v-for="item in filteredArticles"
:key="item.id"
class="article-card"
@click="handleArticleClick(item)"
>
<BatchSelectionMark
v-if="articleBatch.selectionMode.value && item.canDelete"
:selected="articleBatch.isSelected(item)"
:label="`谱文:${item.title}`"
@toggle="articleBatch.toggleSelection(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>
<AppDialog
:visible="articleBatch.confirmationVisible.value"
:close-on-mask="false"
eyebrow="批量删除"
title="将选中的谱文移入回收站?"
:message="articleBatch.confirmationMessage.value"
:confirm-text="articleBatch.deleting.value ? '正在删除' : '移入回收站'"
cancel-text="继续选择"
show-cancel
@confirm="articleBatch.confirmDelete"
@cancel="articleBatch.cancelDelete"
/>
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import BatchManagementBar from "@/components/BatchManagementBar.vue";
import BatchSelectionMark from "@/components/BatchSelectionMark.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 { useBatchDeletion } from "@/composables/use-batch-deletion.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();
const articleDeleteRequestController = 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 articleBatch = useBatchDeletion({
items: articles,
visibleItems: filteredArticles,
deleteOne: (article) =>
familyArticleApi.deleteArticle(genealogyId.value, article.id, {
requestController: articleDeleteRequestController,
}),
resourceName: "谱文",
isActive: () => pageActive,
onEmpty: () => {
listState.value = "empty";
},
});
const deletableArticles = articleBatch.deletableItems;
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();
articleDeleteRequestController.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;
articleBatch.exitSelectionMode();
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 || "";
articleBatch.exitSelectionMode();
};
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 handleArticleClick = (item) => {
if (articleBatch.selectionMode.value && item?.canDelete) {
articleBatch.toggleSelection(item);
return;
}
openArticle(item);
};
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-batch-notice,
.article-batch-error {
display: block;
margin-bottom: 16rpx;
font-size: clamp(13px, 21rpx, 16px);
line-height: 1.55;
}
.article-batch-notice { color: #426538; }
.article-batch-error { color: $brand-red; }
.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>