待测试

This commit is contained in:
2026-07-28 07:58:26 +08:00
parent 1e9e25fe67
commit 6fbcf21024
22 changed files with 1294 additions and 264 deletions
+134 -40
View File
@@ -301,42 +301,134 @@ const nodeRelationText = (member) => {
return [member.relation, branch].filter(Boolean).join(" · ");
};
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) =>
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 memberById = new Map(
members.value.map((member) => [String(member.id), member]),
);
const maxCount = Math.max(
1,
...Array.from(groups.values()).map((group) => group.length),
);
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);
const startX = (canvasWidth - occupiedWidth) / 2;
return group.map((member, memberIndex) => ({
...member,
x: snapToGrid(startX + memberIndex * MEMBER_GAP),
y: snapToGrid(200 + generationIndex * GENERATION_GAP),
}));
const compareMembers = (left, right) =>
Number(left.generation) - Number(right.generation) ||
String(left.id).localeCompare(String(right.id));
const spousesByPersonId = new Map();
members.value.forEach((member) => {
const partnerId = String(member.spouseOf || "");
if (partnerId && memberById.has(partnerId)) {
spousesByPersonId.set(partnerId, member);
}
});
const primaryMembers = members.value
.filter((member) => !member.spouseOf)
.sort(compareMembers);
const primaryById = new Map(
primaryMembers.map((member) => [String(member.id), member]),
);
const childrenByParent = new Map();
primaryMembers.forEach((member) => {
const parentId = String(member.parentId || "");
if (!parentId || !primaryById.has(parentId)) return;
const children = childrenByParent.get(parentId) || [];
children.push(member);
childrenByParent.set(parentId, children);
});
childrenByParent.forEach((children) => children.sort(compareMembers));
// A family owns one contiguous horizontal span. Parents are centered in
// that span, so a later generation cannot pull a branch beneath another one.
const subtreeWidthById = new Map();
const measureSubtree = (member, ancestors = new Set()) => {
const id = String(member.id);
if (subtreeWidthById.has(id)) return subtreeWidthById.get(id);
if (ancestors.has(id)) return 1;
const nextAncestors = new Set(ancestors);
nextAncestors.add(id);
const children = childrenByParent.get(id) || [];
const descendantsWidth = children.reduce(
(sum, child) => sum + measureSubtree(child, nextAncestors),
0,
);
const ownWidth = spousesByPersonId.has(id) ? 2 : 1;
const width = Math.max(ownWidth, descendantsWidth || 1);
subtreeWidthById.set(id, width);
return width;
};
const roots = primaryMembers.filter(
(member) => !primaryById.has(String(member.parentId || "")),
);
const rootIds = new Set(roots.map((member) => String(member.id)));
primaryMembers.forEach((member) => {
if (!rootIds.has(String(member.id))) return;
measureSubtree(member);
});
const totalWidth = roots.reduce(
(sum, root) => sum + (subtreeWidthById.get(String(root.id)) || 1),
0,
);
const generations = Array.from(
new Set(primaryMembers.map((member) => Number(member.generation))),
).sort((left, right) => left - right);
const generationIndexByNumber = new Map(
generations.map((generation, index) => [generation, index]),
);
const positionedMembers = [];
const positionedIds = new Set();
const placeSubtree = (member, start, width, ancestors = new Set()) => {
const id = String(member.id);
if (ancestors.has(id)) return;
const nextAncestors = new Set(ancestors);
nextAncestors.add(id);
const spouse = spousesByPersonId.get(id);
const center = start + width / 2;
const memberUnitX = spouse ? center - 0.5 : center;
const y = snapToGrid(
200 + (generationIndexByNumber.get(Number(member.generation)) || 0) * GENERATION_GAP,
);
positionedMembers.push({
...member,
x: snapToGrid(80 + memberUnitX * MEMBER_GAP),
y,
});
positionedIds.add(id);
if (spouse) {
positionedMembers.push({
...spouse,
x: snapToGrid(80 + (memberUnitX + 1) * MEMBER_GAP),
y,
});
positionedIds.add(String(spouse.id));
}
const children = childrenByParent.get(id) || [];
const childrenWidth = children.reduce(
(sum, child) => sum + subtreeWidthById.get(String(child.id)),
0,
);
let childStart = start + (width - childrenWidth) / 2;
children.forEach((child) => {
const childWidth = subtreeWidthById.get(String(child.id));
placeSubtree(child, childStart, childWidth, nextAncestors);
childStart += childWidth;
});
};
let rootStart = 0;
roots.forEach((root) => {
const width = subtreeWidthById.get(String(root.id)) || 1;
placeSubtree(root, rootStart, width);
rootStart += width;
});
return positionedMembers.length === members.value.length
? positionedMembers
: members.value
.filter((member) => !positionedIds.has(String(member.id)))
.reduce((result, member, index) => {
result.push({
...member,
x: snapToGrid(80 + (totalWidth + index + 0.5) * MEMBER_GAP),
y: snapToGrid(
200 +
(generationIndexByNumber.get(Number(member.generation)) || 0) *
GENERATION_GAP,
),
});
return result;
}, positionedMembers);
});
const treeMetrics = computed(() => {
const memberList = layoutMembers.value;
@@ -395,21 +487,23 @@ const generationBandStyle = (row) => ({
});
const lineageConnectors = computed(() => {
const memberById = new Map(
layoutMembers.value.map((member) => [member.id, member]),
layoutMembers.value.map((member) => [String(member.id), member]),
);
const spouseByPersonId = new Map();
layoutMembers.value.forEach((member) => {
if (member.spouseOf && memberById.has(member.spouseOf)) {
spouseByPersonId.set(member.spouseOf, member);
const partnerId = String(member.spouseOf || "");
if (partnerId && memberById.has(partnerId)) {
spouseByPersonId.set(partnerId, member);
}
});
const childrenByParent = new Map();
layoutMembers.value.forEach((member) => {
if (member.spouseOf || !member.parentId || !memberById.has(member.parentId))
const parentId = String(member.parentId || "");
if (member.spouseOf || !parentId || !memberById.has(parentId))
return;
const children = childrenByParent.get(member.parentId) || [];
const children = childrenByParent.get(parentId) || [];
children.push(member);
childrenByParent.set(member.parentId, children);
childrenByParent.set(parentId, children);
});
const connectors = [];