|
@@ -3,6 +3,7 @@
|
|
|
{% block title %}{{ '编辑' if member else '录入' }}成员 - 家谱管理系统{% endblock %}
|
|
{% block title %}{{ '编辑' if member else '录入' }}成员 - 家谱管理系统{% endblock %}
|
|
|
|
|
|
|
|
{% block extra_css %}
|
|
{% block extra_css %}
|
|
|
|
|
+<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
|
|
|
<style>
|
|
<style>
|
|
|
.split-container { display: flex; height: calc(100vh - 100px); overflow: hidden; }
|
|
.split-container { display: flex; height: calc(100vh - 100px); overflow: hidden; }
|
|
|
.form-panel { flex: 1.2; padding: 20px; overflow-y: auto; border-right: 1px solid #dee2e6; }
|
|
.form-panel { flex: 1.2; padding: 20px; overflow-y: auto; border-right: 1px solid #dee2e6; }
|
|
@@ -96,10 +97,11 @@
|
|
|
<input type="hidden" name="source_record_id" value="{{ source_record_id if source_record_id else (member.source_record_id if member and member.source_record_id else '') }}">
|
|
<input type="hidden" name="source_record_id" value="{{ source_record_id if source_record_id else (member.source_record_id if member and member.source_record_id else '') }}">
|
|
|
<input type="hidden" name="source_index" value="">
|
|
<input type="hidden" name="source_index" value="">
|
|
|
<div class="section-title">核心信息 (必填)</div>
|
|
<div class="section-title">核心信息 (必填)</div>
|
|
|
- <div class="row g-3">
|
|
|
|
|
|
|
+ <div class="row g-3 mb-4">
|
|
|
<div class="col-md-6">
|
|
<div class="col-md-6">
|
|
|
<label class="form-label">姓名(繁体) <span class="text-danger">*</span></label>
|
|
<label class="form-label">姓名(繁体) <span class="text-danger">*</span></label>
|
|
|
- <input type="text" name="name" class="form-control" required value="{{ member.name if member else '' }}">
|
|
|
|
|
|
|
+ <input type="text" name="name" id="nameInput" class="form-control" required value="{{ member.name if member else '' }}">
|
|
|
|
|
+ <div id="nameCheckResult" class="mt-2"></div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="col-md-6">
|
|
<div class="col-md-6">
|
|
|
<label class="form-label">姓名(简体)</label>
|
|
<label class="form-label">姓名(简体)</label>
|
|
@@ -126,8 +128,29 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <div class="section-title">状态信息</div>
|
|
|
|
|
+ <div class="row g-3 mb-4">
|
|
|
|
|
+ <div class="col-md-6">
|
|
|
|
|
+ <label class="form-label">是否过世</label>
|
|
|
|
|
+ <select name="is_pass_away" class="form-select">
|
|
|
|
|
+ <option value="0" {{ 'selected' if member and member.is_pass_away == 0 else '' }}>健在</option>
|
|
|
|
|
+ <option value="1" {{ 'selected' if member and member.is_pass_away == 1 else '' }}>已故</option>
|
|
|
|
|
+ <option value="2" {{ 'selected' if member and member.is_pass_away == 2 else '' }}>未知</option>
|
|
|
|
|
+ </select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-6">
|
|
|
|
|
+ <label class="form-label">婚姻状况</label>
|
|
|
|
|
+ <select name="marital_status" class="form-select">
|
|
|
|
|
+ <option value="0" {{ 'selected' if member and member.marital_status == 0 else '' }}>未知</option>
|
|
|
|
|
+ <option value="1" {{ 'selected' if member and member.marital_status == 1 else '' }}>未婚</option>
|
|
|
|
|
+ <option value="2" {{ 'selected' if member and member.marital_status == 2 else '' }}>已婚</option>
|
|
|
|
|
+ <option value="3" {{ 'selected' if member and member.marital_status == 3 else '' }}>离异/丧偶</option>
|
|
|
|
|
+ </select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<div class="section-title">关系录入 (选择关联成员及关系)</div>
|
|
<div class="section-title">关系录入 (选择关联成员及关系)</div>
|
|
|
- <div class="row g-3">
|
|
|
|
|
|
|
+ <div class="row g-3 mb-4">
|
|
|
<div class="col-md-5">
|
|
<div class="col-md-5">
|
|
|
<label class="form-label">关联成员</label>
|
|
<label class="form-label">关联成员</label>
|
|
|
<select name="related_mid" class="form-select">
|
|
<select name="related_mid" class="form-select">
|
|
@@ -161,108 +184,112 @@
|
|
|
</select>
|
|
</select>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- <div class="section-title">谱系详情</div>
|
|
|
|
|
- <div class="row g-3">
|
|
|
|
|
- <div class="col-md-4">
|
|
|
|
|
- <label class="form-label">曾用名</label>
|
|
|
|
|
- <input type="text" name="former_name" class="form-control" value="{{ member.former_name if member else '' }}">
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-4">
|
|
|
|
|
- <label class="form-label">幼名/乳名</label>
|
|
|
|
|
- <input type="text" name="childhood_name" class="form-control" value="{{ member.childhood_name if member else '' }}">
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-4">
|
|
|
|
|
- <label class="form-label">字辈</label>
|
|
|
|
|
- <input type="text" name="name_word" class="form-control" value="{{ member.name_word if member else '' }}">
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-4">
|
|
|
|
|
- <label class="form-label">堂内排行</label>
|
|
|
|
|
- <input type="text" name="family_rank" class="form-control" value="{{ member.family_rank if member else '' }}">
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-4">
|
|
|
|
|
- <label class="form-label">世系世代</label>
|
|
|
|
|
- <input type="text" name="name_word_generation" class="form-control" value="{{ member.name_word_generation if member else '' }}">
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-6">
|
|
|
|
|
- <label class="form-label">名号/封号</label>
|
|
|
|
|
- <input type="text" name="name_title" class="form-control" value="{{ member.name_title if member else '' }}">
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-6">
|
|
|
|
|
- <label class="form-label">分房/堂号</label>
|
|
|
|
|
- <input type="text" name="branch_family_hall" class="form-control" value="{{ member.branch_family_hall if member else '' }}">
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-6">
|
|
|
|
|
- <label class="form-label">聚居地</label>
|
|
|
|
|
- <input type="text" name="cluster_place" class="form-control" value="{{ member.cluster_place if member else '' }}">
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="section-title">状态与联系</div>
|
|
|
|
|
- <div class="row g-3">
|
|
|
|
|
- <div class="col-md-4">
|
|
|
|
|
- <label class="form-label">是否过世</label>
|
|
|
|
|
- <select name="is_pass_away" class="form-select">
|
|
|
|
|
- <option value="0" {{ 'selected' if member and member.is_pass_away == 0 else '' }}>健在</option>
|
|
|
|
|
- <option value="1" {{ 'selected' if member and member.is_pass_away == 1 else '' }}>已故</option>
|
|
|
|
|
- </select>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-4">
|
|
|
|
|
- <label class="form-label">婚姻状况</label>
|
|
|
|
|
- <select name="marital_status" class="form-select">
|
|
|
|
|
- <option value="0" {{ 'selected' if member and member.marital_status == 0 else '' }}>未知</option>
|
|
|
|
|
- <option value="1" {{ 'selected' if member and member.marital_status == 1 else '' }}>未婚</option>
|
|
|
|
|
- <option value="2" {{ 'selected' if member and member.marital_status == 2 else '' }}>已婚</option>
|
|
|
|
|
- <option value="3" {{ 'selected' if member and member.marital_status == 3 else '' }}>离异/丧偶</option>
|
|
|
|
|
- </select>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-4">
|
|
|
|
|
- <label class="form-label">民族</label>
|
|
|
|
|
- <input type="text" name="nation" class="form-control" value="{{ member.nation if member else '' }}">
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-6">
|
|
|
|
|
- <label class="form-label">手机号</label>
|
|
|
|
|
- <input type="text" name="phone" class="form-control" value="{{ member.phone if member else '' }}">
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-6">
|
|
|
|
|
- <label class="form-label">微信号</label>
|
|
|
|
|
- <input type="text" name="wechat_account" class="form-control" value="{{ member.wechat_account if member else '' }}">
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-12">
|
|
|
|
|
- <label class="form-label">现居住址</label>
|
|
|
|
|
- <input type="text" name="residential_address" class="form-control" value="{{ member.residential_address if member else '' }}">
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="section-title">个人履历</div>
|
|
|
|
|
- <div class="row g-3">
|
|
|
|
|
- <div class="col-md-6">
|
|
|
|
|
- <label class="form-label">职业</label>
|
|
|
|
|
- <textarea name="occupation" class="form-control" rows="2">{{ member.occupation if member else '' }}</textarea>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-6">
|
|
|
|
|
- <label class="form-label">教育背景</label>
|
|
|
|
|
- <textarea name="educational" class="form-control" rows="2">{{ member.educational if member else '' }}</textarea>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-12">
|
|
|
|
|
- <label class="form-label">标签</label>
|
|
|
|
|
- <input type="text" name="tags" class="form-control" placeholder="例如:抗战老兵, 教师 (用逗号分隔)" value="{{ member.tags if member else '' }}">
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="col-md-12">
|
|
|
|
|
- <label class="form-label">人员备注</label>
|
|
|
|
|
- <textarea name="notes" class="form-control" rows="3">{{ member.notes if member else '' }}</textarea>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <div class="section-title">人员备注</div>
|
|
|
|
|
+ <div class="row g-3 mb-4">
|
|
|
<div class="col-md-12">
|
|
<div class="col-md-12">
|
|
|
- <label class="form-label">个人成就</label>
|
|
|
|
|
- <textarea name="personal_achievements" class="form-control" rows="3">{{ member.personal_achievements if member else '' }}</textarea>
|
|
|
|
|
|
|
+ <textarea name="notes" class="form-control" rows="2">{{ member.notes if member else '' }}</textarea>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- <div class="d-grid gap-2 mt-5 mb-5">
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 悬浮的保存按钮,始终保持在一屏内或跟随页面底部 -->
|
|
|
|
|
+ <div class="d-grid gap-2 mb-4 sticky-bottom bg-white py-2 border-top" style="z-index: 1020;">
|
|
|
<button type="submit" class="btn btn-success btn-lg">
|
|
<button type="submit" class="btn btn-success btn-lg">
|
|
|
<i class="bi bi-check-circle me-1"></i> {{ '保存修改' if member else '确认录入' }}
|
|
<i class="bi bi-check-circle me-1"></i> {{ '保存修改' if member else '确认录入' }}
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 折叠的其他信息区 -->
|
|
|
|
|
+ <div class="accordion" id="accordionExtraInfo">
|
|
|
|
|
+ <div class="accordion-item border-0">
|
|
|
|
|
+ <h2 class="accordion-header" id="headingExtra">
|
|
|
|
|
+ <button class="accordion-button collapsed bg-light text-secondary rounded shadow-sm border" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExtra" aria-expanded="false" aria-controls="collapseExtra">
|
|
|
|
|
+ <i class="bi bi-three-dots me-2"></i> 展开更多其他信息(谱系详情、联络、履历等)
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </h2>
|
|
|
|
|
+ <div id="collapseExtra" class="accordion-collapse collapse" aria-labelledby="headingExtra" data-bs-parent="#accordionExtraInfo">
|
|
|
|
|
+ <div class="accordion-body px-0 pt-3 pb-0">
|
|
|
|
|
+
|
|
|
|
|
+ <div class="section-title mt-0">谱系详情</div>
|
|
|
|
|
+ <div class="row g-3 mb-4">
|
|
|
|
|
+ <div class="col-md-4">
|
|
|
|
|
+ <label class="form-label">曾用名</label>
|
|
|
|
|
+ <input type="text" name="former_name" class="form-control" value="{{ member.former_name if member else '' }}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-4">
|
|
|
|
|
+ <label class="form-label">幼名/乳名</label>
|
|
|
|
|
+ <input type="text" name="childhood_name" class="form-control" value="{{ member.childhood_name if member else '' }}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-4">
|
|
|
|
|
+ <label class="form-label">字辈</label>
|
|
|
|
|
+ <input type="text" name="name_word" class="form-control" value="{{ member.name_word if member else '' }}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-4">
|
|
|
|
|
+ <label class="form-label">堂内排行</label>
|
|
|
|
|
+ <input type="text" name="family_rank" class="form-control" value="{{ member.family_rank if member else '' }}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-4">
|
|
|
|
|
+ <label class="form-label">世系世代</label>
|
|
|
|
|
+ <input type="text" name="name_word_generation" class="form-control" value="{{ member.name_word_generation if member else '' }}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-6">
|
|
|
|
|
+ <label class="form-label">名号/封号</label>
|
|
|
|
|
+ <input type="text" name="name_title" class="form-control" value="{{ member.name_title if member else '' }}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-6">
|
|
|
|
|
+ <label class="form-label">分房/堂号</label>
|
|
|
|
|
+ <input type="text" name="branch_family_hall" class="form-control" value="{{ member.branch_family_hall if member else '' }}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-6">
|
|
|
|
|
+ <label class="form-label">聚居地</label>
|
|
|
|
|
+ <input type="text" name="cluster_place" class="form-control" value="{{ member.cluster_place if member else '' }}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="section-title">联络信息</div>
|
|
|
|
|
+ <div class="row g-3 mb-4">
|
|
|
|
|
+ <div class="col-md-4">
|
|
|
|
|
+ <label class="form-label">民族</label>
|
|
|
|
|
+ <input type="text" name="nation" class="form-control" value="{{ member.nation if member else '' }}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-4">
|
|
|
|
|
+ <label class="form-label">手机号</label>
|
|
|
|
|
+ <input type="text" name="phone" class="form-control" value="{{ member.phone if member else '' }}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-4">
|
|
|
|
|
+ <label class="form-label">微信号</label>
|
|
|
|
|
+ <input type="text" name="wechat_account" class="form-control" value="{{ member.wechat_account if member else '' }}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-12">
|
|
|
|
|
+ <label class="form-label">现居住址</label>
|
|
|
|
|
+ <input type="text" name="residential_address" class="form-control" value="{{ member.residential_address if member else '' }}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="section-title">个人履历</div>
|
|
|
|
|
+ <div class="row g-3 mb-2">
|
|
|
|
|
+ <div class="col-md-6">
|
|
|
|
|
+ <label class="form-label">职业</label>
|
|
|
|
|
+ <textarea name="occupation" class="form-control" rows="2">{{ member.occupation if member else '' }}</textarea>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-6">
|
|
|
|
|
+ <label class="form-label">教育背景</label>
|
|
|
|
|
+ <textarea name="educational" class="form-control" rows="2">{{ member.educational if member else '' }}</textarea>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-12">
|
|
|
|
|
+ <label class="form-label">标签</label>
|
|
|
|
|
+ <input type="text" name="tags" class="form-control" placeholder="例如:抗战老兵, 教师 (用逗号分隔)" value="{{ member.tags if member else '' }}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col-md-12">
|
|
|
|
|
+ <label class="form-label">个人成就</label>
|
|
|
|
|
+ <textarea name="personal_achievements" class="form-control" rows="3">{{ member.personal_achievements if member else '' }}</textarea>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</form>
|
|
</form>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -272,13 +299,26 @@
|
|
|
<!-- AI 推理日志及结果面板 -->
|
|
<!-- AI 推理日志及结果面板 -->
|
|
|
<div id="aiLogPanel" class="position-fixed bottom-0 end-0 p-3 bg-dark text-white shadow"
|
|
<div id="aiLogPanel" class="position-fixed bottom-0 end-0 p-3 bg-dark text-white shadow"
|
|
|
style="display: none; width: 450px; max-height: 85vh; border-radius: 8px 0 0 0; z-index: 1050; opacity: 0.95; overflow-y: auto;">
|
|
style="display: none; width: 450px; max-height: 85vh; border-radius: 8px 0 0 0; z-index: 1050; opacity: 0.95; overflow-y: auto;">
|
|
|
- <div class="d-flex justify-content-between align-items-center mb-2 border-bottom border-secondary pb-2 sticky-top bg-dark pt-1">
|
|
|
|
|
- <span class="fw-bold"><i class="bi bi-robot"></i> AI 识别助手</span>
|
|
|
|
|
- <button class="btn btn-sm btn-outline-light py-0" onclick="closeAiLog()">×</button>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 吸顶头部与当前选中详情 -->
|
|
|
|
|
+ <div class="sticky-top bg-dark pb-2" style="z-index: 1060; margin-top: -1rem; padding-top: 1rem; border-bottom: 1px solid #444;">
|
|
|
|
|
+ <div class="d-flex justify-content-between align-items-center mb-2 pb-1">
|
|
|
|
|
+ <span class="fw-bold"><i class="bi bi-robot"></i> AI 识别助手</span>
|
|
|
|
|
+ <button class="btn btn-sm btn-outline-light py-0" onclick="closeAiLog()">×</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 当前选中详情 -->
|
|
|
|
|
+ <div id="aiCurrentDetail" class="mt-2 p-2 bg-secondary bg-opacity-25 rounded border border-info shadow-sm" style="display:none; max-height: 250px; overflow-y: auto;">
|
|
|
|
|
+ <div class="d-flex justify-content-between align-items-center mb-2 border-bottom border-secondary pb-1">
|
|
|
|
|
+ <strong class="text-info"><i class="bi bi-info-circle"></i> 当前填充详情</strong>
|
|
|
|
|
+ <button class="btn btn-sm btn-link text-muted py-0 text-decoration-none" onclick="document.getElementById('aiCurrentDetail').style.display='none'">×</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div id="aiDetailContent" class="small text-light" style="word-break: break-all;"></div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 推理过程 -->
|
|
<!-- 推理过程 -->
|
|
|
- <div class="mb-3">
|
|
|
|
|
|
|
+ <div class="mb-3 mt-3">
|
|
|
<button class="btn btn-sm btn-link text-decoration-none text-light p-0 mb-1 d-flex align-items-center" type="button" data-bs-toggle="collapse" data-bs-target="#collapseReasoning">
|
|
<button class="btn btn-sm btn-link text-decoration-none text-light p-0 mb-1 d-flex align-items-center" type="button" data-bs-toggle="collapse" data-bs-target="#collapseReasoning">
|
|
|
<i class="bi bi-cpu me-1"></i> 推理过程 <span class="badge bg-secondary ms-2" id="reasoningStatus">进行中...</span>
|
|
<i class="bi bi-cpu me-1"></i> 推理过程 <span class="badge bg-secondary ms-2" id="reasoningStatus">进行中...</span>
|
|
|
</button>
|
|
</button>
|
|
@@ -287,15 +327,6 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 当前选中详情 -->
|
|
|
|
|
- <div id="aiCurrentDetail" class="mb-3 p-2 bg-secondary bg-opacity-25 rounded border border-info" style="display:none;">
|
|
|
|
|
- <div class="d-flex justify-content-between align-items-center mb-2 border-bottom border-secondary pb-1">
|
|
|
|
|
- <strong class="text-info"><i class="bi bi-info-circle"></i> 当前填充详情</strong>
|
|
|
|
|
- <button class="btn btn-sm btn-link text-muted py-0 text-decoration-none" onclick="document.getElementById('aiCurrentDetail').style.display='none'">×</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div id="aiDetailContent" class="small text-light" style="word-break: break-all; max-height: 200px; overflow-y: auto;"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
<!-- 识别结果列表 -->
|
|
<!-- 识别结果列表 -->
|
|
|
<div id="aiResultSection" style="display: none;">
|
|
<div id="aiResultSection" style="display: none;">
|
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
@@ -365,7 +396,9 @@
|
|
|
{% endblock %}
|
|
{% endblock %}
|
|
|
|
|
|
|
|
{% block extra_js %}
|
|
{% block extra_js %}
|
|
|
|
|
+<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
|
|
|
<script>
|
|
<script>
|
|
|
|
|
+ let tomSelectInstance = null;
|
|
|
function toggleBirthdayUnknown() {
|
|
function toggleBirthdayUnknown() {
|
|
|
const cb = document.getElementById('birthdayUnknown');
|
|
const cb = document.getElementById('birthdayUnknown');
|
|
|
const input = document.querySelector('input[name="birthday"]');
|
|
const input = document.querySelector('input[name="birthday"]');
|
|
@@ -426,7 +459,21 @@
|
|
|
// Call validation when relation changes too
|
|
// Call validation when relation changes too
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
const relatedSelect = document.querySelector('select[name="related_mid"]');
|
|
const relatedSelect = document.querySelector('select[name="related_mid"]');
|
|
|
- if (relatedSelect) relatedSelect.addEventListener('change', validateAge);
|
|
|
|
|
|
|
+ if (relatedSelect) {
|
|
|
|
|
+ relatedSelect.addEventListener('change', validateAge);
|
|
|
|
|
+ if (typeof TomSelect !== 'undefined') {
|
|
|
|
|
+ tomSelectInstance = new TomSelect(relatedSelect, {
|
|
|
|
|
+ create: false,
|
|
|
|
|
+ sortField: null,
|
|
|
|
|
+ searchField: ['text'],
|
|
|
|
|
+ render: {
|
|
|
|
|
+ no_results: function(data, escape) {
|
|
|
|
|
+ return '<div class="no-results">未找到匹配项</div>';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// Initialize birthday unknown state
|
|
// Initialize birthday unknown state
|
|
|
toggleBirthdayUnknown();
|
|
toggleBirthdayUnknown();
|
|
@@ -962,13 +1009,20 @@
|
|
|
|
|
|
|
|
const form = document.querySelector('form');
|
|
const form = document.querySelector('form');
|
|
|
form.reset(); // Clear previous data first
|
|
form.reset(); // Clear previous data first
|
|
|
|
|
+ if (tomSelectInstance) {
|
|
|
|
|
+ tomSelectInstance.clear();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// Set Source Index
|
|
// Set Source Index
|
|
|
const sourceIndexInput = form.querySelector('[name="source_index"]');
|
|
const sourceIndexInput = form.querySelector('[name="source_index"]');
|
|
|
if (sourceIndexInput) sourceIndexInput.value = index;
|
|
if (sourceIndexInput) sourceIndexInput.value = index;
|
|
|
|
|
|
|
|
// 1. 姓名
|
|
// 1. 姓名
|
|
|
- if (person.name) form.querySelector('[name="name"]').value = person.name;
|
|
|
|
|
|
|
+ if (person.name) {
|
|
|
|
|
+ const nameInput = form.querySelector('[name="name"]');
|
|
|
|
|
+ nameInput.value = person.name;
|
|
|
|
|
+ nameInput.dispatchEvent(new Event('input')); // 触发重名检测
|
|
|
|
|
+ }
|
|
|
if (person.simplified_name) {
|
|
if (person.simplified_name) {
|
|
|
const snInput = form.querySelector('[name="simplified_name"]');
|
|
const snInput = form.querySelector('[name="simplified_name"]');
|
|
|
if (snInput) snInput.value = person.simplified_name;
|
|
if (snInput) snInput.value = person.simplified_name;
|
|
@@ -995,27 +1049,87 @@
|
|
|
toggleBirthdayUnknown();
|
|
toggleBirthdayUnknown();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ let isDeceased = false;
|
|
|
|
|
+ let isDeceasedUnknown = true;
|
|
|
|
|
+
|
|
|
if (person.birthday) {
|
|
if (person.birthday) {
|
|
|
let dateVal = person.birthday;
|
|
let dateVal = person.birthday;
|
|
|
- // 尝试标准化
|
|
|
|
|
- const dateMatch = dateVal.match(/(\d{4})[-/年](\d{1,2})[-/月](\d{1,2})/);
|
|
|
|
|
- if (dateMatch) {
|
|
|
|
|
- const y = dateMatch[1];
|
|
|
|
|
- const m = dateMatch[2].padStart(2, '0');
|
|
|
|
|
- const d = dateMatch[3].padStart(2, '0');
|
|
|
|
|
- dateVal = `${y}-${m}-${d}`;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Handle partial dates like "1890年" or "1890年?月?日"
|
|
|
|
|
+ const partialYearMatch = dateVal.match(/^(\d{4})[^\d]*$/) || dateVal.match(/(\d{4})年\s*[??Xxx]\s*月/i);
|
|
|
|
|
+ if (partialYearMatch) {
|
|
|
|
|
+ dateVal = `${partialYearMatch[1]}-01-01`;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 尝试标准化完整日期
|
|
|
|
|
+ const dateMatch = dateVal.match(/(\d{4})[-/年](\d{1,2})[-/月](\d{1,2})/);
|
|
|
|
|
+ if (dateMatch) {
|
|
|
|
|
+ const y = dateMatch[1];
|
|
|
|
|
+ const m = dateMatch[2].padStart(2, '0');
|
|
|
|
|
+ const d = dateMatch[3].padStart(2, '0');
|
|
|
|
|
+ dateVal = `${y}-${m}-${d}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 只有当日期格式正确时才填充
|
|
|
|
|
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateVal)) {
|
|
|
|
|
+ form.querySelector('[name="birthday"]').value = dateVal;
|
|
|
|
|
|
|
|
// Auto "Is Deceased" Logic (e.g. older than 100 years from now)
|
|
// Auto "Is Deceased" Logic (e.g. older than 100 years from now)
|
|
|
- const birthYear = parseInt(y);
|
|
|
|
|
|
|
+ const birthYear = parseInt(dateVal.substring(0, 4));
|
|
|
const currentYear = new Date().getFullYear();
|
|
const currentYear = new Date().getFullYear();
|
|
|
if (currentYear - birthYear > 100) {
|
|
if (currentYear - birthYear > 100) {
|
|
|
- const passAwaySelect = form.querySelector('[name="is_pass_away"]');
|
|
|
|
|
- if (passAwaySelect) passAwaySelect.value = '1';
|
|
|
|
|
|
|
+ isDeceased = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ isDeceasedUnknown = false;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Parse failed, set to unknown
|
|
|
|
|
+ if (birthdayUnknownCb) {
|
|
|
|
|
+ birthdayUnknownCb.checked = true;
|
|
|
|
|
+ toggleBirthdayUnknown();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- // 只有当日期格式正确时才填充,否则不填或者留给用户
|
|
|
|
|
- if (/^\d{4}-\d{2}-\d{2}$/.test(dateVal)) {
|
|
|
|
|
- form.querySelector('[name="birthday"]').value = dateVal;
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // No birthday found, automatically check unknown
|
|
|
|
|
+ if (birthdayUnknownCb) {
|
|
|
|
|
+ birthdayUnknownCb.checked = true;
|
|
|
|
|
+ toggleBirthdayUnknown();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 当自己年龄不详时,通过父母年龄推断是否在世
|
|
|
|
|
+ if (isDeceasedUnknown && person.matches && person.matches.father && person.matches.father.length > 0) {
|
|
|
|
|
+ const father = person.matches.father[0];
|
|
|
|
|
+ if (father.birthday) {
|
|
|
|
|
+ const fatherBirthYear = new Date(father.birthday * 1000).getFullYear();
|
|
|
|
|
+ const currentYear = new Date().getFullYear();
|
|
|
|
|
+ // 假设如果父亲是120年前出生的,子女大概率也已超过100岁
|
|
|
|
|
+ if (currentYear - fatherBirthYear > 120) {
|
|
|
|
|
+ isDeceased = true;
|
|
|
|
|
+ isDeceasedUnknown = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 已故状态
|
|
|
|
|
+ const passAwaySelect = form.querySelector('[name="is_pass_away"]');
|
|
|
|
|
+ if (passAwaySelect) {
|
|
|
|
|
+ // "殁", "葬", "卒" in raw text usually means deceased. If AI extracted death_date, also true.
|
|
|
|
|
+ if (isDeceased || person.death_date) {
|
|
|
|
|
+ passAwaySelect.value = '1'; // 已故
|
|
|
|
|
+ } else if (isDeceasedUnknown) {
|
|
|
|
|
+ passAwaySelect.value = '2'; // 未知
|
|
|
|
|
+ } else {
|
|
|
|
|
+ passAwaySelect.value = '0'; // 默认健在,除非有证据
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 婚姻状况
|
|
|
|
|
+ const maritalSelect = form.querySelector('[name="marital_status"]');
|
|
|
|
|
+ if (maritalSelect) {
|
|
|
|
|
+ if (person.spouse_name) {
|
|
|
|
|
+ maritalSelect.value = '2'; // 已婚
|
|
|
|
|
+ } else {
|
|
|
|
|
+ maritalSelect.value = '0'; // 未知
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1062,7 +1176,13 @@
|
|
|
if (newInfo && !currentNotes.includes(newInfo)) {
|
|
if (newInfo && !currentNotes.includes(newInfo)) {
|
|
|
notesField.value = currentNotes ? (currentNotes + '\n' + newInfo) : newInfo;
|
|
notesField.value = currentNotes ? (currentNotes + '\n' + newInfo) : newInfo;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 确保无论如何都触发一遍自动匹配事件
|
|
|
|
|
+ notesField.dispatchEvent(new Event('input', {bubbles: true}));
|
|
|
|
|
+ if (window.checkSpouseInNotes) {
|
|
|
|
|
+ window.checkSpouseInNotes();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// --- Auto-Linking Logic ---
|
|
// --- Auto-Linking Logic ---
|
|
|
if (person.matches) {
|
|
if (person.matches) {
|
|
|
// Priority: Father > Spouse (Configurable?)
|
|
// Priority: Father > Spouse (Configurable?)
|
|
@@ -1074,7 +1194,11 @@
|
|
|
const relTypeSelect = form.querySelector('[name="relation_type"]');
|
|
const relTypeSelect = form.querySelector('[name="relation_type"]');
|
|
|
|
|
|
|
|
if (relSelect && relTypeSelect) {
|
|
if (relSelect && relTypeSelect) {
|
|
|
- relSelect.value = father.id;
|
|
|
|
|
|
|
+ if (typeof tomSelectInstance !== 'undefined' && tomSelectInstance) {
|
|
|
|
|
+ tomSelectInstance.setValue(father.id);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ relSelect.value = father.id;
|
|
|
|
|
+ }
|
|
|
relTypeSelect.value = '1'; // 父子
|
|
relTypeSelect.value = '1'; // 父子
|
|
|
// Trigger change event if needed by other logic (not needed here yet)
|
|
// Trigger change event if needed by other logic (not needed here yet)
|
|
|
}
|
|
}
|
|
@@ -1084,7 +1208,11 @@
|
|
|
const relTypeSelect = form.querySelector('[name="relation_type"]');
|
|
const relTypeSelect = form.querySelector('[name="relation_type"]');
|
|
|
|
|
|
|
|
if (relSelect && relTypeSelect) {
|
|
if (relSelect && relTypeSelect) {
|
|
|
- relSelect.value = spouse.id;
|
|
|
|
|
|
|
+ if (typeof tomSelectInstance !== 'undefined' && tomSelectInstance) {
|
|
|
|
|
+ tomSelectInstance.setValue(spouse.id);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ relSelect.value = spouse.id;
|
|
|
|
|
+ }
|
|
|
relTypeSelect.value = '10'; // 夫妻
|
|
relTypeSelect.value = '10'; // 夫妻
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -1253,16 +1381,41 @@
|
|
|
return name;
|
|
return name;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function isFemaleSex(sexValue) {
|
|
|
|
|
+ if (sexValue === null || sexValue === undefined) return false;
|
|
|
|
|
+ const s = String(sexValue).trim().toLowerCase();
|
|
|
|
|
+ return s === '女' || s === '2' || s === 'female' || s === 'f';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function normalizeLookupName(name) {
|
|
|
|
|
+ if (!name) return '';
|
|
|
|
|
+ return manualSimplify(String(name)).trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Extracted function to process AI data and render tree
|
|
// Extracted function to process AI data and render tree
|
|
|
async function processAiData(data) {
|
|
async function processAiData(data) {
|
|
|
|
|
+ const spouseNameSet = new Set();
|
|
|
|
|
+ data.forEach(p => {
|
|
|
|
|
+ const n = normalizeLookupName(p.spouse_name);
|
|
|
|
|
+ if (n) spouseNameSet.add(n);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
// Clean Names First
|
|
// Clean Names First
|
|
|
data.forEach(p => {
|
|
data.forEach(p => {
|
|
|
// Determine "Original" (Raw) and "Simplified" (Cleaned)
|
|
// Determine "Original" (Raw) and "Simplified" (Cleaned)
|
|
|
let rawName = p.original_name || p.name;
|
|
let rawName = p.original_name || p.name;
|
|
|
- let simName = p.name; // This is the Simplified one from AI if original_name exists
|
|
|
|
|
|
|
+ let simName = p.name || p.original_name; // Prefer AI simplified name; fallback to raw
|
|
|
|
|
+
|
|
|
|
|
+ const ownName1 = normalizeLookupName(p.name);
|
|
|
|
|
+ const ownName2 = normalizeLookupName(p.original_name);
|
|
|
|
|
+ const isFemaleSpouse = isFemaleSex(p.sex) && (
|
|
|
|
|
+ !!normalizeLookupName(p.spouse_name) ||
|
|
|
|
|
+ (ownName1 && spouseNameSet.has(ownName1)) ||
|
|
|
|
|
+ (ownName2 && spouseNameSet.has(ownName2))
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
- // Clean the Simplified Name(本人:带“留”姓规则)
|
|
|
|
|
- p.simplified_name = cleanName(simName);
|
|
|
|
|
|
|
+ // 女性配偶:只繁转简,不拼接“留”;其他人维持原规则
|
|
|
|
|
+ p.simplified_name = isFemaleSpouse ? manualSimplify(simName) : cleanName(simName);
|
|
|
|
|
|
|
|
// Set the name to be the Raw Name for storage in 'name' column
|
|
// Set the name to be the Raw Name for storage in 'name' column
|
|
|
p.name = rawName;
|
|
p.name = rawName;
|
|
@@ -1591,5 +1744,105 @@
|
|
|
btn.disabled = false;
|
|
btn.disabled = false;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ // Check for duplicate name
|
|
|
|
|
+ let nameCheckTimeout = null;
|
|
|
|
|
+ const nameInput = document.getElementById('nameInput');
|
|
|
|
|
+ const nameCheckResult = document.getElementById('nameCheckResult');
|
|
|
|
|
+
|
|
|
|
|
+ if (nameInput) {
|
|
|
|
|
+ nameInput.addEventListener('input', function() {
|
|
|
|
|
+ clearTimeout(nameCheckTimeout);
|
|
|
|
|
+ const nameVal = this.value.trim();
|
|
|
|
|
+ if (!nameVal) {
|
|
|
|
|
+ nameCheckResult.innerHTML = '';
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nameCheckTimeout = setTimeout(() => {
|
|
|
|
|
+ fetch(`/manager/api/check_name?name=${encodeURIComponent(nameVal)}`)
|
|
|
|
|
+ .then(r => r.json())
|
|
|
|
|
+ .then(data => {
|
|
|
|
|
+ if (data.success && data.exists) {
|
|
|
|
|
+ let html = `<div class="alert alert-warning py-2 mb-0 mt-2 small">
|
|
|
|
|
+ <i class="bi bi-exclamation-triangle-fill"></i> 发现 <strong>${data.matches.length}</strong> 个同名记录,请确认是否为同一人:
|
|
|
|
|
+ <ul class="mb-0 mt-1 ps-3">`;
|
|
|
|
|
+ data.matches.forEach(m => {
|
|
|
|
|
+ let sex = m.sex === 1 ? '男' : (m.sex === 2 ? '女' : '未知');
|
|
|
|
|
+ let deadStr = m.is_pass_away == 1 ? ' (已故)' : (m.is_pass_away == 2 ? ' (未知)' : '');
|
|
|
|
|
+ html += `<li><a href="/manager/member_detail/${m.id}" target="_blank" class="alert-link">${m.name}</a> - ${sex} - 出生: ${m.birthday_str}${deadStr}</li>`;
|
|
|
|
|
+ });
|
|
|
|
|
+ html += `</ul></div>`;
|
|
|
|
|
+ nameCheckResult.innerHTML = html;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ nameCheckResult.innerHTML = '';
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(err => console.error('Error checking name:', err));
|
|
|
|
|
+ }, 600);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Auto-link spouse from notes if female
|
|
|
|
|
+ const notesInput = document.querySelector('textarea[name="notes"]');
|
|
|
|
|
+ const sexSelect = document.querySelector('select[name="sex"]');
|
|
|
|
|
+
|
|
|
|
|
+ // Attach to window so fillForm can explicitly call it
|
|
|
|
|
+ window.checkSpouseInNotes = function() {
|
|
|
|
|
+ if (!notesInput || !sexSelect) return;
|
|
|
|
|
+
|
|
|
|
|
+ // Only trigger if female
|
|
|
|
|
+ if (sexSelect.value === '2') {
|
|
|
|
|
+ const val = notesInput.value;
|
|
|
|
|
+ // Match cases like "配偶:张三", "配偶:张三", "配偶 张三", "配偶张三"
|
|
|
|
|
+ // We use a robust regex to get the word after 配偶
|
|
|
|
|
+ const match = val.match(/配偶[::\s]*([^\s;;,,。]+)/);
|
|
|
|
|
+ if (match && match[1]) {
|
|
|
|
|
+ const spouseName = match[1].trim();
|
|
|
|
|
+ const normalizedSpouse = spouseName.replace(/公$/, '').replace(/^留/, '');
|
|
|
|
|
+ const relatedSelect = document.querySelector('select[name="related_mid"]');
|
|
|
|
|
+ const relationTypeSelect = document.querySelector('select[name="relation_type"]');
|
|
|
|
|
+
|
|
|
|
|
+ if (relatedSelect && relationTypeSelect) {
|
|
|
|
|
+ for (let i = 0; i < relatedSelect.options.length; i++) {
|
|
|
|
|
+ const opt = relatedSelect.options[i];
|
|
|
|
|
+ if (!opt.value) continue;
|
|
|
|
|
+
|
|
|
|
|
+ const optText = opt.text.trim();
|
|
|
|
|
+ // Extract name before " (ID:" robustly
|
|
|
|
|
+ const optName = optText.replace(/\s*\(ID:.*$/, '').trim();
|
|
|
|
|
+ const normalizedOpt = optName.replace(/公$/, '').replace(/^留/, '');
|
|
|
|
|
+
|
|
|
|
|
+ // Match exact or without '公' suffix and without '留' prefix
|
|
|
|
|
+ if (optName === spouseName || normalizedOpt === normalizedSpouse ||
|
|
|
|
|
+ (normalizedOpt && normalizedSpouse && (normalizedOpt.includes(normalizedSpouse) || normalizedSpouse.includes(normalizedOpt)))) {
|
|
|
|
|
+ // If not already selected, select it and set relation to Spouse
|
|
|
|
|
+ if (relatedSelect.value !== opt.value) {
|
|
|
|
|
+ if (typeof tomSelectInstance !== 'undefined' && tomSelectInstance) {
|
|
|
|
|
+ tomSelectInstance.setValue(opt.value);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ relatedSelect.value = opt.value;
|
|
|
|
|
+ }
|
|
|
|
|
+ relationTypeSelect.value = '10'; // 10 is Spouse
|
|
|
|
|
+
|
|
|
|
|
+ // Optional visual feedback to user
|
|
|
|
|
+ notesInput.style.transition = "background-color 0.3s";
|
|
|
|
|
+ notesInput.style.backgroundColor = "#e8f5e9";
|
|
|
|
|
+ setTimeout(() => notesInput.style.backgroundColor = "", 1000);
|
|
|
|
|
+
|
|
|
|
|
+ console.log("Auto-linked spouse: ", optName);
|
|
|
|
|
+ }
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (notesInput && sexSelect) {
|
|
|
|
|
+ notesInput.addEventListener('input', window.checkSpouseInNotes);
|
|
|
|
|
+ sexSelect.addEventListener('change', window.checkSpouseInNotes);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
</script>
|
|
</script>
|
|
|
{% endblock %}
|
|
{% endblock %}
|