待测试

This commit is contained in:
rain
2026-07-27 17:17:31 +08:00
parent 1eae3bbef4
commit 04287e6777
35 changed files with 4238 additions and 22750 deletions
+202 -67
View File
@@ -89,6 +89,7 @@
v-for="connector in lineageConnectors"
:key="connector.id"
class="lineage-connector"
:class="{ 'lineage-connector--spouse': connector.kind === 'spouse' }"
:style="connector.style"
/>
@@ -97,30 +98,33 @@
:key="member.id"
:id="`tree-member-${member.id}`"
class="member-node"
:class="{ 'member-node--selected': selected?.id === member.id }"
:class="{
'member-node--selected': selected?.id === member.id,
'member-node--spouse': Boolean(member.spouseOf),
}"
:style="nodeGridStyle(member)"
@click="openMemberPanel(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"
/>
<image
class="member-node__avatar"
src="/static/assets/foundation/transparent/meta-member.png"
mode="aspectFit"
/>
<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 class="member-node__surface">
<view class="member-node__portrait">
<image
class="member-node__avatar"
src="/static/assets/foundation/transparent/tab-profile.png"
mode="aspectFit"
/>
</view>
<image
class="member-node__divider"
src="/static/assets/foundation/transparent/auth-divider-knot.png"
mode="aspectFit"
/>
<view class="member-node__copy">
<text class="node-name">{{ member.name }}</text>
<text class="node-relation"
>{{ nodeRelationText(member) }}</text
>
<text class="node-years">{{ member.years }}</text>
</view>
</view>
</view>
</template>
@@ -168,7 +172,7 @@
>
<image
class="member-action-profile__avatar"
src="/static/assets/foundation/transparent/meta-member.png"
src="/static/assets/foundation/transparent/tab-profile.png"
mode="aspectFit"
/>
<view class="member-action-profile__copy">
@@ -202,7 +206,7 @@
</template>
<script setup>
import { computed, ref } from "vue";
import { computed, nextTick, ref } from "vue";
import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
@@ -234,9 +238,11 @@ let treeLoadGeneration = 0;
let pageActive = true;
let skipNextShowRefresh = true;
const GRID_UNIT = 5;
const NODE_HALF_HEIGHT = 47;
const MEMBER_GAP = 250;
const GENERATION_GAP = 220;
const NODE_HALF_WIDTH = 80;
const NODE_HALF_HEIGHT = 94;
const FAMILY_LINK_OFFSET = 18;
const MEMBER_GAP = 178;
const GENERATION_GAP = 260;
const members = ref([]);
const memberActions = Object.freeze([
@@ -257,6 +263,10 @@ const memberActions = Object.freeze([
]);
const snapToGrid = (value) => Math.ceil(value / GRID_UNIT) * GRID_UNIT;
const nodeRelationText = (member) => {
const branch = String(member.branch || "").replace(/字辈$/, "");
return [member.relation, branch].filter(Boolean).join(" · ");
};
const layoutMembers = computed(() => {
const generations = Array.from(
new Set(members.value.map((member) => Number(member.generation))),
@@ -269,12 +279,14 @@ const layoutMembers = computed(() => {
.sort(
(left, right) =>
String(left.parentId || "").localeCompare(String(right.parentId || "")) ||
String(left.spouseOf || left.id).localeCompare(String(right.spouseOf || right.id)) ||
Number(Boolean(left.spouseOf)) - Number(Boolean(right.spouseOf)) ||
String(left.id).localeCompare(String(right.id)),
),
]),
);
const maxCount = Math.max(1, ...Array.from(groups.values()).map((group) => group.length));
const canvasWidth = Math.max(720, maxCount * MEMBER_GAP + 100);
const canvasWidth = Math.max(660, maxCount * MEMBER_GAP + 80);
return generations.flatMap((generation, generationIndex) => {
const group = groups.get(generation) || [];
const occupiedWidth = Math.max(0, (group.length - 1) * MEMBER_GAP);
@@ -282,7 +294,7 @@ const layoutMembers = computed(() => {
return group.map((member, memberIndex) => ({
...member,
x: snapToGrid(startX + memberIndex * MEMBER_GAP),
y: snapToGrid(135 + generationIndex * GENERATION_GAP),
y: snapToGrid(200 + generationIndex * GENERATION_GAP),
}));
});
});
@@ -290,7 +302,7 @@ 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 width = Math.max(660, snapToGrid(maxX + NODE_HALF_WIDTH + 50));
const height = Math.max(640, snapToGrid(maxY + 250));
return {
width,
@@ -345,9 +357,15 @@ const lineageConnectors = computed(() => {
const memberById = new Map(
layoutMembers.value.map((member) => [member.id, member]),
);
const spouseByPersonId = new Map();
layoutMembers.value.forEach((member) => {
if (member.spouseOf && memberById.has(member.spouseOf)) {
spouseByPersonId.set(member.spouseOf, member);
}
});
const childrenByParent = new Map();
layoutMembers.value.forEach((member) => {
if (!member.parentId || !memberById.has(member.parentId)) return;
if (member.spouseOf || !member.parentId || !memberById.has(member.parentId)) return;
const children = childrenByParent.get(member.parentId) || [];
children.push(member);
childrenByParent.set(member.parentId, children);
@@ -356,18 +374,46 @@ const lineageConnectors = computed(() => {
const connectors = [];
childrenByParent.forEach((children, parentId) => {
const parent = memberById.get(parentId);
const childTop = Math.min(...children.map((child) => child.y - NODE_HALF_HEIGHT));
const spouse = spouseByPersonId.get(parentId);
const parentAnchorX = spouse
? snapToGrid((parent.x + spouse.x) / 2)
: parent.x;
const parentBottom = parent.y + NODE_HALF_HEIGHT;
const branchY = snapToGrid((parentBottom + childTop) / 2);
const parentAnchorY = spouse
? snapToGrid(parentBottom + FAMILY_LINK_OFFSET)
: parentBottom;
const childTop = Math.min(...children.map((child) => child.y - NODE_HALF_HEIGHT));
const branchY = snapToGrid((parentAnchorY + 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}`,
});
const horizontalStyle = (left, right, y) => ({
gridColumn: `${Math.round(left / GRID_UNIT) + 1} / ${Math.round(right / GRID_UNIT) + 2}`,
gridRow: `${Math.round(y / GRID_UNIT) + 1} / span 1`,
});
if (spouse) {
connectors.push({
id: `${parentId}-${spouse.id}-family-parent`,
kind: "family",
style: verticalStyle(parent.x, parentBottom, parentAnchorY),
});
connectors.push({
id: `${parentId}-${spouse.id}-family-spouse`,
kind: "family",
style: verticalStyle(spouse.x, spouse.y + NODE_HALF_HEIGHT, parentAnchorY),
});
connectors.push({
id: `${parentId}-${spouse.id}-family-bridge`,
kind: "family",
style: horizontalStyle(Math.min(parent.x, spouse.x), Math.max(parent.x, spouse.x), parentAnchorY),
});
}
connectors.push({
id: `${parentId}-trunk`,
style: verticalStyle(parent.x, parentBottom, branchY),
style: verticalStyle(parentAnchorX, parentAnchorY, branchY),
});
connectors.push({
id: `${parentId}-branch`,
@@ -381,6 +427,23 @@ const lineageConnectors = computed(() => {
style: verticalStyle(child.x, branchY, child.y - NODE_HALF_HEIGHT),
}));
});
layoutMembers.value.forEach((member) => {
if (!member.spouseOf) return;
const partner = memberById.get(member.spouseOf);
if (!partner || partner.generation !== member.generation) return;
if (childrenByParent.has(partner.id)) return;
const left = Math.min(member.x, partner.x) + NODE_HALF_WIDTH;
const right = Math.max(member.x, partner.x) - NODE_HALF_WIDTH;
if (right <= left) return;
connectors.push({
id: `${partner.id}-${member.id}-spouse`,
kind: "spouse",
style: {
gridColumn: `${Math.round(left / GRID_UNIT) + 1} / ${Math.round(right / GRID_UNIT) + 2}`,
gridRow: `${Math.round(member.y / GRID_UNIT) + 1} / span 1`,
},
});
});
return connectors;
});
@@ -444,6 +507,10 @@ const loadTree = async (query = {}) => {
(item) => String(item.id) === String(query.selectedId),
) || layoutMembers.value[0] || null;
treeState.value = members.value.length ? "tree" : "empty";
if (treeState.value === "tree" && selected.value) {
await nextTick();
recenterSelectedMember();
}
} catch (error) {
if (
!pageActive ||
@@ -481,22 +548,46 @@ const handleTreeScroll = (event) => {
treeHasDrifted.value = Math.abs(scrollLeft - centeredTreeScrollLeft.value) > 80;
};
const getFamilyPartner = (member) => {
if (!member) return null;
const partnerId = member.spouseOf || layoutMembers.value.find(
(item) => String(item.spouseOf || "") === String(member.id),
)?.id;
const partner = layoutMembers.value.find(
(item) => String(item.id) === String(partnerId || ""),
);
return partner && partner.generation === member.generation ? partner : null;
};
const recenterSelectedMember = async () => {
if (!selected.value) return;
const spouse = getFamilyPartner(selected.value);
ignoreTreeScroll = true;
const [scrollRect, nodeRect] = await new Promise((resolve) => {
const [scrollRect, nodeRect, spouseRect] = await new Promise((resolve) => {
const query = uni.createSelectorQuery();
query.select(".tree-scroll").boundingClientRect();
query
.select(`#tree-member-${selected.value.id}`)
.boundingClientRect();
if (spouse) {
query.select(`#tree-member-${spouse.id}`).boundingClientRect();
}
query.exec(resolve);
});
if (scrollRect && nodeRect) {
const targetLeft = spouseRect
? Math.min(nodeRect.left, spouseRect.left)
: nodeRect.left;
const targetRight = spouseRect
? Math.max(
nodeRect.left + nodeRect.width,
spouseRect.left + spouseRect.width,
)
: nodeRect.left + nodeRect.width;
const centeredScrollLeft = Math.max(
0,
currentTreeScrollLeft.value +
nodeRect.left + nodeRect.width / 2 - (scrollRect.left + scrollRect.width / 2),
(targetLeft + targetRight) / 2 - (scrollRect.left + scrollRect.width / 2),
);
treeScrollLeft.value = centeredScrollLeft;
currentTreeScrollLeft.value = centeredScrollLeft;
@@ -613,7 +704,7 @@ const openMemberAction = (action) => {
}
.tree-stage--lineage {
display: grid;
grid-template-columns: 190rpx minmax(0, 1fr);
grid-template-columns: 144rpx minmax(0, 1fr);
align-items: start;
}
.tree-scroll {
@@ -624,7 +715,7 @@ const openMemberAction = (action) => {
.generation-rail {
display: grid;
z-index: 4;
width: 190rpx;
width: 144rpx;
height: 100%;
box-sizing: border-box;
border-right: 1rpx solid rgba(143, 108, 63, 0.2);
@@ -635,7 +726,7 @@ const openMemberAction = (action) => {
}
.tree-canvas {
display: grid;
margin: 0 16rpx;
margin: 0 12rpx;
}
.tree-canvas--state {
display: block;
@@ -655,7 +746,7 @@ const openMemberAction = (action) => {
display: grid;
justify-self: center;
z-index: 3;
width: 166rpx;
width: 126rpx;
height: 58rpx;
color: #8f6c3f;
font-size: 22rpx;
@@ -679,7 +770,7 @@ const openMemberAction = (action) => {
}
.generation-band__copy text:last-child {
margin-top: 1rpx;
font-size: 19rpx;
font-size: 18rpx;
}
.lineage-connector {
align-self: stretch;
@@ -687,36 +778,57 @@ const openMemberAction = (action) => {
z-index: 1;
background: #b78a42;
}
.lineage-connector--spouse {
background: rgba(183, 117, 76, 0.82);
}
.member-node {
display: grid;
position: relative;
align-self: start;
justify-self: start;
z-index: 2;
width: 224rpx;
height: 86rpx;
width: 160rpx;
height: 188rpx;
transform: translate(-50%, -50%);
text-align: center;
}
.member-node__skin {
.member-node__surface {
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
align-items: center;
box-sizing: border-box;
padding: 9rpx 8rpx 8rpx;
border: 1rpx solid #b78a42;
border-radius: 14rpx;
background: rgba(255, 252, 244, 0.96);
box-shadow: 0 5rpx 12rpx rgba(103, 72, 33, 0.12);
}
.member-node__skin,
.member-node__copy {
grid-area: 1 / 1;
.member-node__portrait {
display: flex;
width: 62rpx;
height: 62rpx;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 1rpx solid #c49a57;
border-radius: 50%;
background: #fffdf7;
overflow: hidden;
}
.member-node__avatar {
grid-area: 1 / 1;
z-index: 3;
align-self: start;
justify-self: start;
width: 42rpx;
height: 42rpx;
margin: 13rpx 0 0 18rpx;
width: 48rpx;
height: 48rpx;
}
.member-node__divider {
width: 56rpx;
height: 14rpx;
margin-top: 3rpx;
}
.member-node__copy {
z-index: 1;
padding-left: 44rpx;
width: 100%;
margin-top: 1rpx;
padding: 0;
}
.node-name,
.node-relation,
@@ -724,25 +836,48 @@ const openMemberAction = (action) => {
display: block;
}
.node-name {
padding-top: 5rpx;
color: $ink;
display: -webkit-box;
color: #6b2419;
font-family: "STKaiti", "KaiTi", serif;
font-size: 25rpx;
overflow: hidden;
font-size: 24rpx;
font-weight: 700;
line-height: 1.1;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
white-space: normal;
}
.node-relation {
margin-top: 0;
color: $brand-red;
font-size: 21rpx;
overflow: hidden;
margin-top: 2rpx;
color: #644b2e;
font-size: 18rpx;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-years {
margin-top: 0;
color: $ink-muted;
font-size: 20rpx;
overflow: hidden;
margin-top: 2rpx;
color: #695b4c;
font-size: 17rpx;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.member-node--spouse .member-node__surface {
border-color: #c39a61;
background: rgba(255, 250, 241, 0.98);
}
.member-node--selected {
width: 236rpx;
height: 94rpx;
z-index: 3;
}
.member-node--selected .member-node__surface {
border-color: #c55344;
box-shadow: 0 6rpx 14rpx rgba(159, 23, 15, 0.16);
}
.member-node--selected .member-node__portrait {
border-color: #c55344;
}
.member-node--selected .node-name {
color: $brand-red;