Files
jiapuapp/pages/genealogy/overview.vue
T

699 lines
21 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" @click="toFamily">
<view>
<text>家族近况</text>
<text>进入家族圈查看动态</text>
</view>
<image
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</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,
goRoot,
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 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 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,
.overview-family image {
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 {
display: grid;
grid-template-columns: minmax(0, 1fr) 26rpx;
align-items: center;
gap: 20rpx;
margin: 14rpx 42rpx 0;
padding: 20rpx 0;
border-top: 1rpx solid rgba(152, 119, 72, 0.28);
color: $ink-muted;
font-size: clamp(13px, 20rpx, 16px);
line-height: 1.5;
}
.overview-family text {
display: block;
}
.overview-family text:first-child {
margin-bottom: 3rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(16px, 27rpx, 20px);
font-weight: 700;
}
.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>