|
|
@@ -0,0 +1,649 @@
|
|
|
+{% extends "layout.html" %}
|
|
|
+
|
|
|
+{% block title %}世系查询 - 家谱管理系统{% endblock %}
|
|
|
+
|
|
|
+{% block extra_css %}
|
|
|
+<style>
|
|
|
+ .empty-state {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 60vh;
|
|
|
+ color: rgba(255,255,255,0.5);
|
|
|
+ }
|
|
|
+
|
|
|
+ .tree-container {
|
|
|
+ padding: 20px;
|
|
|
+ min-height: 60vh;
|
|
|
+ background: #1a1a2e;
|
|
|
+ border-radius: 12px;
|
|
|
+ border: 1px solid rgba(255,215,0,0.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Tree node styles */
|
|
|
+ .tree-wrapper {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding: 20px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tree-node {
|
|
|
+ background: linear-gradient(135deg, #2d3436, #1e272e);
|
|
|
+ border: 2px solid #4a69bd;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 12px 24px;
|
|
|
+ margin: 10px 0;
|
|
|
+ text-align: center;
|
|
|
+ min-width: 160px;
|
|
|
+ transition: all 0.3s;
|
|
|
+ cursor: pointer;
|
|
|
+ box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
|
|
+ }
|
|
|
+
|
|
|
+ .tree-node:hover {
|
|
|
+ background: linear-gradient(135deg, #4a69bd, #2d3436);
|
|
|
+ transform: scale(1.05);
|
|
|
+ box-shadow: 0 6px 20px rgba(74, 105, 189, 0.4);
|
|
|
+ }
|
|
|
+
|
|
|
+ .tree-node.center {
|
|
|
+ background: linear-gradient(135deg, #ffd700, #ff8c00);
|
|
|
+ border-color: #ffd700;
|
|
|
+ box-shadow: 0 0 30px rgba(255,215,0,0.5);
|
|
|
+ }
|
|
|
+
|
|
|
+ .tree-node.center .node-name {
|
|
|
+ color: #1a1a2e !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tree-node.center .node-name.simplified {
|
|
|
+ color: rgba(26,26,46,0.8) !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tree-node.center .node-info {
|
|
|
+ color: rgba(26,26,46,0.8) !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .node-name {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #fff;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .node-name.simplified {
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(255,255,255,0.7);
|
|
|
+ }
|
|
|
+
|
|
|
+ .node-info {
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(255,255,255,0.8);
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Connection lines */
|
|
|
+ .connection-line {
|
|
|
+ width: 4px;
|
|
|
+ height: 40px;
|
|
|
+ background: linear-gradient(to bottom, #4a69bd, #2d3436);
|
|
|
+ margin: 0 auto;
|
|
|
+ border-radius: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .connection-line.horizontal {
|
|
|
+ width: 60px;
|
|
|
+ height: 4px;
|
|
|
+ margin: 8px auto;
|
|
|
+ background: linear-gradient(to right, #4a69bd, #2d3436);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Children container */
|
|
|
+ .children-container {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 30px;
|
|
|
+ margin-top: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .child-group {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Expand/Collapse button */
|
|
|
+ .expand-btn {
|
|
|
+ background: linear-gradient(135deg, #4a69bd, #2d3436);
|
|
|
+ border: 2px solid #4a69bd;
|
|
|
+ border-radius: 50%;
|
|
|
+ width: 36px;
|
|
|
+ height: 36px;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: bold;
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin: 15px auto;
|
|
|
+ transition: all 0.3s;
|
|
|
+ box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
|
|
+ }
|
|
|
+
|
|
|
+ .expand-btn:hover {
|
|
|
+ background: linear-gradient(135deg, #ffd700, #ff8c00);
|
|
|
+ border-color: #ffd700;
|
|
|
+ transform: rotate(90deg);
|
|
|
+ box-shadow: 0 4px 15px rgba(255,215,0,0.4);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Generation label */
|
|
|
+ .generation-label {
|
|
|
+ color: #ffd700;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 700;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ text-align: center;
|
|
|
+ padding: 10px 20px;
|
|
|
+ background: rgba(255,215,0,0.1);
|
|
|
+ border: 1px solid rgba(255,215,0,0.3);
|
|
|
+ border-radius: 25px;
|
|
|
+ display: inline-block;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Search box */
|
|
|
+ .search-box {
|
|
|
+ max-width: 350px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Section styling */
|
|
|
+ .ancestors-section, .children-section {
|
|
|
+ margin: 30px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Header styling */
|
|
|
+ .page-header {
|
|
|
+ color: #ffd700;
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 700;
|
|
|
+ text-shadow: 0 0 10px rgba(255,215,0,0.3);
|
|
|
+ }
|
|
|
+</style>
|
|
|
+{% endblock %}
|
|
|
+
|
|
|
+{% block content %}
|
|
|
+<div class="container-fluid">
|
|
|
+ <!-- Header -->
|
|
|
+ <div class="row mb-6">
|
|
|
+ <div class="col-md-12">
|
|
|
+ <div class="d-flex justify-content-between align-items-center">
|
|
|
+ <h2 class="page-header">世系查询</h2>
|
|
|
+ <div class="search-box">
|
|
|
+ <div class="input-group">
|
|
|
+ <input type="text" id="searchInput" class="form-control" placeholder="搜索成员姓名(支持简繁)" />
|
|
|
+ <button class="btn btn-primary" onclick="searchMember()">
|
|
|
+ <i class="bi bi-search"></i>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Main Content -->
|
|
|
+ <div class="row">
|
|
|
+ <div class="col-md-12">
|
|
|
+ <div id="treeContent">
|
|
|
+ <!-- Empty State -->
|
|
|
+ <div class="empty-state" id="emptyState">
|
|
|
+ <i class="bi bi-search text-6xl mb-4"></i>
|
|
|
+ <p class="text-lg">请在右上角搜索框中输入成员姓名</p>
|
|
|
+ <p class="text-sm mt-2">支持简体和繁体姓名搜索</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Tree View -->
|
|
|
+ <div id="treeView" style="display: none;" class="tree-container">
|
|
|
+ <!-- Ancestors Section -->
|
|
|
+ <div class="ancestors-section" id="ancestorsTree"></div>
|
|
|
+
|
|
|
+ <!-- Siblings and Center Person Section -->
|
|
|
+ <div class="siblings-section" id="siblingsTree"></div>
|
|
|
+
|
|
|
+ <!-- Children Section -->
|
|
|
+ <div class="children-section" id="childrenTree"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<script>
|
|
|
+// Search member
|
|
|
+async function searchMember() {
|
|
|
+ const keyword = document.getElementById('searchInput').value.trim();
|
|
|
+ if (!keyword) {
|
|
|
+ alert('请输入搜索关键词');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Show loading state
|
|
|
+ const searchBtn = document.querySelector('.search-box button');
|
|
|
+ const originalBtnHtml = searchBtn.innerHTML;
|
|
|
+ searchBtn.disabled = true;
|
|
|
+ searchBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch('/manager/api/search_member', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ keyword: keyword }),
|
|
|
+ credentials: 'include'
|
|
|
+ });
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+ if (result.success && result.members) {
|
|
|
+ if (result.members.length === 1) {
|
|
|
+ // Only one match, load directly
|
|
|
+ document.getElementById('emptyState').style.display = 'none';
|
|
|
+ document.getElementById('treeView').style.display = 'block';
|
|
|
+ await loadLineage(result.members[0].id);
|
|
|
+ } else {
|
|
|
+ // Multiple matches, show selection dialog
|
|
|
+ showMemberSelection(result.members);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ alert('未找到匹配的成员');
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ // Restore button
|
|
|
+ searchBtn.disabled = false;
|
|
|
+ searchBtn.innerHTML = originalBtnHtml;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Show member selection dialog
|
|
|
+function showMemberSelection(members) {
|
|
|
+ // Remove existing dialog
|
|
|
+ const existingDialog = document.getElementById('memberSelectionDialog');
|
|
|
+ if (existingDialog) {
|
|
|
+ existingDialog.remove();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Store members in sessionStorage
|
|
|
+ sessionStorage.setItem('searchMembers', JSON.stringify(members));
|
|
|
+
|
|
|
+ // Create dialog
|
|
|
+ const dialog = document.createElement('div');
|
|
|
+ dialog.id = 'memberSelectionDialog';
|
|
|
+ dialog.style.cssText = `
|
|
|
+ position: fixed;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ background: #1a1a2e;
|
|
|
+ border: 2px solid #4a69bd;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 24px;
|
|
|
+ z-index: 10000;
|
|
|
+ min-width: 400px;
|
|
|
+ box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
|
|
+ `;
|
|
|
+
|
|
|
+ // Dialog content
|
|
|
+ dialog.innerHTML = `
|
|
|
+ <h3 style="color: #ffd700; margin-bottom: 16px; text-align: center;">找到多个匹配成员,请输入编号选择:</h3>
|
|
|
+ <div style="margin-bottom: 16px; max-height: 200px; overflow-y: auto;">
|
|
|
+ ${members.map((member, index) => `
|
|
|
+ <div style="padding: 10px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
|
|
+ <span style="color: #ffd700; margin-right: 10px;">${index + 1}.</span>
|
|
|
+ <span style="color: #fff; font-weight: 600;">${member.name}</span>
|
|
|
+ ${member.simplified_name && member.simplified_name !== member.name ?
|
|
|
+ `<span style="color: rgba(255,255,255,0.7);">(${member.simplified_name})</span>` : ''}
|
|
|
+ </div>
|
|
|
+ `).join('')}
|
|
|
+ </div>
|
|
|
+ <input type="text" id="selectionInput"
|
|
|
+ placeholder="请输入编号选择(1-${members.length})"
|
|
|
+ style="width: 100%; padding: 10px; margin-bottom: 16px;
|
|
|
+ background: #2d3436; border: 1px solid #4a69bd;
|
|
|
+ border-radius: 8px; color: #fff; text-align: center;
|
|
|
+ font-size: 16px;" />
|
|
|
+ <div style="display: flex; justify-content: center; gap: 16px;">
|
|
|
+ <button onclick="selectMember()"
|
|
|
+ style="padding: 10px 30px; background: linear-gradient(135deg, #4a69bd, #2d3436);
|
|
|
+ border: 2px solid #4a69bd; border-radius: 8px; color: #fff;
|
|
|
+ cursor: pointer; font-weight: 600;">确定</button>
|
|
|
+ <button onclick="closeSelectionDialog()"
|
|
|
+ style="padding: 10px 30px; background: #2d3436;
|
|
|
+ border: 2px solid #666; border-radius: 8px; color: #aaa;
|
|
|
+ cursor: pointer;">取消</button>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // Add overlay
|
|
|
+ const overlay = document.createElement('div');
|
|
|
+ overlay.id = 'dialogOverlay';
|
|
|
+ overlay.style.cssText = `
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: rgba(0,0,0,0.7);
|
|
|
+ z-index: 9999;
|
|
|
+ `;
|
|
|
+ overlay.onclick = closeSelectionDialog;
|
|
|
+
|
|
|
+ document.body.appendChild(overlay);
|
|
|
+ document.body.appendChild(dialog);
|
|
|
+
|
|
|
+ // Focus input
|
|
|
+ document.getElementById('selectionInput').focus();
|
|
|
+
|
|
|
+ // Enter key
|
|
|
+ document.getElementById('selectionInput').addEventListener('keypress', function(e) {
|
|
|
+ if (e.key === 'Enter') {
|
|
|
+ selectMember();
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// Close selection dialog
|
|
|
+function closeSelectionDialog() {
|
|
|
+ document.getElementById('memberSelectionDialog')?.remove();
|
|
|
+ document.getElementById('dialogOverlay')?.remove();
|
|
|
+}
|
|
|
+
|
|
|
+// Select member
|
|
|
+function selectMember() {
|
|
|
+ const input = document.getElementById('selectionInput');
|
|
|
+ const index = parseInt(input.value) - 1;
|
|
|
+
|
|
|
+ const membersStr = sessionStorage.getItem('searchMembers');
|
|
|
+ if (!membersStr) {
|
|
|
+ alert('数据错误,请重新搜索');
|
|
|
+ closeSelectionDialog();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const members = JSON.parse(membersStr);
|
|
|
+
|
|
|
+ if (index >= 0 && index < members.length) {
|
|
|
+ // Load lineage for selected member
|
|
|
+ document.getElementById('emptyState').style.display = 'none';
|
|
|
+ document.getElementById('treeView').style.display = 'block';
|
|
|
+ loadLineage(members[index].id);
|
|
|
+
|
|
|
+ // Close dialog
|
|
|
+ closeSelectionDialog();
|
|
|
+ } else {
|
|
|
+ alert(`请输入有效的编号(1-${members.length})`);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Load lineage data
|
|
|
+async function loadLineage(memberId) {
|
|
|
+ // Show loading state - preserve container elements
|
|
|
+ document.getElementById('ancestorsTree').innerHTML = `
|
|
|
+ <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px;">
|
|
|
+ <div class="spinner-border text-yellow-400" style="width: 3rem; height: 3rem;" role="status">
|
|
|
+ <span class="visually-hidden">Loading...</span>
|
|
|
+ </div>
|
|
|
+ <p class="mt-4 text-white text-lg">正在加载祖先数据...</p>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ document.getElementById('siblingsTree').innerHTML = `
|
|
|
+ <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px;">
|
|
|
+ <div class="spinner-border text-yellow-400" style="width: 3rem; height: 3rem;" role="status">
|
|
|
+ <span class="visually-hidden">Loading...</span>
|
|
|
+ </div>
|
|
|
+ <p class="mt-4 text-white text-lg">正在加载同辈数据...</p>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ document.getElementById('childrenTree').innerHTML = `
|
|
|
+ <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px;">
|
|
|
+ <div class="spinner-border text-yellow-400" style="width: 3rem; height: 3rem;" role="status">
|
|
|
+ <span class="visually-hidden">Loading...</span>
|
|
|
+ </div>
|
|
|
+ <p class="mt-4 text-white text-lg">正在加载后代数据...</p>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(`/manager/api/get_lineage/${memberId}`, {
|
|
|
+ credentials: 'include'
|
|
|
+ });
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+ if (result.success) {
|
|
|
+ console.log('Lineage data received:', result.data);
|
|
|
+ // Use requestAnimationFrame for smoother rendering
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ renderLineage(result.data);
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ console.error('API error:', result.message);
|
|
|
+ document.getElementById('ancestorsTree').innerHTML = '';
|
|
|
+ document.getElementById('siblingsTree').innerHTML = `
|
|
|
+ <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 200px;">
|
|
|
+ <i class="bi bi-x-circle text-red-500 text-6xl mb-4"></i>
|
|
|
+ <p class="text-white text-lg">加载失败</p>
|
|
|
+ <p class="text-gray-400 text-sm">${result.message || '服务器错误,请稍后重试'}</p>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ document.getElementById('childrenTree').innerHTML = '';
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Fetch error:', error);
|
|
|
+ document.getElementById('ancestorsTree').innerHTML = '';
|
|
|
+ document.getElementById('siblingsTree').innerHTML = `
|
|
|
+ <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 200px;">
|
|
|
+ <i class="bi bi-x-circle text-red-500 text-6xl mb-4"></i>
|
|
|
+ <p class="text-white text-lg">加载失败</p>
|
|
|
+ <p class="text-gray-400 text-sm">网络错误: ${error.message}</p>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ document.getElementById('childrenTree').innerHTML = '';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Render lineage tree
|
|
|
+function renderLineage(data) {
|
|
|
+ console.log('Rendering lineage:', data);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const ancestorsHtml = renderAncestors(data.ancestors);
|
|
|
+ console.log('Ancestors HTML generated:', ancestorsHtml.length);
|
|
|
+ document.getElementById('ancestorsTree').innerHTML = ancestorsHtml;
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Error rendering ancestors:', e);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const siblingsWithCenterHtml = renderSiblingsWithCenter(data.center, data.siblings);
|
|
|
+ console.log('Siblings HTML generated:', siblingsWithCenterHtml.length);
|
|
|
+ document.getElementById('siblingsTree').innerHTML = siblingsWithCenterHtml;
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Error rendering siblings:', e);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const childrenHtml = renderChildrenTree(data.children);
|
|
|
+ console.log('Children HTML generated:', childrenHtml.length);
|
|
|
+ document.getElementById('childrenTree').innerHTML = childrenHtml;
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Error rendering children:', e);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Render ancestors
|
|
|
+function renderAncestors(ancestors) {
|
|
|
+ if (!ancestors || ancestors.length === 0) return '';
|
|
|
+
|
|
|
+ let html = '<div class="text-center mb-6"><span class="generation-label">祖先谱系</span></div>';
|
|
|
+ html += '<div class="tree-wrapper">';
|
|
|
+
|
|
|
+ ancestors.reverse().forEach((person, index) => {
|
|
|
+ if (index > 0) {
|
|
|
+ html += '<div class="connection-line"></div>';
|
|
|
+ }
|
|
|
+ html += renderTreeNode(person, false);
|
|
|
+ if (person.has_children) {
|
|
|
+ html += `
|
|
|
+ <button class="expand-btn" onclick="toggleChildren(this, ${person.id})">+</button>
|
|
|
+ <div class="children-container" style="display: none;" data-parent-id="${person.id}">
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ html += '</div>';
|
|
|
+ return html;
|
|
|
+}
|
|
|
+
|
|
|
+// Render center person
|
|
|
+function renderCenterPerson(person) {
|
|
|
+ return `
|
|
|
+ <div class="connection-line"></div>
|
|
|
+ <div class="tree-node center">
|
|
|
+ <div class="node-name">${person.name}</div>
|
|
|
+ ${person.simplified_name && person.simplified_name !== person.name ? `<div class="node-name simplified">(${person.simplified_name})</div>` : ''}
|
|
|
+ <div class="node-info">
|
|
|
+ ${person.name_word ? `${person.name_word} · ` : ''}
|
|
|
+ ${person.name_word_generation || ''}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+}
|
|
|
+
|
|
|
+// Render siblings with center person in the middle
|
|
|
+function renderSiblingsWithCenter(center, siblings) {
|
|
|
+ const allSiblings = siblings || [];
|
|
|
+
|
|
|
+ // Insert center person at the middle position
|
|
|
+ const middleIndex = Math.floor(allSiblings.length / 2);
|
|
|
+ const items = [];
|
|
|
+
|
|
|
+ for (let i = 0; i < allSiblings.length; i++) {
|
|
|
+ if (i === middleIndex) {
|
|
|
+ items.push({ type: 'center', person: center });
|
|
|
+ }
|
|
|
+ items.push({ type: 'sibling', person: allSiblings[i] });
|
|
|
+ }
|
|
|
+
|
|
|
+ // If no siblings, just show center
|
|
|
+ if (allSiblings.length === 0) {
|
|
|
+ items.push({ type: 'center', person: center });
|
|
|
+ }
|
|
|
+
|
|
|
+ return `
|
|
|
+ <div class="text-center mt-6 mb-4"><span class="generation-label">同辈兄弟姐妹</span></div>
|
|
|
+ <div class="children-container">
|
|
|
+ ${items.map(item => `
|
|
|
+ <div class="child-group ${item.type === 'center' ? 'center-child' : ''}">
|
|
|
+ <div class="connection-line horizontal"></div>
|
|
|
+ ${renderTreeNode(item.person, item.type === 'center')}
|
|
|
+ ${item.type !== 'center' && item.person.has_children ? `
|
|
|
+ <button class="expand-btn" onclick="toggleChildren(this, ${item.person.id})">+</button>
|
|
|
+ <div class="children-container" style="display: none;" data-parent-id="${item.person.id}">
|
|
|
+ </div>
|
|
|
+ ` : ''}
|
|
|
+ </div>
|
|
|
+ `).join('')}
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+}
|
|
|
+
|
|
|
+// Render children tree
|
|
|
+function renderChildrenTree(children) {
|
|
|
+ if (!children || children.length === 0) return '';
|
|
|
+
|
|
|
+ return `
|
|
|
+ <div class="text-center mt-6 mb-4"><span class="generation-label">后代谱系</span></div>
|
|
|
+ ${renderChildrenRecursive(children)}
|
|
|
+ `;
|
|
|
+}
|
|
|
+
|
|
|
+// Render children recursively
|
|
|
+function renderChildrenRecursive(children, level = 0) {
|
|
|
+ if (!children || children.length === 0) return '';
|
|
|
+
|
|
|
+ return `
|
|
|
+ <div class="children-container">
|
|
|
+ ${children.map(child => `
|
|
|
+ <div class="child-group">
|
|
|
+ <div class="connection-line"></div>
|
|
|
+ ${renderTreeNode(child, false)}
|
|
|
+ ${child.has_children ? `
|
|
|
+ <button class="expand-btn" onclick="toggleChildren(this, ${child.id})">+</button>
|
|
|
+ <div class="children-container" style="display: none;" data-parent-id="${child.id}">
|
|
|
+ </div>
|
|
|
+ ` : ''}
|
|
|
+ </div>
|
|
|
+ `).join('')}
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+}
|
|
|
+
|
|
|
+// Render tree node
|
|
|
+function renderTreeNode(person, isCenter = false) {
|
|
|
+ return `
|
|
|
+ <div class="tree-node ${isCenter ? 'center' : ''}">
|
|
|
+ <div class="node-name">${person.name}</div>
|
|
|
+ ${person.simplified_name && person.simplified_name !== person.name ? `<div class="node-name simplified">(${person.simplified_name})</div>` : ''}
|
|
|
+ <div class="node-info">
|
|
|
+ ${person.name_word ? `${person.name_word} · ` : ''}
|
|
|
+ ${person.name_word_generation || ''}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+}
|
|
|
+
|
|
|
+// Toggle children visibility with lazy loading
|
|
|
+async function toggleChildren(btn, parentId) {
|
|
|
+ const container = btn.nextElementSibling;
|
|
|
+ const isExpanded = container.style.display !== 'none';
|
|
|
+
|
|
|
+ if (isExpanded) {
|
|
|
+ container.style.display = 'none';
|
|
|
+ btn.innerHTML = '+';
|
|
|
+ } else {
|
|
|
+ // Check if children are already loaded
|
|
|
+ if (container.innerHTML.trim() === '') {
|
|
|
+ // Load children lazily
|
|
|
+ btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(`/manager/api/get_descendants/${parentId}`, {
|
|
|
+ credentials: 'include'
|
|
|
+ });
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.success && result.children) {
|
|
|
+ // Render children
|
|
|
+ container.innerHTML = renderChildrenRecursive(result.children);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Failed to load children:', error);
|
|
|
+ } finally {
|
|
|
+ btn.innerHTML = '−';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ container.style.display = 'flex';
|
|
|
+ btn.innerHTML = '−';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Enter key search
|
|
|
+document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
|
|
+ if (e.key === 'Enter') {
|
|
|
+ searchMember();
|
|
|
+ }
|
|
|
+});
|
|
|
+</script>
|
|
|
+{% endblock %}
|