Files
jiapuapp/pages/genealogy/g05-genealogy-overview.vue
T
2026-07-27 06:50:23 +08:00

663 lines
18 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.
<!-- 页面编号G-05用途家谱总览与加载空数据失败状态 -->
<template>
<view class="overview-page">
<GenealogyPageBackground />
<view class="overview-page__header">
<PageHeader
:title="
viewMode === 'public'
? '家谱公开预览'
: viewMode === 'preview'
? '创建流程预览'
: '家谱总览'
"
custom-back
@back="requestBack"
/>
</view>
<view
class="overview-surface"
:class="{ 'overview-surface--state': overviewState !== 'ready' }"
>
<template
v-if="overviewState === 'ready' && genealogy && viewMode === 'member'"
>
<view class="overview-ready">
<view class="overview-hero">
<text class="overview-hero__kicker"
>{{ genealogy.hall || "堂号待补" }} ·
{{ genealogy.location || "所在地待补" }}</text
>
<text class="overview-hero__title">{{ genealogy.name }}</text>
<text class="overview-hero__motto">{{
genealogy.intro || "简介待补充"
}}</text>
<view class="overview-hero__stats">
<text> {{ genealogy.memberCount || 0 }} </text>
<text v-if="genealogy.personCount !== null"
>世系 {{ genealogy.personCount }} </text
>
<text>{{ getGenealogyAccessPresetLabel(genealogy.accessPreset) }}</text>
</view>
<view class="overview-hero__stats">
<text v-if="genealogy.joinTime"
>加入于 {{ formatGenealogyTime(genealogy.joinTime) }}</text
>
</view>
</view>
<view
class="overview-actions"
:class="{ 'overview-actions--member': accessRole === 'member' }"
>
<view class="overview-action overview-action--tree" @click="toTree">
<text class="overview-action__title">世系树</text
><text>查看家脉关系</text>
</view>
<view
class="overview-action overview-action--poem"
@click="toGenerationPoems"
>
<text class="overview-action__title">字辈谱</text
><text>传承行辈次序</text>
</view>
<view
v-if="accessRole === 'owner'"
class="overview-action overview-action--review"
@click="toApplications"
>
<text class="overview-action__title">入谱审核</text
><text>处理加入申请</text>
</view>
<view
v-if="accessRole === 'owner'"
class="overview-action overview-action--settings"
@click="toSettings"
>
<text class="overview-action__title">家谱设置</text
><text>维护公开范围与基础资料</text>
</view>
<template v-else>
<view class="overview-summary">
<text class="overview-action__title">成员身份</text
><text>已加入 · 普通成员</text>
</view>
<view class="overview-summary">
<text class="overview-action__title">访问范围</text
><text>{{ getGenealogyAccessPresetLabel(genealogy.accessPreset) }}</text>
</view>
</template>
</view>
<view class="overview-family" @click="toFamily">
<text>家族近况</text><text>进入家族圈查看动态</text
><text>查看</text>
</view>
<view class="overview-note">
<text class="overview-note__title">成员资料按家谱访问规则保护</text>
<text>名称公开范围与家谱简介由谱主在设置中维护</text>
</view>
</view>
</template>
<view
v-else-if="overviewState === 'ready' && genealogy"
class="overview-public"
>
<view class="overview-public__hero">
<text class="overview-public__eyebrow">{{
viewMode === "preview" ? "本地流程预览" : "公开家谱"
}}</text>
<text class="overview-public__title">{{ genealogy.name }}</text>
<text class="overview-public__source">{{ genealogy.source || "来源信息待同步" }}</text>
</view>
<view class="overview-public__details">
<view
><text>姓氏</text><text>{{ genealogy.surname }}</text></view
>
<view
><text>地区</text><text>{{ genealogy.location || "待补充" }}</text></view
>
<view
><text>堂号</text><text>{{ genealogy.hall || "待补充" }}</text></view
>
<view
><text>当前支系</text><text>{{ genealogy.branchName || "待补充" }}</text></view
>
<view
><text>{{ viewMode === "preview" ? "首代人物" : "所属上级谱" }}</text
><text>{{ (viewMode === "preview" ? genealogy.ancestorName : genealogy.parentName) || "待补充" }}</text></view
>
</view>
<view
v-if="viewMode === 'public' && (genealogy.manager || genealogy.certification)"
class="overview-public__trust"
>
<text>{{ genealogy.manager || "管理者待确认" }}</text>
<text>{{ genealogy.certification || "认证信息待确认" }}</text>
<text>{{ genealogy.memberCount || 0 }} 位成员</text>
<text>更新于 {{ genealogy.updatedAt || "待同步" }}</text>
</view>
<view class="overview-public__notice">
<text>{{ viewMode === "preview" ? "预览说明" : "公开说明" }}</text>
<text>{{ genealogy.publicDescription || "公开说明待补充" }}</text>
</view>
<view
v-if="viewMode === 'public' && publicActionLabel"
class="overview-public__action"
@click="applyToJoin"
>
<text>{{ publicActionLabel }}</text>
</view>
</view>
<view
v-else-if="overviewState === 'loading'"
class="overview-state overview-state--loading"
>
<AppLoading
text="正在展开家谱"
description="请稍候,正在读取家谱概览。"
/>
</view>
<view
v-else
class="overview-state overview-state-panel"
:class="{
'overview-state--loading': overviewState === 'loading',
'overview-state--empty': overviewState === 'empty',
'overview-state--error': overviewState === 'error',
'overview-state--no-permission': overviewState === 'no-permission',
}"
>
<view class="overview-state__content">
<image
class="overview-state__seal"
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
<text class="overview-state-panel__title">{{ stateTitle }}</text>
<text class="overview-state-panel__copy">{{ stateCopy }}</text>
<view
class="overview-state-panel__action"
@click="
overviewState === 'error' ? reloadOverview() : toGenealogies()
"
>
<text>{{
overviewState === "error" ? "重新查看" : "返回我的家谱"
}}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onBackPress, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
appApi,
createRequestController,
isRequestCancelled,
} from "@/utils/api.js";
import { getGenealogyAccessPresetLabel } from "@/utils/genealogy-contracts.js";
import {
goBack,
goRoot,
handleBackPress,
openPage,
returnTo,
} from "@/utils/navigation.js";
const genealogy = ref(null);
const genealogyId = ref("");
const overviewState = ref("loading");
const loadError = ref("");
const viewMode = ref("member");
const accessRole = ref("guest");
const publicRelation = ref("unknown");
const publicCanApply = ref(false);
const overviewRequestController = createRequestController();
let loadGeneration = 0;
let pageActive = true;
let skipNextShowRefresh = true;
const publicActionLabel = computed(() => {
if (publicRelation.value === "pending") return "查看申请进度";
if (!publicCanApply.value) return "";
return (
({
available: "申请加入这部家谱",
rejected: "修改后重新申请",
removed: "重新申请加入",
})[publicRelation.value] || ""
);
});
const stateTitle = computed(
() =>
({
loading: "正在展开家谱…",
empty: "还没有可查看的家谱",
error: "家谱暂时无法打开",
"no-permission": "当前账号无权查看",
})[overviewState.value] || "",
);
const stateCopy = computed(
() =>
loadError.value ||
{
loading: "请稍候,正在读取家谱概览。",
empty: "请从“我的家谱”选择一部家谱后再进入。",
error: "可能是网络波动或家谱不存在。",
"no-permission": "这部家谱未向当前账号开放,请返回我的家谱或联系管理员。",
}[overviewState.value] ||
"",
);
const formatGenealogyTime = (value) =>
typeof value === "string" && value.length >= 10 ? value.slice(0, 10) : value;
const loadGenealogy = async (query = {}) => {
const generation = ++loadGeneration;
overviewRequestController.abort();
overviewState.value = "loading";
loadError.value = "";
genealogy.value = null;
genealogyId.value = String(query.genealogyId || genealogyId.value || "");
viewMode.value = "member";
accessRole.value = "guest";
publicRelation.value = "unknown";
publicCanApply.value = false;
if (query.state === "empty" || !genealogyId.value) {
overviewState.value = "empty";
return;
}
if (query.state === "error") {
overviewState.value = "error";
return;
}
if (query.state === "no-permission") {
overviewState.value = "no-permission";
return;
}
if (query.state === "loading") return;
try {
const result = await appApi.getOverview(genealogyId.value, {
requestController: overviewRequestController,
});
if (!pageActive || generation !== loadGeneration) return;
genealogy.value = result;
viewMode.value = "member";
accessRole.value = result.accessRole;
overviewState.value = "ready";
} catch (error) {
if (
!pageActive ||
generation !== loadGeneration ||
isRequestCancelled(error)
) {
return;
}
if ([401, 403, 404].includes(error?.httpStatus)) {
overviewState.value = "no-permission";
return;
}
loadError.value = error?.message || "家谱概览读取失败,请稍后重试。";
overviewState.value = "error";
}
};
const requestBack = () => goBack();
onBackPress((event) => handleBackPress(event, requestBack));
onLoad(loadGenealogy);
onShow(() => {
if (skipNextShowRefresh) {
skipNextShowRefresh = false;
return;
}
if (genealogyId.value) loadGenealogy({ genealogyId: genealogyId.value });
});
onUnload(() => {
pageActive = false;
loadGeneration += 1;
overviewRequestController.abort();
});
const reloadOverview = () => loadGenealogy({ genealogyId: genealogyId.value });
const toGenealogies = () => returnTo("G01", {});
const toTree = () =>
openPage("T01", { genealogyId: genealogyId.value }, "G05");
const toFamily = () => goRoot("F01", { genealogyId: genealogyId.value });
const toApplications = () =>
openPage("G10", { genealogyId: genealogyId.value }, "G05");
const toSettings = () =>
openPage("G11", { genealogyId: genealogyId.value }, "G05");
const toGenerationPoems = () =>
openPage("G12", { genealogyId: genealogyId.value }, "G05");
const applyToJoin = () => {
if (publicRelation.value === "pending")
return openPage("G09", { status: "pending" }, "G05");
if (!publicCanApply.value) return false;
return openPage(
"G08",
{ genealogyId: genealogyId.value, source: "search" },
"G05",
);
};
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.overview-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.overview-page__header {
z-index: 3;
}
.overview-surface {
@include adaptive.adaptive-g05-overview-surface;
z-index: 2;
width: 100%;
min-height: min(483px, calc(100vw * 1.3422));
flex: 0 0 auto;
}
.overview-surface--state {
display: flex;
height: calc(100vh - 112rpx);
min-height: 1040rpx;
padding: 28rpx;
box-sizing: border-box;
background: none;
}
.overview-ready {
display: flex;
min-height: min(540px, calc(100vw * 1.5));
flex-direction: column;
gap: 34rpx;
padding: 10rpx 0 42rpx;
box-sizing: border-box;
}
.overview-hero {
margin: 0 8%;
color: #fff8ec;
}
.overview-hero__kicker {
display: block;
color: #dec483;
font-size: 22rpx;
letter-spacing: 3rpx;
}
.overview-hero__title {
display: block;
margin-top: 12rpx;
font-family: "STKaiti", "KaiTi", serif;
font-size: 46rpx;
font-weight: 700;
letter-spacing: 4rpx;
}
.overview-hero__motto {
display: block;
margin-top: 6rpx;
color: #e9dcc1;
font-size: 23rpx;
line-height: 1.55;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.overview-hero__stats {
display: inline-flex;
gap: 18rpx;
margin-top: 12rpx;
color: #dbc990;
font-size: 22rpx;
transform: translateY(-18rpx);
}
.overview-hero__stats + .overview-hero__stats { margin-left: 18rpx; }
.overview-actions {
display: grid;
min-height: 386rpx;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
gap: 3.4% 3%;
margin: 0 7.2%;
}
.overview-actions--member {
min-height: 386rpx;
}
.overview-action,
.overview-summary {
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
padding: 24rpx 22rpx;
box-sizing: border-box;
color: $ink-muted;
font-size: 22rpx;
}
.overview-action__title {
margin-bottom: 9rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 30rpx;
font-weight: 700;
}
.overview-family {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
align-self: start;
gap: 16rpx;
margin: 0 7.5%;
color: $ink-muted;
font-size: 22rpx;
}
.overview-family text:first-child {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 28rpx;
font-weight: 700;
}
.overview-family text:last-child {
color: $brand-red;
}
.overview-note {
display: flex;
flex-direction: column;
justify-content: center;
margin: 0 8.5%;
color: $ink-muted;
font-size: 22rpx;
line-height: 1.6;
margin-top: 100rpx;
}
.overview-note__title {
margin-bottom: 8rpx;
color: $brand-red;
font-family: "STKaiti", "KaiTi", serif;
font-size: 28rpx;
font-weight: 700;
}
.overview-state {
@include adaptive.adaptive-genealogy-state-panel;
display: flex;
width: 100%;
min-height: 0;
align-items: center;
justify-content: center;
text-align: center;
}
.overview-state__content {
z-index: 1;
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 48rpx 34rpx;
}
.overview-state--loading .app-loading {
width: 100%;
height: 100%;
}
.overview-state__seal {
width: 132rpx;
height: 136rpx;
}
.overview-state-panel__title {
margin-top: 28rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 42rpx;
font-weight: 700;
}
.overview-state-panel__copy {
max-width: 520rpx;
margin-top: 18rpx;
color: $ink-muted;
font-size: 27rpx;
line-height: 1.7;
}
.overview-state-panel__action {
display: flex;
width: 560rpx;
min-height: 124rpx;
margin-top: 34rpx;
align-items: center;
justify-content: center;
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
center / contain no-repeat;
}
.overview-state-panel__action text {
z-index: 1;
color: #fff9ed;
font-size: 31rpx;
font-weight: 700;
}
.overview-public {
display: flex;
min-height: min(540px, calc(100vw * 1.5));
flex-direction: column;
gap: 0;
padding: 58rpx 0 28rpx;
box-sizing: border-box;
}
.overview-public__hero {
margin: 0 8%;
color: #fff8ec;
}
.overview-public__eyebrow {
display: block;
color: #dec483;
font-size: 21rpx;
letter-spacing: 3rpx;
}
.overview-public__title {
display: block;
margin-top: 10rpx;
font-family: "STKaiti", "KaiTi", serif;
font-size: 43rpx;
font-weight: 700;
letter-spacing: 4rpx;
}
.overview-public__source {
display: block;
margin-top: 7rpx;
color: #e9dcc1;
font-size: 22rpx;
}
.overview-public__details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18rpx 28rpx;
min-height: 240rpx;
margin: 126rpx 9% 0;
}
.overview-public__details > view {
display: flex;
flex-direction: column;
}
.overview-public__details > view:nth-child(5) {
grid-column: 1 / -1;
align-self: start;
flex-direction: row;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 0 24rpx;
}
.overview-public__details > view:nth-child(5) text:first-child {
flex: 0 0 auto;
}
.overview-public__details > view:nth-child(5) text:last-child {
margin-top: 0;
}
.overview-public__details text:first-child {
color: $brand-red;
font-size: 21rpx;
}
.overview-public__details text:last-child {
margin-top: 6rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 27rpx;
}
.overview-public__trust {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 10rpx 24rpx;
margin: 28rpx 9% 0;
color: $ink-muted;
font-size: 21rpx;
line-height: 1.45;
}
.overview-public__notice {
display: flex;
margin: 54rpx 9% 0;
color: $ink-muted;
font-size: 19rpx;
line-height: 26rpx;
}
.overview-public__notice text:first-child {
flex: 0 0 auto;
margin-right: 12rpx;
color: $brand-red;
font-weight: 700;
}
.overview-public__action {
display: flex;
min-height: 82rpx;
align-items: center;
justify-content: center;
margin: 204rpx 9% 0;
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
center / contain no-repeat;
}
.overview-public__action text {
z-index: 1;
color: #fff9ed;
font-size: 27rpx;
font-weight: 700;
letter-spacing: 3rpx;
}
@media (min-width: 400px) {
.overview-action {
padding: 0 28rpx;
}
}
</style>