Files
jiapuapp/pages/tree/t01-tree-overview.vue
T
2026-07-20 17:23:13 +08:00

678 lines
16 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-01用途纵向世系树阅读同代横向提示空态与失败态 -->
<template>
<view
class="tree-page"
:class="{
'tree-state--tree': treeState === 'tree',
'tree-state--landscape': treeState === 'landscape',
'tree-state--empty': treeState === 'empty',
'tree-state--error': treeState === 'error',
}"
>
<ModulePageBackground module="tree" />
<view class="tree-page__header">
<PageHeader title="世系树" action="成员目录" @action="toDirectory" />
</view>
<view v-if="treeState !== 'loading'" class="tree-toolbar">
<view class="tree-toolbar__title">
<text>汤氏家谱</text>
<text>主支 · 1214 </text>
</view>
<view class="tree-toolbar__actions">
<text @click="treeState = 'landscape'">阅读提示</text>
<text @click="toRelationship">关系维护</text>
</view>
</view>
<view class="tree-stage">
<view v-if="treeState === 'tree'" class="generation-rail">
<view
v-for="row in generationRows"
:key="row.generation"
class="generation-band"
:style="generationRowStyle(row)"
>
<image
src="/static/assets/modules/genealogy/transparent/section-divider.png"
mode="scaleToFill"
/>
<view class="generation-band__copy">
<text>{{ row.label }}</text>
<text>{{ row.summary }}</text>
</view>
</view>
</view>
<scroll-view
class="tree-scroll"
:class="{ 'tree-scroll--lineage': treeState === 'tree' }"
scroll-x
:scroll-left="initialScrollLeft"
:show-scrollbar="false"
>
<view
class="tree-canvas"
:class="{ 'tree-canvas--state': treeState !== 'tree' }"
>
<AppLoading
v-if="treeState === 'loading'"
text="正在整理世系"
description="请稍候,正在准备家谱成员关系。"
/>
<template v-else-if="treeState === 'tree'">
<view class="lineage-pan-cue">
<text>同代分支可左右查看</text>
</view>
<view class="lineage-connector lineage-connector--root" />
<view class="lineage-connector lineage-connector--root-branch" />
<view class="lineage-connector lineage-connector--gen13-left" />
<view class="lineage-connector lineage-connector--gen13-right" />
<view class="lineage-connector lineage-connector--left-trunk" />
<view class="lineage-connector lineage-connector--left-branch" />
<view class="lineage-connector lineage-connector--gen14-left" />
<view class="lineage-connector lineage-connector--gen14-middle" />
<view class="lineage-connector lineage-connector--right-trunk" />
<view class="lineage-connector lineage-connector--right-branch" />
<view class="lineage-connector lineage-connector--gen14-right" />
<view
v-for="member in members"
:key="member.id"
class="member-node"
:class="{ 'member-node--selected': selected?.id === member.id }"
:style="nodeStyle(member)"
@click="selected = member"
>
<image
class="member-node__skin"
:src="
selected?.id === member.id
? '/static/assets/modules/tree/transparent/t01-member-node-selected.png'
: '/static/assets/modules/tree/transparent/t01-member-node-standard.png'
"
mode="scaleToFill"
/>
<text class="node-name">{{ member.name }}</text>
<text class="node-relation"
>{{ member.relation }} · {{ member.branch }}</text
>
<text class="node-years">{{ member.years }}</text>
</view>
</template>
<view v-else class="tree-state-card">
<image
class="tree-state-card__skin"
src="/static/assets/modules/tree/transparent/t01-state-panel.png"
mode="scaleToFill"
/>
<view class="tree-state-card__content">
<text class="tree-state-card__eyebrow">{{
stateCopy.eyebrow
}}</text>
<text class="tree-state-card__title">{{ stateCopy.title }}</text>
<text class="tree-state-card__copy">{{ stateCopy.copy }}</text>
<view class="tree-state-card__action" @click="handleStateAction">
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/>
<text>{{ stateCopy.action }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<view v-if="treeState === 'tree' && selected" class="member-sheet">
<image
class="member-sheet__skin"
src="/static/assets/modules/tree/transparent/t01-member-drawer.png"
mode="scaleToFill"
/>
<view class="member-sheet__copy">
<text class="sheet-name">{{ selected.name }}</text>
<text class="sheet-meta"
> {{ selected.generation }} · {{ selected.relation }} ·
{{ selected.branch }}</text
>
</view>
<view class="sheet-actions">
<AppButton
class="sheet-action"
type="secondary"
label="查看资料"
@click="toMember"
/>
<AppButton
class="sheet-action"
label="添加亲属"
@click="toAddRelative"
/>
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { genealogyContext } from "@/utils/genealogy-context.js";
const genealogyId = ref("");
const treeState = ref("loading");
const selected = ref(null);
const initialScrollLeft = ref(90);
const generationRows = [
{ generation: 12, label: "第十二世", summary: "始祖", y: 135 },
{ generation: 13, label: "第十三世", summary: "两房", y: 350 },
{ generation: 14, label: "第十四世", summary: "三支", y: 650 },
];
const members = ref([
{
id: 101,
name: "汤文远",
relation: "始祖",
years: "1940—2012",
generation: 12,
branch: "主支",
x: 450,
y: 135,
},
{
id: 102,
name: "汤正国",
relation: "长子",
years: "1965—",
generation: 13,
branch: "长房",
x: 275,
y: 350,
},
{
id: 103,
name: "汤正华",
relation: "次子",
years: "1968—",
generation: 13,
branch: "二房",
x: 625,
y: 350,
},
{
id: 104,
name: "汤凯",
relation: "长孙",
years: "1992—",
generation: 14,
branch: "长房",
x: 150,
y: 650,
},
{
id: 105,
name: "汤悦",
relation: "长孙女",
years: "1995—",
generation: 14,
branch: "长房",
x: 400,
y: 650,
},
{
id: 106,
name: "汤晨",
relation: "次孙",
years: "1998—",
generation: 14,
branch: "二房",
x: 690,
y: 650,
},
]);
const stateCopy = computed(
() =>
({
landscape: {
eyebrow: "世系阅读提示",
title: "上下看世代,左右看同代分支",
copy: "始祖位于上方,后代逐世向下展开;同一世代分支较多时,可左右拖动查看。",
action: "进入世系树",
},
empty: {
eyebrow: "尚未建立世系",
title: "从第一位先祖开始记录",
copy: "录入首代成员后,即可继续补充子女、配偶与后代关系。",
action: "录入首代成员",
},
error: {
eyebrow: "世系暂不可用",
title: "暂时无法读取成员关系",
copy: "请从当前家谱重新进入;已经录入的成员资料不会受到影响。",
action: "重新查看",
},
})[treeState.value] || {},
);
onLoad((query) => {
genealogyId.value =
query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
if (
query.state === "loading" ||
["landscape", "empty", "error"].includes(query.state)
) {
treeState.value = query.state;
return;
}
if (!genealogyId.value) {
treeState.value = "empty";
return;
}
genealogyContext.setCurrentGenealogyId(genealogyId.value);
selected.value =
members.value.find(
(item) => String(item.id) === String(query.selectedId),
) || members.value[0];
treeState.value = "tree";
});
const nodeStyle = (member) => ({
left: `${member.x}rpx`,
top: `${member.y}rpx`,
});
const generationRowStyle = (row) => ({
top: `${row.y - 88}rpx`,
});
const handleStateAction = () => {
if (treeState.value === "empty") {
uni.navigateTo({
url: `/pages/tree/t04-add-relative?genealogyId=${genealogyId.value || 1001}&mode=first`,
});
return;
}
selected.value = members.value[0];
treeState.value = "tree";
};
const toMember = () =>
uni.navigateTo({
url: `/pages/tree/t03-member-profile?genealogyId=${genealogyId.value}&personId=${selected.value.id}`,
});
const toDirectory = () =>
uni.navigateTo({
url: `/pages/tree/t07-member-directory?genealogyId=${genealogyId.value}`,
});
const toRelationship = () =>
uni.navigateTo({
url: `/pages/tree/t06-edit-relationship?genealogyId=${genealogyId.value}`,
});
const toAddRelative = () =>
uni.navigateTo({
url: `/pages/tree/t04-add-relative?genealogyId=${genealogyId.value}&personId=${selected.value.id}`,
});
</script>
<style scoped lang="scss">
.tree-page {
position: relative;
height: 100vh;
overflow: hidden;
background: $paper;
}
.tree-page__header,
.tree-toolbar,
.tree-stage,
.member-sheet {
position: relative;
z-index: 2;
}
.tree-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18rpx 28rpx 12rpx;
}
.tree-toolbar__title text {
display: block;
}
.tree-toolbar__title text:first-child {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 28rpx;
font-weight: 700;
}
.tree-toolbar__title text:last-child {
margin-top: 5rpx;
color: $ink-muted;
font-size: 22rpx;
}
.tree-toolbar__actions {
display: flex;
gap: 24rpx;
color: $brand-red;
font-size: 23rpx;
}
.tree-stage {
position: relative;
height: calc(100vh - 272rpx);
}
.tree-scroll {
width: 100%;
height: 100%;
white-space: nowrap;
}
.generation-rail {
position: absolute;
top: 0;
bottom: 0;
left: 0;
z-index: 4;
width: 190rpx;
box-sizing: border-box;
border-right: 1rpx solid rgba(143, 108, 63, 0.2);
}
.tree-scroll--lineage {
width: calc(100% - 190rpx);
margin-left: 190rpx;
}
.tree-canvas {
position: relative;
width: 900rpx;
height: 900rpx;
margin: 0 16rpx;
}
.tree-canvas--state {
width: calc(100vw - 32rpx);
}
.lineage-pan-cue {
position: absolute;
top: 8rpx;
right: 22rpx;
z-index: 3;
color: #9a7748;
font-size: 20rpx;
letter-spacing: 1rpx;
}
.generation-band {
position: absolute;
left: 12rpx;
z-index: 3;
width: 166rpx;
height: 58rpx;
color: #8f6c3f;
font-size: 22rpx;
text-align: center;
}
.generation-band image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.generation-band__copy {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.generation-band__copy text:last-child {
margin-top: 1rpx;
font-size: 19rpx;
}
.lineage-connector {
position: absolute;
z-index: 1;
background: #b78a42;
}
.lineage-connector--root {
left: 449rpx;
top: 177rpx;
width: 2rpx;
height: 88rpx;
}
.lineage-connector--root-branch {
left: 275rpx;
top: 264rpx;
width: 350rpx;
height: 2rpx;
}
.lineage-connector--gen13-left {
left: 274rpx;
top: 264rpx;
width: 2rpx;
height: 44rpx;
}
.lineage-connector--gen13-right {
left: 624rpx;
top: 264rpx;
width: 2rpx;
height: 44rpx;
}
.lineage-connector--left-trunk {
left: 274rpx;
top: 392rpx;
width: 2rpx;
height: 149rpx;
}
.lineage-connector--left-branch {
left: 150rpx;
top: 540rpx;
width: 250rpx;
height: 2rpx;
}
.lineage-connector--gen14-left {
left: 149rpx;
top: 540rpx;
width: 2rpx;
height: 68rpx;
}
.lineage-connector--gen14-middle {
left: 399rpx;
top: 540rpx;
width: 2rpx;
height: 68rpx;
}
.lineage-connector--right-trunk {
left: 624rpx;
top: 392rpx;
width: 2rpx;
height: 149rpx;
}
.lineage-connector--right-branch {
left: 625rpx;
top: 540rpx;
width: 65rpx;
height: 2rpx;
}
.lineage-connector--gen14-right {
left: 689rpx;
top: 540rpx;
width: 2rpx;
height: 68rpx;
}
.member-node {
position: absolute;
z-index: 2;
width: 224rpx;
height: 86rpx;
transform: translate(-50%, -50%);
text-align: center;
}
.member-node__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.node-name,
.node-relation,
.node-years {
position: relative;
z-index: 1;
display: block;
}
.node-name {
padding-top: 5rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 25rpx;
font-weight: 700;
}
.node-relation {
margin-top: 0;
color: $brand-red;
font-size: 21rpx;
}
.node-years {
margin-top: 0;
color: $ink-muted;
font-size: 20rpx;
}
.member-node--selected {
width: 236rpx;
height: 94rpx;
}
.member-node--selected .node-name {
color: $brand-red;
}
.tree-state-card {
position: relative;
z-index: 2;
width: calc(100% - 48rpx);
height: 360rpx;
margin: 150rpx 24rpx 0;
text-align: center;
white-space: normal;
}
.tree-state-card__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.tree-state-card__content {
position: relative;
z-index: 1;
padding: 62rpx 58rpx 42rpx;
}
.tree-state-card__eyebrow {
display: block;
color: $brand-red;
font-size: 22rpx;
letter-spacing: 3rpx;
}
.tree-state-card__title {
display: block;
margin-top: 12rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 34rpx;
font-weight: 700;
}
.tree-state-card__copy {
display: block;
margin-top: 16rpx;
color: $ink-muted;
font-size: 24rpx;
line-height: 1.65;
}
.tree-state-card__action {
position: relative;
width: 360rpx;
height: 70rpx;
margin: 22rpx auto 0;
}
.tree-state-card__action image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.tree-state-card__action text {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #fff9ed;
font-size: 24rpx;
font-weight: 700;
}
.member-sheet {
position: fixed;
right: 18rpx;
bottom: calc(14rpx + env(safe-area-inset-bottom));
left: 18rpx;
display: flex;
height: 240rpx;
flex-direction: column;
padding: 38rpx 44rpx 24rpx;
box-sizing: border-box;
}
.member-sheet__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.member-sheet__copy {
position: relative;
z-index: 1;
}
.sheet-name {
display: block;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 31rpx;
font-weight: 700;
}
.sheet-meta {
display: block;
margin-top: 8rpx;
color: $ink-muted;
font-size: 22rpx;
}
.sheet-actions {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
gap: 16rpx;
margin-top: auto;
}
.sheet-action {
width: 250rpx;
min-height: 88rpx;
}
@media (max-width: 360px) {
.tree-toolbar {
padding-right: 20rpx;
padding-left: 20rpx;
}
.tree-toolbar__actions {
gap: 14rpx;
}
.member-sheet {
height: 240rpx;
padding-top: 32rpx;
padding-right: 36rpx;
padding-left: 36rpx;
padding-bottom: 20rpx;
}
}
</style>