Files
jiapuapp/pages/tree/t03-member-profile.vue
T
2026-07-23 08:24:07 +08:00

311 lines
14 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-03用途 personId 展示成员档案并进入其真实成员状态 -->
<template>
<view
class="member-page"
:data-current-person-id="personId"
:data-trail-length="memberTrail.length"
:data-trail-index="trailIndex"
:class="{
'member-state--detail': memberState === 'detail',
'member-state--restricted': memberState === 'restricted',
'member-state--error': memberState === 'error',
}"
>
<ModulePageBackground module="tree" />
<view class="member-page__header">
<PageHeader title="成员档案"
:action="canPreviewEdit ? '编辑预览' : ''"
custom-back
@back="requestBack"
@action="toEdit"
/>
</view>
<view class="member-context">
<text>{{ genealogyName }}</text>
<text>{{ memberContextDescription }}</text>
</view>
<view class="member-panel">
<AppLoading v-if="memberState === 'loading'" text="正在读取成员档案" description="请稍候,正在整理成员资料。" />
<view v-else-if="memberState === 'detail' && member" class="member-detail">
<view class="member-heading">
<view class="member-heading__seal"><text>{{ member.name.slice(0, 1) }}</text></view>
<view>
<text>{{ member.name }}</text>
<text> {{ member.generation }} · {{ member.relation }} · {{ member.branch }}</text>
</view>
</view>
<view class="member-status-entry" @click="openMemberState">
<text>{{ statusLabel }}</text>
<text>{{ statusDescription }}</text>
<text>查看</text>
</view>
<text class="member-section-title">基本资料</text>
<view v-for="item in details" :key="item.label" class="member-info-row">
<text>{{ item.label }}</text><text>{{ item.value || "未填写" }}</text>
</view>
<text class="member-section-title member-section-title--relation">亲属关系</text>
<view class="member-relatives">
<view
v-for="relative in member.relatives"
:key="relative.id"
:data-person-id="relative.id"
@click="openRelative(relative.id)"
>
<text>{{ relative.relation }}</text><text>{{ relative.name }}</text>
</view>
<text v-if="!member.relatives.length">尚未记录可查看的亲属</text>
</view>
<view class="member-record-actions">
<view role="button" aria-label="查看成长日志" @click="toGrowthJournal"><text>成长日志</text></view>
<view role="button" aria-label="查看人生事服务状态" @click="toLifeEvents"><text>人生事待开放</text></view>
</view>
<view v-if="canPreviewEdit" class="member-profile-action" @click="toEdit"><text>制作成员编辑预览</text></view>
</view>
<view v-else-if="memberState === 'restricted' && member" class="member-restricted">
<view class="member-heading">
<view class="member-heading__seal"><text>{{ member.name.slice(0, 1) }}</text></view>
<view>
<text>{{ member.name }}</text>
<text> {{ member.generation }} · {{ member.relation }}</text>
</view>
</view>
<text>{{ restrictedCopy.title }}</text>
<text>{{ restrictedCopy.copy }}</text>
<view class="member-restricted-action" @click="toTree"><text>返回上一位成员</text></view>
</view>
<view v-else class="member-error">
<text>成员档案暂不可用</text>
<text>{{ errorMessage }}</text>
<view class="member-profile-action" @click="toTree"><text>返回世系树</text></view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { onBackPress, onLoad, onShow } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { findTreeMemberPresentationFixture } from "@/data/mock.js";
import {
consumeNavigationResult,
handleBackPress,
openPage,
runBackGuard,
} from "@/utils/navigation.js";
const genealogyId = ref("");
const personId = ref("");
const memberState = ref("loading");
const member = ref(null);
const errorMessage = ref("");
const genealogyName = ref("汤氏家谱");
const memberTrail = reactive([]);
const trailIndex = ref(-1);
const details = computed(() => member.value ? [
{ label: "字辈", value: member.value.generationName },
{ label: "出生日期", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.birthDate },
{ label: "生卒信息", value: member.value.years },
{ label: "祖居地", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.birthplace },
{ label: "所属支系", value: member.value.branch },
] : []);
const canPreviewEdit = computed(() => memberState.value === "detail");
const restrictedCopy = computed(() =>
member.value?.status === "forbidden"
? {
title: "当前成员档案访问受限",
copy: "页面只保留基础身份,不展示生平、居住地、支系、亲属或编辑入口。",
}
: {
title: "部分成员资料未公开",
copy: "页面只保留姓名、关系与世代,不展示其他资料或操作入口。",
},
);
const statusLabel = computed(() => ({ privacy: "隐私资料", deceased: "离世纪念", forbidden: "访问受限" })[member.value?.status] || "成员状态");
const statusDescription = computed(() => ({
privacy: "部分资料仅向授权成员展示",
deceased: "查看生平保留与纪念资料说明",
forbidden: "当前账号只能查看有限身份信息",
})[member.value?.status] || "查看成员状态说明");
const memberContextDescription = computed(() => ({
loading: "正在读取成员资料",
detail: "成员身份与亲属关系",
restricted: "隐私成员仅展示基础身份",
error: "请重新选择成员",
}[memberState.value] || "请重新选择成员"));
const loadMember = async (nextPersonId) => {
const normalizedPersonId = String(nextPersonId || "");
const fixture = findTreeMemberPresentationFixture(genealogyId.value, normalizedPersonId);
if (!fixture) {
errorMessage.value = "这位成员不存在或已不属于当前家谱。";
return false;
}
const restricted = ["privacy", "forbidden"].includes(fixture.status);
const relatives = restricted ? [] : fixture.relatives
.map((relative) => {
const relativeMember = findTreeMemberPresentationFixture(
genealogyId.value,
relative.personId,
);
return relativeMember
? { id: relativeMember.id, name: relativeMember.name, relation: relative.relation }
: null;
})
.filter(Boolean);
personId.value = normalizedPersonId;
member.value = {
...fixture,
relatives,
};
errorMessage.value = "";
memberState.value = restricted ? "restricted" : "detail";
return true;
};
const initializeMemberTrail = async (initialPersonId) => {
memberTrail.splice(0, memberTrail.length);
trailIndex.value = -1;
const normalizedPersonId = String(initialPersonId || "");
const loaded = await loadMember(normalizedPersonId);
if (!loaded) return false;
memberTrail.splice(0, memberTrail.length, normalizedPersonId);
trailIndex.value = 0;
return true;
};
const openRelative = async (nextPersonId) => {
const normalizedPersonId = String(nextPersonId || "");
if (!normalizedPersonId || normalizedPersonId === personId.value) return false;
// 先读取目标成员,只有真实读取成功后才截断前进历史并推进轨迹;失败时
// 当前成员、当前索引和既有历史全部保持不变。
const loaded = await loadMember(normalizedPersonId);
if (!loaded) return false;
memberTrail.splice(trailIndex.value + 1);
memberTrail.push(normalizedPersonId);
trailIndex.value = memberTrail.length - 1;
return true;
};
const popMemberTrail = async () => {
while (trailIndex.value > 0) {
const targetIndex = trailIndex.value - 1;
const historicalPersonId = memberTrail[targetIndex];
const loaded = await loadMember(historicalPersonId);
if (loaded) {
trailIndex.value = targetIndex;
return true;
}
// 历史成员可能已删除或失去权限。移除失效项后,当前成员在数组中的
// 索引同步左移,再继续寻找更早的有效成员,绝不创建新的 T03 页面。
memberTrail.splice(targetIndex, 1);
trailIndex.value -= 1;
}
return false;
};
const requestBack = () =>
runBackGuard({
internalTrail: trailIndex.value > 0,
"pop-internal-trail": popMemberTrail,
});
onLoad((query) => {
genealogyId.value = String(query.genealogyId || "");
const initialPersonId = String(query.personId || "");
if (query.state === "loading") return;
if (query.state === "error" || !genealogyId.value || !initialPersonId) {
memberState.value = "error";
errorMessage.value = !initialPersonId
? "没有指定成员,请从世系树重新选择。"
: "当前家谱上下文无效,请重新进入。";
return;
}
void initializeMemberTrail(initialPersonId).then((loaded) => {
if (loaded) return;
member.value = null;
memberState.value = "error";
});
});
onShow(() => {
const result = consumeNavigationResult("T03");
if (result?.operation === "member-open-requested" && result.entityId) {
void openRelative(result.entityId);
}
});
onBackPress((event) => handleBackPress(event, requestBack));
const toEdit = () => {
if (!canPreviewEdit.value) return false;
return openPage("T05", { genealogyId: genealogyId.value, personId: personId.value }, "T03");
};
const openMemberState = () =>
openPage("T08", { genealogyId: genealogyId.value, personId: personId.value }, "T03");
const toGrowthJournal = () =>
openPage("R08", { genealogyId: genealogyId.value, personId: personId.value }, "T03");
const toLifeEvents = () =>
openPage("R09", { genealogyId: genealogyId.value, personId: personId.value }, "T03");
const toTree = requestBack;
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.member-page { display: flex; min-height: 100vh; flex-direction: column; padding-bottom: 28rpx; box-sizing: border-box; background: $paper; }
.member-page__header, .member-context, .member-panel { z-index: 2; }
.member-context { @include adaptive.adaptive-tree-field; width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 16rpx 24rpx; }
.member-context text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
.member-context text:first-child { color: $ink; font-size: 26rpx; font-weight: 700; }
.member-panel { @include adaptive.adaptive-tree-panel; width: calc(100% - 32rpx); margin: 16rpx auto 0; padding: 9% 8%; }
.member-panel > .app-loading { margin-top: 30%; }
.member-heading { display: flex; align-items: center; gap: 20rpx; }
.member-heading__seal { display: flex; width: 74rpx; height: 74rpx; flex: 0 0 74rpx; align-items: center; justify-content: center; background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / contain no-repeat; }
.member-heading__seal text { color: #fff7e7; font-size: 28rpx; }
.member-heading > view:last-child text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
.member-heading > view:last-child text:last-child { font-size: 23rpx; }
.member-heading > view:last-child text:first-child { color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 36rpx; font-weight: 700; }
.member-status-entry { @include adaptive.adaptive-genealogy-list-card; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14rpx; margin-top: 18rpx; padding: 18rpx 22rpx; }
.member-status-entry text { color: $ink-muted; font-size: 22rpx; line-height: 1.4; }
.member-status-entry text:first-child, .member-status-entry text:last-child { color: $brand-red; font-weight: 700; }
.member-section-title { display: block; margin-top: 24rpx; color: $brand-red; font-size: 24rpx; font-weight: 700; }
.member-info-row { @include adaptive.adaptive-tree-field; display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 64rpx; align-items: center; gap: 18rpx; margin-top: 8rpx; padding: 8rpx 18rpx; }
.member-info-row text { color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
.member-info-row text:last-child { color: $ink; text-align: right; }
.member-relatives { margin-top: 8rpx; font-size: 23rpx; }
.member-relatives > view { display: flex; justify-content: space-between; gap: 20rpx; padding: 12rpx 18rpx; color: $ink; font-size: 23rpx; }
.member-relatives > view text:last-child { color: $brand-red; }
.member-relatives > text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.5; }
.member-record-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12rpx; margin-top: 20rpx; }
.member-record-actions > view { @include adaptive.adaptive-scroll-button(secondary); display: flex; min-height: 72rpx; align-items: center; justify-content: center; padding: 8rpx 14rpx; text-align: center; }
.member-record-actions text { color: $brand-red; font-size: 22rpx; font-weight: 700; line-height: 1.35; }
.member-profile-action { @include adaptive.adaptive-scroll-button(primary); display: flex; min-height: 76rpx; align-items: center; justify-content: center; margin-top: 22rpx; }
.member-profile-action text { color: #fff9ed; font-size: 24rpx; font-weight: 700; }
.member-restricted { padding-top: 14rpx; }
.member-restricted > text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.6; text-align: center; }
.member-restricted > text:first-of-type { margin-top: 28rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 31rpx; font-weight: 700; }
.member-restricted > text + text { margin-top: 12rpx; }
.member-restricted-action { @include adaptive.adaptive-scroll-button(secondary); display: flex; min-height: 76rpx; align-items: center; justify-content: center; margin-top: 22rpx; }
.member-restricted-action text { color: $brand-red; font-size: 24rpx; font-weight: 700; }
.member-error { margin-top: 30%; text-align: center; }
.member-error > text { display: block; color: $ink-muted; font-size: 24rpx; line-height: 1.55; }
.member-error > text:nth-child(2) { font-size: 24rpx; }
.member-error > text:first-child { color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; }
@media (min-width: 400px) { .member-context, .member-panel { width: calc(100% - 48rpx); } }
</style>