801 lines
21 KiB
Vue
801 lines
21 KiB
Vue
<!-- 页面编号: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>主支 · 第 12—14 世</text>
|
||
</view>
|
||
<view class="tree-toolbar__actions">
|
||
<text
|
||
v-if="treeState === 'tree' && treeHasDrifted && selected"
|
||
class="tree-recenter"
|
||
role="button"
|
||
aria-label="回到当前成员"
|
||
hover-class="tree-action--pressed"
|
||
@click="recenterSelectedMember"
|
||
>回到当前</text
|
||
>
|
||
<text @click="treeState = 'landscape'">阅读提示</text>
|
||
<text @click="toRelationship">关系维护</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view
|
||
class="tree-stage"
|
||
:class="{ 'tree-stage--lineage': treeState === 'tree' }"
|
||
>
|
||
<view
|
||
v-if="treeState === 'tree'"
|
||
class="generation-rail"
|
||
:style="generationRailStyle"
|
||
>
|
||
<view
|
||
v-for="row in generationRows"
|
||
:key="row.generation"
|
||
class="generation-band"
|
||
:style="generationBandStyle(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>
|
||
|
||
<view v-if="treeState === 'tree'" class="lineage-pan-cue">
|
||
<text>同代分支可左右查看</text>
|
||
</view>
|
||
|
||
<scroll-view
|
||
class="tree-scroll"
|
||
:class="{ 'tree-scroll--lineage': treeState === 'tree' }"
|
||
scroll-x
|
||
:scroll-left="treeState === 'tree' ? treeScrollLeft : 0"
|
||
:show-scrollbar="false"
|
||
:style="treeScrollStyle"
|
||
@scroll="handleTreeScroll"
|
||
>
|
||
<view
|
||
class="tree-canvas"
|
||
:class="{ 'tree-canvas--state': treeState !== 'tree' }"
|
||
:style="treeState === 'tree' ? treeMetricsStyle : undefined"
|
||
>
|
||
<AppLoading
|
||
v-if="treeState === 'loading'"
|
||
text="正在整理世系"
|
||
description="请稍候,正在准备家谱成员关系。"
|
||
/>
|
||
|
||
<template v-else-if="treeState === 'tree'">
|
||
<view
|
||
v-for="connector in lineageConnectors"
|
||
:key="connector.id"
|
||
class="lineage-connector"
|
||
:style="connector.style"
|
||
/>
|
||
|
||
<view
|
||
v-for="member in layoutMembers"
|
||
:key="member.id"
|
||
:id="`tree-member-${member.id}`"
|
||
class="member-node"
|
||
:class="{ 'member-node--selected': selected?.id === member.id }"
|
||
:style="nodeGridStyle(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"
|
||
/>
|
||
<view class="member-node__copy">
|
||
<text class="node-name">{{ member.name }}</text>
|
||
<text class="node-relation"
|
||
>{{ member.relation }} · {{ member.branch }}</text
|
||
>
|
||
<text class="node-years">{{ member.years }}</text>
|
||
</view>
|
||
</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, onUnload } 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 treeScrollLeft = ref(90);
|
||
const currentTreeScrollLeft = ref(90);
|
||
const centeredTreeScrollLeft = ref(90);
|
||
const treeHasDrifted = ref(false);
|
||
let ignoreTreeScroll = false;
|
||
let recenterTimer = null;
|
||
const GRID_UNIT = 5;
|
||
const NODE_HALF_HEIGHT = 47;
|
||
const MEMBER_GAP = 250;
|
||
const GENERATION_GAP = 220;
|
||
const generationMeta = {
|
||
12: { label: "第十二世", summary: "始祖" },
|
||
13: { label: "第十三世", summary: "两房" },
|
||
14: { label: "第十四世", summary: "三支" },
|
||
};
|
||
const members = ref([
|
||
{
|
||
id: 101,
|
||
name: "汤文远",
|
||
relation: "始祖",
|
||
years: "1940—2012",
|
||
generation: 12,
|
||
branch: "主支",
|
||
},
|
||
{
|
||
id: 102,
|
||
name: "汤正国",
|
||
relation: "长子",
|
||
years: "1965—",
|
||
generation: 13,
|
||
parentId: 101,
|
||
branch: "长房",
|
||
},
|
||
{
|
||
id: 103,
|
||
name: "汤正华",
|
||
relation: "次子",
|
||
years: "1968—",
|
||
generation: 13,
|
||
parentId: 101,
|
||
branch: "二房",
|
||
},
|
||
{
|
||
id: 104,
|
||
name: "汤凯",
|
||
relation: "长孙",
|
||
years: "1992—",
|
||
generation: 14,
|
||
parentId: 102,
|
||
branch: "长房",
|
||
},
|
||
{
|
||
id: 105,
|
||
name: "汤悦",
|
||
relation: "长孙女",
|
||
years: "1995—",
|
||
generation: 14,
|
||
parentId: 102,
|
||
branch: "长房",
|
||
},
|
||
{
|
||
id: 106,
|
||
name: "汤晨",
|
||
relation: "次孙",
|
||
years: "1998—",
|
||
generation: 14,
|
||
parentId: 103,
|
||
branch: "二房",
|
||
},
|
||
]);
|
||
|
||
const snapToGrid = (value) => Math.ceil(value / GRID_UNIT) * GRID_UNIT;
|
||
const layoutMembers = computed(() => {
|
||
const generations = Array.from(
|
||
new Set(members.value.map((member) => Number(member.generation))),
|
||
).sort((left, right) => left - right);
|
||
const groups = new Map(
|
||
generations.map((generation) => [
|
||
generation,
|
||
members.value
|
||
.filter((member) => Number(member.generation) === generation)
|
||
.sort(
|
||
(left, right) =>
|
||
Number(left.parentId || 0) - Number(right.parentId || 0) ||
|
||
Number(left.id) - Number(right.id),
|
||
),
|
||
]),
|
||
);
|
||
const maxCount = Math.max(1, ...Array.from(groups.values()).map((group) => group.length));
|
||
const canvasWidth = Math.max(720, maxCount * MEMBER_GAP + 100);
|
||
return generations.flatMap((generation, generationIndex) => {
|
||
const group = groups.get(generation) || [];
|
||
const occupiedWidth = Math.max(0, (group.length - 1) * MEMBER_GAP);
|
||
const startX = (canvasWidth - occupiedWidth) / 2;
|
||
return group.map((member, memberIndex) => ({
|
||
...member,
|
||
x: snapToGrid(startX + memberIndex * MEMBER_GAP),
|
||
y: snapToGrid(135 + generationIndex * GENERATION_GAP),
|
||
}));
|
||
});
|
||
});
|
||
const treeMetrics = computed(() => {
|
||
const memberList = layoutMembers.value;
|
||
const maxX = Math.max(0, ...memberList.map((member) => member.x));
|
||
const maxY = Math.max(0, ...memberList.map((member) => member.y));
|
||
const width = Math.max(720, snapToGrid(maxX + 210));
|
||
const height = Math.max(640, snapToGrid(maxY + 250));
|
||
return {
|
||
width,
|
||
height,
|
||
columns: width / GRID_UNIT,
|
||
rows: height / GRID_UNIT,
|
||
};
|
||
});
|
||
const treeMetricsStyle = computed(() => ({
|
||
width: `${treeMetrics.value.width}rpx`,
|
||
height: `${treeMetrics.value.height}rpx`,
|
||
gridTemplateColumns: `repeat(${treeMetrics.value.columns}, ${GRID_UNIT}rpx)`,
|
||
gridTemplateRows: `repeat(${treeMetrics.value.rows}, ${GRID_UNIT}rpx)`,
|
||
}));
|
||
const generationRailStyle = computed(() => ({
|
||
height: `${treeMetrics.value.height}rpx`,
|
||
gridTemplateRows: `repeat(${treeMetrics.value.rows}, ${GRID_UNIT}rpx)`,
|
||
}));
|
||
const treeScrollStyle = computed(() => ({
|
||
height: `${treeMetrics.value.height}rpx`,
|
||
}));
|
||
const generationRows = computed(() => {
|
||
const groups = new Map();
|
||
layoutMembers.value.forEach((member) => {
|
||
const group = groups.get(member.generation) || [];
|
||
group.push(member);
|
||
groups.set(member.generation, group);
|
||
});
|
||
return Array.from(groups.entries())
|
||
.sort(([left], [right]) => left - right)
|
||
.map(([generation, group]) => {
|
||
const y = Math.min(...group.map((member) => member.y));
|
||
return {
|
||
generation,
|
||
label: generationMeta[generation]?.label || `第 ${generation} 世`,
|
||
summary: generationMeta[generation]?.summary || `${group.length} 位成员`,
|
||
y,
|
||
};
|
||
});
|
||
});
|
||
const generationBandStyle = (row) => ({
|
||
gridRow: `${Math.max(1, Math.round((row.y - 88) / GRID_UNIT) + 1)} / span 12`,
|
||
});
|
||
const lineageConnectors = computed(() => {
|
||
const memberById = new Map(
|
||
layoutMembers.value.map((member) => [member.id, member]),
|
||
);
|
||
const childrenByParent = new Map();
|
||
layoutMembers.value.forEach((member) => {
|
||
if (!member.parentId || !memberById.has(member.parentId)) return;
|
||
const children = childrenByParent.get(member.parentId) || [];
|
||
children.push(member);
|
||
childrenByParent.set(member.parentId, children);
|
||
});
|
||
|
||
const connectors = [];
|
||
childrenByParent.forEach((children, parentId) => {
|
||
const parent = memberById.get(parentId);
|
||
const childTop = Math.min(...children.map((child) => child.y - NODE_HALF_HEIGHT));
|
||
const parentBottom = parent.y + NODE_HALF_HEIGHT;
|
||
const branchY = snapToGrid((parentBottom + childTop) / 2);
|
||
const minX = Math.min(...children.map((child) => child.x));
|
||
const maxX = Math.max(...children.map((child) => child.x));
|
||
const verticalStyle = (x, top, bottom) => ({
|
||
gridColumn: `${Math.round(x / GRID_UNIT) + 1} / span 1`,
|
||
gridRow: `${Math.round(top / GRID_UNIT) + 1} / ${Math.round(bottom / GRID_UNIT) + 1}`,
|
||
});
|
||
connectors.push({
|
||
id: `${parentId}-trunk`,
|
||
style: verticalStyle(parent.x, parentBottom, branchY),
|
||
});
|
||
connectors.push({
|
||
id: `${parentId}-branch`,
|
||
style: {
|
||
gridColumn: `${Math.round(minX / GRID_UNIT) + 1} / ${Math.round(maxX / GRID_UNIT) + 2}`,
|
||
gridRow: `${Math.round(branchY / GRID_UNIT) + 1} / span 1`,
|
||
},
|
||
});
|
||
children.forEach((child) => connectors.push({
|
||
id: `${parentId}-${child.id}`,
|
||
style: verticalStyle(child.x, branchY, child.y - NODE_HALF_HEIGHT),
|
||
}));
|
||
});
|
||
return connectors;
|
||
});
|
||
|
||
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 =
|
||
layoutMembers.value.find(
|
||
(item) => String(item.id) === String(query.selectedId),
|
||
) || layoutMembers.value[0];
|
||
treeState.value = "tree";
|
||
});
|
||
|
||
onUnload(() => {
|
||
if (recenterTimer) clearTimeout(recenterTimer);
|
||
});
|
||
|
||
const handleTreeScroll = (event) => {
|
||
if (ignoreTreeScroll || treeState.value !== "tree") return;
|
||
const scrollLeft = Number(event?.detail?.scrollLeft || 0);
|
||
currentTreeScrollLeft.value = scrollLeft;
|
||
treeHasDrifted.value = Math.abs(scrollLeft - centeredTreeScrollLeft.value) > 80;
|
||
};
|
||
|
||
const recenterSelectedMember = async () => {
|
||
if (!selected.value) return;
|
||
ignoreTreeScroll = true;
|
||
const [scrollRect, nodeRect] = await new Promise((resolve) => {
|
||
const query = uni.createSelectorQuery();
|
||
query.select(".tree-scroll").boundingClientRect();
|
||
query
|
||
.select(`#tree-member-${selected.value.id}`)
|
||
.boundingClientRect();
|
||
query.exec(resolve);
|
||
});
|
||
if (scrollRect && nodeRect) {
|
||
const centeredScrollLeft = Math.max(
|
||
0,
|
||
currentTreeScrollLeft.value +
|
||
nodeRect.left + nodeRect.width / 2 - (scrollRect.left + scrollRect.width / 2),
|
||
);
|
||
treeScrollLeft.value = centeredScrollLeft;
|
||
currentTreeScrollLeft.value = centeredScrollLeft;
|
||
centeredTreeScrollLeft.value = centeredScrollLeft;
|
||
}
|
||
treeHasDrifted.value = false;
|
||
if (recenterTimer) clearTimeout(recenterTimer);
|
||
recenterTimer = setTimeout(() => {
|
||
ignoreTreeScroll = false;
|
||
recenterTimer = null;
|
||
}, 320);
|
||
};
|
||
|
||
const nodeGridStyle = (member) => ({
|
||
gridColumn: `${member.x / 5 + 1}`,
|
||
gridRow: `${member.y / 5 + 1}`,
|
||
});
|
||
const handleStateAction = () => {
|
||
if (treeState.value === "empty") {
|
||
uni.navigateTo({
|
||
url: `/pages/tree/t04-add-relative?genealogyId=${genealogyId.value || 1001}&mode=first`,
|
||
});
|
||
return;
|
||
}
|
||
selected.value = layoutMembers.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 {
|
||
display: grid;
|
||
height: 100vh;
|
||
grid-template-rows: auto auto minmax(0, 1fr);
|
||
overflow: hidden;
|
||
background: $paper;
|
||
}
|
||
.tree-page__header,
|
||
.tree-toolbar,
|
||
.tree-stage {
|
||
z-index: 1;
|
||
}
|
||
.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 {
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
}
|
||
.tree-action--pressed {
|
||
opacity: 0.72;
|
||
}
|
||
.tree-stage--lineage {
|
||
display: grid;
|
||
grid-template-columns: 190rpx minmax(0, 1fr);
|
||
align-items: start;
|
||
}
|
||
.tree-scroll {
|
||
width: 100%;
|
||
height: 100%;
|
||
white-space: nowrap;
|
||
}
|
||
.generation-rail {
|
||
display: grid;
|
||
z-index: 4;
|
||
width: 190rpx;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
border-right: 1rpx solid rgba(143, 108, 63, 0.2);
|
||
}
|
||
.tree-scroll--lineage {
|
||
grid-area: 1 / 2;
|
||
min-width: 0;
|
||
}
|
||
.tree-canvas {
|
||
display: grid;
|
||
margin: 0 16rpx;
|
||
}
|
||
.tree-canvas--state {
|
||
display: block;
|
||
width: calc(100vw - 32rpx);
|
||
}
|
||
.lineage-pan-cue {
|
||
grid-area: 1 / 2;
|
||
align-self: start;
|
||
justify-self: end;
|
||
z-index: 3;
|
||
margin: 8rpx 22rpx 0 0;
|
||
color: #9a7748;
|
||
font-size: 20rpx;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
.generation-band {
|
||
display: grid;
|
||
justify-self: center;
|
||
z-index: 3;
|
||
width: 166rpx;
|
||
height: 58rpx;
|
||
color: #8f6c3f;
|
||
font-size: 22rpx;
|
||
text-align: center;
|
||
}
|
||
.generation-band image,
|
||
.generation-band__copy {
|
||
grid-area: 1 / 1;
|
||
}
|
||
.generation-band image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
.generation-band__copy {
|
||
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 {
|
||
align-self: stretch;
|
||
justify-self: stretch;
|
||
z-index: 1;
|
||
background: #b78a42;
|
||
}
|
||
.member-node {
|
||
display: grid;
|
||
align-self: start;
|
||
justify-self: start;
|
||
z-index: 2;
|
||
width: 224rpx;
|
||
height: 86rpx;
|
||
transform: translate(-50%, -50%);
|
||
text-align: center;
|
||
}
|
||
.member-node__skin {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
.member-node__skin,
|
||
.member-node__copy {
|
||
grid-area: 1 / 1;
|
||
}
|
||
.member-node__copy {
|
||
z-index: 1;
|
||
}
|
||
.node-name,
|
||
.node-relation,
|
||
.node-years {
|
||
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 {
|
||
display: grid;
|
||
z-index: 2;
|
||
width: calc(100% - 48rpx);
|
||
min-height: 360rpx;
|
||
margin: 150rpx 24rpx 0;
|
||
text-align: center;
|
||
white-space: normal;
|
||
}
|
||
.tree-state-card__skin {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
.tree-state-card__skin,
|
||
.tree-state-card__content {
|
||
grid-area: 1 / 1;
|
||
}
|
||
.tree-state-card__content {
|
||
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 {
|
||
display: grid;
|
||
width: 360rpx;
|
||
min-height: 70rpx;
|
||
margin: 22rpx auto 0;
|
||
}
|
||
.tree-state-card__action image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
.tree-state-card__action image,
|
||
.tree-state-card__action text {
|
||
grid-area: 1 / 1;
|
||
}
|
||
.tree-state-card__action text {
|
||
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;
|
||
min-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 {
|
||
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 {
|
||
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 {
|
||
min-height: 240rpx;
|
||
padding-top: 32rpx;
|
||
padding-right: 36rpx;
|
||
padding-left: 36rpx;
|
||
padding-bottom: 20rpx;
|
||
}
|
||
}
|
||
</style>
|