feat: migrate app routes and business modules

This commit is contained in:
2026-08-12 18:22:59 +08:00
parent 555aa00043
commit cc706378c2
247 changed files with 28623 additions and 14988 deletions
+290
View File
@@ -0,0 +1,290 @@
<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="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)"
>
<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 { 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 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 () => {
try {
return await familyArticleApi.getArticleCategories(genealogyId.value, {
requestController: articleCategoryRequestController,
});
} catch (error) {
if (isRequestCancelled(error)) throw error;
return [];
}
};
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;
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 72rpx;
}
.article-list-items {
margin-top: 24rpx;
}
.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);
}
.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;
}
.article-card text {
display: block;
}
.article-card__title {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(17px, 31rpx, 22px);
font-weight: 700;
}
.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;
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>