Преглед на файлове

commit 优化世系查询展示

Hai Lin преди 6 дни
родител
ревизия
b2eb5080f0
променени са 2 файла, в които са добавени 308 реда и са изтрити 40 реда
  1. 86 18
      app.py
  2. 222 22
      templates/lineage_query.html

+ 86 - 18
app.py

@@ -1256,12 +1256,16 @@ def get_lineage(member_id):
             if not center:
                 return jsonify({"success": False, "message": "成员不存在"})
             
-            # Step 2: Get ancestors (simple loop, limited depth)
+            # Step 2: Get ancestors with their siblings (generations)
             step_start = time.time()
-            ancestors = []
+            generations = []  # Array of generations, each with main ancestor and siblings
             current_id = member_id
             max_depth = 15
-            for _ in range(max_depth):
+            ancestor_ids = []  # Track ancestor IDs for exclusion when expanding
+            displayed_ids = set()  # Track IDs that are already displayed
+            displayed_ids.add(member_id)  # Center person is displayed
+            
+            for depth in range(max_depth):
                 cursor.execute("""
                     SELECT p.id, p.name, p.simplified_name, p.name_word, p.name_word_generation,
                            EXISTS(SELECT 1 FROM family_relation_info WHERE parent_mid = p.id AND relation_type IN (1, 2)) as has_children
@@ -1271,11 +1275,52 @@ def get_lineage(member_id):
                     LIMIT 1
                 """, (current_id,))
                 parent = cursor.fetchone()
+                
                 if not parent:
                     break
-                ancestors.append(parent)
+                
+                ancestor_ids.append(parent['id'])
+                displayed_ids.add(parent['id'])
+                
+                # Get siblings of this ancestor
+                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
+                    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
+                    ORDER BY c.id
+                    LIMIT 30
+                """, (parent['id'], current_id))
+                parent_siblings = cursor.fetchall()
+                
+                # Mark sibling IDs as displayed
+                for sibling in parent_siblings:
+                    displayed_ids.add(sibling['id'])
+                
+                # Check if parent has any children NOT already displayed
+                # Only show expand button if there are undisplayed children
+                cursor.execute("""
+                    SELECT COUNT(*) as count
+                    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)
+                """, (parent['id'],))
+                total_children = cursor.fetchone()['count']
+                
+                show_expand = total_children > len(parent_siblings) + 1  # +1 for the current child
+                
+                parent['show_expand'] = show_expand
+                
+                generations.append({
+                    'ancestor': parent,
+                    'siblings': parent_siblings,
+                    'depth': depth
+                })
+                
                 current_id = parent['id']
-            print(f"[Lineage Query] Step 2 - Get ancestors ({len(ancestors)}): {time.time() - step_start:.3f}s")
+            
+            print(f"[Lineage Query] Step 2 - Get generations ({len(generations)}): {time.time() - step_start:.3f}s")
             
             # Step 3: Get immediate children only (limited count)
             step_start = time.time()
@@ -1295,11 +1340,11 @@ 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 (brothers and sisters)
+            # Step 4: Get siblings of center person
             step_start = time.time()
             siblings = []
-            if ancestors:
-                parent_id = ancestors[0]['id']  # First ancestor is the father
+            if generations:
+                parent_id = generations[0]['ancestor']['id']  # Father
                 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
@@ -1319,7 +1364,8 @@ def get_lineage(member_id):
                 "success": True,
                 "data": {
                     "center": center,
-                    "ancestors": ancestors,
+                    "generations": generations,
+                    "ancestor_ids": ancestor_ids,
                     "siblings": siblings,
                     "children": children
                 }
@@ -1335,18 +1381,40 @@ def get_descendants(parent_id):
     if 'user_id' not in session:
         return jsonify({"success": False, "message": "Unauthorized"}), 401
     
+    # Get excluded IDs from query parameter
+    excluded_ids = request.args.get('exclude', '')
+    excluded_list = []
+    if excluded_ids:
+        excluded_list = [int(id.strip()) for id in excluded_ids.split(',') if id.strip().isdigit()]
+    
+    print(f"[get_descendants] Parent ID: {parent_id}, Excluded IDs: {excluded_list}")
+    
     conn = get_db_connection()
     try:
         with conn.cursor() as cursor:
-            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
-                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)
-                ORDER BY c.id
-                LIMIT 20
-            """, (parent_id,))
+            if excluded_list:
+                # Build query with exclusion
+                placeholders = ', '.join(['%s'] * len(excluded_list))
+                cursor.execute(f"""
+                    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
+                    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 NOT IN ({placeholders})
+                    ORDER BY c.id
+                    LIMIT 20
+                """, (parent_id,) + tuple(excluded_list))
+            else:
+                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
+                    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)
+                    ORDER BY c.id
+                    LIMIT 20
+                """, (parent_id,))
+            
             children = cursor.fetchall()
             
             for child in children:

+ 222 - 22
templates/lineage_query.html

@@ -66,6 +66,34 @@
         color: rgba(26,26,46,0.8) !important;
     }
     
+    .tree-node.direct-ancestor {
+        background: linear-gradient(135deg, #4a90d9, #2d5a87);
+        border-color: #4a90d9;
+        box-shadow: 0 0 20px rgba(74, 144, 217, 0.4);
+    }
+    
+    .tree-node.direct-ancestor .node-name {
+        color: #fff !important;
+    }
+    
+    .tree-node.direct-ancestor .node-name.simplified {
+        color: rgba(255,255,255,0.8) !important;
+    }
+    
+    .tree-node.direct-ancestor .node-info {
+        color: rgba(255,255,255,0.9) !important;
+    }
+    
+    .tree-node.clickable {
+        cursor: pointer;
+        transition: transform 0.2s, box-shadow 0.2s;
+    }
+    
+    .tree-node.clickable:hover {
+        transform: translateY(-3px);
+        box-shadow: 0 8px 25px rgba(255, 255, 255, 0.15);
+    }
+    
     .node-name {
         font-size: 18px;
         font-weight: 700;
@@ -88,7 +116,15 @@
     .connection-line {
         width: 4px;
         height: 40px;
-        background: linear-gradient(to bottom, #4a69bd, #2d3436);
+        background: linear-gradient(to bottom, #4a90d9, #2d3436);
+        margin: 0 auto;
+        border-radius: 2px;
+    }
+    
+    .connection-line.vertical-line {
+        width: 4px;
+        height: 40px;
+        background: linear-gradient(to bottom, #4a90d9, #2d3436);
         margin: 0 auto;
         border-radius: 2px;
     }
@@ -100,19 +136,37 @@
         background: linear-gradient(to right, #4a69bd, #2d3436);
     }
     
+    .connection-line.horizontal.main-line {
+        width: 60px;
+        height: 4px;
+        margin: 8px auto;
+        background: linear-gradient(to right, #4a90d9, #4a90d9);
+    }
+    
     /* Children container */
     .children-container {
         display: flex;
-        flex-wrap: wrap;
-        justify-content: center;
+        flex-wrap: nowrap;
+        justify-content: flex-start;
         gap: 30px;
         margin-top: 20px;
+        align-items: flex-start;
+        min-width: max-content;
     }
     
     .child-group {
         display: flex;
         flex-direction: column;
         align-items: center;
+        flex-shrink: 0;
+    }
+    
+    .child-group.direct-line {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        flex-shrink: 0;
+        position: relative;
     }
     
     /* Expand/Collapse button */
@@ -172,6 +226,69 @@
         font-weight: 700;
         text-shadow: 0 0 10px rgba(255,215,0,0.3);
     }
+    
+    /* Tree container with scrollable area */
+    .tree-container {
+        background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
+        border-radius: 16px;
+        padding: 30px;
+        overflow: auto;
+        position: relative;
+        box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+        width: calc(100% + 2px);
+        margin: 0 -1px;
+    }
+    
+    .tree-container::-webkit-scrollbar {
+        width: 8px;
+        height: 8px;
+    }
+    
+    .tree-container::-webkit-scrollbar-track {
+        background: rgba(255, 255, 255, 0.1);
+        border-radius: 4px;
+    }
+    
+    .tree-container::-webkit-scrollbar-thumb {
+        background: rgba(255, 215, 0, 0.5);
+        border-radius: 4px;
+    }
+    
+    .tree-container::-webkit-scrollbar-thumb:hover {
+        background: rgba(255, 215, 0, 0.7);
+    }
+    
+    /* Tree wrapper for horizontal scrolling */
+    .tree-wrapper {
+        width: 100%;
+        overflow-x: auto;
+        padding-bottom: 10px;
+        scrollbar-width: thin;
+        scrollbar-color: rgba(255,215,0,0.5) rgba(255,255,255,0.1);
+    }
+    
+    .tree-wrapper::-webkit-scrollbar {
+        width: 8px;
+        height: 8px;
+    }
+    
+    .tree-wrapper::-webkit-scrollbar-track {
+        background: rgba(255, 255, 255, 0.1);
+        border-radius: 4px;
+    }
+    
+    .tree-wrapper::-webkit-scrollbar-thumb {
+        background: rgba(255, 215, 0, 0.5);
+        border-radius: 4px;
+    }
+    
+    /* Generation row wrapper */
+    .generation-row {
+        display: flex;
+        justify-content: center;
+        width: 100%;
+        overflow-x: auto;
+    }
 </style>
 {% endblock %}
 
@@ -456,11 +573,11 @@ function renderLineage(data) {
     console.log('Rendering lineage:', data);
     
     try {
-        const ancestorsHtml = renderAncestors(data.ancestors);
-        console.log('Ancestors HTML generated:', ancestorsHtml.length);
-        document.getElementById('ancestorsTree').innerHTML = ancestorsHtml;
+        const generationsHtml = renderGenerations(data.generations);
+        console.log('Generations HTML generated:', generationsHtml.length);
+        document.getElementById('ancestorsTree').innerHTML = generationsHtml;
     } catch (e) {
-        console.error('Error rendering ancestors:', e);
+        console.error('Error rendering generations:', e);
     }
     
     try {
@@ -480,25 +597,60 @@ function renderLineage(data) {
     }
 }
 
-// Render ancestors
-function renderAncestors(ancestors) {
-    if (!ancestors || ancestors.length === 0) return '';
+// 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">';
     
-    ancestors.reverse().forEach((person, index) => {
+    const reversedGenerations = [...generations].reverse();
+    
+    reversedGenerations.forEach((gen, index) => {
         if (index > 0) {
-            html += '<div class="connection-line"></div>';
+            html += '<div class="connection-line vertical-line"></div>';
         }
-        html += renderTreeNode(person, false);
-        if (person.has_children) {
-            html += `
-                <button class="expand-btn" onclick="toggleChildren(this, ${person.id})">+</button>
-                <div class="children-container" style="display: none;" data-parent-id="${person.id}">
+        
+        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>';
@@ -590,9 +742,14 @@ function renderChildrenRecursive(children, level = 0) {
 }
 
 // Render tree node
-function renderTreeNode(person, isCenter = false) {
+function renderTreeNode(person, isCenter = false, isDirectLine = false) {
+    let className = 'clickable ';
+    if (isCenter) className += 'center';
+    else if (isDirectLine) className += 'direct-ancestor';
+    else className = className.trim();
+    
     return `
-        <div class="tree-node ${isCenter ? 'center' : ''}">
+        <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">
@@ -603,6 +760,11 @@ function renderTreeNode(person, isCenter = false) {
     `;
 }
 
+// Open person detail in new tab
+function openPersonDetail(personId) {
+    window.open(`/manager/member_detail/${personId}`, '_blank');
+}
+
 // Toggle children visibility with lazy loading
 async function toggleChildren(btn, parentId) {
     const container = btn.nextElementSibling;
@@ -617,8 +779,14 @@ async function toggleChildren(btn, parentId) {
             // Load children lazily
             btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
             
+            // Get already displayed descendant IDs to exclude
+            const excludedIds = getExcludedDescendantIds(btn);
+            const excludeParam = excludedIds.length > 0 ? `?exclude=${excludedIds.join(',')}` : '';
+            
+            console.log(`[ToggleChildren] Parent ID: ${parentId}, Excluded IDs: ${excludedIds}, URL: /manager/api/get_descendants/${parentId}${excludeParam}`);
+            
             try {
-                const response = await fetch(`/manager/api/get_descendants/${parentId}`, {
+                const response = await fetch(`/manager/api/get_descendants/${parentId}${excludeParam}`, {
                     credentials: 'include'
                 });
                 const result = await response.json();
@@ -639,6 +807,38 @@ async function toggleChildren(btn, parentId) {
     }
 }
 
+// Get descendant IDs that are already displayed in the tree (to avoid duplicates)
+function getExcludedDescendantIds(btn) {
+    const excluded = new Set();
+    
+    // Helper function to extract IDs from tree nodes
+    const extractIdsFromNodes = (container) => {
+        if (!container) return;
+        const nodes = container.querySelectorAll('.tree-node');
+        nodes.forEach(node => {
+            const id = node.getAttribute('data-id');
+            if (id && !isNaN(parseInt(id))) {
+                excluded.add(parseInt(id));
+            }
+        });
+    };
+    
+    // Get IDs from ancestors tree
+    const ancestorsTree = document.getElementById('ancestorsTree');
+    extractIdsFromNodes(ancestorsTree);
+    
+    // Get IDs from siblings tree (center person and siblings)
+    const siblingsTree = document.getElementById('siblingsTree');
+    extractIdsFromNodes(siblingsTree);
+    
+    // Get IDs from children tree
+    const childrenTree = document.getElementById('childrenTree');
+    extractIdsFromNodes(childrenTree);
+    
+    console.log('Excluded IDs:', Array.from(excluded));
+    return Array.from(excluded);
+}
+
 // Enter key search
 document.getElementById('searchInput').addEventListener('keypress', function(e) {
     if (e.key === 'Enter') {