Files
jiapuapp/pages/records/r01-people-list.vue
T
2026-07-24 07:56:33 +08:00

341 lines
9.1 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.
<!-- 页面编号R-01用途人物录列表搜索空态与失败状态 -->
<template>
<view
class="people-page"
:class="{
'people-state--ready': peopleState === 'ready',
'people-state--loading': peopleState === 'loading',
'people-state--empty': peopleState === 'empty',
'people-state--error': peopleState === 'error',
}"
>
<ModulePageBackground module="records" />
<view class="people-page__header"><PageHeader title="人物录" /></view>
<view class="people-content">
<AppLoading
v-if="peopleState === 'loading'"
text="正在读取人物录"
description="请稍候,正在读取当前家谱的人物资料。"
/>
<template v-else-if="peopleState === 'ready'">
<view class="people-search">
<input
v-model="keywordInput"
confirm-type="search"
placeholder="搜索姓名、身份或世代"
placeholder-class="people-search__placeholder"
@confirm="applySearch"
/>
<view
class="people-search__action"
role="button"
aria-label="搜索人物"
@click="applySearch"
><text>{{ keyword ? "重置" : "搜索" }}</text></view
>
</view>
<view v-if="people.length" class="people-list">
<view
v-for="person in people"
:key="person.id"
class="person-card"
@click="openPerson(person)"
>
<view class="person-card__copy">
<text class="person-card__name">{{ person.name }}</text>
<text class="person-card__meta"
>{{ person.relation }} · {{ person.generation }} </text
>
<text class="person-card__hint">查看人物档案</text>
</view>
</view>
<AppButton
v-if="hasMore"
class="people-primary-action"
type="secondary"
block
:label="loadingMore ? '正在加载…' : '加载更多人物'"
@click="loadMore"
/>
</view>
<view v-else class="people-result-empty">
<view class="people-state-card__copy"
><text>没有找到相关人物</text
><text>请更换姓名身份或世代关键词后再试</text></view
>
<AppButton
type="secondary"
block
label="清空搜索"
@click="clearSearch"
/>
</view>
</template>
<view v-else class="people-state-card">
<view class="people-state-card__copy">
<text>{{
peopleState === "empty"
? "还没有人物记录"
: peopleState === "invalid"
? "人物录入口无效"
: "人物录暂不可用"
}}</text>
<text>{{
peopleState === "empty"
? "从第一位值得铭记的家人开始建立人物录。"
: peopleState === "invalid"
? "没有找到可访问的成员家谱,页面不会展示其他家谱人物。"
: "请稍后重新进入,已有档案不会受到影响。"
}}</text>
</view>
<AppButton
v-if="peopleState !== 'empty'"
:type="peopleState === 'empty' ? 'primary' : 'secondary'"
block
:label="peopleState === 'error' ? '重新查看' : '返回上一页'"
@click="handleStateAction"
/>
</view>
</view>
</view>
</template>
<script setup>
import { onLoad, onUnload } from "@dcloudio/uni-app";
import { computed, ref } from "vue";
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 people = ref([]);
const peopleState = ref("ready");
const keywordInput = ref("");
const keyword = ref("");
const total = ref(0);
const pageNum = ref(1);
const loadingMore = ref(false);
const peopleRequestController = createRequestController();
let loadSequence = 0;
const hasValidContext = computed(() => Boolean(genealogyId.value));
const hasMore = computed(() => people.value.length < total.value);
const applySearch = () => {
if (keyword.value) {
clearSearch();
return;
}
keyword.value = keywordInput.value.trim();
pageNum.value = 1;
void loadPeople();
};
const clearSearch = () => {
keywordInput.value = "";
keyword.value = "";
pageNum.value = 1;
void loadPeople();
};
const loadPeople = async ({ append = false } = {}) => {
if (!hasValidContext.value) return;
const activeLoad = ++loadSequence;
if (append) loadingMore.value = true;
else peopleState.value = "loading";
try {
const result = await appApi.getPersonPage(
genealogyId.value,
{ pageNum: pageNum.value, pageSize: 10, keyword: keyword.value },
{ requestController: peopleRequestController },
);
if (activeLoad !== loadSequence) return;
people.value = append ? [...people.value, ...result.rows] : result.rows;
total.value = result.total;
peopleState.value = people.value.length ? "ready" : "empty";
} catch (error) {
if (activeLoad !== loadSequence || isRequestCancelled(error)) return;
if (!append) people.value = [];
peopleState.value = "error";
} finally {
if (activeLoad === loadSequence) loadingMore.value = false;
}
};
const loadMore = () => {
if (loadingMore.value || !hasMore.value) return;
pageNum.value += 1;
void loadPeople({ append: true });
};
const openPerson = (person) =>
hasValidContext.value
? openPage(
"R02",
{
genealogyId: genealogyId.value,
mode: "view",
personId: String(person.id),
},
"R01",
)
: Promise.resolve(false);
const handleStateAction = () => {
if (peopleState.value === "invalid") return goBack();
if (peopleState.value === "error") {
pageNum.value = 1;
return loadPeople();
}
return goBack();
};
onLoad((query) => {
genealogyId.value = String(query.genealogyId || "");
if (!hasValidContext.value || query.state === "error") {
people.value = [];
peopleState.value = "invalid";
return;
}
void loadPeople();
});
onUnload(() => {
loadSequence += 1;
peopleRequestController.abort();
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.people-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.people-page__header,
.people-content {
z-index: 1;
}
.people-content {
flex: 1;
padding: 22rpx 24rpx 100rpx;
}
.people-search {
@include adaptive.adaptive-records-field;
display: grid;
grid-template-columns: minmax(0, 1fr) 126rpx;
width: 100%;
min-height: 44px;
align-items: center;
}
.people-search input {
z-index: 1;
width: 100%;
min-width: 0;
height: 100%;
padding-left: 31rpx;
box-sizing: border-box;
color: $ink;
font-size: 24rpx;
}
.people-search__placeholder {
color: #8a7965;
}
.people-search__action {
z-index: 2;
display: flex;
width: 126rpx;
height: 100%;
min-height: 44px;
align-items: center;
justify-content: center;
color: $brand-red;
font-size: 24rpx;
font-weight: 700;
}
.people-list {
margin-top: 12px;
}
.person-card {
@include adaptive.adaptive-records-person;
display: flex;
width: 100%;
min-height: clamp(92px, 190rpx, 108px);
align-items: center;
margin-top: 12px;
}
.person-card:first-child {
margin-top: 0;
}
.person-card__copy {
display: flex;
width: 100%;
flex-direction: column;
justify-content: center;
}
.person-card__name {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: 31rpx;
font-weight: 700;
}
.person-card__meta {
margin-top: 7rpx;
color: $ink-muted;
font-size: 23rpx;
}
.person-card__hint {
margin-top: 8rpx;
color: #806a51;
font-size: 21rpx;
}
.people-primary-action {
margin: 22rpx auto 0;
}
.people-result-empty,
.people-state-card {
margin-top: 38rpx;
background: url("/static/assets/modules/records/transparent/r01-person-name-card.png") top center / 100% 220rpx no-repeat;
text-align: center;
}
.people-state-card__copy {
display: flex;
min-height: 118px;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 54rpx;
}
.people-result-empty text,
.people-state-card text {
display: block;
}
.people-result-empty text:first-child,
.people-state-card text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: 31rpx;
font-weight: 700;
}
.people-result-empty text:last-child,
.people-state-card text:last-child {
margin-top: 13rpx;
color: $ink-muted;
font-size: 23rpx;
line-height: 1.5;
}
.people-result-empty .app-button,
.people-state-card .app-button {
margin: 20rpx auto 0;
}
@media (min-width: 400px) {
.people-content {
padding-right: 32rpx;
padding-left: 32rpx;
}
}
</style>