Files
jiapuapp/pages/genealogy/g05-genealogy-overview.vue
T

839 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.
<!-- 页面编号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',
'overview-surface--member':
overviewState === 'ready' && viewMode === 'member',
}"
>
<template
v-if="overviewState === 'ready' && genealogy && viewMode === 'member'"
>
<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
v-if="accessRole === 'owner'"
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="accessRole === 'owner'"
class="overview-action overview-action--settings"
@click="toSettings"
>
<image
class="overview-action__icon"
src="/static/assets/modules/genealogy/transparent/g05-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 === '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,
genealogyName: genealogy.name,
source: "search",
},
"G05",
);
};
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.overview-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.overview-page__header {
z-index: 3;
}
.overview-surface {
@include adaptive-g05-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/g05-overview-surface.png")
center top / auto 1220rpx 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-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/a01-scroll-primary-v3.png")
center / contain no-repeat;
}
.overview-state-panel__action text {
z-index: 1;
color: #fff9ed;
font-size: clamp(17px, 31rpx, 22px);
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: clamp(13px, 21rpx, 16px);
letter-spacing: 3rpx;
}
.overview-public__title {
display: block;
margin-top: 10rpx;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(22px, 43rpx, 28px);
font-weight: 700;
letter-spacing: 4rpx;
}
.overview-public__source {
display: block;
margin-top: 7rpx;
color: #e9dcc1;
font-size: clamp(14px, 22rpx, 17px);
}
.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: clamp(13px, 21rpx, 16px);
}
.overview-public__details text:last-child {
margin-top: 6rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(16px, 27rpx, 20px);
}
.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: clamp(13px, 21rpx, 16px);
line-height: 1.45;
}
.overview-public__notice {
display: flex;
margin: 54rpx 9% 0;
color: $ink-muted;
font-size: clamp(13px, 19rpx, 16px);
line-height: max(1.35em, clamp(16px, 26rpx, 20px));
}
.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: clamp(16px, 27rpx, 20px);
font-weight: 700;
letter-spacing: 3rpx;
}
@media (min-width: 400px) {
.overview-action {
padding: 0 28rpx;
}
}
</style>