Files
jiapuapp/pages/genealogy/overview.vue
T
2026-09-13 17:45:52 +08:00

794 lines
24 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.
<template>
<view class="overview-page">
<GenealogyPageBackground />
<view class="overview-page__header">
<PageHeader
title="家谱总览"
custom-back
@back="requestBack"
/>
</view>
<view
class="overview-surface"
:class="{
'overview-surface--state': overviewState !== 'ready',
'overview-surface--member': overviewState === 'ready',
}"
>
<template v-if="overviewState === 'ready' && genealogy">
<view class="overview-ready">
<view class="overview-hero">
<view class="overview-hero__identity">
<view class="overview-hero__emblem">
<image
src="/static/assets/modules/genealogy/transparent/shortcut-tree.png"
mode="aspectFit"
/>
</view>
<view class="overview-hero__headings">
<text class="overview-hero__title">{{ genealogy.name }}</text>
<text class="overview-hero__kicker"
>{{ genealogy.hall || "堂号待补" }} ·
{{ genealogy.location || "所在地待补" }}</text
>
</view>
</view>
<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>
<text v-if="genealogy.joinTime"
>加入于 {{ formatGenealogyTime(genealogy.joinTime) }}</text
>
</view>
</view>
<view class="overview-actions">
<view class="overview-action overview-action--tree" @click="toTree">
<view class="overview-action__icon overview-action__icon--primary">
<image
src="/static/assets/modules/genealogy/transparent/shortcut-tree.png"
mode="aspectFit"
/>
</view>
<view class="overview-action__copy">
<text class="overview-action__title">世系树</text>
<text>查看家脉关系</text>
</view>
<image
class="overview-action__chevron"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</view>
<view class="overview-action" @click="openInvitationManager">
<image
class="overview-action__icon"
src="/static/assets/modules/genealogy/transparent/shortcut-members.png"
mode="aspectFit"
/>
<view class="overview-action__copy">
<text class="overview-action__title">邀请家人</text>
<text>生成或撤销我发出的邀请码</text>
</view>
<image
class="overview-action__chevron"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</view>
<view class="overview-action" @click="toMembers">
<image
class="overview-action__icon"
src="/static/assets/modules/genealogy/transparent/shortcut-members.png"
mode="aspectFit"
/>
<view class="overview-action__copy">
<text class="overview-action__title">成员管理</text>
<text>查看成员身份角色和人物绑定</text>
</view>
<image
class="overview-action__chevron"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</view>
<view
v-if="canManageGenealogy"
class="overview-action overview-action--review"
@click="toApplications"
>
<image
class="overview-action__icon"
src="/static/assets/modules/genealogy/transparent/shortcut-application.png"
mode="aspectFit"
/>
<view class="overview-action__copy">
<text class="overview-action__title">入谱审核</text>
<text>处理加入申请</text>
</view>
<image
class="overview-action__chevron"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</view>
<view
class="overview-action overview-action--poem"
@click="toGenerationPoems"
>
<image
class="overview-action__icon"
src="/static/assets/foundation/transparent/tab-genealogy.png"
mode="aspectFit"
/>
<view class="overview-action__copy">
<text class="overview-action__title">字辈谱</text>
<text>传承行辈次序</text>
</view>
<image
class="overview-action__chevron"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</view>
<view
v-if="canManageGenealogy"
class="overview-action overview-action--settings"
@click="toSettings"
>
<image
class="overview-action__icon"
src="/static/assets/modules/genealogy/transparent/settings-gear.png"
mode="aspectFit"
/>
<view class="overview-action__copy">
<text class="overview-action__title">家谱设置</text>
<text>维护公开范围与基础资料</text>
</view>
<image
class="overview-action__chevron"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</view>
<view class="overview-action" @click="toPersonDocuments">
<image
class="overview-action__icon"
src="/static/assets/modules/genealogy/transparent/shortcut-members.png"
mode="aspectFit"
/>
<view class="overview-action__copy">
<text class="overview-action__title">重要证件</text>
<text>集中查看家谱证件档案</text>
</view>
<image
class="overview-action__chevron"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</view>
<view v-if="canManageGenealogy" class="overview-action" @click="toCapabilityCenter">
<image class="overview-action__icon" src="/static/assets/modules/genealogy/transparent/settings-gear.png" mode="aspectFit" />
<view class="overview-action__copy">
<text class="overview-action__title">资料与回收站</text>
<text>查看资料完整度恢复误删的家谱内容</text>
</view>
<image class="overview-action__chevron" src="/static/assets/foundation/transparent/chevron-right.png" mode="aspectFit" />
</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-content">
<view class="overview-family-content__heading">
<text>家族内容</text>
<text>按用途进入动态影像与家族事务</text>
</view>
<view
v-for="group in familyContentGroups"
:key="group.key"
class="overview-family-group"
>
<text class="overview-family-group__title">{{ group.title }}</text>
<view class="overview-family-grid">
<button
v-for="item in group.items"
:key="item.key"
class="overview-family-link"
@click="openFamilySection(item.key)"
>{{ item.label }}</button>
</view>
</view>
</view>
<view class="overview-note">
<text class="overview-note__title">成员资料按家谱访问规则保护</text>
<text>名称公开范围与家谱简介由谱主在设置中维护</text>
</view>
</view>
</template>
<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>
<InvitationManager
ref="invitationManager"
:genealogy-id="genealogyId"
@busy-change="invitationBusy = $event"
@transient-change="invitationTransientOpen = $event"
/>
</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 InvitationManager from "@/components/genealogy/InvitationManager.vue";
import GenealogyPageBackground from "@/components/genealogy/PageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { genealogyApi } from "@/services/api/genealogy-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { getGenealogyAccessPresetLabel } from "@/utils/genealogy/access-policy.js";
import {
goBack,
handleBackPress,
openPage,
returnTo,
} from "@/utils/navigation/gateway.js";
const genealogy = ref(null);
const genealogyId = ref("");
const overviewState = ref("loading");
const overviewLoadError = ref("");
const canManageGenealogy = ref(false);
const invitationManager = ref(null);
const invitationBusy = ref(false);
const invitationTransientOpen = ref(false);
const genealogyOverviewRequestController = createRequestController();
let loadGeneration = 0;
let pageActive = true;
let skipNextShowRefresh = true;
const stateTitle = computed(
() =>
({
loading: "正在展开家谱…",
empty: "还没有可查看的家谱",
error: "家谱暂时无法打开",
"no-permission": "暂时无法查看",
})[overviewState.value] || "",
);
const stateCopy = computed(
() =>
overviewLoadError.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;
genealogyOverviewRequestController.abort();
overviewState.value = "loading";
overviewLoadError.value = "";
genealogy.value = null;
genealogyId.value = String(query.genealogyId || genealogyId.value || "");
canManageGenealogy.value = false;
if (!/^[1-9]\d*$/.test(genealogyId.value)) {
overviewState.value = "empty";
return;
}
try {
const overviewDetails = await genealogyApi.getOverview(genealogyId.value, {
requestController: genealogyOverviewRequestController,
});
if (!pageActive || generation !== loadGeneration) return;
genealogy.value = overviewDetails;
canManageGenealogy.value = overviewDetails.canManage;
overviewState.value = "ready";
} catch (error) {
if (
!pageActive ||
generation !== loadGeneration ||
isRequestCancelled(error)
) {
return;
}
if ([401, 403, 404].includes(error?.httpStatus)) {
overviewState.value = "no-permission";
return;
}
overviewLoadError.value = getRequestErrorMessage(error, "家谱概览加载失败,请稍后重试。");
overviewState.value = "error";
}
};
onLoad(loadGenealogy);
onShow(() => {
if (skipNextShowRefresh) {
skipNextShowRefresh = false;
return;
}
if (genealogyId.value) loadGenealogy({ genealogyId: genealogyId.value });
});
onUnload(() => {
pageActive = false;
loadGeneration += 1;
genealogyOverviewRequestController.abort();
});
const reloadOverview = () => loadGenealogy({ genealogyId: genealogyId.value });
const toGenealogies = () => returnTo("G01", {});
const toTree = () => openPage("T01", { genealogyId: genealogyId.value }, "G05");
const familyContentGroups = [
{
key: "memories",
title: "家族记录",
items: [
{ key: "feed", label: "家族动态" },
{ key: "articles", label: "谱文" },
{ key: "albums", label: "相册" },
{ key: "videos", label: "家族视频" },
],
},
{
key: "affairs",
title: "家族事务",
items: [
{ key: "rituals", label: "礼仪" },
{ key: "memos", label: "备忘" },
{ key: "benefactors", label: "家族恩人" },
{ key: "people", label: "人物录" },
{ key: "gifts", label: "贺礼簿" },
{ key: "merits", label: "功德录" },
],
},
];
const openFamilySection = (key) => {
if (!genealogyId.value) return;
if (key === "feed") return openPage("F12", { genealogyId: genealogyId.value }, "G05");
const routes = {
articles: "F04",
albums: "F07",
rituals: "R05",
memos: "R10",
benefactors: "R10",
people: "R01",
gifts: "R03",
merits: "R11",
videos: "F10",
};
return openPage(
routes[key],
{
genealogyId: genealogyId.value,
...(key === "benefactors" ? { memoType: "benefactor" } : {}),
},
"G05",
);
};
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 toMembers = () =>
openPage("G13", { genealogyId: genealogyId.value }, "G05");
const toPersonDocuments = () =>
openPage("R12", { genealogyId: genealogyId.value }, "G05");
const openInvitationManager = () => {
if (overviewState.value !== "ready" || !genealogyId.value) return;
invitationManager.value?.open();
};
const toCapabilityCenter = () => openPage("G14", { genealogyId: genealogyId.value }, "G05");
const requestBack = () => {
if (invitationBusy.value || invitationTransientOpen.value) {
invitationManager.value?.closeTransient();
return true;
}
return goBack();
};
onBackPress((event) => handleBackPress(event, requestBack));
</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-genealogy-overview-surface;
z-index: 2;
width: 100%;
min-height: min(483px, calc(100vw * 1.3422));
flex: 0 0 auto;
}
.overview-surface--member {
min-height: 100%;
border: 0;
border-image: none;
background: #fbf4e7;
}
.overview-surface--state {
display: flex;
height: calc(100vh - 112rpx);
min-height: 1040rpx;
padding: 28rpx;
box-sizing: border-box;
background: none;
}
.overview-ready {
display: flex;
flex-direction: column;
min-height: 0;
padding: 0 0 calc(34rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.overview-hero {
display: flex;
min-height: 286rpx;
flex-direction: column;
justify-content: flex-end;
margin: 0;
padding: 34rpx 42rpx 28rpx;
background: #10293b
url("/static/assets/modules/genealogy/opaque/overview-surface.png")
center top / auto 400% no-repeat;
color: #fff8ec;
box-sizing: border-box;
}
.overview-hero__identity {
display: grid;
grid-template-columns: 108rpx minmax(0, 1fr);
align-items: center;
gap: 22rpx;
}
.overview-hero__emblem {
display: grid;
width: 108rpx;
height: 108rpx;
place-items: center;
border: 2rpx solid rgba(225, 190, 111, 0.86);
border-radius: 50%;
background: rgba(16, 41, 59, 0.56);
box-sizing: border-box;
}
.overview-hero__emblem image {
width: 64rpx;
height: 64rpx;
}
.overview-hero__kicker {
display: block;
color: #dec483;
font-size: clamp(13px, 20rpx, 16px);
line-height: 1.5;
}
.overview-hero__title {
display: block;
margin-bottom: 6rpx;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(22px, 42rpx, 28px);
font-weight: 700;
letter-spacing: 2rpx;
}
.overview-hero__motto {
display: block;
margin-top: 22rpx;
color: #e9dcc1;
font-size: clamp(13px, 21rpx, 16px);
line-height: 1.55;
}
.overview-hero__stats {
display: flex;
flex-wrap: wrap;
gap: 8rpx 0;
margin-top: 18rpx;
color: #dbc990;
font-size: clamp(13px, 19rpx, 16px);
line-height: 1.4;
}
.overview-hero__stats text {
display: flex;
align-items: center;
}
.overview-hero__stats text + text::before {
width: 1rpx;
height: 20rpx;
margin: 0 14rpx;
background: rgba(220, 201, 144, 0.55);
content: "";
}
.overview-actions {
display: flex;
flex-direction: column;
padding: 28rpx 42rpx 0;
background: #fbf4e7;
box-sizing: border-box;
}
.overview-action,
.overview-summary {
display: grid;
grid-template-columns: 72rpx minmax(0, 1fr) 28rpx;
min-height: 126rpx;
align-items: center;
gap: 24rpx;
min-width: 0;
padding: 18rpx 0;
border-top: 1rpx solid rgba(152, 119, 72, 0.28);
box-sizing: border-box;
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
line-height: 1.5;
}
.overview-action--tree {
min-height: 176rpx;
grid-template-columns: 128rpx minmax(0, 1fr) 28rpx;
gap: 28rpx;
border-top: 0;
}
.overview-action__icon {
width: 64rpx;
height: 64rpx;
justify-self: center;
object-fit: contain;
}
.overview-action__icon--primary {
display: grid;
width: 118rpx;
height: 118rpx;
place-items: center;
border: 2rpx solid rgba(160, 37, 30, 0.88);
border-radius: 50%;
background: #aa3129;
box-sizing: border-box;
}
.overview-action__icon--primary image {
width: 68rpx;
height: 68rpx;
filter: brightness(0) invert(1);
}
.overview-action__copy {
min-width: 0;
}
.overview-action__copy text,
.overview-summary text {
display: block;
}
.overview-action__chevron {
width: 26rpx;
height: 26rpx;
opacity: 0.68;
}
.overview-action__title {
margin-bottom: 5rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(17px, 30rpx, 22px);
font-weight: 700;
}
.overview-action--tree .overview-action__title {
font-size: clamp(20px, 40rpx, 26px);
}
.overview-summary {
grid-template-columns: minmax(0, 1fr);
gap: 0;
}
.overview-family-content {
margin: 14rpx 42rpx 0;
padding: 24rpx 0 0;
border-top: 1rpx solid rgba(152, 119, 72, 0.28);
}
.overview-family-content__heading text {
display: block;
}
.overview-family-content__heading text:first-child {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(18px, 31rpx, 22px);
font-weight: 700;
}
.overview-family-content__heading text:last-child {
margin-top: 5rpx;
color: $ink-muted;
font-size: clamp(13px, 20rpx, 16px);
line-height: 1.5;
}
.overview-family-group {
margin-top: 22rpx;
}
.overview-family-group__title {
display: block;
color: #7f4f16;
font-size: clamp(14px, 22rpx, 17px);
font-weight: 700;
}
.overview-family-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12rpx;
margin-top: 12rpx;
}
.overview-family-link {
display: flex;
min-height: 76rpx;
align-items: center;
justify-content: center;
margin: 0;
padding: 8rpx 10rpx;
border: 1rpx solid rgba(181, 137, 63, 0.34);
border-radius: 8rpx;
background: rgba(247, 237, 218, 0.76);
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(14px, 22rpx, 17px);
font-weight: 700;
line-height: 1.35;
}
.overview-family-link::after {
border: 0;
}
.overview-note {
display: flex;
flex-direction: column;
margin: 18rpx 42rpx 0;
padding: 18rpx 0 0;
border-top: 1rpx solid rgba(152, 119, 72, 0.28);
color: $ink-muted;
font-size: clamp(13px, 19rpx, 16px);
line-height: 1.55;
}
.overview-note__title {
margin-bottom: 5rpx;
color: $brand-red;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(15px, 24rpx, 18px);
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: clamp(22px, 42rpx, 28px);
font-weight: 700;
}
.overview-state-panel__copy {
max-width: 520rpx;
margin-top: 18rpx;
color: $ink-muted;
font-size: clamp(16px, 27rpx, 20px);
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/scroll-primary.png")
center / contain no-repeat;
}
.overview-state-panel__action text {
z-index: 1;
color: #fff9ed;
font-size: clamp(17px, 31rpx, 22px);
font-weight: 700;
}
@media (min-width: 400px) {
.overview-action {
padding: 0 28rpx;
}
}
</style>