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

commit 优化 新增聚落地图

林海 преди 5 дни
родител
ревизия
46518a3423
променени са 8 файла, в които са добавени 1594 реда и са изтрити 147 реда
  1. 347 102
      app.py
  2. 5 0
      cookies.txt
  3. 273 31
      templates/add_member.html
  4. 4 1
      templates/layout.html
  5. 32 0
      templates/lineage_query.html
  6. 32 8
      templates/member_detail.html
  7. 856 0
      templates/settlements.html
  8. 45 5
      templates/tree.html

+ 347 - 102
app.py

@@ -1193,8 +1193,8 @@ def tree_data():
             # 获取所有成员
             cursor.execute("SELECT id, name, simplified_name, sex, family_rank, name_word_generation FROM family_member_info")
             members = cursor.fetchall()
-            # 获取所有关系 (1:父子 2:母子 10:夫妻 11:兄弟 12:姐妹)
-            cursor.execute("SELECT parent_mid, child_mid, relation_type FROM family_relation_info")
+            # 获取所有关系 (1:父子 2:母子 10:夫妻 11:兄弟 12:姐妹),包括子类型
+            cursor.execute("SELECT parent_mid, child_mid, relation_type, sub_relation_type FROM family_relation_info")
             relations = cursor.fetchall()
             
             return jsonify({"members": members, "relations": relations})
@@ -1266,19 +1266,36 @@ def get_lineage(member_id):
             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
+                           EXISTS(SELECT 1 FROM family_relation_info WHERE parent_mid = p.id AND relation_type IN (1, 2)) as has_children,
+                           r.sub_relation_type
                     FROM family_relation_info r
                     JOIN family_member_info p ON r.parent_mid = p.id
                     WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
-                    LIMIT 1
                 """, (current_id,))
-                parent = cursor.fetchone()
+                parents = cursor.fetchall()
                 
-                if not parent:
+                if not parents:
                     break
                 
+                # 优先选择直系父母(非出继),如果都是出继/入继,选择入继
+                parent = None
+                adoptive_parent = None
+                
+                for p in parents:
+                    if p['sub_relation_type'] == 2:  # 出继(亲生父母)
+                        parent = p
+                    elif p['sub_relation_type'] == 3:  # 入继(养父母)
+                        adoptive_parent = p
+                    else:  # 普通关系(亲生)
+                        parent = p
+                
+                # 如果没有找到普通父母,使用入继父母
+                if not parent:
+                    parent = adoptive_parent
+                
                 ancestor_ids.append(parent['id'])
                 displayed_ids.add(parent['id'])
                 
@@ -1341,9 +1358,12 @@ def get_lineage(member_id):
             
             # Step 3: Get immediate children only (limited count)
             step_start = time.time()
+            
+            # 获取所有子女(包括出继和入继)
             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,
+                       r.sub_relation_type
                 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)
@@ -1352,6 +1372,23 @@ def get_lineage(member_id):
             """, (member_id,))
             children = cursor.fetchall()
             
+            # 对于出继的子女,需要获取他们入继到的家庭信息
+            for child in children:
+                if child['sub_relation_type'] == 2:  # 出继
+                    # 查找该子女入继到的父母
+                    cursor.execute("""
+                        SELECT p.id, p.name, p.simplified_name
+                        FROM family_relation_info r
+                        JOIN family_member_info p ON r.parent_mid = p.id
+                        WHERE r.child_mid = %s AND r.sub_relation_type = 3
+                        LIMIT 1
+                    """, (child['id'],))
+                    adoptive_parent = cursor.fetchone()
+                    if adoptive_parent:
+                        child['adoptive_parent_name'] = adoptive_parent['name']
+                        if adoptive_parent['simplified_name'] and adoptive_parent['simplified_name'] != adoptive_parent['name']:
+                            child['adoptive_parent_name'] += f" ({adoptive_parent['simplified_name']})"
+            
             # Initialize children array
             for child in children:
                 child['children'] = []
@@ -1364,7 +1401,8 @@ def get_lineage(member_id):
                 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
+                           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
                     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
@@ -1688,41 +1726,70 @@ def add_member():
                 except ValueError:
                     birthday_ts = 0
 
-            # 关系数据
-            related_mid = request.form.get('related_mid')
-            relation_type = request.form.get('relation_type')
-            sub_relation_type = request.form.get('sub_relation_type', 0)
+            # 关系数据 - 支持多条关系
+            relations = []
+            # Parse relations from form data
+            i = 0
+            while True:
+                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')
+                
+                if not parent_mid or not rel_type:
+                    break
+                
+                relations.append({
+                    'parent_mid': int(parent_mid),
+                    'relation_type': int(rel_type),
+                    'sub_relation_type': int(sub_rel_type)
+                })
+                i += 1
+            
+            # For backward compatibility, check old-style single relation
+            if not relations:
+                related_mid = request.form.get('related_mid')
+                relation_type = request.form.get('relation_type')
+                if related_mid and relation_type:
+                    relations.append({
+                        'parent_mid': int(related_mid),
+                        'relation_type': int(relation_type),
+                        'sub_relation_type': int(request.form.get('sub_relation_type', '0'))
+                    })
             
             # 年龄校验逻辑
-            if related_mid and relation_type in ['1', '2']: # 1:父子 2:母子
-                with conn.cursor() as cursor:
-                    cursor.execute("SELECT name, birthday FROM family_member_info WHERE id = %s", (related_mid,))
-                    parent = cursor.fetchone()
-                    if parent and parent['birthday'] > 0 and birthday_ts > 0:
-                        if birthday_ts < parent['birthday']:
-                            error_msg = f"数据冲突:成员年龄不能比其父亲/母亲({parent['name']})大,请检查并修正出生日期。"
-                            flash(error_msg)
-                            
-                            # Re-fetch data for rendering
-                            cursor.execute("SELECT id, name FROM family_member_info ORDER BY name")
-                            all_members = cursor.fetchall()
-                            cursor.execute("SELECT * FROM genealogy_records ORDER BY page_number ASC")
-                            images = cursor.fetchall()
-
-                            if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json:
-                                return jsonify({
-                                    "success": False, 
-                                    "message": error_msg
-                                }), 400
-                            
-                            selected_member_name = ''
-                            return render_template('add_member.html', all_members=all_members, images=images, 
-                                   prefilled_content=prefilled_content, source_oss_url=source_oss_url, source_record_id=source_record_id, selected_member_name=selected_member_name)
-
+            for rel in relations:
+                if rel['relation_type'] in [1, 2]: # 1:父子 2:母子
+                    with conn.cursor() as cursor:
+                        cursor.execute("SELECT name, birthday FROM family_member_info WHERE id = %s", (rel['parent_mid'],))
+                        parent = cursor.fetchone()
+                        if parent and parent['birthday'] > 0 and birthday_ts > 0:
+                            if birthday_ts < parent['birthday']:
+                                error_msg = f"数据冲突:成员年龄不能比其父亲/母亲({parent['name']})大,请检查并修正出生日期。"
+                                flash(error_msg)
+                                
+                                # Re-fetch data for rendering
+                                cursor.execute("SELECT id, name FROM family_member_info ORDER BY name")
+                                all_members = cursor.fetchall()
+                                cursor.execute("SELECT * FROM genealogy_records ORDER BY page_number ASC")
+                                images = cursor.fetchall()
+
+                                if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json:
+                                    return jsonify({
+                                        "success": False, 
+                                        "message": error_msg
+                                    }), 400
+                                
+                                selected_member_name = ''
+                                return render_template('add_member.html', all_members=all_members, images=images, 
+                                       prefilled_content=prefilled_content, source_oss_url=source_oss_url, source_record_id=source_record_id, selected_member_name=selected_member_name)
+                    break
+            
             # 获取表单数据
             data = {
                 'name': request.form['name'],
                 'simplified_name': request.form.get('simplified_name'),
+                'genealogy_original_traditional': request.form.get('genealogy_original_traditional'),
+                'genealogy_original_simplified': request.form.get('genealogy_original_simplified'),
                 'former_name': request.form.get('former_name'),
                 'childhood_name': request.form.get('childhood_name'),
                 'name_word': request.form.get('name_word'),
@@ -1768,20 +1835,20 @@ def add_member():
                 member_id = cursor.lastrowid
                 print(f"[Add Member] Inserted member with ID: {member_id}")
                 
-                # 录入关系
-                if related_mid and relation_type:
-                    rel_type = int(relation_type)
-                    parent_mid = int(related_mid)
-                    child_mid = 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)
+                """
+                
+                for rel in relations:
+                    rel_type = rel['relation_type']
+                    parent_mid = rel['parent_mid']
+                    sub_relation_type = rel['sub_relation_type']
                     gen_diff = 1 if rel_type in [1, 2] else 0
-                        
-                    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)
-                    """
-                    print(f"[Add Member] Inserting relation: parent_mid={parent_mid}, child_mid={child_mid}, relation_type={rel_type}")
-                    cursor.execute(sql_relation, (parent_mid, child_mid, 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}")
+                    cursor.execute(sql_relation, (parent_mid, member_id, rel_type, sub_relation_type, member_id, gen_diff))
                 
                 # Update AI Record Status if applicable
                 source_record_id = data.get('source_record_id')
@@ -1862,40 +1929,68 @@ def edit_member(member_id):
                 except ValueError:
                     birthday_ts = 0
 
-            # 关系数据
-            related_mid = request.form.get('related_mid')
-            relation_type = request.form.get('relation_type')
-            sub_relation_type = request.form.get('sub_relation_type', 0)
+            # 关系数据 - 支持多条关系
+            relations = []
+            i = 0
+            while True:
+                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')
+                
+                if not parent_mid or not rel_type:
+                    break
+                
+                relations.append({
+                    'parent_mid': int(parent_mid),
+                    'relation_type': int(rel_type),
+                    'sub_relation_type': int(sub_rel_type)
+                })
+                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:
+                    relations.append({
+                        'parent_mid': int(related_mid),
+                        'relation_type': int(relation_type),
+                        'sub_relation_type': int(request.form.get('sub_relation_type', '0'))
+                    })
 
             # 年龄校验逻辑
-            if related_mid and relation_type in ['1', '2']:
-                with conn.cursor() as cursor:
-                    cursor.execute("SELECT name, birthday FROM family_member_info WHERE id = %s", (related_mid,))
-                    parent = cursor.fetchone()
-                    if parent and parent['birthday'] > 0 and birthday_ts > 0:
-                        if birthday_ts < parent['birthday']:
-                            flash(f"数据冲突:成员年龄不能比其父亲/母亲({parent['name']})大,请检查并修正出生日期。")
-                            # 重新加载编辑页所需数据
-                            cursor.execute("SELECT * FROM family_member_info WHERE id = %s", (member_id,))
-                            member = cursor.fetchone()
-                            member['birthday_date'] = birthday_str # 保持用户输入
-                            cursor.execute("SELECT id, name FROM family_member_info WHERE id != %s ORDER BY name", (member_id,))
-                            all_members = cursor.fetchall()
-                            cursor.execute("SELECT * FROM genealogy_records ORDER BY page_number ASC")
-                            images = cursor.fetchall()
-                            
-                            if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json:
-                                return jsonify({
-                                    "success": False, 
-                                    "message": f"数据冲突:成员年龄不能比其父亲/母亲({parent['name']})大,请检查并修正出生日期。"
-                                }), 400
-                            
-                            selected_member_name = ''
-                            return render_template('add_member.html', member=member, images=images, all_members=all_members, selected_member_name=selected_member_name)
+            for rel in relations:
+                if rel['relation_type'] in [1, 2]:
+                    with conn.cursor() as cursor:
+                        cursor.execute("SELECT name, birthday FROM family_member_info WHERE id = %s", (rel['parent_mid'],))
+                        parent = cursor.fetchone()
+                        if parent and parent['birthday'] > 0 and birthday_ts > 0:
+                            if birthday_ts < parent['birthday']:
+                                flash(f"数据冲突:成员年龄不能比其父亲/母亲({parent['name']})大,请检查并修正出生日期。")
+                                # 重新加载编辑页所需数据
+                                cursor.execute("SELECT * FROM family_member_info WHERE id = %s", (member_id,))
+                                member = cursor.fetchone()
+                                member['birthday_date'] = birthday_str # 保持用户输入
+                                cursor.execute("SELECT id, name FROM family_member_info WHERE id != %s ORDER BY name", (member_id,))
+                                all_members = cursor.fetchall()
+                                cursor.execute("SELECT * FROM genealogy_records ORDER BY page_number ASC")
+                                images = cursor.fetchall()
+                                
+                                if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json:
+                                    return jsonify({
+                                        "success": False, 
+                                        "message": f"数据冲突:成员年龄不能比其父亲/母亲({parent['name']})大,请检查并修正出生日期。"
+                                    }), 400
+                                
+                                selected_member_name = ''
+                                return render_template('add_member.html', member=member, images=images, all_members=all_members, selected_member_name=selected_member_name)
+                    break
 
             data = {
                 'name': request.form['name'],
                 'simplified_name': request.form.get('simplified_name'),
+                'genealogy_original_traditional': request.form.get('genealogy_original_traditional'),
+                'genealogy_original_simplified': request.form.get('genealogy_original_simplified'),
                 'former_name': request.form.get('former_name'),
                 'childhood_name': request.form.get('childhood_name'),
                 'name_word': request.form.get('name_word'),
@@ -1928,11 +2023,6 @@ def edit_member(member_id):
                 'create_uid': session['user_id']  # 记录当前操作人
             }
             
-            # 关系数据
-            related_mid = request.form.get('related_mid')
-            relation_type = request.form.get('relation_type')
-            sub_relation_type = request.form.get('sub_relation_type', 0)
-            
             with conn.cursor() as cursor:
                 print(f"[Edit Member] Updating member data: {data}")
                 update_parts = [f"{k} = %s" for k in data.keys()]
@@ -1942,23 +2032,23 @@ def edit_member(member_id):
                 cursor.execute(sql, list(data.values()) + [member_id])
                 print(f"[Edit Member] Updated member with ID: {member_id}")
                 
-                # 更新关系
-                if related_mid and relation_type:
-                    rel_type = int(relation_type)
-                    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,))
-                    
-                    parent_mid = int(related_mid)
-                    child_mid = 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)
+                """
+                
+                for rel in relations:
+                    rel_type = rel['relation_type']
+                    parent_mid = rel['parent_mid']
+                    sub_relation_type = rel['sub_relation_type']
                     gen_diff = 1 if rel_type in [1, 2] else 0
-                    
-                    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)
-                    """
-                    print(f"[Edit Member] Inserting relation: parent_mid={parent_mid}, child_mid={child_mid}, relation_type={rel_type}")
-                    cursor.execute(sql_relation, (parent_mid, child_mid, 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}")
+                    cursor.execute(sql_relation, (parent_mid, member_id, rel_type, sub_relation_type, member_id, gen_diff))
                 
                 # Update AI Record Status if applicable
                 source_record_id = data.get('source_record_id')
@@ -2060,9 +2150,9 @@ def member_detail(member_id):
             
             member['birthday_str'] = format_timestamp(member.get('birthday'))
             
-            # 获取关系
+            # 获取关系(包含子类型)
             cursor.execute("""
-                SELECT m.id, m.name, r.relation_type 
+                SELECT m.id, m.name, r.relation_type, r.sub_relation_type 
                 FROM family_relation_info r 
                 JOIN family_member_info m ON r.parent_mid = m.id 
                 WHERE r.child_mid = %s
@@ -2070,7 +2160,7 @@ def member_detail(member_id):
             parents = cursor.fetchall()
             
             cursor.execute("""
-                SELECT m.id, m.name, r.relation_type 
+                SELECT m.id, m.name, r.relation_type, r.sub_relation_type 
                 FROM family_relation_info r 
                 JOIN family_member_info m ON r.child_mid = m.id 
                 WHERE r.parent_mid = %s
@@ -2805,5 +2895,160 @@ def process_pdf_pages(file_path, pdf_oss_url, uploader):
     except Exception as e:
         print(f"Error processing PDF: {e}")
 
+# --- Settlement Routes ---
+@app.route('/manager/settlements')
+def settlements():
+    if 'user_id' not in session:
+        return redirect(url_for('login'))
+    return render_template('settlements.html')
+
+@app.route('/manager/api/settlements', methods=['GET'])
+def get_settlements():
+    if 'user_id' not in session:
+        return jsonify({"success": False, "message": "Unauthorized"}), 401
+    
+    conn = get_db_connection()
+    try:
+        with conn.cursor() as cursor:
+            cursor.execute("""
+                SELECT s.*, m.name as representative_name, m.simplified_name as representative_simplified_name
+                FROM family_settlements s
+                LEFT JOIN family_member_info m ON s.representative_id = m.id
+                ORDER BY s.created_at DESC
+            """)
+            settlements = cursor.fetchall()
+            
+            # Convert Decimal to float/int for JSON serialization
+            result = []
+            for s in settlements:
+                item = dict(s)
+                if item.get('latitude'):
+                    item['latitude'] = float(item['latitude'])
+                if item.get('longitude'):
+                    item['longitude'] = float(item['longitude'])
+                if item.get('population'):
+                    item['population'] = int(item['population'])
+                result.append(item)
+            
+            return jsonify({"success": True, "settlements": result})
+    finally:
+        conn.close()
+
+@app.route('/manager/api/settlements/<int:id>', methods=['GET'])
+def get_settlement(id):
+    if 'user_id' not in session:
+        return jsonify({"success": False, "message": "Unauthorized"}), 401
+    
+    conn = get_db_connection()
+    try:
+        with conn.cursor() as cursor:
+            cursor.execute("""
+                SELECT s.*, m.name as representative_name, m.simplified_name as representative_simplified_name
+                FROM family_settlements s
+                LEFT JOIN family_member_info m ON s.representative_id = m.id
+                WHERE s.id = %s
+            """, (id,))
+            settlement = cursor.fetchone()
+            if settlement:
+                # Convert Decimal to float/int for JSON serialization
+                item = dict(settlement)
+                if item.get('latitude'):
+                    item['latitude'] = float(item['latitude'])
+                if item.get('longitude'):
+                    item['longitude'] = float(item['longitude'])
+                if item.get('population'):
+                    item['population'] = int(item['population'])
+                return jsonify({"success": True, "settlement": item})
+            else:
+                return jsonify({"success": False, "message": "聚落不存在"})
+    finally:
+        conn.close()
+
+@app.route('/manager/api/settlements', methods=['POST'])
+def add_settlement():
+    if 'user_id' not in session:
+        return jsonify({"success": False, "message": "Unauthorized"}), 401
+    
+    if not session.get('is_super_admin'):
+        return jsonify({"success": False, "message": "权限不足"}), 403
+    
+    data = request.get_json()
+    
+    conn = get_db_connection()
+    try:
+        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)
+            """, (
+                data.get('name'),
+                data.get('region'),
+                data.get('latitude') or None,
+                data.get('longitude') or None,
+                data.get('population') or 0,
+                data.get('representative_id') or None,
+                data.get('description'),
+                data.get('surname_type') or 0,
+                data.get('new_surname') or None
+            ))
+            conn.commit()
+            return jsonify({"success": True, "message": "添加成功"})
+    finally:
+        conn.close()
+
+@app.route('/manager/api/settlements/<int:id>', methods=['PUT'])
+def update_settlement(id):
+    if 'user_id' not in session:
+        return jsonify({"success": False, "message": "Unauthorized"}), 401
+    
+    if not session.get('is_super_admin'):
+        return jsonify({"success": False, "message": "权限不足"}), 403
+    
+    data = request.get_json()
+    
+    conn = get_db_connection()
+    try:
+        with conn.cursor() as cursor:
+            cursor.execute("""
+                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
+                WHERE id=%s
+            """, (
+                data.get('name'),
+                data.get('region'),
+                data.get('latitude') or None,
+                data.get('longitude') or None,
+                data.get('population') or 0,
+                data.get('representative_id') or None,
+                data.get('description'),
+                data.get('surname_type') or 0,
+                data.get('new_surname') or None,
+                id
+            ))
+            conn.commit()
+            return jsonify({"success": True, "message": "更新成功"})
+    finally:
+        conn.close()
+
+@app.route('/manager/api/settlements/<int:id>', methods=['DELETE'])
+def delete_settlement(id):
+    if 'user_id' not in session:
+        return jsonify({"success": False, "message": "Unauthorized"}), 401
+    
+    if not session.get('is_super_admin'):
+        return jsonify({"success": False, "message": "权限不足"}), 403
+    
+    conn = get_db_connection()
+    try:
+        with conn.cursor() as cursor:
+            cursor.execute("DELETE FROM family_settlements WHERE id=%s", (id,))
+            conn.commit()
+            return jsonify({"success": True, "message": "删除成功"})
+    finally:
+        conn.close()
+
 if __name__ == '__main__':
     app.run(debug=False, port=5001)

+ 5 - 0
cookies.txt

@@ -0,0 +1,5 @@
+# Netscape HTTP Cookie File
+# https://curl.se/docs/http-cookies.html
+# This file was generated by libcurl! Edit at your own risk.
+
+#HttpOnly_localhost	FALSE	/	FALSE	0	session	eyJpc19zdXBlcl9hZG1pbiI6dHJ1ZSwidXNlcl9pZCI6MiwidXNlcm5hbWUiOiJsaW5oYWkifQ.af7Zjw.DfDJBWSr_-U7oyuZmhMquyzje2E

+ 273 - 31
templates/add_member.html

@@ -120,6 +120,14 @@
                             <label class="form-label">姓名(简体)</label>
                             <input type="text" name="simplified_name" class="form-control" value="{{ member.simplified_name if member else '' }}">
                         </div>
+                        <div class="col-md-12">
+                            <label class="form-label">族谱原文(繁体)</label>
+                            <input type="text" name="genealogy_original_traditional" class="form-control" value="{{ member.genealogy_original_traditional if member and member.genealogy_original_traditional and member.genealogy_original_traditional != 'None' else '' }}" placeholder="录入族谱原文中的繁体姓名及相关信息">
+                        </div>
+                        <div class="col-md-12">
+                            <label class="form-label">族谱原文(简体)</label>
+                            <input type="text" name="genealogy_original_simplified" class="form-control" value="{{ member.genealogy_original_simplified if member and member.genealogy_original_simplified and member.genealogy_original_simplified != 'None' else '' }}" placeholder="录入族谱原文中的简体姓名及相关信息">
+                        </div>
                         <div class="col-md-6">
                             <label class="form-label">性别 <span class="text-danger">*</span></label>
                             <select name="sex" class="form-select" required>
@@ -194,37 +202,95 @@
                     </div>
 
                     <div class="section-title">关系录入 (选择关联成员及关系)</div>
-                    <div class="row g-3 mb-4">
-                        <div class="col-md-5">
-                            <label class="form-label">关联成员</label>
-                            <div class="input-group">
-                                <input type="text" id="related-member-display" class="form-control" placeholder="点击选择关联成员" readonly value="{{ selected_member_name }}">
-                                <input type="hidden" name="related_mid" id="related_mid" value="{{ current_relation.parent_mid if current_relation else '' }}">
-                                <button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#memberSelectModal">
-                                    <i class="bi bi-search"></i>
+                    <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="col-md-4">
+                                <label class="form-label">关联成员</label>
+                                <div class="input-group">
+                                    <input type="text" class="form-control related-member-display" placeholder="点击选择关联成员" readonly value="{{ selected_member_name }}">
+                                    <input type="hidden" name="relations[0][parent_mid]" class="related_mid" value="{{ current_relation.parent_mid }}">
+                                    <button type="button" class="btn btn-outline-primary select-member-btn" data-index="0" data-bs-toggle="modal" data-bs-target="#memberSelectModal">
+                                        <i class="bi bi-search"></i>
+                                    </button>
+                                </div>
+                            </div>
+                            <div class="col-md-3">
+                                <label class="form-label">关系类型</label>
+                                <select name="relations[0][relation_type]" class="form-select relation-type">
+                                    <option value="">-- 请选择 --</option>
+                                    <option value="1" {{ 'selected' if current_relation.relation_type == 1 else '' }}>父子 (关联人为父)</option>
+                                    <option value="2" {{ 'selected' if current_relation.relation_type == 2 else '' }}>母子 (关联人为母)</option>
+                                    <option value="10" {{ 'selected' if current_relation.relation_type == 10 else '' }}>夫妻</option>
+                                    <option value="11" {{ 'selected' if current_relation.relation_type == 11 else '' }}>兄弟</option>
+                                    <option value="12" {{ 'selected' if current_relation.relation_type == 12 else '' }}>姐妹</option>
+                                </select>
+                            </div>
+                            <div class="col-md-3">
+                                <label class="form-label">子类型</label>
+                                <select name="relations[0][sub_relation_type]" class="form-select sub-relation-type">
+                                    <option value="0" {{ 'selected' if current_relation.sub_relation_type == 0 else '' }}>亲生/正妻</option>
+                                    <option value="1" {{ 'selected' if current_relation.sub_relation_type == 1 else '' }}>养父</option>
+                                    <option value="2" {{ 'selected' if current_relation.sub_relation_type == 2 else '' }}>出继(亲生父母)</option>
+                                    <option value="3" {{ 'selected' if current_relation.sub_relation_type == 3 else '' }}>入继(养父母)</option>
+                                    <option value="10" {{ 'selected' if current_relation.sub_relation_type == 10 else '' }}>妾</option>
+                                    <option value="11" {{ 'selected' if current_relation.sub_relation_type == 11 else '' }}>外室</option>
+                                </select>
+                            </div>
+                            <div class="col-md-2 d-flex align-items-end">
+                                <button type="button" class="btn btn-danger w-100 remove-relation-btn" style="display: none;">
+                                    <i class="bi bi-trash"></i>
                                 </button>
                             </div>
                         </div>
-                        <div class="col-md-4">
-                            <label class="form-label">关系类型</label>
-                            <select name="relation_type" class="form-select">
-                                <option value="">-- 请选择 --</option>
-                                <option value="1" {{ 'selected' if current_relation and current_relation.relation_type == 1 else '' }}>父子 (关联人为父)</option>
-                                <option value="2" {{ 'selected' if current_relation and current_relation.relation_type == 2 else '' }}>母子 (关联人为母)</option>
-                                <option value="10" {{ 'selected' if current_relation and current_relation.relation_type == 10 else '' }}>夫妻</option>
-                                <option value="11" {{ 'selected' if current_relation and current_relation.relation_type == 11 else '' }}>兄弟</option>
-                                <option value="12" {{ 'selected' if current_relation and current_relation.relation_type == 12 else '' }}>姐妹</option>
-                            </select>
+                        {% else %}
+                        <div class="row g-3 mb-3 relation-row" data-index="0">
+                            <div class="col-md-4">
+                                <label class="form-label">关联成员</label>
+                                <div class="input-group">
+                                    <input type="text" class="form-control related-member-display" placeholder="点击选择关联成员" readonly>
+                                    <input type="hidden" name="relations[0][parent_mid]" class="related_mid">
+                                    <button type="button" class="btn btn-outline-primary select-member-btn" data-index="0" data-bs-toggle="modal" data-bs-target="#memberSelectModal">
+                                        <i class="bi bi-search"></i>
+                                    </button>
+                                </div>
+                            </div>
+                            <div class="col-md-3">
+                                <label class="form-label">关系类型</label>
+                                <select name="relations[0][relation_type]" class="form-select relation-type">
+                                    <option value="">-- 请选择 --</option>
+                                    <option value="1">父子 (关联人为父)</option>
+                                    <option value="2">母子 (关联人为母)</option>
+                                    <option value="10">夫妻</option>
+                                    <option value="11">兄弟</option>
+                                    <option value="12">姐妹</option>
+                                </select>
+                            </div>
+                            <div class="col-md-3">
+                                <label class="form-label">子类型</label>
+                                <select name="relations[0][sub_relation_type]" class="form-select sub-relation-type">
+                                    <option value="0">亲生/正妻</option>
+                                    <option value="1">养父</option>
+                                    <option value="2">出继(亲生父母)</option>
+                                    <option value="3">入继(养父母)</option>
+                                    <option value="10">妾</option>
+                                    <option value="11">外室</option>
+                                </select>
+                            </div>
+                            <div class="col-md-2 d-flex align-items-end">
+                                <button type="button" class="btn btn-danger w-100 remove-relation-btn" style="display: none;">
+                                    <i class="bi bi-trash"></i>
+                                </button>
+                            </div>
                         </div>
-                        <div class="col-md-3">
-                            <label class="form-label">子类型</label>
-                            <select name="sub_relation_type" class="form-select">
-                                <option value="0" {{ 'selected' if current_relation and current_relation.sub_relation_type == 0 else '' }}>亲生/正妻</option>
-                                <option value="1" {{ 'selected' if current_relation and current_relation.sub_relation_type == 1 else '' }}>养父</option>
-                                <option value="2" {{ 'selected' if current_relation and current_relation.sub_relation_type == 2 else '' }}>过继</option>
-                                <option value="10" {{ 'selected' if current_relation and current_relation.sub_relation_type == 10 else '' }}>妾</option>
-                                <option value="11" {{ 'selected' if current_relation and current_relation.sub_relation_type == 11 else '' }}>外室</option>
-                            </select>
+                        {% endif %}
+                    </div>
+                    <div class="row mb-4">
+                        <div class="col-md-12">
+                            <button type="button" class="btn btn-outline-success" id="add-relation-btn">
+                                <i class="bi bi-plus-circle"></i> 添加关系
+                            </button>
                         </div>
                     </div>
                     
@@ -832,6 +898,151 @@
         }
     });
 
+    // --- Multi-relation handling ---
+    let currentRelationIndex = 0;
+
+    document.addEventListener('DOMContentLoaded', function() {
+        // Set initial index based on existing rows
+        const relationRows = document.querySelectorAll('.relation-row');
+        currentRelationIndex = relationRows.length;
+        
+        // Show remove button if there are multiple rows
+        updateRemoveButtons();
+        
+        // Add click handler for add relation button
+        const addBtn = document.getElementById('add-relation-btn');
+        if (addBtn) {
+            addBtn.addEventListener('click', addRelationRow);
+        }
+        
+        // Add click handlers for select member buttons
+        document.querySelectorAll('.select-member-btn').forEach(btn => {
+            btn.addEventListener('click', function() {
+                const index = parseInt(this.dataset.index);
+                window.currentRelationIndex = index;
+            });
+        });
+        
+        // Add click handlers for remove buttons
+        document.querySelectorAll('.remove-relation-btn').forEach(btn => {
+            btn.addEventListener('click', removeRelationRow);
+        });
+    });
+
+    function addRelationRow() {
+        const container = document.getElementById('relations-container');
+        const newIndex = currentRelationIndex++;
+        
+        const newRow = document.createElement('div');
+        newRow.className = 'row g-3 mb-3 relation-row';
+        newRow.dataset.index = newIndex;
+        
+        newRow.innerHTML = `
+            <div class="col-md-4">
+                <label class="form-label">关联成员</label>
+                <div class="input-group">
+                    <input type="text" class="form-control related-member-display" placeholder="点击选择关联成员" readonly>
+                    <input type="hidden" name="relations[${newIndex}][parent_mid]" class="related_mid">
+                    <button type="button" class="btn btn-outline-primary select-member-btn" data-index="${newIndex}" data-bs-toggle="modal" data-bs-target="#memberSelectModal">
+                        <i class="bi bi-search"></i>
+                    </button>
+                </div>
+            </div>
+            <div class="col-md-3">
+                <label class="form-label">关系类型</label>
+                <select name="relations[${newIndex}][relation_type]" class="form-select relation-type">
+                    <option value="">-- 请选择 --</option>
+                    <option value="1">父子 (关联人为父)</option>
+                    <option value="2">母子 (关联人为母)</option>
+                    <option value="10">夫妻</option>
+                    <option value="11">兄弟</option>
+                    <option value="12">姐妹</option>
+                </select>
+            </div>
+            <div class="col-md-3">
+                <label class="form-label">子类型</label>
+                <select name="relations[${newIndex}][sub_relation_type]" class="form-select sub-relation-type">
+                    <option value="0">亲生/正妻</option>
+                    <option value="1">养父</option>
+                    <option value="2">出继(亲生父母)</option>
+                    <option value="3">入继(养父母)</option>
+                    <option value="10">妾</option>
+                    <option value="11">外室</option>
+                </select>
+            </div>
+            <div class="col-md-2 d-flex align-items-end">
+                <button type="button" class="btn btn-danger w-100 remove-relation-btn">
+                    <i class="bi bi-trash"></i>
+                </button>
+            </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);
+        
+        updateRemoveButtons();
+    }
+
+    function removeRelationRow() {
+        const row = this.closest('.relation-row');
+        if (row) {
+            row.remove();
+            updateRemoveButtons();
+        }
+    }
+
+    function updateRemoveButtons() {
+        const rows = document.querySelectorAll('.relation-row');
+        rows.forEach((row, index) => {
+            const removeBtn = row.querySelector('.remove-relation-btn');
+            if (removeBtn) {
+                // Show remove button only if there's more than one row
+                removeBtn.style.display = rows.length > 1 ? 'block' : 'none';
+            }
+            // Update index attribute
+            row.dataset.index = index;
+        });
+    }
+
+    // Override selectMember to support multiple relations
+    window.selectMember = function(member) {
+        const index = window.currentRelationIndex || 0;
+        const rows = document.querySelectorAll('.relation-row');
+        if (rows[index]) {
+            const displayInput = rows[index].querySelector('.related-member-display');
+            const hiddenInput = rows[index].querySelector('.related_mid');
+            
+            if (displayInput) {
+                let displayText = member.name;
+                if (member.simplified_name && member.simplified_name !== member.name) {
+                    displayText += ` (${member.simplified_name})`;
+                }
+                if (member.name_word) {
+                    displayText += ` · ${member.name_word}`;
+                }
+                displayInput.value = displayText;
+            }
+            if (hiddenInput) {
+                hiddenInput.value = member.id;
+            }
+        }
+        
+        // Close modal
+        const modal = bootstrap.Modal.getInstance(document.getElementById('memberSelectModal'));
+        if (modal) {
+            modal.hide();
+        }
+    };
+
     // --- AJAX Form Submission ---
     document.addEventListener('DOMContentLoaded', () => {
         const form = document.querySelector('form');
@@ -2400,13 +2611,44 @@
         loadMembers(1, search);
     }
 
-    // 选择成员
+    // 选择成员(支持多条关系)
     function selectMember(member) {
-        document.getElementById('related-member-display').value = member.name;
-        document.getElementById('related_mid').value = member.id;
+        const index = window.currentRelationIndex || 0;
+        const rows = document.querySelectorAll('.relation-row');
+        
+        if (rows[index]) {
+            const displayInput = rows[index].querySelector('.related-member-display');
+            const hiddenInput = rows[index].querySelector('.related_mid');
+            
+            if (displayInput) {
+                let displayText = member.name;
+                if (member.simplified_name && member.simplified_name !== member.name) {
+                    displayText += ` (${member.simplified_name})`;
+                }
+                if (member.name_word) {
+                    displayText += ` · ${member.name_word}`;
+                }
+                displayInput.value = displayText;
+            }
+            if (hiddenInput) {
+                hiddenInput.value = member.id;
+            }
+        } else {
+            // Fallback for old single relation mode
+            const displayInput = document.getElementById('related-member-display');
+            const hiddenInput = document.getElementById('related_mid');
+            if (displayInput) {
+                displayInput.value = member.name;
+            }
+            if (hiddenInput) {
+                hiddenInput.value = member.id;
+            }
+        }
         
         // 检查是否是父子关系,如果是,显示父亲的世系世代
-        const relationType = document.querySelector('select[name="relation_type"]').value;
+        const relationTypeSelect = rows[index] ? rows[index].querySelector('select.relation-type') : document.querySelector('select[name="relation_type"]');
+        const relationType = relationTypeSelect ? relationTypeSelect.value : '';
+        
         if (relationType == 1) { // 父子关系
             fetch(`/manager/api/member/${member.id}`)
                 .then(response => response.json())

+ 4 - 1
templates/layout.html

@@ -78,11 +78,14 @@
                         <i class="bi bi-people me-2"></i> 成员列表
                     </a>
                     <a href="{{ url_for('tree') }}" class="{% if request.endpoint == 'tree' %}active{% endif %}">
-                        <i class="bi bi-diagram-3 me-2"></i> 系树状图
+                        <i class="bi bi-diagram-3 me-2"></i> 家谱世系树状图
                     </a>
                     <a href="{{ url_for('lineage_query') }}" class="{% if request.endpoint == 'lineage_query' %}active{% endif %}">
                         <i class="bi bi-tree me-2"></i> 世系查询
                     </a>
+                    <a href="{{ url_for('settlements') }}" class="{% if request.endpoint == 'settlements' %}active{% endif %}">
+                        <i class="bi bi-globe me-2"></i> 聚落地图
+                    </a>
                     <div class="mt-5 border-top pt-3">
                         <p class="px-3 small text-muted">用户: {{ session['username'] }}</p>
                         <a href="{{ url_for('logout') }}" class="text-danger">

+ 32 - 0
templates/lineage_query.html

@@ -289,6 +289,25 @@
         width: 100%;
         overflow-x: auto;
     }
+    
+    /* Adoption styles */
+    .tree-node.adopted-out {
+        border: 2px dashed #ff6b6b !important;
+        background: rgba(255, 107, 107, 0.1) !important;
+    }
+    
+    .tree-node.adopted-in {
+        border: 2px solid #4ecdc4 !important;
+        background: rgba(78, 205, 196, 0.1) !important;
+    }
+    
+    .adoption-label {
+        font-size: 10px;
+        color: #ff6b6b;
+        font-weight: bold;
+        margin-top: 4px;
+        text-align: center;
+    }
 </style>
 {% endblock %}
 
@@ -752,6 +771,18 @@ function renderTreeNode(person, isCenter = false, isDirectLine = false) {
     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'; // 入继
+    }
+    
+    // 出继方显示"出继给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>
@@ -760,6 +791,7 @@ function renderTreeNode(person, isCenter = false, isDirectLine = false) {
                 ${person.name_word ? `${person.name_word} · ` : ''}
                 ${person.name_word_generation || ''}
             </div>
+            ${adoptionLabel}
         </div>
     `;
 }

+ 32 - 8
templates/member_detail.html

@@ -135,6 +135,14 @@
                         <span class="info-label">姓名(简体):</span>
                         <span class="info-value">{{ member.simplified_name or '-' }}</span>
                     </div>
+                    <div class="col-md-12">
+                        <span class="info-label d-block mb-1">族谱原文(繁体):</span>
+                        <span class="info-value">{{ member.genealogy_original_traditional if member.genealogy_original_traditional and member.genealogy_original_traditional != 'None' else '-' }}</span>
+                    </div>
+                    <div class="col-md-12">
+                        <span class="info-label d-block mb-1">族谱原文(简体):</span>
+                        <span class="info-value">{{ member.genealogy_original_simplified if member.genealogy_original_simplified and member.genealogy_original_simplified != 'None' else '-' }}</span>
+                    </div>
                     <div class="col-md-6">
                         <span class="info-label">性别:</span>
                         <span class="info-value">{{ '男' if member.sex == 1 else '女' }}</span>
@@ -295,10 +303,18 @@
                         <a href="{{ url_for('member_detail', member_id=p.id) }}" class="text-decoration-none fw-bold">
                             {{ p.name }}
                         </a>
-                        <span class="badge bg-success small">
-                            {% set rel_map = {1: '父亲', 2: '母亲', 10: '配偶', 11: '兄弟', 12: '姐妹'} %}
-                            {{ rel_map.get(p.relation_type, '关联人') }}
-                        </span>
+                        <div class="d-flex gap-2">
+                            <span class="badge bg-success small">
+                                {% set rel_map = {1: '父亲', 2: '母亲', 10: '配偶', 11: '兄弟', 12: '姐妹'} %}
+                                {{ rel_map.get(p.relation_type, '关联人') }}
+                            </span>
+                            {% if p.sub_relation_type %}
+                            <span class="badge bg-warning small">
+                                {% set sub_rel_map = {0: '亲生', 1: '养父', 2: '出继', 3: '入继', 10: '妾', 11: '外室'} %}
+                                {{ sub_rel_map.get(p.sub_relation_type, '') }}
+                            </span>
+                            {% endif %}
+                        </div>
                     </div>
                 </div>
                 {% endfor %}
@@ -313,10 +329,18 @@
                         <a href="{{ url_for('member_detail', member_id=c.id) }}" class="text-decoration-none fw-bold">
                             {{ c.name }}
                         </a>
-                        <span class="badge bg-primary small">
-                            {% set rel_map = {1: '子女', 2: '子女', 10: '配偶', 11: '兄弟', 12: '姐妹'} %}
-                            {{ rel_map.get(c.relation_type, '后辈') }}
-                        </span>
+                        <div class="d-flex gap-2">
+                            <span class="badge bg-primary small">
+                                {% set rel_map = {1: '子女', 2: '子女', 10: '配偶', 11: '兄弟', 12: '姐妹'} %}
+                                {{ rel_map.get(c.relation_type, '后辈') }}
+                            </span>
+                            {% if c.sub_relation_type %}
+                            <span class="badge bg-warning small">
+                                {% set sub_rel_map = {0: '亲生', 1: '养父', 2: '出继', 3: '入继', 10: '妾', 11: '外室'} %}
+                                {{ sub_rel_map.get(c.sub_relation_type, '') }}
+                            </span>
+                            {% endif %}
+                        </div>
                     </div>
                 </div>
                 {% endfor %}

+ 856 - 0
templates/settlements.html

@@ -0,0 +1,856 @@
+{% extends 'layout.html' %}
+
+{% block content %}
+<script>
+    window.isSuperAdmin = {{ session.get('is_super_admin') | tojson }};
+</script>
+
+<script type="text/javascript">
+    window.AMapPromise = new Promise(function(resolve, reject) {
+        if (typeof AMap !== 'undefined') {
+            resolve(AMap);
+            return;
+        }
+        const script = document.createElement('script');
+        script.src = 'https://webapi.amap.com/maps?v=2.0&key=7bbc2afa6f49b58024780bf647c6f038';
+        script.onload = function() { resolve(AMap); };
+        script.onerror = function() { reject('高德地图加载失败'); };
+        document.head.appendChild(script);
+    });
+</script>
+
+<div class="container-fluid">
+    <div class="row">
+        <div class="col-md-12">
+            <div class="d-flex justify-content-between align-items-center mb-4">
+                <h2 class="page-header">聚落地图</h2>
+                <div class="d-flex gap-2">
+                    <div class="btn-group" role="group">
+                        <button type="button" class="btn btn-primary" id="view-map-btn">地图查看</button>
+                        <button type="button" class="btn btn-outline-primary" id="view-list-btn">列表查看</button>
+                    </div>
+                    {% if session.get('is_super_admin') %}
+                    <button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addSettlementModal">
+                        <i class="bi bi-plus-circle"></i> 添加聚落
+                    </button>
+                    {% endif %}
+                </div>
+            </div>
+
+            <div id="map-view" class="mb-4" style="display: block;">
+                <div id="map-container" style="min-height: 500px; max-height: 80vh; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden;">
+                    <div class="map-loading" style="position: absolute; inset: 0; background: linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 100%); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 100;">
+                        <div class="spinner-border text-primary" role="status">
+                            <span class="visually-hidden">加载中...</span>
+                        </div>
+                        <p class="mt-3 text-gray-500">地图加载中...</p>
+                    </div>
+                </div>
+            </div>
+
+            <div id="list-view" class="mb-4" style="display: none;">
+                <div class="card">
+                    <div class="card-body">
+                        <div class="table-responsive">
+                            <table class="table table-hover">
+                                <thead>
+                                    <tr>
+                                        <th>编号</th>
+                                        <th>聚落名称</th>
+                                        <th>所属区域</th>
+                                        <th>人数</th>
+                                        <th>代表人物</th>
+                                        <th>姓氏类型</th>
+                                        <th>创建时间</th>
+                                        {% if session.get('is_super_admin') %}
+                                        <th>操作</th>
+                                        {% endif %}
+                                    </tr>
+                                </thead>
+                                <tbody id="settlements-table-body">
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="modal fade" id="addSettlementModal" tabindex="-1" aria-labelledby="addSettlementModalLabel" aria-hidden="true">
+    <div class="modal-dialog modal-lg">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="addSettlementModalLabel">添加聚落</h5>
+                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+            </div>
+            <div class="modal-body">
+                <form id="settlement-form">
+                    <input type="hidden" id="settlement-id">
+                    <div class="row g-3">
+                        <div class="col-md-6">
+                            <label class="form-label">聚落名称 *</label>
+                            <input type="text" class="form-control" id="settlement-name" required>
+                        </div>
+                        <div class="col-md-6">
+                            <label class="form-label">所属区域 *</label>
+                            <div class="input-group">
+                                <input type="text" class="form-control" id="settlement-region" placeholder="搜索城市或区域名称" required>
+                                <button type="button" class="btn btn-outline-primary" id="search-region-btn">
+                                    <i class="bi bi-search"></i>
+                                </button>
+                            </div>
+                            <div id="region-suggestions" class="mt-2" style="display: none;"></div>
+                        </div>
+                        <div class="col-md-6 d-none">
+                            <label class="form-label">纬度</label>
+                            <input type="number" class="form-control" id="settlement-latitude" step="0.0000001">
+                        </div>
+                        <div class="col-md-6 d-none">
+                            <label class="form-label">经度</label>
+                            <input type="number" class="form-control" id="settlement-longitude" step="0.0000001">
+                        </div>
+                        <div class="col-md-12">
+                            <div class="alert alert-info alert-dismissible fade show" id="location-info" style="display: none;">
+                                <i class="bi bi-info-circle me-2"></i>
+                                <span id="location-info-text"></span>
+                                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
+                            </div>
+                        </div>
+                        <div class="col-md-6">
+                            <label class="form-label">聚落人数</label>
+                            <input type="number" class="form-control" id="settlement-population" min="0">
+                        </div>
+                        <div class="col-md-6">
+                            <label class="form-label">代表人物</label>
+                            <div class="input-group">
+                                <input type="text" id="settlement-representative-display" class="form-control" placeholder="点击选择代表人物" readonly>
+                                <input type="hidden" id="settlement-representative-id">
+                                <button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#memberSelectModal">
+                                    <i class="bi bi-search"></i>
+                                </button>
+                            </div>
+                        </div>
+                        <div class="col-md-6">
+                            <label class="form-label">姓氏类型</label>
+                            <select class="form-select" id="settlement-surname-type">
+                                <option value="0">留姓</option>
+                                <option value="1">改姓</option>
+                            </select>
+                        </div>
+                        <div class="col-md-6" id="new-surname-container" style="display: none;">
+                            <label class="form-label">改后姓氏</label>
+                            <input type="text" class="form-control" id="settlement-new-surname" placeholder="输入改后的姓氏">
+                        </div>
+                        <div class="col-md-12">
+                            <label class="form-label">备注说明</label>
+                            <textarea class="form-control" id="settlement-description" rows="3"></textarea>
+                        </div>
+                    </div>
+                </form>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-primary" id="save-settlement-btn">保存</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="modal fade" id="memberSelectModal" tabindex="-1" aria-labelledby="memberSelectModalLabel" aria-hidden="true">
+    <div class="modal-dialog modal-lg">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="memberSelectModalLabel">选择代表人物</h5>
+                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+            </div>
+            <div class="modal-body">
+                <div class="mb-3">
+                    <input type="text" id="member-search-input" class="form-control" placeholder="搜索成员名称...">
+                </div>
+                <div id="member-list-container" style="max-height: 400px; overflow-y: auto;">
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="modal fade" id="settlementDetailModal" tabindex="-1" aria-labelledby="settlementDetailModalLabel" aria-hidden="true">
+    <div class="modal-dialog">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="settlementDetailModalLabel">聚落详情</h5>
+                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+            </div>
+            <div class="modal-body" id="settlement-detail-content">
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
+                {% if session.get('is_super_admin') %}
+                <button type="button" class="btn btn-primary" id="edit-settlement-btn">编辑</button>
+                {% endif %}
+            </div>
+        </div>
+    </div>
+</div>
+
+<script>
+let map = null;
+let markers = [];
+let settlementsData = [];
+
+function resizeMapContainer() {
+    const mapContainer = document.getElementById('map-container');
+    const headerHeight = document.querySelector('.page-header')?.offsetHeight || 80;
+    const toolbarHeight = 60;
+    const padding = 40;
+    const windowHeight = window.innerHeight;
+    const sidebarWidth = 250;
+    const availableHeight = windowHeight - headerHeight - toolbarHeight - padding;
+    
+    if (window.innerWidth > 768) {
+        mapContainer.style.height = Math.max(500, Math.min(availableHeight, windowHeight * 0.85)) + 'px';
+    } else {
+        mapContainer.style.height = Math.max(400, availableHeight * 0.8) + 'px';
+    }
+    
+    if (window.map) {
+        window.map.resize();
+    }
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+    loadSettlements();
+    initRegionSearch();
+    
+    resizeMapContainer();
+    window.addEventListener('resize', resizeMapContainer);
+    
+    document.getElementById('addSettlementModal').addEventListener('show.bs.modal', function() {
+        if (!window.isEditingSettlement) {
+            resetSettlementForm();
+        }
+        window.isEditingSettlement = false;
+    });
+    
+    document.getElementById('view-map-btn').addEventListener('click', function() {
+        document.getElementById('map-view').style.display = 'block';
+        document.getElementById('list-view').style.display = 'none';
+        this.classList.add('btn-primary');
+        this.classList.remove('btn-outline-primary');
+        document.getElementById('view-list-btn').classList.add('btn-outline-primary');
+        document.getElementById('view-list-btn').classList.remove('btn-primary');
+    });
+    
+    document.getElementById('view-list-btn').addEventListener('click', function() {
+        document.getElementById('map-view').style.display = 'none';
+        document.getElementById('list-view').style.display = 'block';
+        this.classList.add('btn-primary');
+        this.classList.remove('btn-outline-primary');
+        document.getElementById('view-map-btn').classList.add('btn-outline-primary');
+        document.getElementById('view-map-btn').classList.remove('btn-primary');
+    });
+    
+    document.getElementById('member-search-input').addEventListener('input', function() {
+        searchMembers(this.value);
+    });
+    
+    document.getElementById('save-settlement-btn').addEventListener('click', saveSettlement);
+    
+    document.getElementById('edit-settlement-btn').addEventListener('click', function() {
+        const id = document.getElementById('settlement-id').value;
+        if (id) {
+            loadSettlementForEdit(id);
+        }
+        const modal = bootstrap.Modal.getInstance(document.getElementById('settlementDetailModal'));
+        modal.hide();
+    });
+    
+    document.getElementById('settlement-surname-type').addEventListener('change', function() {
+        toggleNewSurnameField(this.value);
+    });
+});
+
+function loadSettlements() {
+    fetch('/manager/api/settlements', { credentials: 'include' })
+        .then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                settlementsData = data.settlements;
+                renderSettlementList(data.settlements);
+                initMapWithData(data.settlements);
+            }
+        })
+        .catch(error => {
+            console.error('加载聚落失败:', error);
+        });
+}
+
+function initMapWithData(settlements) {
+    window.AMapPromise.then(function(AMap) {
+        document.querySelector('.map-loading').style.display = 'none';
+        
+        if (!map) {
+            let initialCenter = [116.397428, 39.90923];
+            let initialZoom = 4;
+            
+            if (settlements.length > 0) {
+                const firstLng = parseFloat(settlements[0].longitude) || 116.397428;
+                const firstLat = parseFloat(settlements[0].latitude) || 39.90923;
+                initialCenter = [firstLng, firstLat];
+                initialZoom = settlements.length === 1 ? 10 : 6;
+            }
+            
+            map = new AMap.Map('map-container', {
+                center: initialCenter,
+                zoom: initialZoom,
+                resizeEnable: true,
+                mapStyle: 'amap://styles/normal',
+                features: ['bg', 'road', 'building', 'point', 'label'],
+                viewMode: '2D'
+            });
+            
+            AMap.plugin(['AMap.Scale', 'AMap.ToolBar', 'AMap.LabelsLayer'], function() {
+                map.addControl(new AMap.Scale({ position: 'LB' }));
+                map.addControl(new AMap.ToolBar({ position: 'RB' }));
+            });
+        }
+        
+        clearMarkers();
+        
+        if (settlements.length > 0) {
+                const bounds = new AMap.Bounds();
+                
+                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,
+                        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);
+                    bounds.extend([lng, lat]);
+                });
+                
+                if (settlements.length === 1) {
+                    const lng = parseFloat(settlements[0].longitude) || 116.397428;
+                    const lat = parseFloat(settlements[0].latitude) || 39.90923;
+                    map.setCenter([lng, lat]);
+                    map.setZoom(10);
+                } else {
+                    setTimeout(function() {
+                        map.setFitView(bounds, false, [50, 50, 50, 50]);
+                    }, 100);
+                }
+            }
+    }).catch(function(err) {
+        console.error('高德地图加载失败:', err);
+        document.querySelector('.map-loading p').textContent = '地图加载失败';
+    });
+}
+
+function clearMarkers() {
+    markers.forEach(marker => { map.remove(marker); });
+    markers = [];
+}
+
+function renderSettlementList(settlements) {
+    const tbody = document.getElementById('settlements-table-body');
+    tbody.innerHTML = '';
+    
+    if (settlements.length === 0) {
+        tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-500">暂无聚落数据</td></tr>';
+        return;
+    }
+    
+    settlements.forEach((settlement, index) => {
+        const surnameType = getSurnameTypeText(settlement.surname_type);
+        const newSurname = settlement.new_surname ? '(' + settlement.new_surname + ')' : '';
+        const row = document.createElement('tr');
+        row.innerHTML = `
+            <td>${index + 1}</td>
+            <td>${settlement.name}</td>
+            <td>${settlement.region || '-'}</td>
+            <td>${settlement.population || 0}</td>
+            <td>${settlement.representative_name || '-'}</td>
+            <td>${surnameType}${newSurname}</td>
+            <td>${formatDateTime(settlement.created_at)}</td>
+            ${window.isSuperAdmin ? `
+            <td>
+                <button class="btn btn-sm btn-primary edit-settlement-btn" data-id="${settlement.id}">编辑</button>
+                <button class="btn btn-sm btn-danger delete-settlement-btn" data-id="${settlement.id}">删除</button>
+            </td>
+            ` : ''}
+        `;
+        
+        row.addEventListener('click', function() { showSettlementDetail(settlement); });
+        tbody.appendChild(row);
+    });
+    
+    document.querySelectorAll('.edit-settlement-btn').forEach(btn => {
+        btn.addEventListener('click', function(e) {
+            e.stopPropagation();
+            loadSettlementForEdit(this.dataset.id);
+        });
+    });
+    
+    document.querySelectorAll('.delete-settlement-btn').forEach(btn => {
+        btn.addEventListener('click', function(e) {
+            e.stopPropagation();
+            if (confirm('确定要删除这个聚落吗?')) {
+                deleteSettlement(this.dataset.id);
+            }
+        });
+    });
+}
+
+function showSettlementDetail(settlement) {
+    document.getElementById('settlement-id').value = settlement.id;
+    const surnameType = getSurnameTypeText(settlement.surname_type);
+    const content = document.getElementById('settlement-detail-content');
+    content.innerHTML = `
+        <div class="mb-4">
+            <h4>${settlement.name}</h4>
+            ${settlement.region ? '<p class="text-muted">区域:' + settlement.region + '</p>' : ''}
+        </div>
+        <div class="row">
+            <div class="col-md-6">
+                <div class="card">
+                    <div class="card-body">
+                        <h6 class="card-title">基本信息</h6>
+                        <p class="card-text"><strong>人数:</strong>${settlement.population || 0} 人</p>
+                        ${settlement.latitude ? '<p class="card-text"><strong>纬度:</strong>' + settlement.latitude + '</p>' : ''}
+                        ${settlement.longitude ? '<p class="card-text"><strong>经度:</strong>' + settlement.longitude + '</p>' : ''}
+                        <p class="card-text"><strong>姓氏类型:</strong>${surnameType}${settlement.new_surname ? '(改后:' + settlement.new_surname + ')' : ''}</p>
+                    </div>
+                </div>
+            </div>
+            <div class="col-md-6">
+                <div class="card">
+                    <div class="card-body">
+                        <h6 class="card-title">代表人物</h6>
+                        <p class="card-text">${settlement.representative_name || '未设置'}</p>
+                    </div>
+                </div>
+            </div>
+        </div>
+        ${settlement.description ? '<div class="mt-4"><h6>备注说明</h6><p>' + settlement.description + '</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) : ''}
+        </div>
+    `;
+    
+    const modal = new bootstrap.Modal(document.getElementById('settlementDetailModal'));
+    modal.show();
+}
+
+function showSettlementDetailById(id) {
+    fetch('/manager/api/settlements/' + id)
+        .then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                showSettlementDetail(data.settlement);
+            }
+        });
+}
+
+function loadSettlementForEdit(id) {
+    fetch('/manager/api/settlements/' + id)
+        .then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                const s = data.settlement;
+                document.getElementById('settlement-id').value = s.id;
+                document.getElementById('settlement-name').value = s.name;
+                document.getElementById('settlement-region').value = s.region || '';
+                document.getElementById('settlement-latitude').value = s.latitude || '';
+                document.getElementById('settlement-longitude').value = s.longitude || '';
+                document.getElementById('settlement-population').value = s.population || '';
+                document.getElementById('settlement-representative-id').value = s.representative_id || '';
+                document.getElementById('settlement-representative-display').value = s.representative_name || '';
+                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 || '';
+                
+                toggleNewSurnameField(s.surname_type);
+                document.getElementById('addSettlementModalLabel').textContent = '编辑聚落';
+                
+                if (s.region && s.latitude) {
+                    document.getElementById('location-info-text').innerHTML = '已选择区域:' + s.region + ',坐标:' + s.latitude + ', ' + s.longitude;
+                    document.getElementById('location-info').style.display = 'block';
+                }
+                
+                window.isEditingSettlement = true;
+                const modal = new bootstrap.Modal(document.getElementById('addSettlementModal'));
+                modal.show();
+            }
+        });
+}
+
+function resetSettlementForm() {
+    document.getElementById('settlement-id').value = '';
+    document.getElementById('settlement-name').value = '';
+    document.getElementById('settlement-region').value = '';
+    document.getElementById('settlement-latitude').value = '';
+    document.getElementById('settlement-longitude').value = '';
+    document.getElementById('settlement-population').value = '';
+    document.getElementById('settlement-representative-id').value = '';
+    document.getElementById('settlement-representative-display').value = '';
+    document.getElementById('settlement-description').value = '';
+    document.getElementById('settlement-surname-type').value = '0';
+    document.getElementById('settlement-new-surname').value = '';
+    document.getElementById('location-info').style.display = 'none';
+    document.getElementById('region-suggestions').style.display = 'none';
+    document.getElementById('new-surname-container').style.display = 'none';
+    document.getElementById('addSettlementModalLabel').textContent = '添加聚落';
+}
+
+function saveSettlement() {
+    const data = {
+        id: document.getElementById('settlement-id').value,
+        name: document.getElementById('settlement-name').value,
+        region: document.getElementById('settlement-region').value,
+        latitude: document.getElementById('settlement-latitude').value,
+        longitude: document.getElementById('settlement-longitude').value,
+        population: document.getElementById('settlement-population').value,
+        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
+    };
+    
+    const url = data.id ? '/manager/api/settlements/' + data.id : '/manager/api/settlements';
+    const method = data.id ? 'PUT' : 'POST';
+    
+    fetch(url, {
+        method: method,
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify(data),
+        credentials: 'include'
+    })
+    .then(response => response.json())
+    .then(data => {
+        if (data.success) {
+            alert('保存成功!');
+            const modal = bootstrap.Modal.getInstance(document.getElementById('addSettlementModal'));
+            modal.hide();
+            loadSettlements();
+        } else {
+            alert(data.message || '保存失败');
+        }
+    })
+    .catch(error => {
+        console.error('保存失败:', error);
+        alert('保存失败,请检查网络连接');
+    });
+}
+
+function deleteSettlement(id) {
+    fetch('/manager/api/settlements/' + id, {
+        method: 'DELETE',
+        credentials: 'include'
+    })
+    .then(response => response.json())
+    .then(data => {
+        if (data.success) {
+            loadSettlements();
+        } else {
+            alert(data.message || '删除失败');
+        }
+    });
+}
+
+function searchMembers(keyword) {
+    fetch('/manager/api/search_member', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ keyword: keyword })
+    })
+    .then(response => response.json())
+    .then(data => {
+        const container = document.getElementById('member-list-container');
+        if (data.success && data.members.length > 0) {
+            container.innerHTML = data.members.map(member => {
+                const simplified = member.simplified_name && member.simplified_name !== member.name ? '(' + member.simplified_name + ')' : '';
+                return '<div class="member-item p-3 border-bottom cursor-pointer hover:bg-gray-50" onclick="selectRepresentative(' + member.id + ', \'' + member.name + '\', \'' + (member.simplified_name || '') + '\')">' +
+                       '<div style="font-weight: 500;">' + member.name + '</div>' +
+                       (member.simplified_name ? '<div style="font-size: 12px; color: #64748b;">' + simplified + '</div>' : '') +
+                       '</div>';
+            }).join('');
+        } else {
+            container.innerHTML = '<div class="text-center py-4 text-gray-500">未找到匹配的成员</div>';
+        }
+    });
+}
+
+function selectRepresentative(id, name, simplifiedName) {
+    document.getElementById('settlement-representative-id').value = id;
+    let displayText = name;
+    if (simplifiedName && simplifiedName !== name) {
+        displayText += ' (' + simplifiedName + ')';
+    }
+    document.getElementById('settlement-representative-display').value = displayText;
+    const modal = bootstrap.Modal.getInstance(document.getElementById('memberSelectModal'));
+    modal.hide();
+}
+
+function formatDateTime(dateStr) {
+    if (!dateStr) return '-';
+    const date = new Date(dateStr);
+    return date.toLocaleString('zh-CN');
+}
+
+function initRegionSearch() {
+    const regionInput = document.getElementById('settlement-region');
+    const suggestionsDiv = document.getElementById('region-suggestions');
+    const searchBtn = document.getElementById('search-region-btn');
+    
+    searchBtn.addEventListener('click', function() {
+        const keyword = regionInput.value.trim();
+        if (keyword) {
+            searchRegion(keyword);
+        }
+    });
+    
+    let searchTimeout = null;
+    regionInput.addEventListener('input', function() {
+        const keyword = this.value.trim();
+        if (searchTimeout) clearTimeout(searchTimeout);
+        if (keyword.length >= 2) {
+            searchTimeout = setTimeout(() => { searchRegion(keyword); }, 300);
+        } else {
+            suggestionsDiv.style.display = 'none';
+        }
+    });
+    
+    regionInput.addEventListener('keypress', function(e) {
+        if (e.key === 'Enter') {
+            e.preventDefault();
+            const keyword = this.value.trim();
+            if (keyword) {
+                searchRegion(keyword);
+            }
+        }
+    });
+    
+    document.addEventListener('click', function(e) {
+        if (!e.target.closest('#settlement-region') && !e.target.closest('#search-region-btn') && !e.target.closest('#region-suggestions')) {
+            suggestionsDiv.style.display = 'none';
+        }
+    });
+}
+
+function searchRegion(keyword) {
+    const suggestionsDiv = document.getElementById('region-suggestions');
+    
+    window.AMapPromise.then(function(AMap) {
+        AMap.plugin(['AMap.PlaceSearch'], function() {
+            const placeSearch = new AMap.PlaceSearch({
+                city: '全国',
+                type: 'district',
+                pageSize: 10
+            });
+            
+            placeSearch.search(keyword, function(status, result) {
+                if (status === 'complete' && result && result.poiList && result.poiList.pois.length > 0) {
+                    const pois = result.poiList.pois;
+                    const formattedData = pois.map(poi => ({
+                        name: poi.name,
+                        adcode: poi.adcode,
+                        address: poi.address || poi.name,
+                        location: {
+                            lng: poi.location.lng || poi.location[0],
+                            lat: poi.location.lat || poi.location[1]
+                        }
+                    }));
+                    renderRegionSuggestions(formattedData);
+                } else {
+                    searchRegionFallback(keyword);
+                }
+            });
+        });
+    }).catch(function() {
+        searchRegionFallback(keyword);
+    });
+}
+
+function searchRegionFallback(keyword) {
+    const suggestionsDiv = document.getElementById('region-suggestions');
+    
+    const localData = [
+        { name: '泉州市鲤城区', adcode: '350502', address: '福建省泉州市鲤城区', location: { lng: 118.58809, lat: 24.90757 } },
+        { name: '泉州市丰泽区', adcode: '350503', address: '福建省泉州市丰泽区', location: { lng: 118.62062, lat: 24.88269 } },
+        { name: '泉州市洛江区', adcode: '350504', address: '福建省泉州市洛江区', location: { lng: 118.74387, lat: 24.92516 } },
+        { name: '泉州市泉港区', adcode: '350505', address: '福建省泉州市泉港区', location: { lng: 118.96936, lat: 25.13699 } },
+        { name: '泉州市永春县', adcode: '350525', address: '福建省泉州市永春县', location: { lng: 118.38267, lat: 25.34437 } },
+        { name: '泉州市安溪县', adcode: '350524', address: '福建省泉州市安溪县', location: { lng: 118.18598, lat: 25.06531 } },
+        { name: '泉州市德化县', adcode: '350526', address: '福建省泉州市德化县', location: { lng: 118.15722, lat: 25.53543 } },
+        { name: '泉州市金门县', adcode: '350527', address: '福建省泉州市金门县', location: { lng: 118.32556, lat: 24.43788 } },
+        { name: '泉州市南安市', adcode: '350583', address: '福建省泉州市南安市', location: { lng: 118.32211, lat: 24.96694 } },
+        { name: '泉州市晋江市', adcode: '350582', address: '福建省泉州市晋江市', location: { lng: 118.56388, lat: 24.80951 } },
+        { name: '泉州市石狮市', adcode: '350581', address: '福建省泉州市石狮市', location: { lng: 118.65021, lat: 24.74517 } },
+        { name: '漳州市芗城区', adcode: '350602', address: '福建省漳州市芗城区', location: { lng: 117.62588, lat: 24.61406 } },
+        { name: '漳州市龙文区', adcode: '350603', address: '福建省漳州市龙文区', location: { lng: 117.71692, lat: 24.62148 } },
+        { name: '漳州市龙海市', adcode: '350681', address: '福建省漳州市龙海市', location: { lng: 117.80713, lat: 24.47173 } },
+        { name: '漳州市华安县', adcode: '350629', address: '福建省漳州市华安县', location: { lng: 117.4898, lat: 24.7568 } },
+        { name: '厦门市思明区', adcode: '350203', address: '福建省厦门市思明区', location: { lng: 118.08942, lat: 24.47977 } },
+        { name: '厦门市湖里区', adcode: '350206', address: '福建省厦门市湖里区', location: { lng: 118.14991, lat: 24.52864 } },
+        { name: '厦门市同安区', adcode: '350212', address: '福建省厦门市同安区', location: { lng: 118.16728, lat: 24.72657 } },
+        { name: '厦门市翔安区', adcode: '350213', address: '福建省厦门市翔安区', location: { lng: 118.26543, lat: 24.63788 } },
+        { name: '厦门市集美区', adcode: '350211', address: '福建省厦门市集美区', location: { lng: 118.09767, lat: 24.60224 } },
+        { name: '厦门市海沧区', adcode: '350205', address: '福建省厦门市海沧区', location: { lng: 117.97257, lat: 24.53645 } },
+        { name: '龙岩市漳平市', adcode: '350881', address: '福建省龙岩市漳平市', location: { lng: 117.45528, lat: 25.36948 } },
+        { name: '三明市大田县', adcode: '350425', address: '福建省三明市大田县', location: { lng: 117.83028, lat: 25.69053 } },
+        { name: '福州市鼓楼区', adcode: '350102', address: '福建省福州市鼓楼区', location: { lng: 119.29648, lat: 26.08744 } },
+        { name: '福州市台江区', adcode: '350103', address: '福建省福州市台江区', location: { lng: 119.30476, lat: 26.05936 } },
+        { name: '福州市仓山区', adcode: '350104', address: '福建省福州市仓山区', location: { lng: 119.27744, lat: 26.00716 } },
+        { name: '信阳市固始县', adcode: '411525', address: '河南省信阳市固始县', location: { lng: 115.68042, lat: 32.14447 } },
+        { name: '六安市寿县', adcode: '341521', address: '安徽省六安市寿县', location: { lng: 116.68658, lat: 32.57654 } },
+        { name: '六安市霍邱县', adcode: '341522', address: '安徽省六安市霍邱县', location: { lng: 116.10428, lat: 32.38912 } },
+        { name: '六安市李集', adcode: '341522', address: '安徽省六安市霍邱县李集镇', location: { lng: 115.92136, lat: 32.38456 } },
+        { name: '长沙市望城区', adcode: '430112', address: '湖南省长沙市望城区', location: { lng: 112.83488, lat: 28.31353 } },
+        { name: '广州市增城区', adcode: '440118', address: '广东省广州市增城区', location: { lng: 113.86262, lat: 23.23745 } },
+        { name: '吉安市永新县', adcode: '360830', address: '江西省吉安市永新县', location: { lng: 114.22658, lat: 26.90836 } },
+        { name: '丽水市青田县', adcode: '331121', address: '浙江省丽水市青田县', location: { lng: 120.28158, lat: 28.19935 } },
+        { name: '温州市泰顺县', adcode: '330329', address: '浙江省温州市泰顺县', location: { lng: 119.75248, lat: 27.67386 } },
+        { name: '温州市文成县', adcode: '330328', address: '浙江省温州市文成县', location: { lng: 120.08258, lat: 27.68862 } },
+        { name: '杭州市临安区', adcode: '330112', address: '浙江省杭州市临安区', location: { lng: 119.72768, lat: 30.23397 } },
+        { name: '金华市兰溪市', adcode: '330781', address: '浙江省金华市兰溪市', location: { lng: 119.48938, lat: 29.29735 } },
+        { name: '衢州市衢江区', adcode: '330803', address: '浙江省衢州市衢江区', location: { lng: 118.88088, lat: 28.93562 } },
+        { name: '衢州市开化县', adcode: '330824', address: '浙江省衢州市开化县', location: { lng: 118.30888, lat: 29.29412 } },
+        { name: '长春市农安县', adcode: '220122', address: '吉林省长春市农安县', location: { lng: 125.16718, lat: 44.45183 } },
+        { name: '新北市三重区', adcode: '710101', address: '台湾省新北市三重区', location: { lng: 121.48888, lat: 25.05722 } },
+        { name: '台北市', adcode: '710100', address: '台湾省台北市', location: { lng: 121.50906, lat: 25.04433 } },
+        { name: '云林县', adcode: '710600', address: '台湾省云林县', location: { lng: 120.43758, lat: 23.71465 } },
+        { name: '彰化县', adcode: '710500', address: '台湾省彰化县', location: { lng: 120.54869, lat: 24.08279 } },
+        { name: '台中市清水区', adcode: '710206', address: '台湾省台中市清水区', location: { lng: 120.56496, lat: 24.26271 } },
+        { name: '菲律宾马尼拉', adcode: 'PH001', address: '菲律宾马尼拉', location: { lng: 120.9842, lat: 14.5995 } },
+        { name: '北京市', adcode: '110000', address: '北京市', location: { lng: 116.407394, lat: 39.904211 } },
+        { name: '上海市', adcode: '310000', address: '上海市', location: { lng: 121.473701, lat: 31.230416 } },
+        { name: '广州市', adcode: '440100', address: '广东省广州市', location: { lng: 113.264385, lat: 23.12911 } },
+        { name: '深圳市', adcode: '440300', address: '广东省深圳市', location: { lng: 114.057964, lat: 22.543099 } },
+        { name: '杭州市', adcode: '330100', address: '浙江省杭州市', location: { lng: 120.197855, lat: 30.274084 } },
+        { name: '南京市', adcode: '320100', address: '江苏省南京市', location: { lng: 118.796875, lat: 32.060255 } },
+        { name: '成都市', adcode: '510100', address: '四川省成都市', location: { lng: 104.066801, lat: 30.572816 } },
+        { name: '武汉市', adcode: '420100', address: '湖北省武汉市', location: { lng: 114.287924, lat: 30.592855 } }
+    ];
+    
+    const filteredData = localData.filter(item => item.name.includes(keyword) || item.address.includes(keyword));
+    
+    if (filteredData.length > 0) {
+        renderRegionSuggestions(filteredData);
+    } else {
+        suggestionsDiv.innerHTML = '<div class="alert alert-warning">未找到匹配的区域,请尝试其他关键词</div>';
+        suggestionsDiv.style.display = 'block';
+    }
+}
+
+function renderRegionSuggestions(pois) {
+    const suggestionsDiv = document.getElementById('region-suggestions');
+    let html = '<div class="list-group" style="max-height: 300px; overflow-y: auto;">';
+    
+    pois.slice(0, 8).forEach(poi => {
+        const lng = poi.location.lng || poi.location[0];
+        const lat = poi.location.lat || poi.location[1];
+        html += '<button type="button" class="list-group-item list-group-item-action" onclick="selectRegion(\'' + poi.name + '\', ' + lng + ', ' + lat + ')">' +
+                '<div class="d-flex justify-content-between">' +
+                '<span style="font-weight: 500;">' + poi.name + '</span>' +
+                '<span class="text-muted text-sm">' + poi.adcode + '</span>' +
+                '</div>' +
+                (poi.address ? '<div class="text-sm text-muted">' + poi.address + '</div>' : '') +
+                '</button>';
+    });
+    
+    html += '</div>';
+    suggestionsDiv.innerHTML = html;
+    suggestionsDiv.style.display = 'block';
+}
+
+function toggleNewSurnameField(surnameType) {
+    const container = document.getElementById('new-surname-container');
+    if (surnameType == 1) {
+        container.style.display = 'block';
+    } else {
+        container.style.display = 'none';
+        document.getElementById('settlement-new-surname').value = '';
+    }
+}
+
+function getSurnameTypeText(type) {
+    return type == 1 ? '改姓' : '留姓';
+}
+
+function selectRegion(name, lng, lat) {
+    document.getElementById('settlement-region').value = name;
+    document.getElementById('settlement-latitude').value = lat;
+    document.getElementById('settlement-longitude').value = lng;
+    document.getElementById('region-suggestions').style.display = 'none';
+    
+    const infoText = document.getElementById('location-info-text');
+    infoText.innerHTML = '已选择区域:' + name + ',坐标:' + lat.toFixed(6) + ', ' + lng.toFixed(6);
+    document.getElementById('location-info').style.display = 'block';
+}
+
+let tooltipDiv = null;
+
+function showMarkerTooltip(e, settlement) {
+    if (!tooltipDiv) {
+        tooltipDiv = document.createElement('div');
+        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>' +
+                          '<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';
+}
+
+function hideMarkerTooltip() {
+    if (tooltipDiv) {
+        tooltipDiv.style.display = 'none';
+    }
+}
+</script>
+{% endblock %}

+ 45 - 5
templates/tree.html

@@ -1,6 +1,6 @@
 {% extends "layout.html" %}
 
-{% block title %}生物遗传图谱 - 家谱管理系统{% endblock %}
+{% block title %}家谱世系树状图 - 家谱管理系统{% endblock %}
 
 {% block extra_css %}
 <style>
@@ -87,6 +87,14 @@
 
     /* 男女形状与颜色区分:男性方形(蓝),女性圆形(粉红),添加轻微圆角与投影 */
     .node-male rect { stroke: #3B82F6; fill: #EFF6FF; rx: 8px; ry: 8px; }
+    
+    /* 出继节点样式:红色虚线框 */
+    .node-adopted-out rect, .node-adopted-out circle {
+        stroke: #EF4444;
+        stroke-width: 2px;
+        stroke-dasharray: 6, 4;
+        fill: rgba(239, 68, 68, 0.1);
+    }
     .node-female circle { stroke: #EC4899; fill: #FDF2F8; }
     
     /* 未知性别默认 */
@@ -143,7 +151,7 @@
 </div>
 
     <div class="alert alert-light border small py-2">
-        <i class="bi bi-info-circle me-1"></i> 提示:图中按生物遗传图谱格式展示。支持拖拽建立关系,右键点击成员可查看、编辑或新增。
+        <i class="bi bi-info-circle me-1"></i> 提示:图中按家谱世系树状图格式展示。支持拖拽建立关系,右键点击成员可查看、编辑或新增。
     </div>
 
     <div id="tree-container">
@@ -286,9 +294,40 @@
                 return;
             }
 
-        const nodes = members.map(m => ({ id: m.id, name: m.name, simplified_name: m.simplified_name, sex: m.sex }));
-        const hierarchicalLinks = relations.filter(r => r.relation_type === 1 || r.relation_type === 2)
-                                          .map(r => ({ source: r.parent_mid, target: r.child_mid }));
+        // 构建节点,保留所有关系信息
+        const nodes = members.map(m => ({ id: m.id, name: m.name, simplified_name: m.simplified_name, sex: m.sex, adoptedOut: false, adoptedOutTarget: null }));
+        
+        // 获取所有父子关系(包括出继/入继)
+        const allHierarchicalRelations = relations.filter(r => r.relation_type === 1 || r.relation_type === 2);
+        
+        // 对于出继的子女,记录他们入继到的目标
+        const adoptedOutMap = new Map();
+        allHierarchicalRelations.forEach(r => {
+            if (r.sub_relation_type === 2) { // 出继
+                const targetRelation = allHierarchicalRelations.find(r2 => r2.child_mid === r.child_mid && r2.sub_relation_type === 3);
+                if (targetRelation) {
+                    const targetMember = members.find(m => m.id === targetRelation.parent_mid);
+                    if (targetMember) {
+                        adoptedOutMap.set(r.child_mid, targetMember.name);
+                    }
+                }
+            }
+        });
+        
+        // 更新节点的出继标记
+        nodes.forEach(node => {
+            if (adoptedOutMap.has(node.id)) {
+                node.adoptedOut = true;
+                node.adoptedOutTarget = adoptedOutMap.get(node.id);
+            }
+        });
+        
+        // 保留所有父子关系(包括出继),用于构建树
+        const hierarchicalLinks = allHierarchicalRelations.map(r => ({ 
+            source: r.parent_mid, 
+            target: r.child_mid,
+            sub_relation_type: r.sub_relation_type 
+        }));
         const spouseLinks = relations.filter(r => r.relation_type === 10);
         const otherLinks = relations.filter(r => r.relation_type >= 11);
 
@@ -499,6 +538,7 @@
                 let cls = "node" + (d.children ? " node--internal" : " node--leaf");
                 if (d.data.sex === 1) cls += " node-male";
                 else if (d.data.sex === 2) cls += " node-female";
+                if (d.data.adoptedOut) cls += " node-adopted-out";
                 return cls;
             })
             .attr("transform", d => `translate(${d.x},${d.y})`)