Ver código fonte

commit 修改世系查询展示对齐方式

林海 1 mês atrás
pai
commit
01e73b95c3
2 arquivos alterados com 239 adições e 192 exclusões
  1. 2 1
      app.py
  2. 237 191
      templates/lineage_query.html

+ 2 - 1
app.py

@@ -1382,7 +1382,8 @@ def get_lineage(member_id):
             cursor.execute("""
             cursor.execute("""
                 SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation,
                 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,
-                       r.sub_relation_type
+                       r.sub_relation_type,
+                       r.child_order
                 FROM family_relation_info r
                 FROM family_relation_info r
                 JOIN family_member_info c ON r.child_mid = c.id
                 JOIN family_member_info c ON r.child_mid = c.id
                 WHERE r.parent_mid = %s AND r.relation_type IN (1, 2)
                 WHERE r.parent_mid = %s AND r.relation_type IN (1, 2)

+ 237 - 191
templates/lineage_query.html

@@ -12,6 +12,109 @@
         height: 60vh;
         height: 60vh;
         color: rgba(255,255,255,0.5);
         color: rgba(255,255,255,0.5);
     }
     }
+
+    /* ── 新竖列布局 ── */
+    .lineage-view {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        min-width: fit-content;
+        padding: 10px 30px 30px;
+    }
+
+    /* 每一行:中心列 + 右侧兄弟列 */
+    .lin-row {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        min-width: fit-content;
+    }
+
+    /* 中心列:固定宽度,保证所有层级垂直对齐 */
+    .lin-center {
+        min-width: 200px;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        flex-shrink: 0;
+    }
+
+    /* 竖连接线 */
+    .lin-vline {
+        width: 3px;
+        min-height: 36px;
+        background: linear-gradient(to bottom, rgba(74,144,217,0.9), rgba(74,144,217,0.3));
+        border-radius: 2px;
+        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 {
+        display: flex;
+        flex-direction: row;
+        flex-wrap: nowrap;
+        gap: 10px;
+        padding-left: 4px;
+    }
+
+    /* 子女排列区:第一子在左(对齐中心),其余向右 */
+    .lin-children {
+        display: flex;
+        flex-direction: row;
+        align-items: flex-start;
+        flex-wrap: nowrap;
+        gap: 14px;
+    }
+    .lin-child-col {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        flex-shrink: 0;
+    }
+    .child-order-badge {
+        background: rgba(255,215,0,0.12);
+        border: 1px solid rgba(255,215,0,0.35);
+        color: #ffd700;
+        font-size: 11px;
+        font-weight: 600;
+        padding: 2px 10px;
+        border-radius: 10px;
+        margin-bottom: 5px;
+        white-space: nowrap;
+    }
+
+    /* 分区标题 */
+    .section-divider {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        margin: 18px 0 10px;
+        color: rgba(255,215,0,0.6);
+        font-size: 12px;
+        font-weight: 500;
+        white-space: nowrap;
+    }
+    .section-divider::after {
+        content: '';
+        flex: 1;
+        height: 1px;
+        background: rgba(255,215,0,0.15);
+        min-width: 40px;
+    }
     
     
     .tree-container {
     .tree-container {
         padding: 20px;
         padding: 20px;
@@ -308,6 +411,20 @@
         margin-top: 4px;
         margin-top: 4px;
         text-align: center;
         text-align: center;
     }
     }
+
+    /* 兄弟节点:稍小,低调 */
+    .tree-node.sibling-node {
+        background: rgba(74,105,189,0.2);
+        border-color: rgba(74,105,189,0.4);
+        min-width: 110px;
+        padding: 8px 14px;
+    }
+    .tree-node.sibling-node .node-name {
+        font-size: 14px;
+    }
+    .tree-node.sibling-node .node-info {
+        font-size: 11px;
+    }
 </style>
 </style>
 {% endblock %}
 {% endblock %}
 
 
@@ -587,213 +704,142 @@ async function loadLineage(memberId) {
     }
     }
 }
 }
 
 
-// Render lineage tree
-function renderLineage(data) {
-    console.log('Rendering lineage:', data);
-    
-    try {
-        const generationsHtml = renderGenerations(data.generations);
-        console.log('Generations HTML generated:', generationsHtml.length);
-        document.getElementById('ancestorsTree').innerHTML = generationsHtml;
-    } catch (e) {
-        console.error('Error rendering generations:', e);
-    }
-    
-    try {
-        const siblingsWithCenterHtml = renderSiblingsWithCenter(data.center, data.siblings);
-        console.log('Siblings HTML generated:', siblingsWithCenterHtml.length);
-        document.getElementById('siblingsTree').innerHTML = siblingsWithCenterHtml;
-    } catch (e) {
-        console.error('Error rendering siblings:', e);
-    }
-    
-    try {
-        const childrenHtml = renderChildrenTree(data.children);
-        console.log('Children HTML generated:', childrenHtml.length);
-        document.getElementById('childrenTree').innerHTML = childrenHtml;
-    } catch (e) {
-        console.error('Error rendering children:', e);
-    }
-}
+// ── 工具函数 ─────────────────────────────────────────────────────────────────
 
 
-// Render generations with ancestors and their siblings
-function renderGenerations(generations) {
-    if (!generations || generations.length === 0) return '';
-    
-    let html = '<div class="text-center mb-6"><span class="generation-label">祖先谱系</span></div>';
-    html += '<div class="tree-wrapper">';
-    
-    const reversedGenerations = [...generations].reverse();
-    
-    reversedGenerations.forEach((gen, index) => {
-        if (index > 0) {
-            html += '<div class="connection-line vertical-line"></div>';
-        }
-        
-        const leftSiblings = gen.siblings.slice(0, Math.floor(gen.siblings.length / 2));
-        const rightSiblings = gen.siblings.slice(Math.floor(gen.siblings.length / 2));
-        
-        html += `
-            <div class="generation-row">
-                <div class="children-container">
-                    ${leftSiblings.map(sibling => `
-                        <div class="child-group">
-                            <div class="connection-line horizontal"></div>
-                            ${renderTreeNode(sibling, false, false)}
-                            ${sibling.has_children ? `
-                                <button class="expand-btn" onclick="toggleChildren(this, ${sibling.id})">+</button>
-                                <div class="children-container" style="display: none;" data-parent-id="${sibling.id}">
-                                </div>
-                            ` : ''}
-                        </div>
-                    `).join('')}
-                    <div class="child-group direct-line">
-                        <div class="connection-line horizontal main-line"></div>
-                        ${renderTreeNode(gen.ancestor, false, true)}
-                        ${gen.ancestor.show_expand ? `
-                        <button class="expand-btn" onclick="toggleChildren(this, ${gen.ancestor.id})">+</button>
-                        <div class="children-container" style="display: none;" data-parent-id="${gen.ancestor.id}">
-                        </div>
-                    ` : ''}
-                </div>
-                ${rightSiblings.map(sibling => `
-                    <div class="child-group">
-                        <div class="connection-line horizontal"></div>
-                        ${renderTreeNode(sibling, false, false)}
-                        ${sibling.has_children ? `
-                            <button class="expand-btn" onclick="toggleChildren(this, ${sibling.id})">+</button>
-                            <div class="children-container" style="display: none;" data-parent-id="${sibling.id}">
-                            </div>
-                        ` : ''}
-                    </div>
-                `).join('')}
-                </div>
-            </div>
-        `;
-    });
-    
-    html += '</div>';
-    return html;
+// 根据 child_order 生成"长子/次子/三子..."标签
+function getChildOrderLabel(childOrder, fallbackIndex) {
+    const ord = childOrder != null ? childOrder : (fallbackIndex + 1);
+    const labels = ['长', '次', '三', '四', '五', '六', '七', '八', '九', '十'];
+    if (ord >= 1 && ord <= 10) return labels[ord - 1] + '子';
+    return `第${ord}子`;
 }
 }
 
 
-// Render center person
-function renderCenterPerson(person) {
+// 渲染单个节点 HTML
+// type: 'center' | 'ancestor' | 'sibling' | 'child'
+function renderNode(person, type) {
+    if (!person) return '';
+    let cls = 'tree-node clickable';
+    if (type === 'center')   cls += ' center';
+    else if (type === 'ancestor') cls += ' direct-ancestor';
+    // siblings / children use default style (can be slightly smaller)
+    if (type === 'sibling') cls += ' sibling-node';
+
+    if (person.sub_relation_type === 2) cls += ' adopted-out';
+    else if (person.sub_relation_type === 3) cls += ' adopted-in';
+
+    const adoptLabel = person.sub_relation_type === 2
+        ? `<div class="adoption-label">${person.adoptive_parent_name ? '出继给 ' + person.adoptive_parent_name : '出继'}</div>`
+        : '';
+
     return `
     return `
-        <div class="connection-line"></div>
-        <div class="tree-node center">
+        <div class="${cls}" data-id="${person.id}" onclick="openPersonDetail(${person.id})">
             <div class="node-name">${person.name}</div>
             <div class="node-name">${person.name}</div>
-            ${person.simplified_name && person.simplified_name !== person.name ? `<div class="node-name simplified">(${person.simplified_name})</div>` : ''}
+            ${person.simplified_name && person.simplified_name !== person.name
+                ? `<div class="node-name simplified">(${person.simplified_name})</div>` : ''}
             <div class="node-info">
             <div class="node-info">
-                ${person.name_word ? `${person.name_word} · ` : ''}
-                ${person.name_word_generation || ''}
+                ${person.name_word ? person.name_word + ' · ' : ''}${person.name_word_generation || ''}
             </div>
             </div>
-        </div>
-    `;
+            ${adoptLabel}
+        </div>`;
 }
 }
 
 
-// Render siblings with center person in the middle
-function renderSiblingsWithCenter(center, siblings) {
-    const allSiblings = siblings || [];
-    
-    // Insert center person at the middle position
-    const middleIndex = Math.floor(allSiblings.length / 2);
-    const items = [];
-    
-    for (let i = 0; i < allSiblings.length; i++) {
-        if (i === middleIndex) {
-            items.push({ type: 'center', person: center });
-        }
-        items.push({ type: 'sibling', person: allSiblings[i] });
-    }
-    
-    // If no siblings, just show center
-    if (allSiblings.length === 0) {
-        items.push({ type: 'center', person: center });
-    }
-    
+// 渲染兄弟列表(横向,带横线连接)
+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 `
     return `
-        <div class="text-center mt-6 mb-4"><span class="generation-label">同辈兄弟姐妹</span></div>
-        <div class="generation-row">
-            <div class="children-container">
-                ${items.map(item => `
-                    <div class="child-group ${item.type === 'center' ? 'center-child' : ''}">
-                    <div class="connection-line horizontal"></div>
-                    ${renderTreeNode(item.person, item.type === 'center')}
-                    ${item.type !== 'center' && item.person.has_children ? `
-                        <button class="expand-btn" onclick="toggleChildren(this, ${item.person.id})">+</button>
-                        <div class="children-container" style="display: none;" data-parent-id="${item.person.id}">
-                        </div>
-                    ` : ''}
-                    </div>
-                `).join('')}
-                </div>
+        <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>
             </div>
-    `;
+        </div>`;
 }
 }
 
 
-// Render children tree
-function renderChildrenTree(children) {
-    if (!children || children.length === 0) return '';
-    
-    return `
-        <div class="text-center mt-6 mb-4"><span class="generation-label">后代谱系</span></div>
-        <div class="generation-row">
-            ${renderChildrenRecursive(children)}
-        </div>
-    `;
-}
+// ── 主渲染函数 ────────────────────────────────────────────────────────────────
 
 
-// Render children recursively
-function renderChildrenRecursive(children, level = 0) {
-    if (!children || children.length === 0) return '';
-    
-    return `
-        <div class="children-container">
-            ${children.map(child => `
-                <div class="child-group">
-                    <div class="connection-line"></div>
-                    ${renderTreeNode(child, false)}
-                    ${child.has_children ? `
-                        <button class="expand-btn" onclick="toggleChildren(this, ${child.id})">+</button>
-                        <div class="children-container" style="display: none;" data-parent-id="${child.id}">
-                        </div>
-                    ` : ''}
-                </div>
-            `).join('')}
-        </div>
-    `;
-}
+function renderLineage(data) {
+    const { center, generations, siblings, children } = data;
+    const ancestorGens = [...generations].reverse(); // 从最远祖先 → 父亲
 
 
-// Render tree node
-function renderTreeNode(person, isCenter = false, isDirectLine = false) {
-    let className = 'clickable ';
-    if (isCenter) className += 'center';
-    else if (isDirectLine) className += 'direct-ancestor';
-    else className = className.trim();
-    
-    // Add adoption styles
-    if (person.sub_relation_type === 2) {
-        className += ' adopted-out'; // 出继
-    } else if (person.sub_relation_type === 3) {
-        className += ' adopted-in'; // 入继
+    let html = '<div class="lineage-view">';
+
+    // ── 1. 祖先竖列(最远→父亲)────────────────────────────────────────────
+    if (ancestorGens.length > 0) {
+        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-row">`;
+        html += `  <div class="lin-center">${renderNode(gen.ancestor, 'ancestor')}</div>`;
+        // 该祖先的兄弟(向右展示)
+        html += renderSiblingsList(gen.siblings || []);
+        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="section-divider">查询人物</div>`;
+    html += `<div class="lin-row">`;
+    html += `  <div class="lin-center">${renderNode(center, 'center')}</div>`;
+    // 中心人物的兄弟姐妹(向右展示)
+    html += renderSiblingsList(siblings || []);
+    html += `</div>`;
+
+    // ── 3. 子女横排(第一子对齐,其余向右)──────────────────────────────────
+    if (children && children.length > 0) {
+        html += `<div class="lin-row"><div class="lin-center"><div class="lin-vline"></div></div></div>`;
+        html += `<div class="section-divider">子女</div>`;
+        html += `<div class="lin-row">`;
+        html += `  <div class="lin-children">`;
+        children.forEach((child, idx) => {
+            const badge = getChildOrderLabel(child.child_order, idx);
+            html += `<div class="lin-child-col">`;
+            html += `  <div class="child-order-badge">${badge}</div>`;
+            html += renderNode(child, 'child');
+            if (child.has_children) {
+                html += `<button class="expand-btn" onclick="toggleChildren(this,${child.id})">+</button>`;
+                html += `<div class="children-container" style="display:none;" data-parent-id="${child.id}"></div>`;
+            }
+            html += `</div>`;
+        });
+        html += `  </div>`;
+        html += `</div>`;
     }
     }
-    
-    // 出继方显示"出继给xxx"
-    const adoptionLabel = person.sub_relation_type === 2 && person.adoptive_parent_name 
-        ? `<div class="adoption-label">出继给 ${person.adoptive_parent_name}</div>` 
-        : (person.sub_relation_type === 2 ? '<div class="adoption-label">出继</div>' : '');
-    
-    return `
-        <div class="tree-node ${className}" data-id="${person.id}" onclick="openPersonDetail(${person.id})">
-            <div class="node-name">${person.name}</div>
-            ${person.simplified_name && person.simplified_name !== person.name ? `<div class="node-name simplified">(${person.simplified_name})</div>` : ''}
-            <div class="node-info">
-                ${person.name_word ? `${person.name_word} · ` : ''}
-                ${person.name_word_generation || ''}
-            </div>
-            ${adoptionLabel}
-        </div>
-    `;
+
+    html += '</div>'; // lineage-view
+
+    // 写入容器(ancestorsTree 承载全部内容,其余清空)
+    document.getElementById('ancestorsTree').innerHTML = html;
+    document.getElementById('siblingsTree').innerHTML  = '';
+    document.getElementById('childrenTree').innerHTML  = '';
+}
+
+// 展开子孙(按钮旁的懒加载容器)
+function renderChildrenRecursive(children) {
+    if (!children || children.length === 0) return '';
+    return `<div class="lin-children" style="flex-wrap:wrap;gap:12px;">
+        ${children.map((child, idx) => {
+            const badge = getChildOrderLabel(child.child_order, idx);
+            return `<div class="lin-child-col">
+                <div class="child-order-badge">${badge}</div>
+                ${renderNode(child, 'child')}
+                ${child.has_children
+                    ? `<button class="expand-btn" onclick="toggleChildren(this,${child.id})">+</button>
+                       <div class="children-container" style="display:none;" data-parent-id="${child.id}"></div>`
+                    : ''}
+            </div>`;
+        }).join('')}
+    </div>`;
 }
 }
 
 
 // Open person detail in new tab
 // Open person detail in new tab