Files
jiapuapp/pages/tree/t07-member-directory.vue
T
2026-07-24 18:03:14 +08:00

351 lines
9.4 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.
<!-- 页面编号T-07用途成员目录搜索空态与失败状态 -->
<template>
<view
class="directory-page"
:class="{
'directory-state--loading': directoryState === 'loading',
'directory-state--list': directoryState === 'list',
'directory-state--empty': directoryState === 'empty',
'directory-state--error': directoryState === 'error',
}"
>
<ModulePageBackground module="tree" />
<view class="directory-page__header"><PageHeader title="成员目录" /></view>
<view v-if="hasValidContext" class="directory-context">
<text class="directory-context__name">当前家谱</text>
<text class="directory-context__meta"
>已加载 {{ members.length }} / {{ total }} 位成员</text
>
</view>
<view
v-if="directoryState === 'list' || directoryState === 'empty'"
class="directory-search"
>
<input
v-model="keyword"
aria-label="成员搜索关键词"
placeholder="按姓名、身份或世代查找"
placeholder-class="directory-placeholder"
@confirm="searchMembers"
/>
<view
class="directory-search__action"
role="button"
aria-label="查找成员"
hover-class="action-hover"
@click="searchMembers"
><text>查找</text></view
>
</view>
<view class="directory-content">
<AppLoading
v-if="directoryState === 'loading'"
text="正在整理成员目录"
description="请稍候,正在读取成员与世代信息。"
/>
<template v-else-if="directoryState === 'list'">
<view class="directory-summary"
><text>家谱成员</text
><text>{{ members.length }} / {{ total }} · 按世代排列</text></view
>
<view
v-for="item in members"
:key="item.id"
class="directory-card"
@click="openMember(item)"
>
<view class="directory-card__copy">
<text class="directory-card__name">{{ item.name }}</text>
<text class="directory-card__meta">{{ memberMeta(item) }}</text>
<text class="directory-card__status">{{ memberStatus(item) }}</text>
</view>
</view>
<AppButton
v-if="hasMore"
block
type="secondary"
:label="loadingMore ? '正在加载…' : '加载更多成员'"
@click="loadMore"
/>
</template>
<view v-else class="directory-state-card">
<view
><text>{{
directoryState === "empty" ? "没有找到相关成员" : "成员目录暂不可用"
}}</text
><text>{{
directoryState === "empty"
? "换一个姓名、身份或世代关键词再试。"
: "请从当前家谱重新进入,成员资料不会受到影响。"
}}</text></view
>
</view>
<AppButton
v-if="directoryState === 'error'"
block
type="secondary"
:label="hasValidContext ? '重新查看' : '返回上一页'"
@click="retryDirectory"
/>
</view>
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import { goBack, openPage } from "@/utils/navigation.js";
const genealogyId = ref("");
const keyword = ref("");
const directoryState = ref("loading");
const members = ref([]);
const total = ref(0);
const pageNum = ref(1);
const loadingMore = ref(false);
const directoryRequestController = createRequestController();
let loadSequence = 0;
const hasValidContext = computed(() => Boolean(genealogyId.value));
const memberMeta = (member) =>
`第 ${member.generation} 世 · ${member.generationName || "字辈待补"} · ${member.branch}`;
const memberStatus = (member) =>
member.personStatus ? `人物状态:${member.personStatus}` : "人物状态待补";
const hasMore = computed(() => members.value.length < total.value);
const loadMembers = async ({ append = false } = {}) => {
if (!hasValidContext.value) return;
const activeLoad = ++loadSequence;
if (append) loadingMore.value = true;
else directoryState.value = "loading";
try {
const result = await appApi.getPersonPage(
genealogyId.value,
{ pageNum: pageNum.value, pageSize: 10, keyword: keyword.value },
{ requestController: directoryRequestController },
);
if (activeLoad !== loadSequence) return;
members.value = append ? [...members.value, ...result.rows] : result.rows;
total.value = result.total;
directoryState.value = members.value.length ? "list" : "empty";
} catch (error) {
if (activeLoad !== loadSequence || isRequestCancelled(error)) return;
if (!append) members.value = [];
directoryState.value = "error";
} finally {
if (activeLoad === loadSequence) loadingMore.value = false;
}
};
onLoad((query) => {
genealogyId.value = String(query.genealogyId || "");
if (!hasValidContext.value || query.state === "error") {
directoryState.value = "error";
return;
}
void loadMembers();
});
const searchMembers = () => {
pageNum.value = 1;
void loadMembers();
};
const loadMore = () => {
if (loadingMore.value || !hasMore.value) return;
pageNum.value += 1;
void loadMembers({ append: true });
};
const retryDirectory = () => {
if (!hasValidContext.value) return goBack();
pageNum.value = 1;
return loadMembers();
};
const openMember = (item) =>
hasValidContext.value
? openPage("T03", { genealogyId: genealogyId.value, personId: String(item.id) }, "T07")
: Promise.resolve(false);
onUnload(() => {
loadSequence += 1;
directoryRequestController.abort();
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.directory-page {
display: flex;
min-height: 100vh;
flex-direction: column;
overflow-y: auto;
background: $paper;
}
.directory-page__header,
.directory-context,
.directory-search,
.directory-content {
z-index: 2;
}
.directory-context {
display: flex;
align-items: baseline;
justify-content: space-between;
box-sizing: border-box;
padding: 22rpx 32rpx 0;
}
.directory-context__name {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 32rpx;
font-weight: 700;
}
.directory-context__meta {
color: #62584c;
font-size: 24rpx;
font-weight: 500;
}
.directory-search {
@include adaptive.adaptive-tree-field;
display: grid;
width: calc(100% - 48rpx);
min-height: 44px;
margin: 20rpx auto 0;
}
.directory-search input {
z-index: 1;
grid-area: 1 / 1;
min-height: 44px;
margin-right: 110rpx;
margin-left: 26rpx;
color: $ink;
font-size: 26rpx;
line-height: 44px;
}
.directory-search__action {
z-index: 1;
display: flex;
grid-area: 1 / 1;
justify-self: end;
min-width: 88rpx;
min-height: 44px;
align-items: center;
justify-content: center;
margin-right: 8rpx;
color: $brand-red;
font-size: 24rpx;
font-weight: 700;
}
.directory-placeholder {
color: #776956;
}
.directory-content {
padding: 22rpx 24rpx 50rpx;
}
.directory-summary {
display: flex;
justify-content: space-between;
margin: 0 8rpx 18rpx;
}
.directory-summary text:first-child {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 29rpx;
font-weight: 700;
}
.directory-summary text:last-child {
color: $ink-muted;
font-size: 23rpx;
}
.directory-card {
@include adaptive.adaptive-genealogy-list-card;
display: flex;
width: 100%;
min-height: 196rpx;
align-items: center;
margin-bottom: 16rpx;
padding: 18rpx 28rpx;
}
.directory-card__copy {
z-index: 1;
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
justify-content: center;
}
.directory-card__name {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 31rpx;
font-weight: 700;
line-height: 1.3;
overflow-wrap: anywhere;
}
.directory-card__meta {
margin-top: 7rpx;
color: #62584c;
font-size: 24rpx;
line-height: 1.45;
overflow-wrap: anywhere;
}
.directory-card__status {
align-self: flex-start;
margin-top: 8rpx;
padding: 2rpx 10rpx;
border: 1rpx solid rgba(174, 45, 36, 0.32);
color: $brand-red;
font-size: 22rpx;
line-height: 30rpx;
}
.directory-state-card {
@include adaptive.adaptive-genealogy-list-card;
display: flex;
width: 100%;
min-height: 220rpx;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
padding: 48rpx 12%;
}
.directory-state-card {
margin-top: 70rpx;
}
.directory-state-card > view {
z-index: 1;
text-align: center;
}
.directory-state-card text {
display: block;
}
.directory-state-card text:first-child {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 30rpx;
font-weight: 700;
}
.directory-state-card text:last-child {
margin-top: 14rpx;
color: $ink-muted;
font-size: 24rpx;
line-height: 1.5;
}
.directory-content > .app-button {
margin: 20rpx auto 0;
}
@media screen and (max-width: 340px) {
.directory-card {
min-height: 192rpx;
padding-right: 22rpx;
padding-left: 22rpx;
}
.directory-card__name {
font-size: 29rpx;
}
.directory-card__meta {
font-size: 22rpx;
}
.directory-card__status {
font-size: 20rpx;
}
}
</style>