|
@@ -9,7 +9,8 @@
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
- height: 60vh;
|
|
|
|
|
|
|
+ height: calc(100vh - 155px);
|
|
|
|
|
+ min-height: 400px;
|
|
|
color: rgba(255,255,255,0.5);
|
|
color: rgba(255,255,255,0.5);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -18,19 +19,43 @@
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
align-items: flex-start;
|
|
align-items: flex-start;
|
|
|
- min-width: fit-content;
|
|
|
|
|
- padding: 10px 30px 30px;
|
|
|
|
|
|
|
+ min-width: max-content;
|
|
|
|
|
+ padding: 10px 60px 60px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /* 每一行:中心列 + 右侧兄弟列 */
|
|
|
|
|
|
|
+ /* 每一行容器 */
|
|
|
.lin-row {
|
|
.lin-row {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
flex-direction: row;
|
|
|
- align-items: center;
|
|
|
|
|
|
|
+ align-items: flex-start;
|
|
|
min-width: fit-content;
|
|
min-width: fit-content;
|
|
|
|
|
+ justify-content: flex-start;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /* 中心列:固定宽度,保证所有层级垂直对齐 */
|
|
|
|
|
|
|
+ /* 同代横排(祖先/中心 + 兄弟按 child_order 合并排序) */
|
|
|
|
|
+ .gen-peer-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: row;
|
|
|
|
|
+ flex-wrap: nowrap;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ padding: 0 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 每个人的列:排行徽章 + 节点卡 */
|
|
|
|
|
+ .gen-peer-col {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 直系祖先/中心所在列:加竖线指示器 */
|
|
|
|
|
+ .gen-peer-col.direct-col {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 竖连接线:居中于 lin-row */
|
|
|
.lin-center {
|
|
.lin-center {
|
|
|
min-width: 200px;
|
|
min-width: 200px;
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -39,7 +64,6 @@
|
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /* 竖连接线 */
|
|
|
|
|
.lin-vline {
|
|
.lin-vline {
|
|
|
width: 3px;
|
|
width: 3px;
|
|
|
min-height: 36px;
|
|
min-height: 36px;
|
|
@@ -48,36 +72,22 @@
|
|
|
margin: 0 auto;
|
|
margin: 0 auto;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /* 右侧兄弟区:横线 + 节点列表 */
|
|
|
|
|
- .lin-side {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: row;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- flex-wrap: nowrap;
|
|
|
|
|
- margin-left: 4px;
|
|
|
|
|
- }
|
|
|
|
|
- .lin-hline {
|
|
|
|
|
- width: 28px;
|
|
|
|
|
- height: 3px;
|
|
|
|
|
- background: rgba(74,105,189,0.5);
|
|
|
|
|
- border-radius: 2px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- }
|
|
|
|
|
- .lin-siblings {
|
|
|
|
|
|
|
+ /* 连接线行(居中) */
|
|
|
|
|
+ .lin-vline-row {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
flex-direction: row;
|
|
|
- flex-wrap: nowrap;
|
|
|
|
|
- gap: 10px;
|
|
|
|
|
- padding-left: 4px;
|
|
|
|
|
|
|
+ min-width: fit-content;
|
|
|
|
|
+ padding: 0 6px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /* 子女排列区:第一子在左(对齐中心),其余向右 */
|
|
|
|
|
|
|
+ /* 子女排列区:由 JS alignAndCenter 动态设置 margin-left 居中 */
|
|
|
.lin-children {
|
|
.lin-children {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
flex-direction: row;
|
|
|
align-items: flex-start;
|
|
align-items: flex-start;
|
|
|
flex-wrap: nowrap;
|
|
flex-wrap: nowrap;
|
|
|
gap: 14px;
|
|
gap: 14px;
|
|
|
|
|
+ margin-left: 0;
|
|
|
}
|
|
}
|
|
|
.lin-child-col {
|
|
.lin-child-col {
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -141,10 +151,13 @@
|
|
|
|
|
|
|
|
.tree-container {
|
|
.tree-container {
|
|
|
padding: 20px;
|
|
padding: 20px;
|
|
|
- min-height: 60vh;
|
|
|
|
|
|
|
+ height: calc(100vh - 155px);
|
|
|
|
|
+ min-height: 500px;
|
|
|
background: #1a1a2e;
|
|
background: #1a1a2e;
|
|
|
border-radius: 12px;
|
|
border-radius: 12px;
|
|
|
border: 1px solid rgba(255,215,0,0.2);
|
|
border: 1px solid rgba(255,215,0,0.2);
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+ position: relative;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* Tree node styles */
|
|
/* Tree node styles */
|
|
@@ -357,12 +370,8 @@
|
|
|
.tree-container {
|
|
.tree-container {
|
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
|
border-radius: 16px;
|
|
border-radius: 16px;
|
|
|
- padding: 30px;
|
|
|
|
|
- overflow: auto;
|
|
|
|
|
- position: relative;
|
|
|
|
|
|
|
+ padding: 20px;
|
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
|
- width: calc(100% + 2px);
|
|
|
|
|
- margin: 0 -1px;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.tree-container::-webkit-scrollbar {
|
|
.tree-container::-webkit-scrollbar {
|
|
@@ -776,20 +785,41 @@ function renderNode(person, type) {
|
|
|
</div>`;
|
|
</div>`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 渲染兄弟列表(横向,带横线连接)
|
|
|
|
|
-function renderSiblingsList(siblings) {
|
|
|
|
|
- if (!siblings || siblings.length === 0) return '';
|
|
|
|
|
- const MAX_SHOW = 6;
|
|
|
|
|
- const shown = siblings.slice(0, MAX_SHOW);
|
|
|
|
|
- const more = siblings.length - MAX_SHOW;
|
|
|
|
|
- return `
|
|
|
|
|
- <div class="lin-side">
|
|
|
|
|
- <div class="lin-hline"></div>
|
|
|
|
|
- <div class="lin-siblings">
|
|
|
|
|
- ${shown.map(s => renderNode(s, 'sibling')).join('')}
|
|
|
|
|
- ${more > 0 ? `<div class="tree-node sibling-node" style="opacity:.7;">+${more} 人</div>` : ''}
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+// 渲染同代横排:direct(祖先/中心)+ siblings 合并按 child_order 排序
|
|
|
|
|
+// mainType: 'ancestor' | 'center'
|
|
|
|
|
+function renderPeerRow(main, siblings, mainType) {
|
|
|
|
|
+ if (!main) return '';
|
|
|
|
|
+ const all = [
|
|
|
|
|
+ { ...main, _isDirect: true },
|
|
|
|
|
+ ...(siblings || [])
|
|
|
|
|
+ ].sort((a, b) => {
|
|
|
|
|
+ const oa = (a.child_order != null ? a.child_order : 9999);
|
|
|
|
|
+ const ob = (b.child_order != null ? b.child_order : 9999);
|
|
|
|
|
+ return oa !== ob ? oa - ob : (a.id - b.id);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const MAX_SHOW = 12;
|
|
|
|
|
+ const shown = all.slice(0, MAX_SHOW);
|
|
|
|
|
+ const more = all.length - MAX_SHOW;
|
|
|
|
|
+
|
|
|
|
|
+ let cols = shown.map((p, i) => {
|
|
|
|
|
+ const badge = getChildOrderLabel(p.child_order, i);
|
|
|
|
|
+ const type = p._isDirect ? mainType : 'sibling';
|
|
|
|
|
+ const directCls = p._isDirect ? ' direct-col' : '';
|
|
|
|
|
+ return `<div class="gen-peer-col${directCls}">
|
|
|
|
|
+ <div class="child-order-badge">${badge}</div>
|
|
|
|
|
+ ${renderNode(p, type)}
|
|
|
</div>`;
|
|
</div>`;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+
|
|
|
|
|
+ if (more > 0) {
|
|
|
|
|
+ cols += `<div class="gen-peer-col">
|
|
|
|
|
+ <div class="child-order-badge" style="visibility:hidden">-</div>
|
|
|
|
|
+ <div class="tree-node sibling-node" style="opacity:.6;min-width:80px;font-size:13px;">+${more} 人</div>
|
|
|
|
|
+ </div>`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return `<div class="gen-peer-row">${cols}</div>`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ── 主渲染函数 ────────────────────────────────────────────────────────────────
|
|
// ── 主渲染函数 ────────────────────────────────────────────────────────────────
|
|
@@ -835,32 +865,27 @@ function _renderAncestorView(center, generations, siblings, children, hasMore, t
|
|
|
html += `<div class="section-divider">祖先世系</div>`;
|
|
html += `<div class="section-divider">祖先世系</div>`;
|
|
|
}
|
|
}
|
|
|
ancestorGens.forEach((gen, idx) => {
|
|
ancestorGens.forEach((gen, idx) => {
|
|
|
- // 竖连接线(第一个不加顶部线,后面每个加)
|
|
|
|
|
|
|
+ // 竖连接线(第一个不加顶部线)
|
|
|
if (idx > 0) {
|
|
if (idx > 0) {
|
|
|
- html += `<div class="lin-row"><div class="lin-center"><div class="lin-vline"></div></div></div>`;
|
|
|
|
|
|
|
+ html += `<div class="lin-vline-row"><div class="lin-vline" style="margin-left:6px;"></div></div>`;
|
|
|
}
|
|
}
|
|
|
html += `<div class="lin-row">`;
|
|
html += `<div class="lin-row">`;
|
|
|
- html += ` <div class="lin-center">${renderNode(gen.ancestor, 'ancestor')}</div>`;
|
|
|
|
|
- // 该祖先的兄弟(向右展示)
|
|
|
|
|
- html += renderSiblingsList(gen.siblings || []);
|
|
|
|
|
|
|
+ html += renderPeerRow(gen.ancestor, gen.siblings || [], 'ancestor');
|
|
|
html += `</div>`;
|
|
html += `</div>`;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// ── 2. 中心人物 ──────────────────────────────────────────────────────────
|
|
// ── 2. 中心人物 ──────────────────────────────────────────────────────────
|
|
|
- // 连接线
|
|
|
|
|
if (ancestorGens.length > 0) {
|
|
if (ancestorGens.length > 0) {
|
|
|
- html += `<div class="lin-row"><div class="lin-center"><div class="lin-vline"></div></div></div>`;
|
|
|
|
|
|
|
+ html += `<div class="lin-vline-row"><div class="lin-vline" style="margin-left:6px;"></div></div>`;
|
|
|
}
|
|
}
|
|
|
html += `<div class="section-divider">查询人物</div>`;
|
|
html += `<div class="section-divider">查询人物</div>`;
|
|
|
html += `<div class="lin-row">`;
|
|
html += `<div class="lin-row">`;
|
|
|
- html += ` <div class="lin-center">${renderNode(center, 'center')}</div>`;
|
|
|
|
|
- // 中心人物的兄弟姐妹(向右展示)
|
|
|
|
|
- html += renderSiblingsList(siblings || []);
|
|
|
|
|
|
|
+ html += renderPeerRow(center, siblings || [], 'center');
|
|
|
html += `</div>`;
|
|
html += `</div>`;
|
|
|
|
|
|
|
|
- // ── 3. 子女横排(第一子对齐,其余向右)──────────────────────────────────
|
|
|
|
|
|
|
+ // ── 3. 子女横排(按 child_order 排序)────────────────────────────────────
|
|
|
if (children && children.length > 0) {
|
|
if (children && children.length > 0) {
|
|
|
- html += `<div class="lin-row"><div class="lin-center"><div class="lin-vline"></div></div></div>`;
|
|
|
|
|
|
|
+ html += `<div class="lin-vline-row"><div class="lin-vline" style="margin-left:6px;"></div></div>`;
|
|
|
html += `<div class="section-divider">子女</div>`;
|
|
html += `<div class="section-divider">子女</div>`;
|
|
|
html += `<div class="lin-row">`;
|
|
html += `<div class="lin-row">`;
|
|
|
html += ` <div class="lin-children">`;
|
|
html += ` <div class="lin-children">`;
|
|
@@ -885,6 +910,75 @@ function _renderAncestorView(center, generations, siblings, children, hasMore, t
|
|
|
document.getElementById('ancestorsTree').innerHTML = html;
|
|
document.getElementById('ancestorsTree').innerHTML = html;
|
|
|
document.getElementById('siblingsTree').innerHTML = '';
|
|
document.getElementById('siblingsTree').innerHTML = '';
|
|
|
document.getElementById('childrenTree').innerHTML = '';
|
|
document.getElementById('childrenTree').innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染后对齐直系列并居中显示
|
|
|
|
|
+ requestAnimationFrame(() => alignAndCenter());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 对齐直系列并将中心人物滚动至视口中央
|
|
|
|
|
+function alignAndCenter() {
|
|
|
|
|
+ const treeView = document.getElementById('treeView');
|
|
|
|
|
+ const lineageView = document.querySelector('#ancestorsTree .lineage-view');
|
|
|
|
|
+ if (!treeView || !lineageView) return;
|
|
|
|
|
+
|
|
|
|
|
+ // ── Step 1: 让每代行的 direct-col 对齐到同一 x 坐标 ──────────────────────
|
|
|
|
|
+ const directCols = Array.from(lineageView.querySelectorAll('.gen-peer-col.direct-col'));
|
|
|
|
|
+ if (directCols.length > 0) {
|
|
|
|
|
+ const lineageLeft = lineageView.getBoundingClientRect().left;
|
|
|
|
|
+
|
|
|
|
|
+ // 记录每个 direct-col 相对于 lineageView 的左偏移
|
|
|
|
|
+ const offsets = directCols.map(col =>
|
|
|
|
|
+ col.getBoundingClientRect().left - lineageLeft
|
|
|
|
|
+ );
|
|
|
|
|
+ const maxOffset = Math.max(...offsets);
|
|
|
|
|
+
|
|
|
|
|
+ // 给偏移不足的那一行在其 gen-peer-row 上补 padding-left
|
|
|
|
|
+ directCols.forEach((col, i) => {
|
|
|
|
|
+ const diff = maxOffset - offsets[i];
|
|
|
|
|
+ if (diff > 0) {
|
|
|
|
|
+ const row = col.closest('.gen-peer-row');
|
|
|
|
|
+ if (row) {
|
|
|
|
|
+ const cur = parseFloat(row.style.paddingLeft) || 6;
|
|
|
|
|
+ row.style.paddingLeft = (cur + diff) + 'px';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ── Step 2: 布局刷新后,子女居中 + 滚动至中心人物居中 ───────────────────
|
|
|
|
|
+ requestAnimationFrame(() => {
|
|
|
|
|
+ const centerNode = lineageView.querySelector('.tree-node.center');
|
|
|
|
|
+ if (!centerNode) return;
|
|
|
|
|
+
|
|
|
|
|
+ const lvRect = lineageView.getBoundingClientRect();
|
|
|
|
|
+ const cnRect = centerNode.getBoundingClientRect();
|
|
|
|
|
+
|
|
|
|
|
+ // 中心人物的水平中心(相对于 lineageView 左边缘)
|
|
|
|
|
+ const centerX = cnRect.left + cnRect.width / 2 - lvRect.left;
|
|
|
|
|
+
|
|
|
|
|
+ // 将 .lin-children 居中对齐到中心人物
|
|
|
|
|
+ const linChildren = lineageView.querySelector('.lin-children');
|
|
|
|
|
+ if (linChildren) {
|
|
|
|
|
+ const childrenW = linChildren.scrollWidth;
|
|
|
|
|
+ const desiredMargin = Math.max(0, centerX - childrenW / 2);
|
|
|
|
|
+ linChildren.style.marginLeft = desiredMargin + 'px';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 布局再次刷新后滚动视口
|
|
|
|
|
+ requestAnimationFrame(() => {
|
|
|
|
|
+ const tvRect = treeView.getBoundingClientRect();
|
|
|
|
|
+ const cnRect2 = centerNode.getBoundingClientRect();
|
|
|
|
|
+
|
|
|
|
|
+ // 水平居中
|
|
|
|
|
+ const hDelta = (cnRect2.left + cnRect2.width / 2) - (tvRect.left + tvRect.width / 2);
|
|
|
|
|
+ treeView.scrollLeft += hDelta;
|
|
|
|
|
+
|
|
|
|
|
+ // 垂直居中:让中心人物处于视口中部偏上(1/3 处),上方留给祖先
|
|
|
|
|
+ const targetY = tvRect.top + tvRect.height * 0.4;
|
|
|
|
|
+ const vDelta = (cnRect2.top + cnRect2.height / 2) - targetY;
|
|
|
|
|
+ treeView.scrollTop += vDelta;
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 继续向上追溯:加载 ancestor_id 以上的祖先链,并前插到当前列表
|
|
// 继续向上追溯:加载 ancestor_id 以上的祖先链,并前插到当前列表
|