|
@@ -66,6 +66,34 @@
|
|
|
color: rgba(26,26,46,0.8) !important;
|
|
color: rgba(26,26,46,0.8) !important;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ .tree-node.direct-ancestor {
|
|
|
|
|
+ background: linear-gradient(135deg, #4a90d9, #2d5a87);
|
|
|
|
|
+ border-color: #4a90d9;
|
|
|
|
|
+ box-shadow: 0 0 20px rgba(74, 144, 217, 0.4);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node.direct-ancestor .node-name {
|
|
|
|
|
+ color: #fff !important;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node.direct-ancestor .node-name.simplified {
|
|
|
|
|
+ color: rgba(255,255,255,0.8) !important;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node.direct-ancestor .node-info {
|
|
|
|
|
+ color: rgba(255,255,255,0.9) !important;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node.clickable {
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: transform 0.2s, box-shadow 0.2s;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node.clickable:hover {
|
|
|
|
|
+ transform: translateY(-3px);
|
|
|
|
|
+ box-shadow: 0 8px 25px rgba(255, 255, 255, 0.15);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
.node-name {
|
|
.node-name {
|
|
|
font-size: 18px;
|
|
font-size: 18px;
|
|
|
font-weight: 700;
|
|
font-weight: 700;
|
|
@@ -88,7 +116,15 @@
|
|
|
.connection-line {
|
|
.connection-line {
|
|
|
width: 4px;
|
|
width: 4px;
|
|
|
height: 40px;
|
|
height: 40px;
|
|
|
- background: linear-gradient(to bottom, #4a69bd, #2d3436);
|
|
|
|
|
|
|
+ background: linear-gradient(to bottom, #4a90d9, #2d3436);
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .connection-line.vertical-line {
|
|
|
|
|
+ width: 4px;
|
|
|
|
|
+ height: 40px;
|
|
|
|
|
+ background: linear-gradient(to bottom, #4a90d9, #2d3436);
|
|
|
margin: 0 auto;
|
|
margin: 0 auto;
|
|
|
border-radius: 2px;
|
|
border-radius: 2px;
|
|
|
}
|
|
}
|
|
@@ -100,19 +136,37 @@
|
|
|
background: linear-gradient(to right, #4a69bd, #2d3436);
|
|
background: linear-gradient(to right, #4a69bd, #2d3436);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ .connection-line.horizontal.main-line {
|
|
|
|
|
+ width: 60px;
|
|
|
|
|
+ height: 4px;
|
|
|
|
|
+ margin: 8px auto;
|
|
|
|
|
+ background: linear-gradient(to right, #4a90d9, #4a90d9);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/* Children container */
|
|
/* Children container */
|
|
|
.children-container {
|
|
.children-container {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
|
|
+ flex-wrap: nowrap;
|
|
|
|
|
+ justify-content: flex-start;
|
|
|
gap: 30px;
|
|
gap: 30px;
|
|
|
margin-top: 20px;
|
|
margin-top: 20px;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ min-width: max-content;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.child-group {
|
|
.child-group {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .child-group.direct-line {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ position: relative;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* Expand/Collapse button */
|
|
/* Expand/Collapse button */
|
|
@@ -172,6 +226,69 @@
|
|
|
font-weight: 700;
|
|
font-weight: 700;
|
|
|
text-shadow: 0 0 10px rgba(255,215,0,0.3);
|
|
text-shadow: 0 0 10px rgba(255,215,0,0.3);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /* Tree container with scrollable area */
|
|
|
|
|
+ .tree-container {
|
|
|
|
|
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ padding: 30px;
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
|
|
|
+ width: calc(100% + 2px);
|
|
|
|
|
+ margin: 0 -1px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tree-container::-webkit-scrollbar {
|
|
|
|
|
+ width: 8px;
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tree-container::-webkit-scrollbar-track {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tree-container::-webkit-scrollbar-thumb {
|
|
|
|
|
+ background: rgba(255, 215, 0, 0.5);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tree-container::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
+ background: rgba(255, 215, 0, 0.7);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* Tree wrapper for horizontal scrolling */
|
|
|
|
|
+ .tree-wrapper {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ overflow-x: auto;
|
|
|
|
|
+ padding-bottom: 10px;
|
|
|
|
|
+ scrollbar-width: thin;
|
|
|
|
|
+ scrollbar-color: rgba(255,215,0,0.5) rgba(255,255,255,0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tree-wrapper::-webkit-scrollbar {
|
|
|
|
|
+ width: 8px;
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tree-wrapper::-webkit-scrollbar-track {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tree-wrapper::-webkit-scrollbar-thumb {
|
|
|
|
|
+ background: rgba(255, 215, 0, 0.5);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* Generation row wrapper */
|
|
|
|
|
+ .generation-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ overflow-x: auto;
|
|
|
|
|
+ }
|
|
|
</style>
|
|
</style>
|
|
|
{% endblock %}
|
|
{% endblock %}
|
|
|
|
|
|
|
@@ -456,11 +573,11 @@ function renderLineage(data) {
|
|
|
console.log('Rendering lineage:', data);
|
|
console.log('Rendering lineage:', data);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- const ancestorsHtml = renderAncestors(data.ancestors);
|
|
|
|
|
- console.log('Ancestors HTML generated:', ancestorsHtml.length);
|
|
|
|
|
- document.getElementById('ancestorsTree').innerHTML = ancestorsHtml;
|
|
|
|
|
|
|
+ const generationsHtml = renderGenerations(data.generations);
|
|
|
|
|
+ console.log('Generations HTML generated:', generationsHtml.length);
|
|
|
|
|
+ document.getElementById('ancestorsTree').innerHTML = generationsHtml;
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
- console.error('Error rendering ancestors:', e);
|
|
|
|
|
|
|
+ console.error('Error rendering generations:', e);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
@@ -480,25 +597,60 @@ function renderLineage(data) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// Render ancestors
|
|
|
|
|
-function renderAncestors(ancestors) {
|
|
|
|
|
- if (!ancestors || ancestors.length === 0) return '';
|
|
|
|
|
|
|
+// Render generations with ancestors and their siblings
|
|
|
|
|
+function renderGenerations(generations) {
|
|
|
|
|
+ if (!generations || generations.length === 0) return '';
|
|
|
|
|
|
|
|
let html = '<div class="text-center mb-6"><span class="generation-label">祖先谱系</span></div>';
|
|
let html = '<div class="text-center mb-6"><span class="generation-label">祖先谱系</span></div>';
|
|
|
html += '<div class="tree-wrapper">';
|
|
html += '<div class="tree-wrapper">';
|
|
|
|
|
|
|
|
- ancestors.reverse().forEach((person, index) => {
|
|
|
|
|
|
|
+ const reversedGenerations = [...generations].reverse();
|
|
|
|
|
+
|
|
|
|
|
+ reversedGenerations.forEach((gen, index) => {
|
|
|
if (index > 0) {
|
|
if (index > 0) {
|
|
|
- html += '<div class="connection-line"></div>';
|
|
|
|
|
|
|
+ html += '<div class="connection-line vertical-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}">
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const leftSiblings = gen.siblings.slice(0, Math.floor(gen.siblings.length / 2));
|
|
|
|
|
+ const rightSiblings = gen.siblings.slice(Math.floor(gen.siblings.length / 2));
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="generation-row">
|
|
|
|
|
+ <div class="children-container">
|
|
|
|
|
+ ${leftSiblings.map(sibling => `
|
|
|
|
|
+ <div class="child-group">
|
|
|
|
|
+ <div class="connection-line horizontal"></div>
|
|
|
|
|
+ ${renderTreeNode(sibling, false, false)}
|
|
|
|
|
+ ${sibling.has_children ? `
|
|
|
|
|
+ <button class="expand-btn" onclick="toggleChildren(this, ${sibling.id})">+</button>
|
|
|
|
|
+ <div class="children-container" style="display: none;" data-parent-id="${sibling.id}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ` : ''}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `).join('')}
|
|
|
|
|
+ <div class="child-group direct-line">
|
|
|
|
|
+ <div class="connection-line horizontal main-line"></div>
|
|
|
|
|
+ ${renderTreeNode(gen.ancestor, false, true)}
|
|
|
|
|
+ ${gen.ancestor.show_expand ? `
|
|
|
|
|
+ <button class="expand-btn" onclick="toggleChildren(this, ${gen.ancestor.id})">+</button>
|
|
|
|
|
+ <div class="children-container" style="display: none;" data-parent-id="${gen.ancestor.id}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ` : ''}
|
|
|
</div>
|
|
</div>
|
|
|
- `;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ ${rightSiblings.map(sibling => `
|
|
|
|
|
+ <div class="child-group">
|
|
|
|
|
+ <div class="connection-line horizontal"></div>
|
|
|
|
|
+ ${renderTreeNode(sibling, false, false)}
|
|
|
|
|
+ ${sibling.has_children ? `
|
|
|
|
|
+ <button class="expand-btn" onclick="toggleChildren(this, ${sibling.id})">+</button>
|
|
|
|
|
+ <div class="children-container" style="display: none;" data-parent-id="${sibling.id}">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ` : ''}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `).join('')}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
html += '</div>';
|
|
html += '</div>';
|
|
@@ -590,9 +742,14 @@ function renderChildrenRecursive(children, level = 0) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Render tree node
|
|
// Render tree node
|
|
|
-function renderTreeNode(person, isCenter = false) {
|
|
|
|
|
|
|
+function renderTreeNode(person, isCenter = false, isDirectLine = false) {
|
|
|
|
|
+ let className = 'clickable ';
|
|
|
|
|
+ if (isCenter) className += 'center';
|
|
|
|
|
+ else if (isDirectLine) className += 'direct-ancestor';
|
|
|
|
|
+ else className = className.trim();
|
|
|
|
|
+
|
|
|
return `
|
|
return `
|
|
|
- <div class="tree-node ${isCenter ? 'center' : ''}">
|
|
|
|
|
|
|
+ <div class="tree-node ${className}" data-id="${person.id}" onclick="openPersonDetail(${person.id})">
|
|
|
<div class="node-name">${person.name}</div>
|
|
<div class="node-name">${person.name}</div>
|
|
|
${person.simplified_name && person.simplified_name !== person.name ? `<div class="node-name simplified">(${person.simplified_name})</div>` : ''}
|
|
${person.simplified_name && person.simplified_name !== person.name ? `<div class="node-name simplified">(${person.simplified_name})</div>` : ''}
|
|
|
<div class="node-info">
|
|
<div class="node-info">
|
|
@@ -603,6 +760,11 @@ function renderTreeNode(person, isCenter = false) {
|
|
|
`;
|
|
`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Open person detail in new tab
|
|
|
|
|
+function openPersonDetail(personId) {
|
|
|
|
|
+ window.open(`/manager/member_detail/${personId}`, '_blank');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// Toggle children visibility with lazy loading
|
|
// Toggle children visibility with lazy loading
|
|
|
async function toggleChildren(btn, parentId) {
|
|
async function toggleChildren(btn, parentId) {
|
|
|
const container = btn.nextElementSibling;
|
|
const container = btn.nextElementSibling;
|
|
@@ -617,8 +779,14 @@ async function toggleChildren(btn, parentId) {
|
|
|
// Load children lazily
|
|
// Load children lazily
|
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
|
|
|
|
|
|
|
|
|
|
+ // Get already displayed descendant IDs to exclude
|
|
|
|
|
+ const excludedIds = getExcludedDescendantIds(btn);
|
|
|
|
|
+ const excludeParam = excludedIds.length > 0 ? `?exclude=${excludedIds.join(',')}` : '';
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`[ToggleChildren] Parent ID: ${parentId}, Excluded IDs: ${excludedIds}, URL: /manager/api/get_descendants/${parentId}${excludeParam}`);
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
- const response = await fetch(`/manager/api/get_descendants/${parentId}`, {
|
|
|
|
|
|
|
+ const response = await fetch(`/manager/api/get_descendants/${parentId}${excludeParam}`, {
|
|
|
credentials: 'include'
|
|
credentials: 'include'
|
|
|
});
|
|
});
|
|
|
const result = await response.json();
|
|
const result = await response.json();
|
|
@@ -639,6 +807,38 @@ async function toggleChildren(btn, parentId) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Get descendant IDs that are already displayed in the tree (to avoid duplicates)
|
|
|
|
|
+function getExcludedDescendantIds(btn) {
|
|
|
|
|
+ const excluded = new Set();
|
|
|
|
|
+
|
|
|
|
|
+ // Helper function to extract IDs from tree nodes
|
|
|
|
|
+ const extractIdsFromNodes = (container) => {
|
|
|
|
|
+ if (!container) return;
|
|
|
|
|
+ const nodes = container.querySelectorAll('.tree-node');
|
|
|
|
|
+ nodes.forEach(node => {
|
|
|
|
|
+ const id = node.getAttribute('data-id');
|
|
|
|
|
+ if (id && !isNaN(parseInt(id))) {
|
|
|
|
|
+ excluded.add(parseInt(id));
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Get IDs from ancestors tree
|
|
|
|
|
+ const ancestorsTree = document.getElementById('ancestorsTree');
|
|
|
|
|
+ extractIdsFromNodes(ancestorsTree);
|
|
|
|
|
+
|
|
|
|
|
+ // Get IDs from siblings tree (center person and siblings)
|
|
|
|
|
+ const siblingsTree = document.getElementById('siblingsTree');
|
|
|
|
|
+ extractIdsFromNodes(siblingsTree);
|
|
|
|
|
+
|
|
|
|
|
+ // Get IDs from children tree
|
|
|
|
|
+ const childrenTree = document.getElementById('childrenTree');
|
|
|
|
|
+ extractIdsFromNodes(childrenTree);
|
|
|
|
|
+
|
|
|
|
|
+ console.log('Excluded IDs:', Array.from(excluded));
|
|
|
|
|
+ return Array.from(excluded);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// Enter key search
|
|
// Enter key search
|
|
|
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
|
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
|
|
if (e.key === 'Enter') {
|
|
if (e.key === 'Enter') {
|