feat: migrate app routes and business modules
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
const GRID_UNIT = 5;
|
||||
const NODE_HALF_WIDTH = 80;
|
||||
const NODE_HALF_HEIGHT = 112;
|
||||
const FAMILY_LINK_OFFSET = 18;
|
||||
const MEMBER_GAP = 178;
|
||||
const GENERATION_GAP = 296;
|
||||
|
||||
const snapToGrid = (coordinate) =>
|
||||
Math.ceil(coordinate / GRID_UNIT) * GRID_UNIT;
|
||||
|
||||
const compareMembers = (leftMember, rightMember) =>
|
||||
Number(leftMember.generation) - Number(rightMember.generation) ||
|
||||
String(leftMember.id).localeCompare(String(rightMember.id));
|
||||
|
||||
const positionMembers = (members) => {
|
||||
const memberById = new Map(
|
||||
members.map((member) => [String(member.id), member]),
|
||||
);
|
||||
const spousesByPersonId = new Map();
|
||||
members.forEach((member) => {
|
||||
const partnerId = String(member.spouseOf || "");
|
||||
if (partnerId && memberById.has(partnerId)) {
|
||||
spousesByPersonId.set(partnerId, member);
|
||||
}
|
||||
});
|
||||
|
||||
const primaryMembers = members
|
||||
.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));
|
||||
|
||||
// 每个家庭独占一段连续横向空间,父辈始终位于该空间中央。
|
||||
// 不能按世代逐行居中,否则后代较多的支系会被拉到另一支系下方。
|
||||
const subtreeWidthById = new Map();
|
||||
const measureSubtree = (member, ancestorIds = new Set()) => {
|
||||
const memberId = String(member.id);
|
||||
if (subtreeWidthById.has(memberId)) {
|
||||
return subtreeWidthById.get(memberId);
|
||||
}
|
||||
if (ancestorIds.has(memberId)) return 1;
|
||||
|
||||
const nextAncestorIds = new Set(ancestorIds);
|
||||
nextAncestorIds.add(memberId);
|
||||
const children = childrenByParent.get(memberId) || [];
|
||||
const descendantsWidth = children.reduce(
|
||||
(totalWidth, child) =>
|
||||
totalWidth + measureSubtree(child, nextAncestorIds),
|
||||
0,
|
||||
);
|
||||
const ownWidth = spousesByPersonId.has(memberId) ? 2 : 1;
|
||||
const subtreeWidth = Math.max(ownWidth, descendantsWidth || 1);
|
||||
subtreeWidthById.set(memberId, subtreeWidth);
|
||||
return subtreeWidth;
|
||||
};
|
||||
|
||||
const rootMembers = primaryMembers.filter(
|
||||
(member) => !primaryById.has(String(member.parentId || "")),
|
||||
);
|
||||
rootMembers.forEach((rootMember) => measureSubtree(rootMember));
|
||||
|
||||
const occupiedWidth = rootMembers.reduce(
|
||||
(totalWidth, rootMember) =>
|
||||
totalWidth + (subtreeWidthById.get(String(rootMember.id)) || 1),
|
||||
0,
|
||||
);
|
||||
const generations = Array.from(
|
||||
new Set(primaryMembers.map((member) => Number(member.generation))),
|
||||
).sort((leftGeneration, rightGeneration) =>
|
||||
leftGeneration - rightGeneration,
|
||||
);
|
||||
const generationIndexByNumber = new Map(
|
||||
generations.map((generation, index) => [generation, index]),
|
||||
);
|
||||
const positionedMembers = [];
|
||||
const positionedMemberIds = new Set();
|
||||
|
||||
const placeSubtree = (
|
||||
member,
|
||||
startUnit,
|
||||
subtreeWidth,
|
||||
ancestorIds = new Set(),
|
||||
) => {
|
||||
const memberId = String(member.id);
|
||||
if (ancestorIds.has(memberId)) return;
|
||||
|
||||
const nextAncestorIds = new Set(ancestorIds);
|
||||
nextAncestorIds.add(memberId);
|
||||
const spouse = spousesByPersonId.get(memberId);
|
||||
const centerUnit = startUnit + subtreeWidth / 2;
|
||||
const memberUnitX = spouse ? centerUnit - 0.5 : centerUnit;
|
||||
const y = snapToGrid(
|
||||
200 +
|
||||
(generationIndexByNumber.get(Number(member.generation)) || 0) *
|
||||
GENERATION_GAP,
|
||||
);
|
||||
positionedMembers.push({
|
||||
...member,
|
||||
x: snapToGrid(80 + memberUnitX * MEMBER_GAP),
|
||||
y,
|
||||
});
|
||||
positionedMemberIds.add(memberId);
|
||||
|
||||
if (spouse) {
|
||||
positionedMembers.push({
|
||||
...spouse,
|
||||
x: snapToGrid(80 + (memberUnitX + 1) * MEMBER_GAP),
|
||||
y,
|
||||
});
|
||||
positionedMemberIds.add(String(spouse.id));
|
||||
}
|
||||
|
||||
const children = childrenByParent.get(memberId) || [];
|
||||
const childrenWidth = children.reduce(
|
||||
(totalWidth, child) =>
|
||||
totalWidth + (subtreeWidthById.get(String(child.id)) || 1),
|
||||
0,
|
||||
);
|
||||
let childStartUnit = startUnit + (subtreeWidth - childrenWidth) / 2;
|
||||
children.forEach((child) => {
|
||||
const childWidth = subtreeWidthById.get(String(child.id)) || 1;
|
||||
placeSubtree(child, childStartUnit, childWidth, nextAncestorIds);
|
||||
childStartUnit += childWidth;
|
||||
});
|
||||
};
|
||||
|
||||
let rootStartUnit = 0;
|
||||
rootMembers.forEach((rootMember) => {
|
||||
const rootWidth = subtreeWidthById.get(String(rootMember.id)) || 1;
|
||||
placeSubtree(rootMember, rootStartUnit, rootWidth);
|
||||
rootStartUnit += rootWidth;
|
||||
});
|
||||
|
||||
members
|
||||
.filter((member) => !positionedMemberIds.has(String(member.id)))
|
||||
.forEach((member, index) => {
|
||||
positionedMembers.push({
|
||||
...member,
|
||||
x: snapToGrid(80 + (occupiedWidth + index + 0.5) * MEMBER_GAP),
|
||||
y: snapToGrid(
|
||||
200 +
|
||||
(generationIndexByNumber.get(Number(member.generation)) || 0) *
|
||||
GENERATION_GAP,
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
return positionedMembers;
|
||||
};
|
||||
|
||||
const createMetrics = (positionedMembers) => {
|
||||
const maxX = Math.max(0, ...positionedMembers.map((member) => member.x));
|
||||
const maxY = Math.max(0, ...positionedMembers.map((member) => member.y));
|
||||
const width = Math.max(660, snapToGrid(maxX + NODE_HALF_WIDTH + 50));
|
||||
const height = Math.max(640, snapToGrid(maxY + 250));
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
gridUnit: GRID_UNIT,
|
||||
columns: width / GRID_UNIT,
|
||||
rows: height / GRID_UNIT,
|
||||
};
|
||||
};
|
||||
|
||||
const createGenerationRows = (positionedMembers) => {
|
||||
const membersByGeneration = new Map();
|
||||
positionedMembers.forEach((member) => {
|
||||
const generationMembers = membersByGeneration.get(member.generation) || [];
|
||||
generationMembers.push(member);
|
||||
membersByGeneration.set(member.generation, generationMembers);
|
||||
});
|
||||
|
||||
return Array.from(membersByGeneration.entries())
|
||||
.sort(([leftGeneration], [rightGeneration]) =>
|
||||
leftGeneration - rightGeneration,
|
||||
)
|
||||
.map(([generation, generationMembers]) => {
|
||||
const y = Math.min(...generationMembers.map((member) => member.y));
|
||||
return {
|
||||
generation,
|
||||
label: `第 ${generation} 世`,
|
||||
summary: `${generationMembers.length} 位成员`,
|
||||
y,
|
||||
bandStyle: {
|
||||
gridRow: `${Math.max(1, Math.round((y - NODE_HALF_HEIGHT) / GRID_UNIT) + 1)} / span 21`,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const createConnectors = (positionedMembers) => {
|
||||
const memberById = new Map(
|
||||
positionedMembers.map((member) => [String(member.id), member]),
|
||||
);
|
||||
const spouseByPersonId = new Map();
|
||||
positionedMembers.forEach((member) => {
|
||||
const partnerId = String(member.spouseOf || "");
|
||||
if (partnerId && memberById.has(partnerId)) {
|
||||
spouseByPersonId.set(partnerId, member);
|
||||
}
|
||||
});
|
||||
|
||||
const childrenByParent = new Map();
|
||||
positionedMembers.forEach((member) => {
|
||||
const parentId = String(member.parentId || "");
|
||||
if (member.spouseOf || !parentId || !memberById.has(parentId)) return;
|
||||
const children = childrenByParent.get(parentId) || [];
|
||||
children.push(member);
|
||||
childrenByParent.set(parentId, children);
|
||||
});
|
||||
|
||||
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`,
|
||||
});
|
||||
const connectors = [];
|
||||
|
||||
childrenByParent.forEach((children, parentId) => {
|
||||
const parent = memberById.get(parentId);
|
||||
const spouse = spouseByPersonId.get(parentId);
|
||||
const parentAnchorX = spouse
|
||||
? snapToGrid((parent.x + spouse.x) / 2)
|
||||
: parent.x;
|
||||
const parentBottom = parent.y + NODE_HALF_HEIGHT;
|
||||
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 minChildX = Math.min(...children.map((child) => child.x));
|
||||
const maxChildX = Math.max(...children.map((child) => child.x));
|
||||
|
||||
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(parentAnchorX, parentAnchorY, branchY),
|
||||
});
|
||||
connectors.push({
|
||||
id: `${parentId}-branch`,
|
||||
style: horizontalStyle(minChildX, maxChildX, branchY),
|
||||
});
|
||||
children.forEach((child) => {
|
||||
connectors.push({
|
||||
id: `${parentId}-${child.id}`,
|
||||
style: verticalStyle(child.x, branchY, child.y - NODE_HALF_HEIGHT),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
positionedMembers.forEach((member) => {
|
||||
if (!member.spouseOf) return;
|
||||
const partner = memberById.get(String(member.spouseOf));
|
||||
if (!partner || partner.generation !== member.generation) return;
|
||||
if (childrenByParent.has(String(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: horizontalStyle(left, right, member.y),
|
||||
});
|
||||
});
|
||||
|
||||
return connectors;
|
||||
};
|
||||
|
||||
// 这是世系图布局的唯一公开入口。页面只消费坐标、尺寸、世代行和连线,
|
||||
// 不需要了解支系宽度测量、循环关系保护或配偶桥接规则。
|
||||
export const createTreeLayout = (members) => {
|
||||
const positionedMembers = positionMembers(Array.isArray(members) ? members : []);
|
||||
return {
|
||||
members: positionedMembers,
|
||||
metrics: createMetrics(positionedMembers),
|
||||
generationRows: createGenerationRows(positionedMembers),
|
||||
connectors: createConnectors(positionedMembers),
|
||||
};
|
||||
};
|
||||
|
||||
export const treeNodeGridStyle = (member) => ({
|
||||
gridColumn: `${member.x / GRID_UNIT + 1}`,
|
||||
gridRow: `${member.y / GRID_UNIT + 1}`,
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
LINEAGE_PERSON_OPTIONS,
|
||||
LINEAGE_RELATION_OPTIONS,
|
||||
LINEAGE_RELATION_TYPE
|
||||
} from '@/services/api/lineage-person-options.js'
|
||||
|
||||
export const memberFormOptions = LINEAGE_PERSON_OPTIONS
|
||||
export const memberRelationOptions = LINEAGE_RELATION_OPTIONS
|
||||
export const memberRelationTypes = LINEAGE_RELATION_TYPE
|
||||
|
||||
export const findMemberOptionIndex = (options, value) => Math.max(
|
||||
0,
|
||||
options.findIndex((option) => option.value === value)
|
||||
)
|
||||
|
||||
export const findMemberOptionLabel = (options, value) => (
|
||||
options.find((option) => option.value === value)?.label || ''
|
||||
)
|
||||
|
||||
const pickerOptionValue = (options, event, fallback = '') => (
|
||||
options[Number(event.detail.value)]?.value || fallback
|
||||
)
|
||||
|
||||
export const updateMemberFormOption = (form, field, options, event) => {
|
||||
form[field] = pickerOptionValue(options, event)
|
||||
if (field === 'personStatus' && form.personStatus !== '1') {
|
||||
Object.assign(form, {
|
||||
deathDate: '',
|
||||
deathLunar: '',
|
||||
deathPlace: '',
|
||||
burialPlace: ''
|
||||
})
|
||||
}
|
||||
return form[field]
|
||||
}
|
||||
|
||||
export const updateMemberBindingMode = (form, event) => {
|
||||
form.bindingMode = pickerOptionValue(memberFormOptions.bindingMode, event, 'NONE')
|
||||
if (form.bindingMode !== 'SPECIFIED') form.appUserId = ''
|
||||
return form.bindingMode
|
||||
}
|
||||
Reference in New Issue
Block a user