林海 1 месяц назад
Родитель
Сommit
3c472ff209
2 измененных файлов с 180 добавлено и 63 удалено
  1. 28 5
      app.py
  2. 152 58
      templates/lineage_query.html

+ 28 - 5
app.py

@@ -1434,10 +1434,21 @@ def get_lineage(member_id):
                 
                 parent_siblings = []
                 if grandparent:
-                    # Get siblings of parent (father's brothers)
+                    # 获取祖先自身的 child_order(在祖父下的排行)
+                    cursor.execute("""
+                        SELECT COALESCE(child_order, NULL) AS child_order
+                        FROM family_relation_info
+                        WHERE parent_mid = %s AND child_mid = %s AND relation_type IN (1, 2)
+                        LIMIT 1
+                    """, (grandparent['id'], parent['id']))
+                    co_row = cursor.fetchone()
+                    parent['child_order'] = co_row['child_order'] if co_row else None
+
+                    # 获取祖先的兄弟(含 child_order,用于前端排序与徽章)
                     cursor.execute("""
                         SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation,
-                               EXISTS(SELECT 1 FROM family_relation_info WHERE parent_mid = c.id AND relation_type IN (1, 2)) as has_children
+                               EXISTS(SELECT 1 FROM family_relation_info WHERE parent_mid = c.id AND relation_type IN (1, 2)) as has_children,
+                               COALESCE(r.child_order, NULL) AS child_order
                         FROM family_relation_info r
                         JOIN family_member_info c ON r.child_mid = c.id
                         WHERE r.parent_mid = %s AND r.relation_type IN (1, 2) AND c.id != %s
@@ -1529,15 +1540,27 @@ def get_lineage(member_id):
                 child['children'] = []
             print(f"[Lineage Query] Step 3 - Get children ({len(children)}): {time.time() - step_start:.3f}s")
             
-            # Step 4: Get siblings of center person
+            # Step 4: Get siblings of center person + center's own child_order
             step_start = time.time()
             siblings = []
+            center_child_order = None
             if generations:
                 parent_id = generations[0]['ancestor']['id']  # Father
+                # 中心人物自身的排行
+                cursor.execute("""
+                    SELECT COALESCE(child_order, NULL) AS child_order
+                    FROM family_relation_info
+                    WHERE parent_mid = %s AND child_mid = %s AND relation_type IN (1, 2)
+                    LIMIT 1
+                """, (parent_id, member_id))
+                co_row = cursor.fetchone()
+                center_child_order = co_row['child_order'] if co_row else None
+
                 cursor.execute("""
                     SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation,
                            EXISTS(SELECT 1 FROM family_relation_info WHERE parent_mid = c.id AND relation_type IN (1, 2)) as has_children,
-                           r.sub_relation_type
+                           r.sub_relation_type,
+                           COALESCE(r.child_order, NULL) AS child_order
                     FROM family_relation_info r
                     JOIN family_member_info c ON r.child_mid = c.id
                     WHERE r.parent_mid = %s AND r.relation_type IN (1, 2) AND c.id != %s
@@ -1564,7 +1587,7 @@ def get_lineage(member_id):
             return jsonify({
                 "success": True,
                 "data": {
-                    "center": center,
+                    "center": {**center, "child_order": center_child_order},
                     "generations": generations,
                     "ancestor_ids": ancestor_ids,
                     "siblings": siblings,

+ 152 - 58
templates/lineage_query.html

@@ -9,7 +9,8 @@
         flex-direction: column;
         align-items: center;
         justify-content: center;
-        height: 60vh;
+        height: calc(100vh - 155px);
+        min-height: 400px;
         color: rgba(255,255,255,0.5);
     }
 
@@ -18,19 +19,43 @@
         display: flex;
         flex-direction: column;
         align-items: flex-start;
-        min-width: fit-content;
-        padding: 10px 30px 30px;
+        min-width: max-content;
+        padding: 10px 60px 60px;
     }
 
-    /* 每一行:中心列 + 右侧兄弟列 */
+    /* 每一行容器 */
     .lin-row {
         display: flex;
         flex-direction: row;
-        align-items: center;
+        align-items: flex-start;
         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 {
         min-width: 200px;
         display: flex;
@@ -39,7 +64,6 @@
         flex-shrink: 0;
     }
 
-    /* 竖连接线 */
     .lin-vline {
         width: 3px;
         min-height: 36px;
@@ -48,36 +72,22 @@
         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;
         flex-direction: row;
-        flex-wrap: nowrap;
-        gap: 10px;
-        padding-left: 4px;
+        min-width: fit-content;
+        padding: 0 6px;
     }
 
-    /* 子女排列区:第一子在左(对齐中心),其余向右 */
+    /* 子女排列区:由 JS alignAndCenter 动态设置 margin-left 居中 */
     .lin-children {
         display: flex;
         flex-direction: row;
         align-items: flex-start;
         flex-wrap: nowrap;
         gap: 14px;
+        margin-left: 0;
     }
     .lin-child-col {
         display: flex;
@@ -141,10 +151,13 @@
     
     .tree-container {
         padding: 20px;
-        min-height: 60vh;
+        height: calc(100vh - 155px);
+        min-height: 500px;
         background: #1a1a2e;
         border-radius: 12px;
         border: 1px solid rgba(255,215,0,0.2);
+        overflow: auto;
+        position: relative;
     }
     
     /* Tree node styles */
@@ -357,12 +370,8 @@
     .tree-container {
         background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
         border-radius: 16px;
-        padding: 30px;
-        overflow: auto;
-        position: relative;
+        padding: 20px;
         box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
-        width: calc(100% + 2px);
-        margin: 0 -1px;
     }
     
     .tree-container::-webkit-scrollbar {
@@ -776,20 +785,41 @@ function renderNode(person, type) {
         </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>`;
+    }).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>`;
     }
     ancestorGens.forEach((gen, idx) => {
-        // 竖连接线(第一个不加顶部线,后面每个加
+        // 竖连接线(第一个不加顶部线)
         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-center">${renderNode(gen.ancestor, 'ancestor')}</div>`;
-        // 该祖先的兄弟(向右展示)
-        html += renderSiblingsList(gen.siblings || []);
+        html += renderPeerRow(gen.ancestor, gen.siblings || [], 'ancestor');
         html += `</div>`;
     });
 
     // ── 2. 中心人物 ──────────────────────────────────────────────────────────
-    // 连接线
     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="lin-row">`;
-    html += `  <div class="lin-center">${renderNode(center, 'center')}</div>`;
-    // 中心人物的兄弟姐妹(向右展示)
-    html += renderSiblingsList(siblings || []);
+    html += renderPeerRow(center, siblings || [], 'center');
     html += `</div>`;
 
-    // ── 3. 子女横排(第一子对齐,其余向右)──────────────────────────────────
+    // ── 3. 子女横排(按 child_order 排序)────────────────────────────────────
     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="lin-row">`;
         html += `  <div class="lin-children">`;
@@ -885,6 +910,75 @@ function _renderAncestorView(center, generations, siblings, children, hasMore, t
     document.getElementById('ancestorsTree').innerHTML = html;
     document.getElementById('siblingsTree').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 以上的祖先链,并前插到当前列表