Explorar el Código

commit 新增热心宗亲

林海 hace 2 días
padre
commit
6a6bf9c826
Se han modificado 3 ficheros con 269 adiciones y 74 borrados
  1. 149 34
      app.py
  2. 46 5
      templates/add_member.html
  3. 74 35
      templates/settlements.html

+ 149 - 34
app.py

@@ -1335,7 +1335,7 @@ def get_lineage(member_id):
                         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
+                        ORDER BY COALESCE(r.child_order, 99999), c.id
                         LIMIT 30
                     """, (grandparent['id'], parent['id']))
                     parent_siblings = cursor.fetchall()
@@ -1383,7 +1383,7 @@ def get_lineage(member_id):
                 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
+                ORDER BY COALESCE(r.child_order, 99999), c.id
                 LIMIT 30
             """, (member_id,))
             children = cursor.fetchall()
@@ -1422,7 +1422,7 @@ def get_lineage(member_id):
                     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
+                    ORDER BY COALESCE(r.child_order, 99999), c.id
                     LIMIT 30
                 """, (parent_id, member_id))
                 siblings = cursor.fetchall()
@@ -1472,7 +1472,7 @@ def get_descendants(parent_id):
                     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
+                    ORDER BY COALESCE(r.child_order, 99999), c.id
                     LIMIT 20
                 """, (parent_id,) + tuple(excluded_list))
             else:
@@ -1482,7 +1482,7 @@ def get_descendants(parent_id):
                     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
+                    ORDER BY COALESCE(r.child_order, 99999), c.id
                     LIMIT 20
                 """, (parent_id,))
             
@@ -2167,17 +2167,18 @@ def add_member():
                 # 录入关系(支持多条)
                 sql_relation = """
                     INSERT INTO family_relation_info 
-                    (parent_mid, child_mid, relation_type, sub_relation_type, source_mid, generation_diff) 
-                    VALUES (%s, %s, %s, %s, %s, %s)
+                    (parent_mid, child_mid, relation_type, sub_relation_type, source_mid, generation_diff, child_order) 
+                    VALUES (%s, %s, %s, %s, %s, %s, %s)
                 """
-                
+
                 for rel in relations:
                     rel_type = rel['relation_type']
                     parent_mid = rel['parent_mid']
                     sub_relation_type = rel['sub_relation_type']
+                    child_order = rel.get('child_order') if rel_type in [1, 2] else None
                     gen_diff = 1 if rel_type in [1, 2] else 0
-                    print(f"[Add Member] Inserting relation: parent_mid={parent_mid}, child_mid={member_id}, relation_type={rel_type}, sub_relation_type={sub_relation_type}")
-                    cursor.execute(sql_relation, (parent_mid, member_id, rel_type, sub_relation_type, member_id, gen_diff))
+                    print(f"[Add Member] Inserting relation: parent_mid={parent_mid}, child_mid={member_id}, relation_type={rel_type}, sub_relation_type={sub_relation_type}, child_order={child_order}")
+                    cursor.execute(sql_relation, (parent_mid, member_id, rel_type, sub_relation_type, member_id, gen_diff, child_order))
                 
                 # Update AI Record Status if applicable
                 source_record_id = data.get('source_record_id')
@@ -2265,26 +2266,31 @@ def edit_member(member_id):
                 parent_mid = request.form.get(f'relations[{i}][parent_mid]')
                 rel_type = request.form.get(f'relations[{i}][relation_type]')
                 sub_rel_type = request.form.get(f'relations[{i}][sub_relation_type]', '0')
-                
+                child_order_raw = request.form.get(f'relations[{i}][child_order]', '')
+
                 if not parent_mid or not rel_type:
                     break
-                
+
+                child_order = int(child_order_raw) if child_order_raw.strip().isdigit() else None
                 relations.append({
                     'parent_mid': int(parent_mid),
                     'relation_type': int(rel_type),
-                    'sub_relation_type': int(sub_rel_type)
+                    'sub_relation_type': int(sub_rel_type),
+                    'child_order': child_order,
                 })
                 i += 1
-            
+
             # For backward compatibility
             if not relations:
                 related_mid = request.form.get('related_mid')
                 relation_type = request.form.get('relation_type')
                 if related_mid and relation_type:
+                    child_order_raw = request.form.get('child_order', '')
                     relations.append({
                         'parent_mid': int(related_mid),
                         'relation_type': int(relation_type),
-                        'sub_relation_type': int(request.form.get('sub_relation_type', '0'))
+                        'sub_relation_type': int(request.form.get('sub_relation_type', '0')),
+                        'child_order': int(child_order_raw) if child_order_raw.strip().isdigit() else None,
                     })
 
             # 年龄校验逻辑
@@ -2364,20 +2370,21 @@ def edit_member(member_id):
                 # 更新关系(支持多条)
                 print(f"[Edit Member] Deleting existing relations for member ID: {member_id}")
                 cursor.execute("DELETE FROM family_relation_info WHERE source_mid = %s", (member_id,))
-                
+
                 sql_relation = """
                     INSERT INTO family_relation_info 
-                    (parent_mid, child_mid, relation_type, sub_relation_type, source_mid, generation_diff) 
-                    VALUES (%s, %s, %s, %s, %s, %s)
+                    (parent_mid, child_mid, relation_type, sub_relation_type, source_mid, generation_diff, child_order) 
+                    VALUES (%s, %s, %s, %s, %s, %s, %s)
                 """
-                
+
                 for rel in relations:
                     rel_type = rel['relation_type']
                     parent_mid = rel['parent_mid']
                     sub_relation_type = rel['sub_relation_type']
+                    child_order = rel.get('child_order') if rel_type in [1, 2] else None
                     gen_diff = 1 if rel_type in [1, 2] else 0
-                    print(f"[Edit Member] Inserting relation: parent_mid={parent_mid}, child_mid={member_id}, relation_type={rel_type}, sub_relation_type={sub_relation_type}")
-                    cursor.execute(sql_relation, (parent_mid, member_id, rel_type, sub_relation_type, member_id, gen_diff))
+                    print(f"[Edit Member] Inserting relation: parent_mid={parent_mid}, child_mid={member_id}, relation_type={rel_type}, sub_relation_type={sub_relation_type}, child_order={child_order}")
+                    cursor.execute(sql_relation, (parent_mid, member_id, rel_type, sub_relation_type, member_id, gen_diff, child_order))
                 
                 # Update AI Record Status if applicable
                 source_record_id = data.get('source_record_id')
@@ -3308,8 +3315,8 @@ def add_settlement():
         with conn.cursor() as cursor:
             cursor.execute("""
                 INSERT INTO family_settlements 
-                (name, region, latitude, longitude, population, representative_id, description, surname_type, new_surname)
-                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
+                (name, region, latitude, longitude, population, representative_id, description, surname_type, new_surname, enthusiastic_members)
+                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
             """, (
                 data.get('name'),
                 data.get('region'),
@@ -3319,7 +3326,8 @@ def add_settlement():
                 data.get('representative_id') or None,
                 data.get('description'),
                 data.get('surname_type') or 0,
-                data.get('new_surname') or None
+                data.get('new_surname') or None,
+                data.get('enthusiastic_members') or None
             ))
             conn.commit()
             return jsonify({"success": True, "message": "添加成功"})
@@ -3343,7 +3351,7 @@ def update_settlement(id):
                 UPDATE family_settlements 
                 SET name=%s, region=%s, latitude=%s, longitude=%s, 
                     population=%s, representative_id=%s, description=%s,
-                    surname_type=%s, new_surname=%s
+                    surname_type=%s, new_surname=%s, enthusiastic_members=%s
                 WHERE id=%s
             """, (
                 data.get('name'),
@@ -3355,6 +3363,7 @@ def update_settlement(id):
                 data.get('description'),
                 data.get('surname_type') or 0,
                 data.get('new_surname') or None,
+                data.get('enthusiastic_members') or None,
                 id
             ))
             conn.commit()
@@ -3416,6 +3425,48 @@ def init_batch_task_table():
 # 初始化表
 init_batch_task_table()
 
+def migrate_child_order_column():
+    """为 family_relation_info 表添加 child_order 字段(如不存在)"""
+    conn = get_db_connection()
+    try:
+        with conn.cursor() as cursor:
+            cursor.execute("SHOW COLUMNS FROM family_relation_info LIKE 'child_order'")
+            if not cursor.fetchone():
+                cursor.execute(
+                    "ALTER TABLE family_relation_info ADD COLUMN child_order INT DEFAULT NULL COMMENT '第几子,用于兄弟排序'"
+                )
+                conn.commit()
+                print("[DB Migrate] Added child_order column to family_relation_info")
+            else:
+                print("[DB Migrate] child_order column already exists")
+    except Exception as e:
+        print(f"[DB Migrate] Error adding child_order: {e}")
+    finally:
+        conn.close()
+
+migrate_child_order_column()
+
+def migrate_enthusiastic_members_column():
+    """为 family_settlements 表添加 enthusiastic_members 字段(如不存在)"""
+    conn = get_db_connection()
+    try:
+        with conn.cursor() as cursor:
+            cursor.execute("SHOW COLUMNS FROM family_settlements LIKE 'enthusiastic_members'")
+            if not cursor.fetchone():
+                cursor.execute(
+                    "ALTER TABLE family_settlements ADD COLUMN enthusiastic_members TEXT DEFAULT NULL COMMENT '热心宗亲,多人以逗号分隔'"
+                )
+                conn.commit()
+                print("[DB Migrate] Added enthusiastic_members column to family_settlements")
+            else:
+                print("[DB Migrate] enthusiastic_members column already exists")
+    except Exception as e:
+        print(f"[DB Migrate] Error adding enthusiastic_members: {e}")
+    finally:
+        conn.close()
+
+migrate_enthusiastic_members_column()
+
 def async_process_genealogy_task(task_id, member_ids, user_id):
     """异步处理族谱原文任务"""
     results = []
@@ -4400,12 +4451,75 @@ def extract_single_genealogy(member_id):
     finally:
         conn.close()
 
+@app.route('/manager/api/members/batch_resume_task', methods=['GET'])
+def batch_resume_task():
+    """
+    恢复因服务重启而中断的批量任务(GET,方便浏览器直接访问)。
+    可选参数:?task_id=xxx  不传则自动找最近一条中断任务。
+    """
+    if 'user_id' not in session:
+        return jsonify({"success": False, "message": "Unauthorized"}), 401
+
+    task_id = request.args.get('task_id')
+
+    conn = get_db_connection()
+    try:
+        with conn.cursor() as cursor:
+            if task_id:
+                cursor.execute("""
+                    SELECT task_id, status, last_processed_id, total_count, completed_count, failed_count
+                    FROM batch_genealogy_task
+                    WHERE task_id = %s AND user_id = %s
+                """, (task_id, session['user_id']))
+            else:
+                # 找最近一条中断的任务
+                cursor.execute("""
+                    SELECT task_id, status, last_processed_id, total_count, completed_count, failed_count
+                    FROM batch_genealogy_task
+                    WHERE user_id = %s AND status IN ('pending', 'processing', 'interrupted')
+                    ORDER BY created_at DESC
+                    LIMIT 1
+                """, (session['user_id'],))
+            task = cursor.fetchone()
+
+        if not task:
+            return jsonify({"success": False, "message": "未找到可恢复的任务"}), 404
+
+        task_id = task['task_id']
+
+        # 重新标记为 processing,准备恢复线程
+        with conn.cursor() as cursor:
+            cursor.execute("""
+                UPDATE batch_genealogy_task
+                SET status = 'processing'
+                WHERE task_id = %s
+            """, (task_id,))
+        conn.commit()
+
+        threading.Thread(
+            target=async_process_all_empty_genealogy,
+            args=(task_id, session['user_id']),
+            daemon=True
+        ).start()
+
+        return jsonify({
+            "success": True,
+            "task_id": task_id,
+            "message": f"任务已从断点恢复(已完成 {task['completed_count']},从 last_processed_id={task['last_processed_id']} 继续)",
+            "last_processed_id": task['last_processed_id'],
+            "completed_count": task['completed_count'],
+            "total_count": task['total_count'],
+        })
+    finally:
+        conn.close()
+
+
 @app.route('/manager/api/members/batch_process_all_empty', methods=['GET'])
 def batch_process_all_empty():
     """简便批量处理接口:自动处理所有族谱原文为空的成员,支持断点续跑"""
     if 'user_id' not in session:
         return jsonify({"success": False, "message": "Unauthorized"}), 401
-    
+
     conn = get_db_connection()
     try:
         with conn.cursor() as cursor:
@@ -4417,7 +4531,7 @@ def batch_process_all_empty():
             """)
             result = cursor.fetchone()
             total_empty = result['count'] if result else 0
-            
+
             cursor.execute("""
                 SELECT task_id, status, last_processed_id, total_count, completed_count, failed_count
                 FROM batch_genealogy_task 
@@ -4426,33 +4540,34 @@ def batch_process_all_empty():
                 LIMIT 1
             """, (session['user_id'],))
             running_task = cursor.fetchone()
-            
+
             if running_task:
                 return jsonify({
                     "success": False,
-                    "message": "存在正在进行的任务",
+                    "message": "存在正在进行的任务,若服务已重启可调用 POST /manager/api/members/batch_resume_task 恢复",
                     "task_id": running_task['task_id'],
                     "status": running_task['status'],
                     "last_processed_id": running_task['last_processed_id'],
                     "completed_count": running_task['completed_count'],
-                    "total_count": running_task['total_count']
+                    "total_count": running_task['total_count'],
+                    "resume_tip": "POST /manager/api/members/batch_resume_task  body: {\"task_id\": \"" + running_task['task_id'] + "\"}"
                 })
-        
+
         task_id = str(uuid.uuid4())
-        
+
         with conn.cursor() as cursor:
             cursor.execute("""
                 INSERT INTO batch_genealogy_task (task_id, user_id, status, total_count, last_processed_id)
                 VALUES (%s, %s, 'processing', %s, 0)
             """, (task_id, session['user_id'], total_empty))
         conn.commit()
-        
+
         threading.Thread(
             target=async_process_all_empty_genealogy,
             args=(task_id, session['user_id']),
             daemon=True
         ).start()
-        
+
         return jsonify({
             "success": True,
             "task_id": task_id,

+ 46 - 5
templates/add_member.html

@@ -205,7 +205,7 @@
                     <div id="relations-container">
                         <!-- Existing relations will be added here dynamically -->
                         {% if current_relation %}
-                        <div class="row g-3 mb-3 relation-row" data-index="0">
+                        <div class="row g-3 mb-1 relation-row" data-index="0">
                             <div class="col-md-4">
                                 <label class="form-label">关联成员</label>
                                 <div class="input-group">
@@ -243,9 +243,14 @@
                                     <i class="bi bi-trash"></i>
                                 </button>
                             </div>
+                            <div class="col-md-2 child-order-wrapper" style="display: {{ 'block' if current_relation.relation_type in [1,2] else 'none' }};">
+                                <label class="form-label">第几子</label>
+                                <input type="number" name="relations[0][child_order]" class="form-control child-order-input"
+                                       min="1" placeholder="排行(选填)" value="{{ current_relation.child_order if current_relation.child_order else '' }}">
+                            </div>
                         </div>
                         {% else %}
-                        <div class="row g-3 mb-3 relation-row" data-index="0">
+                        <div class="row g-3 mb-1 relation-row" data-index="0">
                             <div class="col-md-4">
                                 <label class="form-label">关联成员</label>
                                 <div class="input-group">
@@ -283,6 +288,11 @@
                                     <i class="bi bi-trash"></i>
                                 </button>
                             </div>
+                            <div class="col-md-2 child-order-wrapper" style="display: none;">
+                                <label class="form-label">第几子</label>
+                                <input type="number" name="relations[0][child_order]" class="form-control child-order-input"
+                                       min="1" placeholder="排行(选填)">
+                            </div>
                         </div>
                         {% endif %}
                     </div>
@@ -975,23 +985,54 @@
                     <i class="bi bi-trash"></i>
                 </button>
             </div>
+            <div class="col-md-2 child-order-wrapper" style="display: none;">
+                <label class="form-label">第几子</label>
+                <input type="number" name="relations[${newIndex}][child_order]" class="form-control child-order-input"
+                       min="1" placeholder="排行(选填)">
+            </div>
         `;
         
         container.appendChild(newRow);
-        
+
         // Add click handlers for new buttons
         const selectBtn = newRow.querySelector('.select-member-btn');
         selectBtn.addEventListener('click', function() {
             const index = parseInt(this.dataset.index);
             window.currentRelationIndex = index;
         });
-        
+
         const removeBtn = newRow.querySelector('.remove-relation-btn');
         removeBtn.addEventListener('click', removeRelationRow);
-        
+
+        // Show/hide 第几子 based on relation type
+        const relTypeSelect = newRow.querySelector('.relation-type');
+        relTypeSelect.addEventListener('change', function() {
+            toggleChildOrderField(this);
+        });
+
         updateRemoveButtons();
     }
 
+    function toggleChildOrderField(selectEl) {
+        const row = selectEl.closest('.relation-row');
+        if (!row) return;
+        const wrapper = row.querySelector('.child-order-wrapper');
+        if (!wrapper) return;
+        const val = selectEl.value;
+        if (val === '1' || val === '2') {
+            wrapper.style.display = 'block';
+        } else {
+            wrapper.style.display = 'none';
+            const input = wrapper.querySelector('.child-order-input');
+            if (input) input.value = '';
+        }
+    }
+
+    // Init toggle for pre-rendered rows
+    document.querySelectorAll('#relations-container .relation-type').forEach(function(sel) {
+        sel.addEventListener('change', function() { toggleChildOrderField(this); });
+    });
+
     function removeRelationRow() {
         const row = this.closest('.relation-row');
         if (row) {

+ 74 - 35
templates/settlements.html

@@ -1,6 +1,12 @@
 {% extends 'layout.html' %}
 
 {% block content %}
+<style>
+/* 水滴标记 hover 放大,锚点在尖端底部 */
+.settlement-drop-marker:hover {
+    transform: scale(1.25) !important;
+}
+</style>
 <script>
     window.isSuperAdmin = {{ session.get('is_super_admin') | tojson }};
 </script>
@@ -147,6 +153,13 @@
                             <label class="form-label">备注说明</label>
                             <textarea class="form-control" id="settlement-description" rows="3"></textarea>
                         </div>
+                        {% if session.get('is_super_admin') %}
+                        <div class="col-md-12">
+                            <label class="form-label">热心宗亲</label>
+                            <input type="text" class="form-control" id="settlement-enthusiastic-members" placeholder="多人请用逗号分隔,如:张三,李四,王五">
+                            <div class="form-text">仅超级管理员可见,录入后在地图hover和详情中显示</div>
+                        </div>
+                        {% endif %}
                     </div>
                 </form>
             </div>
@@ -328,37 +341,37 @@ function initMapWithData(settlements) {
                 settlements.forEach(settlement => {
                     const lng = parseFloat(settlement.longitude) || 116.397428;
                     const lat = parseFloat(settlement.latitude) || 39.90923;
-                    
-                    const circle = new AMap.Circle({
-                        center: [lng, lat],
-                        radius: 50000,
-                        strokeColor: '#3B82F6',
-                        strokeOpacity: 0.8,
-                        strokeWeight: 3,
-                        fillColor: '#3B82F6',
-                        fillOpacity: 0.15,
+
+                    // 水滴型标记(SVG:圆头朝上,尖端朝下)
+                    // hover 放大效果用 CSS :hover 实现,不依赖 getContent() DOM 操作
+                    const markerContent = `
+                        <div class="settlement-drop-marker" title="${settlement.name}" style="
+                            position: relative; width: 32px; height: 42px; cursor: pointer;
+                            filter: drop-shadow(0 2px 4px rgba(0,0,0,0.35));
+                            transition: transform 0.15s ease; transform-origin: bottom center;
+                        ">
+                            <svg viewBox="0 0 32 42" xmlns="http://www.w3.org/2000/svg" width="32" height="42">
+                                <path d="M16 0 C7.163 0 0 7.163 0 16 C0 26 16 42 16 42 C16 42 32 26 32 16 C32 7.163 24.837 0 16 0 Z"
+                                      fill="#3B82F6" stroke="#1D4ED8" stroke-width="1.5"/>
+                                <circle cx="16" cy="15" r="7" fill="white" opacity="0.9"/>
+                            </svg>
+                        </div>`;
+
+                    const marker = new AMap.Marker({
+                        position: [lng, lat],
+                        content: markerContent,
+                        offset: new AMap.Pixel(-16, -42),
                         cursor: 'pointer'
                     });
-                    
-                    circle.settlementData = settlement;
-                    circle.on('click', function() { showSettlementDetail(settlement); });
-                    circle.on('mouseover', function(e) { 
-                        circle.setOptions({
-                            fillOpacity: 0.25,
-                            strokeOpacity: 1
-                        });
-                        showMarkerTooltip(e, settlement); 
-                    });
-                    circle.on('mouseout', function() { 
-                        circle.setOptions({
-                            fillOpacity: 0.15,
-                            strokeOpacity: 0.8
-                        });
-                        hideMarkerTooltip(); 
-                    });
-                    
-                    map.add(circle);
-                    markers.push(circle);
+
+                    marker.settlementData = settlement;
+                    marker.on('click', function() { showSettlementDetail(settlement); });
+                    marker.on('mouseover', function(e) { showMarkerTooltip(e, settlement); });
+                    marker.on('mousemove', function(e) { showMarkerTooltip(e, settlement); });
+                    marker.on('mouseout',  function()  { hideMarkerTooltip(); });
+
+                    map.add(marker);
+                    markers.push(marker);
                     bounds.extend([lng, lat]);
                 });
                 
@@ -465,6 +478,7 @@ function showSettlementDetail(settlement) {
             </div>
         </div>
         ${settlement.description ? '<div class="mt-4"><h6>备注说明</h6><p>' + settlement.description + '</p></div>' : ''}
+        ${window.isSuperAdmin && settlement.enthusiastic_members ? '<div class="mt-4"><h6 style="color:#d97706;">热心宗亲</h6><p>' + settlement.enthusiastic_members + '</p></div>' : ''}
         <div class="mt-4 text-muted text-sm">
             创建时间:${formatDateTime(settlement.created_at)}
             ${settlement.updated_at !== settlement.created_at ? '<br>更新时间:' + formatDateTime(settlement.updated_at) : ''}
@@ -502,6 +516,8 @@ function loadSettlementForEdit(id) {
                 document.getElementById('settlement-description').value = s.description || '';
                 document.getElementById('settlement-surname-type').value = s.surname_type || 0;
                 document.getElementById('settlement-new-surname').value = s.new_surname || '';
+                const emEl2 = document.getElementById('settlement-enthusiastic-members');
+                if (emEl2) emEl2.value = s.enthusiastic_members || '';
                 
                 toggleNewSurnameField(s.surname_type);
                 document.getElementById('addSettlementModalLabel').textContent = '编辑聚落';
@@ -530,6 +546,8 @@ function resetSettlementForm() {
     document.getElementById('settlement-description').value = '';
     document.getElementById('settlement-surname-type').value = '0';
     document.getElementById('settlement-new-surname').value = '';
+    const emEl = document.getElementById('settlement-enthusiastic-members');
+    if (emEl) emEl.value = '';
     document.getElementById('location-info').style.display = 'none';
     document.getElementById('region-suggestions').style.display = 'none';
     document.getElementById('new-surname-container').style.display = 'none';
@@ -537,6 +555,7 @@ function resetSettlementForm() {
 }
 
 function saveSettlement() {
+    const emSaveEl = document.getElementById('settlement-enthusiastic-members');
     const data = {
         id: document.getElementById('settlement-id').value,
         name: document.getElementById('settlement-name').value,
@@ -547,7 +566,8 @@ function saveSettlement() {
         representative_id: document.getElementById('settlement-representative-id').value,
         description: document.getElementById('settlement-description').value,
         surname_type: document.getElementById('settlement-surname-type').value,
-        new_surname: document.getElementById('settlement-new-surname').value
+        new_surname: document.getElementById('settlement-new-surname').value,
+        enthusiastic_members: emSaveEl ? emSaveEl.value : ''
     };
     
     const url = data.id ? '/manager/api/settlements/' + data.id : '/manager/api/settlements';
@@ -830,21 +850,40 @@ function showMarkerTooltip(e, settlement) {
         tooltipDiv.style.cssText = 'position: fixed; background: rgba(30, 41, 59, 0.95); color: white; padding: 12px; border-radius: 8px; min-width: 200px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 1000; pointer-events: none; font-size: 13px;';
         document.body.appendChild(tooltipDiv);
     }
-    
+
     const surnameType = getSurnameTypeText(settlement.surname_type);
     tooltipDiv.innerHTML = '<div style="font-weight: 600; margin-bottom: 8px; font-size: 14px;">' + settlement.name + '</div>' +
                           (settlement.region ? '<div style="margin-bottom: 4px; color: #94a3b8;">区域:' + settlement.region + '</div>' : '') +
                           '<div style="margin-bottom: 4px; color: #94a3b8;">人数:' + (settlement.population || 0) + ' 人</div>' +
                           (settlement.representative_name ? '<div style="margin-bottom: 4px; color: #94a3b8;">代表:' + settlement.representative_name + '</div>' : '') +
                           '<div style="margin-bottom: 4px; color: #94a3b8;">姓氏:' + surnameType + (settlement.new_surname ? '(改后:' + settlement.new_surname + ')' : '') + '</div>' +
+                          (settlement.description ? '<div style="margin-bottom: 4px; color: #94a3b8; max-width: 220px; word-break: break-all;">备注:' + settlement.description + '</div>' : '') +
+                          (window.isSuperAdmin && settlement.enthusiastic_members ? '<div style="margin-bottom: 4px; color: #fbbf24;">热心宗亲:' + settlement.enthusiastic_members + '</div>' : '') +
                           '<div style="border-top: 1px solid rgba(255,255,255,0.1); padding-top: 8px; margin-top: 4px;">' +
                           '<button onclick="event.stopPropagation(); showSettlementDetail(' + JSON.stringify(settlement).replace(/"/g, '&quot;') + '); hideMarkerTooltip();" style="background: #3B82F6; color: white; border: none; padding: 4px 12px; border-radius: 4px; font-size: 11px; cursor: pointer;">查看详情</button>' +
                           '</div>';
-    
-    const containerRect = document.getElementById('map-container').getBoundingClientRect();
-    tooltipDiv.style.left = (containerRect.left + e.pixel.x + 10) + 'px';
-    tooltipDiv.style.top = (containerRect.top + e.pixel.y - tooltipDiv.offsetHeight / 2) + 'px';
     tooltipDiv.style.display = 'block';
+
+    // 定位:AMap.Marker 的 mouseover 事件提供 originEvent(原生 MouseEvent),
+    // 直接用 clientX/Y 定位;AMap.Circle 提供 e.pixel(相对地图容器的像素坐标)作为兜底
+    let x, y;
+    if (e.originEvent && e.originEvent.clientX != null) {
+        x = e.originEvent.clientX;
+        y = e.originEvent.clientY;
+    } else if (e.pixel) {
+        const containerRect = document.getElementById('map-container').getBoundingClientRect();
+        x = containerRect.left + e.pixel.x;
+        y = containerRect.top  + e.pixel.y;
+    } else {
+        return;
+    }
+
+    const vw = window.innerWidth;
+    const th = tooltipDiv.offsetHeight || 120;
+    const tw = tooltipDiv.offsetWidth  || 220;
+    // 右侧放不下时改为左侧
+    tooltipDiv.style.left = (x + 12 + tw > vw ? x - tw - 12 : x + 12) + 'px';
+    tooltipDiv.style.top  = Math.max(8, y - th / 2) + 'px';
 }
 
 function hideMarkerTooltip() {