|
|
@@ -88,12 +88,9 @@
|
|
|
/* 男女形状与颜色区分:男性方形(蓝),女性圆形(粉红),添加轻微圆角与投影 */
|
|
|
.node-male rect { stroke: #3B82F6; fill: #EFF6FF; rx: 8px; ry: 8px; }
|
|
|
|
|
|
- /* 出继节点样式:红色虚线框 */
|
|
|
+ /* 出继节点样式:同色虚线框,仅边框样式区别,颜色与普通节点保持一致 */
|
|
|
.node-adopted-out rect, .node-adopted-out circle {
|
|
|
- stroke: #EF4444;
|
|
|
- stroke-width: 2px;
|
|
|
- stroke-dasharray: 6, 4;
|
|
|
- fill: rgba(239, 68, 68, 0.1);
|
|
|
+ stroke-dasharray: 6, 4 !important;
|
|
|
}
|
|
|
.node-female circle { stroke: #EC4899; fill: #FDF2F8; }
|
|
|
|
|
|
@@ -343,10 +340,15 @@
|
|
|
// 获取所有父子关系(包括出继/入继)
|
|
|
const allHierarchicalRelations = relations.filter(r => r.relation_type === 1 || r.relation_type === 2);
|
|
|
|
|
|
- // 对于出继的子女,记录他们入继到的目标
|
|
|
+ // 对于出继的子女,只要有 sub_relation_type===2 的关系即标记;同时尝试找到入继目标名称
|
|
|
const adoptedOutMap = new Map();
|
|
|
allHierarchicalRelations.forEach(r => {
|
|
|
if (r.sub_relation_type === 2) { // 出继
|
|
|
+ // 即便未找到入继关系也先标记(target 置 null)
|
|
|
+ if (!adoptedOutMap.has(r.child_mid)) {
|
|
|
+ adoptedOutMap.set(r.child_mid, null);
|
|
|
+ }
|
|
|
+ // 尝试找到对应的入继关系,获取养父名
|
|
|
const targetRelation = allHierarchicalRelations.find(r2 => r2.child_mid === r.child_mid && r2.sub_relation_type === 3);
|
|
|
if (targetRelation) {
|
|
|
const targetMember = members.find(m => m.id === targetRelation.parent_mid);
|
|
|
@@ -371,6 +373,51 @@
|
|
|
target: r.child_mid,
|
|
|
sub_relation_type: r.sub_relation_type
|
|
|
}));
|
|
|
+
|
|
|
+ // ── 推断缺失的入继关系 ──────────────────────────────────────────────
|
|
|
+ // 若同一亲生父的多个出继子女共享同一已知养父,则为缺失入继记录的子女补全推断链接
|
|
|
+ {
|
|
|
+ // 按亲生父分组:parentId → [childId, ...](仅 sub=2 的出继子女)
|
|
|
+ const outByParent = new Map();
|
|
|
+ allHierarchicalRelations.forEach(r => {
|
|
|
+ if (r.sub_relation_type === 2) {
|
|
|
+ if (!outByParent.has(r.parent_mid)) outByParent.set(r.parent_mid, []);
|
|
|
+ outByParent.get(r.parent_mid).push(r.child_mid);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ outByParent.forEach((childMids, _parentId) => {
|
|
|
+ // 收集已知养父 ID 集合
|
|
|
+ const knownTargetIds = new Set();
|
|
|
+ childMids.forEach(childId => {
|
|
|
+ const link = hierarchicalLinks.find(l => l.target === childId && l.sub_relation_type === 3);
|
|
|
+ if (link) knownTargetIds.add(link.source);
|
|
|
+ });
|
|
|
+ // 只有唯一确定的养父时才推断(避免歧义)
|
|
|
+ if (knownTargetIds.size === 1) {
|
|
|
+ const adoptiveParentId = [...knownTargetIds][0];
|
|
|
+ childMids.forEach(childId => {
|
|
|
+ const exists = hierarchicalLinks.some(l => l.source === adoptiveParentId && l.target === childId && l.sub_relation_type === 3);
|
|
|
+ if (!exists) {
|
|
|
+ hierarchicalLinks.push({ source: adoptiveParentId, target: childId, sub_relation_type: 3 });
|
|
|
+ // 同步更新 adoptedOutTarget
|
|
|
+ const targetMember = members.find(m => m.id === adoptiveParentId);
|
|
|
+ if (targetMember && adoptedOutMap.has(childId) && !adoptedOutMap.get(childId)) {
|
|
|
+ adoptedOutMap.set(childId, targetMember.name);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // adoptedOutTarget 同步回 nodes
|
|
|
+ nodes.forEach(node => {
|
|
|
+ if (adoptedOutMap.has(node.id)) {
|
|
|
+ node.adoptedOut = true;
|
|
|
+ node.adoptedOutTarget = adoptedOutMap.get(node.id);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // ────────────────────────────────────────────────────────────────────
|
|
|
+
|
|
|
const spouseLinks = relations.filter(r => r.relation_type === 10);
|
|
|
const otherLinks = relations.filter(r => r.relation_type >= 11);
|
|
|
|
|
|
@@ -390,7 +437,29 @@
|
|
|
// 折叠时不递归子代(但仍保留配偶以维持同层显示)
|
|
|
const children = isCollapsed
|
|
|
? []
|
|
|
- : childLinks.map(l => buildHierarchy(l.target, processedNodes)).filter(c => c !== null);
|
|
|
+ : childLinks.map(l => {
|
|
|
+ if (processedNodes.has(l.target)) {
|
|
|
+ // 入继(sub_relation_type===3)子女已在亲生父处渲染过,
|
|
|
+ // 在养父下创建浅引用节点(不再递归,避免重复/死循环)
|
|
|
+ if (l.sub_relation_type === 3) {
|
|
|
+ const cn = nodes.find(n => n.id === l.target);
|
|
|
+ if (cn) {
|
|
|
+ return {
|
|
|
+ id: cn.id,
|
|
|
+ name: cn.name,
|
|
|
+ simplified_name: cn.simplified_name,
|
|
|
+ sex: cn.sex,
|
|
|
+ adoptedIn: true,
|
|
|
+ hasHierarchicalChildren: false,
|
|
|
+ isCollapsed: false,
|
|
|
+ children: []
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return buildHierarchy(l.target, processedNodes);
|
|
|
+ }).filter(c => c !== null);
|
|
|
|
|
|
const spouses = spouseLinks.filter(l => l.parent_mid === nodeId)
|
|
|
.map(l => {
|
|
|
@@ -566,8 +635,13 @@
|
|
|
const pSex = node.data.sex;
|
|
|
const cSex = child.data && child.data.sex;
|
|
|
let label = "亲子";
|
|
|
- if (pSex === 1) label = cSex === 1 ? "父子" : "父女";
|
|
|
- else if (pSex === 2) label = cSex === 1 ? "母子" : "母女";
|
|
|
+ if (child.data && child.data.adoptedIn) {
|
|
|
+ label = cSex === 1 ? "养子" : "养女";
|
|
|
+ } else if (pSex === 1) {
|
|
|
+ label = cSex === 1 ? "父子" : "父女";
|
|
|
+ } else if (pSex === 2) {
|
|
|
+ label = cSex === 1 ? "母子" : "母女";
|
|
|
+ }
|
|
|
|
|
|
const childLinkMidY = (sibsY + child.y - circleR) / 2;
|
|
|
addBadge(g, child.x, childLinkMidY, label);
|
|
|
@@ -633,6 +707,11 @@
|
|
|
// 图形:男性方形,女性圆形,更符合生物遗传图谱
|
|
|
node.each(function(d) {
|
|
|
const el = d3.select(this);
|
|
|
+ // 出继节点 hover 提示
|
|
|
+ if (d.data.adoptedOut) {
|
|
|
+ const tip = d.data.adoptedOutTarget ? `出继给 ${d.data.adoptedOutTarget}` : '出继';
|
|
|
+ el.append('title').text(tip);
|
|
|
+ }
|
|
|
if (d.data.sex === 1) {
|
|
|
el.append("rect")
|
|
|
.attr("x", -circleR).attr("y", -circleR)
|