林海 5 дней назад
Родитель
Сommit
b2a702c20b
3 измененных файлов с 176 добавлено и 32 удалено
  1. 87 28
      app.py
  2. 14 4
      templates/lineage_query.html
  3. 75 0
      templates/members.html

+ 87 - 28
app.py

@@ -1520,12 +1520,48 @@ def members():
             # 格式化日期
             for m in members:
                 m['birthday_str'] = format_timestamp(m.get('birthday'))
-                # 格式化创建时间 (针对 TIMESTAMP 字段)
                 if m.get('create_time'):
                     m['create_time_str'] = m['create_time'].strftime('%Y-%m-%d')
                 if m.get('modified_time'):
                     m['modified_time_str'] = m['modified_time'].strftime('%Y-%m-%d %H:%M')
-                    
+
+            # 批量查询兄弟信息(只含有 father_id 的成员)
+            father_ids = list({m['father_id'] for m in members if m.get('father_id')})
+            member_ids_set = {m['id'] for m in members}
+            siblings_map = {}  # father_id -> list of {id, name, simplified_name}
+            if father_ids:
+                fmt = ','.join(['%s'] * len(father_ids))
+                cursor.execute(f"""
+                    SELECT fr.parent_mid AS father_id,
+                           m.id AS sibling_id,
+                           m.name AS sibling_name,
+                           m.simplified_name AS sibling_simplified_name
+                    FROM family_relation_info fr
+                    JOIN family_member_info m ON fr.child_mid = m.id
+                    WHERE fr.parent_mid IN ({fmt})
+                      AND fr.relation_type = 1
+                      AND COALESCE(fr.sub_relation_type, 0) != 2
+                    ORDER BY COALESCE(fr.child_order, 9999), m.id
+                """, father_ids)
+                for row in cursor.fetchall():
+                    fid = row['father_id']
+                    if fid not in siblings_map:
+                        siblings_map[fid] = []
+                    siblings_map[fid].append({
+                        'id': row['sibling_id'],
+                        'name': row['sibling_name'],
+                        'simplified_name': row['sibling_simplified_name'],
+                    })
+
+            # 将兄弟列表(排除自身)附加到每个成员
+            for m in members:
+                fid = m.get('father_id')
+                mid = m['id']
+                if fid and fid in siblings_map:
+                    m['siblings'] = [s for s in siblings_map[fid] if s['id'] != mid]
+                else:
+                    m['siblings'] = []
+
     finally:
         print(f"[Members List] Closing database connection")
         conn.close()
@@ -1645,22 +1681,49 @@ def search_member():
     conn = get_db_connection()
     try:
         with conn.cursor() as cursor:
+            # 先去重查人员(每个 ID 只返回一行),再子查询附加父亲和出继信息
+            like = f'%{keyword}%'
             cursor.execute("""
-                SELECT fmi.id, fmi.name, fmi.simplified_name, fmi.name_word_generation,
-                       p.name AS father_name, p.simplified_name AS father_simplified_name
+                SELECT
+                    fmi.id,
+                    fmi.name,
+                    fmi.simplified_name,
+                    fmi.name_word_generation,
+                    -- 父亲:优先非入继父(sub_type != 3),取第一条
+                    (SELECT p.name FROM family_relation_info r
+                     JOIN family_member_info p ON r.parent_mid = p.id
+                     WHERE r.child_mid = fmi.id AND r.relation_type = 1
+                     ORDER BY CASE WHEN COALESCE(r.sub_relation_type,0)=3 THEN 1 ELSE 0 END, r.id
+                     LIMIT 1) AS father_name,
+                    (SELECT p.simplified_name FROM family_relation_info r
+                     JOIN family_member_info p ON r.parent_mid = p.id
+                     WHERE r.child_mid = fmi.id AND r.relation_type = 1
+                     ORDER BY CASE WHEN COALESCE(r.sub_relation_type,0)=3 THEN 1 ELSE 0 END, r.id
+                     LIMIT 1) AS father_simplified_name,
+                    -- 出继标注:同时有 sub_type=2(亲生方)和 sub_type=3(养家方)时生成
+                    (SELECT CONCAT(
+                         '由',
+                         COALESCE(pbio.simplified_name, pbio.name, '?'),
+                         '出继给',
+                         COALESCE(padopt.simplified_name, padopt.name, '?'))
+                     FROM family_relation_info rbio
+                     JOIN family_member_info pbio ON rbio.parent_mid = pbio.id
+                     JOIN family_relation_info radopt ON radopt.child_mid = fmi.id AND radopt.sub_relation_type = 3
+                     JOIN family_member_info padopt ON radopt.parent_mid = padopt.id
+                     WHERE rbio.child_mid = fmi.id AND rbio.sub_relation_type = 2
+                     LIMIT 1) AS adoption_note
                 FROM family_member_info fmi
-                LEFT JOIN family_relation_info r ON r.child_mid = fmi.id AND r.relation_type = 1
-                LEFT JOIN family_member_info p ON r.parent_mid = p.id
                 WHERE fmi.name LIKE %s OR fmi.simplified_name LIKE %s OR fmi.former_name LIKE %s
-                ORDER BY 
-                    CASE WHEN fmi.name = %s THEN 1 
-                         WHEN fmi.simplified_name = %s THEN 2 
-                         WHEN fmi.name LIKE %s THEN 3 
-                         WHEN fmi.simplified_name LIKE %s THEN 4 
+                GROUP BY fmi.id
+                ORDER BY
+                    CASE WHEN fmi.name = %s THEN 1
+                         WHEN fmi.simplified_name = %s THEN 2
+                         WHEN fmi.name LIKE %s THEN 3
+                         WHEN fmi.simplified_name LIKE %s THEN 4
                          ELSE 5 END
-            """, (f'%{keyword}%', f'%{keyword}%', f'%{keyword}%', keyword, keyword, f'{keyword}%', f'{keyword}%'))
+            """, (like, like, like, keyword, keyword, f'{keyword}%', f'{keyword}%'))
             members = cursor.fetchall()
-            
+
             if members:
                 return jsonify({"success": True, "members": members})
             else:
@@ -6148,8 +6211,6 @@ def api_get_ancestors_above(ancestor_id):
     if not token:
         return jsonify({"success": False, "message": "未登录"}), 401
 
-    mode = request.args.get('mode', 'incense')
-
     conn = get_db_connection()
     try:
         with conn.cursor() as cursor:
@@ -6192,7 +6253,7 @@ def api_get_ancestors_above(ancestor_id):
                 if not parents:
                     break
 
-                # 分拣各类父母关系
+                # 父母优先级:入继养父母(3) > 普通亲生(0) > 出继亲生(2)
                 normal_parent = None
                 adoptive_parent = None
                 bio_parent = None
@@ -6203,18 +6264,16 @@ def api_get_ancestors_above(ancestor_id):
                         bio_parent = p
                     else:
                         normal_parent = p
-
-                if mode == 'blood':
-                    parent = normal_parent or bio_parent or adoptive_parent
-                else:
-                    parent = adoptive_parent or normal_parent or bio_parent
-                    if parent is adoptive_parent and adoptive_parent is not None:
-                        bio_name = (bio_parent.get('simplified_name') or bio_parent.get('name')) if bio_parent else None
-                        adopt_label = f"从{bio_name}出继" if bio_name else "出继"
-                        if depth == 0:
-                            anchor_adoption_label_wx = adopt_label
-                        elif generations:
-                            generations[-1]['ancestor']['adoption_label'] = adopt_label
+                parent = adoptive_parent or normal_parent or bio_parent
+
+                # 若走入继路径,在 current_id 对应的人物上标注"从xx出继"
+                if parent is adoptive_parent and adoptive_parent is not None:
+                    bio_name = (bio_parent.get('simplified_name') or bio_parent.get('name')) if bio_parent else None
+                    adopt_label = f"从{bio_name}出继" if bio_name else "出继"
+                    if depth == 0:
+                        anchor_adoption_label_wx = adopt_label
+                    elif generations:
+                        generations[-1]['ancestor']['adoption_label'] = adopt_label
 
                 # 祖先卡片不携带子辈关系类型
                 parent['sub_relation_type'] = None

+ 14 - 4
templates/lineage_query.html

@@ -590,8 +590,15 @@ function showMemberSelection(members) {
         const fatherPart = member.father_name
             ? `<span style="color: rgba(255,255,255,0.5); font-size: 12px; margin-left: 6px;">父: ${member.father_name}${member.father_simplified_name && member.father_simplified_name !== member.father_name ? '(' + member.father_simplified_name + ')' : ''}</span>` : '';
         const idPart = `<span style="color: rgba(255,200,100,0.6); font-size: 11px; margin-left: 6px;">ID:${member.id}</span>`;
+        const adoptionPart = member.adoption_note
+            ? `<div style="margin-top: 3px; margin-left: 2px;">
+                   <span style="display:inline-block; background: rgba(220,80,30,0.18); border: 1px solid rgba(220,100,30,0.45);
+                                color: #f4a460; font-size: 11px; border-radius: 4px; padding: 1px 7px; white-space: nowrap;">
+                       🔄 ${member.adoption_note}
+                   </span>
+               </div>` : '';
         return `
-            <div class="member-select-row" 
+            <div class="member-select-row"
                  onclick="selectMemberByIndex(${index})"
                  style="padding: 10px 12px; border-bottom: 1px solid rgba(255,255,255,0.08);
                         cursor: pointer; display: flex; justify-content: space-between;
@@ -599,9 +606,12 @@ function showMemberSelection(members) {
                  onmouseover="this.style.background='rgba(74,105,189,0.2)'"
                  onmouseout="this.style.background=''">
                 <div style="flex: 1; min-width: 0;">
-                    <span style="color: #ffd700; font-size: 13px; margin-right: 8px; flex-shrink: 0;">${index + 1}.</span>
-                    <span style="color: #fff; font-weight: 600;">${member.name}</span>
-                    ${simplifiedPart}${genPart}${fatherPart}${idPart}
+                    <div>
+                        <span style="color: #ffd700; font-size: 13px; margin-right: 8px; flex-shrink: 0;">${index + 1}.</span>
+                        <span style="color: #fff; font-weight: 600;">${member.name}</span>
+                        ${simplifiedPart}${genPart}${fatherPart}${idPart}
+                    </div>
+                    ${adoptionPart}
                 </div>
                 <a href="/manager/member_detail/${member.id}" target="_blank"
                    onclick="event.stopPropagation()"

+ 75 - 0
templates/members.html

@@ -24,6 +24,60 @@
         opacity: 0.6;
         flex-shrink: 0;
     }
+    /* 兄弟 hover 弹窗 */
+    .siblings-wrapper {
+        position: relative;
+        display: inline-block;
+    }
+    .siblings-popup {
+        display: none;
+        position: absolute;
+        top: 100%;
+        left: 0;
+        z-index: 999;
+        background: #fff;
+        border: 1px solid #d0d7e6;
+        border-radius: 8px;
+        box-shadow: 0 4px 18px rgba(0,0,0,0.13);
+        padding: 8px 4px;
+        min-width: 140px;
+        max-width: 220px;
+        max-height: 260px;
+        overflow-y: auto;
+        white-space: nowrap;
+    }
+    .siblings-wrapper:hover .siblings-popup {
+        display: block;
+    }
+    .siblings-popup a {
+        display: block;
+        padding: 3px 12px;
+        font-size: 12px;
+        color: #3b5998;
+        text-decoration: none;
+        border-radius: 4px;
+        transition: background 0.12s;
+    }
+    .siblings-popup a:hover {
+        background: #f0f4fb;
+        color: #1a3a80;
+    }
+    .sibling-summary {
+        font-size: 11.5px;
+        color: #666;
+        margin-top: 2px;
+        cursor: default;
+    }
+    .sibling-count-badge {
+        display: inline-block;
+        background: #e8eef7;
+        color: #4a69bd;
+        border-radius: 10px;
+        padding: 0 6px;
+        font-size: 11px;
+        vertical-align: middle;
+        margin-left: 2px;
+    }
 </style>
 {% endblock %}
 
@@ -104,6 +158,27 @@
                             {% else %}
                             <span class="text-muted small">-</span>
                             {% endif %}
+                            {# 兄弟信息 #}
+                            {% if member.siblings %}
+                            {% set sibs = member.siblings %}
+                            {% set first = sibs[0] %}
+                            {% set rest_count = sibs|length - 1 %}
+                            <div class="sibling-summary siblings-wrapper" style="display:block; margin-top:3px;">
+                                兄弟:<a href="{{ url_for('member_detail', member_id=first.id) }}"
+                                        target="_blank"
+                                        style="font-size:11.5px; color:#3b5998; text-decoration:none;"
+                                        onclick="event.stopPropagation()">{{ first.simplified_name or first.name }}</a>{% if rest_count > 0 %}<span class="sibling-count-badge">+{{ rest_count }}个兄弟</span>{% endif %}
+                                {% if sibs|length > 1 %}
+                                <div class="siblings-popup">
+                                    {% for sib in sibs %}
+                                    <a href="{{ url_for('member_detail', member_id=sib.id) }}" target="_blank">
+                                        {{ sib.simplified_name or sib.name }}
+                                    </a>
+                                    {% endfor %}
+                                </div>
+                                {% endif %}
+                            </div>
+                            {% endif %}
                         </td>
                         <td>
                             {% if member.occupation %}