lineage_query.html 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. {% extends "layout.html" %}
  2. {% block title %}世系查询 - 家谱管理系统{% endblock %}
  3. {% block extra_css %}
  4. <style>
  5. .empty-state {
  6. display: flex;
  7. flex-direction: column;
  8. align-items: center;
  9. justify-content: center;
  10. height: 60vh;
  11. color: rgba(255,255,255,0.5);
  12. }
  13. .tree-container {
  14. padding: 20px;
  15. min-height: 60vh;
  16. background: #1a1a2e;
  17. border-radius: 12px;
  18. border: 1px solid rgba(255,215,0,0.2);
  19. }
  20. /* Tree node styles */
  21. .tree-wrapper {
  22. display: flex;
  23. flex-direction: column;
  24. align-items: center;
  25. padding: 20px 0;
  26. }
  27. .tree-node {
  28. background: linear-gradient(135deg, #2d3436, #1e272e);
  29. border: 2px solid #4a69bd;
  30. border-radius: 8px;
  31. padding: 12px 24px;
  32. margin: 10px 0;
  33. text-align: center;
  34. min-width: 160px;
  35. transition: all 0.3s;
  36. cursor: pointer;
  37. box-shadow: 0 4px 15px rgba(0,0,0,0.3);
  38. }
  39. .tree-node:hover {
  40. background: linear-gradient(135deg, #4a69bd, #2d3436);
  41. transform: scale(1.05);
  42. box-shadow: 0 6px 20px rgba(74, 105, 189, 0.4);
  43. }
  44. .tree-node.center {
  45. background: linear-gradient(135deg, #ffd700, #ff8c00);
  46. border-color: #ffd700;
  47. box-shadow: 0 0 30px rgba(255,215,0,0.5);
  48. }
  49. .tree-node.center .node-name {
  50. color: #1a1a2e !important;
  51. }
  52. .tree-node.center .node-name.simplified {
  53. color: rgba(26,26,46,0.8) !important;
  54. }
  55. .tree-node.center .node-info {
  56. color: rgba(26,26,46,0.8) !important;
  57. }
  58. .node-name {
  59. font-size: 18px;
  60. font-weight: 700;
  61. color: #fff;
  62. margin-bottom: 4px;
  63. }
  64. .node-name.simplified {
  65. font-size: 13px;
  66. color: rgba(255,255,255,0.7);
  67. }
  68. .node-info {
  69. font-size: 12px;
  70. color: rgba(255,255,255,0.8);
  71. font-weight: 500;
  72. }
  73. /* Connection lines */
  74. .connection-line {
  75. width: 4px;
  76. height: 40px;
  77. background: linear-gradient(to bottom, #4a69bd, #2d3436);
  78. margin: 0 auto;
  79. border-radius: 2px;
  80. }
  81. .connection-line.horizontal {
  82. width: 60px;
  83. height: 4px;
  84. margin: 8px auto;
  85. background: linear-gradient(to right, #4a69bd, #2d3436);
  86. }
  87. /* Children container */
  88. .children-container {
  89. display: flex;
  90. flex-wrap: wrap;
  91. justify-content: center;
  92. gap: 30px;
  93. margin-top: 20px;
  94. }
  95. .child-group {
  96. display: flex;
  97. flex-direction: column;
  98. align-items: center;
  99. }
  100. /* Expand/Collapse button */
  101. .expand-btn {
  102. background: linear-gradient(135deg, #4a69bd, #2d3436);
  103. border: 2px solid #4a69bd;
  104. border-radius: 50%;
  105. width: 36px;
  106. height: 36px;
  107. color: #fff;
  108. font-size: 18px;
  109. font-weight: bold;
  110. cursor: pointer;
  111. display: flex;
  112. align-items: center;
  113. justify-content: center;
  114. margin: 15px auto;
  115. transition: all 0.3s;
  116. box-shadow: 0 2px 10px rgba(0,0,0,0.3);
  117. }
  118. .expand-btn:hover {
  119. background: linear-gradient(135deg, #ffd700, #ff8c00);
  120. border-color: #ffd700;
  121. transform: rotate(90deg);
  122. box-shadow: 0 4px 15px rgba(255,215,0,0.4);
  123. }
  124. /* Generation label */
  125. .generation-label {
  126. color: #ffd700;
  127. font-size: 16px;
  128. font-weight: 700;
  129. margin-bottom: 20px;
  130. text-align: center;
  131. padding: 10px 20px;
  132. background: rgba(255,215,0,0.1);
  133. border: 1px solid rgba(255,215,0,0.3);
  134. border-radius: 25px;
  135. display: inline-block;
  136. }
  137. /* Search box */
  138. .search-box {
  139. max-width: 350px;
  140. }
  141. /* Section styling */
  142. .ancestors-section, .children-section {
  143. margin: 30px 0;
  144. }
  145. /* Header styling */
  146. .page-header {
  147. color: #ffd700;
  148. font-size: 24px;
  149. font-weight: 700;
  150. text-shadow: 0 0 10px rgba(255,215,0,0.3);
  151. }
  152. </style>
  153. {% endblock %}
  154. {% block content %}
  155. <div class="container-fluid">
  156. <!-- Header -->
  157. <div class="row mb-6">
  158. <div class="col-md-12">
  159. <div class="d-flex justify-content-between align-items-center">
  160. <h2 class="page-header">世系查询</h2>
  161. <div class="search-box">
  162. <div class="input-group">
  163. <input type="text" id="searchInput" class="form-control" placeholder="搜索成员姓名(支持简繁)" />
  164. <button class="btn btn-primary" onclick="searchMember()">
  165. <i class="bi bi-search"></i>
  166. </button>
  167. </div>
  168. </div>
  169. </div>
  170. </div>
  171. </div>
  172. <!-- Main Content -->
  173. <div class="row">
  174. <div class="col-md-12">
  175. <div id="treeContent">
  176. <!-- Empty State -->
  177. <div class="empty-state" id="emptyState">
  178. <i class="bi bi-search text-6xl mb-4"></i>
  179. <p class="text-lg">请在右上角搜索框中输入成员姓名</p>
  180. <p class="text-sm mt-2">支持简体和繁体姓名搜索</p>
  181. </div>
  182. <!-- Tree View -->
  183. <div id="treeView" style="display: none;" class="tree-container">
  184. <!-- Ancestors Section -->
  185. <div class="ancestors-section" id="ancestorsTree"></div>
  186. <!-- Siblings and Center Person Section -->
  187. <div class="siblings-section" id="siblingsTree"></div>
  188. <!-- Children Section -->
  189. <div class="children-section" id="childrenTree"></div>
  190. </div>
  191. </div>
  192. </div>
  193. </div>
  194. </div>
  195. <script>
  196. // Search member
  197. async function searchMember() {
  198. const keyword = document.getElementById('searchInput').value.trim();
  199. if (!keyword) {
  200. alert('请输入搜索关键词');
  201. return;
  202. }
  203. // Show loading state
  204. const searchBtn = document.querySelector('.search-box button');
  205. const originalBtnHtml = searchBtn.innerHTML;
  206. searchBtn.disabled = true;
  207. searchBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
  208. try {
  209. const response = await fetch('/manager/api/search_member', {
  210. method: 'POST',
  211. headers: { 'Content-Type': 'application/json' },
  212. body: JSON.stringify({ keyword: keyword }),
  213. credentials: 'include'
  214. });
  215. const result = await response.json();
  216. if (result.success && result.members) {
  217. if (result.members.length === 1) {
  218. // Only one match, load directly
  219. document.getElementById('emptyState').style.display = 'none';
  220. document.getElementById('treeView').style.display = 'block';
  221. await loadLineage(result.members[0].id);
  222. } else {
  223. // Multiple matches, show selection dialog
  224. showMemberSelection(result.members);
  225. }
  226. } else {
  227. alert('未找到匹配的成员');
  228. }
  229. } finally {
  230. // Restore button
  231. searchBtn.disabled = false;
  232. searchBtn.innerHTML = originalBtnHtml;
  233. }
  234. }
  235. // Show member selection dialog
  236. function showMemberSelection(members) {
  237. // Remove existing dialog
  238. const existingDialog = document.getElementById('memberSelectionDialog');
  239. if (existingDialog) {
  240. existingDialog.remove();
  241. }
  242. // Store members in sessionStorage
  243. sessionStorage.setItem('searchMembers', JSON.stringify(members));
  244. // Create dialog
  245. const dialog = document.createElement('div');
  246. dialog.id = 'memberSelectionDialog';
  247. dialog.style.cssText = `
  248. position: fixed;
  249. top: 50%;
  250. left: 50%;
  251. transform: translate(-50%, -50%);
  252. background: #1a1a2e;
  253. border: 2px solid #4a69bd;
  254. border-radius: 12px;
  255. padding: 24px;
  256. z-index: 10000;
  257. min-width: 400px;
  258. box-shadow: 0 10px 40px rgba(0,0,0,0.5);
  259. `;
  260. // Dialog content
  261. dialog.innerHTML = `
  262. <h3 style="color: #ffd700; margin-bottom: 16px; text-align: center;">找到多个匹配成员,请输入编号选择:</h3>
  263. <div style="margin-bottom: 16px; max-height: 200px; overflow-y: auto;">
  264. ${members.map((member, index) => `
  265. <div style="padding: 10px; border-bottom: 1px solid rgba(255,255,255,0.1);">
  266. <span style="color: #ffd700; margin-right: 10px;">${index + 1}.</span>
  267. <span style="color: #fff; font-weight: 600;">${member.name}</span>
  268. ${member.simplified_name && member.simplified_name !== member.name ?
  269. `<span style="color: rgba(255,255,255,0.7);">(${member.simplified_name})</span>` : ''}
  270. </div>
  271. `).join('')}
  272. </div>
  273. <input type="text" id="selectionInput"
  274. placeholder="请输入编号选择(1-${members.length})"
  275. style="width: 100%; padding: 10px; margin-bottom: 16px;
  276. background: #2d3436; border: 1px solid #4a69bd;
  277. border-radius: 8px; color: #fff; text-align: center;
  278. font-size: 16px;" />
  279. <div style="display: flex; justify-content: center; gap: 16px;">
  280. <button onclick="selectMember()"
  281. style="padding: 10px 30px; background: linear-gradient(135deg, #4a69bd, #2d3436);
  282. border: 2px solid #4a69bd; border-radius: 8px; color: #fff;
  283. cursor: pointer; font-weight: 600;">确定</button>
  284. <button onclick="closeSelectionDialog()"
  285. style="padding: 10px 30px; background: #2d3436;
  286. border: 2px solid #666; border-radius: 8px; color: #aaa;
  287. cursor: pointer;">取消</button>
  288. </div>
  289. `;
  290. // Add overlay
  291. const overlay = document.createElement('div');
  292. overlay.id = 'dialogOverlay';
  293. overlay.style.cssText = `
  294. position: fixed;
  295. top: 0;
  296. left: 0;
  297. width: 100%;
  298. height: 100%;
  299. background: rgba(0,0,0,0.7);
  300. z-index: 9999;
  301. `;
  302. overlay.onclick = closeSelectionDialog;
  303. document.body.appendChild(overlay);
  304. document.body.appendChild(dialog);
  305. // Focus input
  306. document.getElementById('selectionInput').focus();
  307. // Enter key
  308. document.getElementById('selectionInput').addEventListener('keypress', function(e) {
  309. if (e.key === 'Enter') {
  310. selectMember();
  311. }
  312. });
  313. }
  314. // Close selection dialog
  315. function closeSelectionDialog() {
  316. document.getElementById('memberSelectionDialog')?.remove();
  317. document.getElementById('dialogOverlay')?.remove();
  318. }
  319. // Select member
  320. function selectMember() {
  321. const input = document.getElementById('selectionInput');
  322. const index = parseInt(input.value) - 1;
  323. const membersStr = sessionStorage.getItem('searchMembers');
  324. if (!membersStr) {
  325. alert('数据错误,请重新搜索');
  326. closeSelectionDialog();
  327. return;
  328. }
  329. const members = JSON.parse(membersStr);
  330. if (index >= 0 && index < members.length) {
  331. // Load lineage for selected member
  332. document.getElementById('emptyState').style.display = 'none';
  333. document.getElementById('treeView').style.display = 'block';
  334. loadLineage(members[index].id);
  335. // Close dialog
  336. closeSelectionDialog();
  337. } else {
  338. alert(`请输入有效的编号(1-${members.length})`);
  339. }
  340. }
  341. // Load lineage data
  342. async function loadLineage(memberId) {
  343. // Show loading state - preserve container elements
  344. document.getElementById('ancestorsTree').innerHTML = `
  345. <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px;">
  346. <div class="spinner-border text-yellow-400" style="width: 3rem; height: 3rem;" role="status">
  347. <span class="visually-hidden">Loading...</span>
  348. </div>
  349. <p class="mt-4 text-white text-lg">正在加载祖先数据...</p>
  350. </div>
  351. `;
  352. document.getElementById('siblingsTree').innerHTML = `
  353. <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px;">
  354. <div class="spinner-border text-yellow-400" style="width: 3rem; height: 3rem;" role="status">
  355. <span class="visually-hidden">Loading...</span>
  356. </div>
  357. <p class="mt-4 text-white text-lg">正在加载同辈数据...</p>
  358. </div>
  359. `;
  360. document.getElementById('childrenTree').innerHTML = `
  361. <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px;">
  362. <div class="spinner-border text-yellow-400" style="width: 3rem; height: 3rem;" role="status">
  363. <span class="visually-hidden">Loading...</span>
  364. </div>
  365. <p class="mt-4 text-white text-lg">正在加载后代数据...</p>
  366. </div>
  367. `;
  368. try {
  369. const response = await fetch(`/manager/api/get_lineage/${memberId}`, {
  370. credentials: 'include'
  371. });
  372. const result = await response.json();
  373. if (result.success) {
  374. console.log('Lineage data received:', result.data);
  375. // Use requestAnimationFrame for smoother rendering
  376. requestAnimationFrame(() => {
  377. renderLineage(result.data);
  378. });
  379. } else {
  380. console.error('API error:', result.message);
  381. document.getElementById('ancestorsTree').innerHTML = '';
  382. document.getElementById('siblingsTree').innerHTML = `
  383. <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 200px;">
  384. <i class="bi bi-x-circle text-red-500 text-6xl mb-4"></i>
  385. <p class="text-white text-lg">加载失败</p>
  386. <p class="text-gray-400 text-sm">${result.message || '服务器错误,请稍后重试'}</p>
  387. </div>
  388. `;
  389. document.getElementById('childrenTree').innerHTML = '';
  390. }
  391. } catch (error) {
  392. console.error('Fetch error:', error);
  393. document.getElementById('ancestorsTree').innerHTML = '';
  394. document.getElementById('siblingsTree').innerHTML = `
  395. <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 200px;">
  396. <i class="bi bi-x-circle text-red-500 text-6xl mb-4"></i>
  397. <p class="text-white text-lg">加载失败</p>
  398. <p class="text-gray-400 text-sm">网络错误: ${error.message}</p>
  399. </div>
  400. `;
  401. document.getElementById('childrenTree').innerHTML = '';
  402. }
  403. }
  404. // Render lineage tree
  405. function renderLineage(data) {
  406. console.log('Rendering lineage:', data);
  407. try {
  408. const ancestorsHtml = renderAncestors(data.ancestors);
  409. console.log('Ancestors HTML generated:', ancestorsHtml.length);
  410. document.getElementById('ancestorsTree').innerHTML = ancestorsHtml;
  411. } catch (e) {
  412. console.error('Error rendering ancestors:', e);
  413. }
  414. try {
  415. const siblingsWithCenterHtml = renderSiblingsWithCenter(data.center, data.siblings);
  416. console.log('Siblings HTML generated:', siblingsWithCenterHtml.length);
  417. document.getElementById('siblingsTree').innerHTML = siblingsWithCenterHtml;
  418. } catch (e) {
  419. console.error('Error rendering siblings:', e);
  420. }
  421. try {
  422. const childrenHtml = renderChildrenTree(data.children);
  423. console.log('Children HTML generated:', childrenHtml.length);
  424. document.getElementById('childrenTree').innerHTML = childrenHtml;
  425. } catch (e) {
  426. console.error('Error rendering children:', e);
  427. }
  428. }
  429. // Render ancestors
  430. function renderAncestors(ancestors) {
  431. if (!ancestors || ancestors.length === 0) return '';
  432. let html = '<div class="text-center mb-6"><span class="generation-label">祖先谱系</span></div>';
  433. html += '<div class="tree-wrapper">';
  434. ancestors.reverse().forEach((person, index) => {
  435. if (index > 0) {
  436. html += '<div class="connection-line"></div>';
  437. }
  438. html += renderTreeNode(person, false);
  439. if (person.has_children) {
  440. html += `
  441. <button class="expand-btn" onclick="toggleChildren(this, ${person.id})">+</button>
  442. <div class="children-container" style="display: none;" data-parent-id="${person.id}">
  443. </div>
  444. `;
  445. }
  446. });
  447. html += '</div>';
  448. return html;
  449. }
  450. // Render center person
  451. function renderCenterPerson(person) {
  452. return `
  453. <div class="connection-line"></div>
  454. <div class="tree-node center">
  455. <div class="node-name">${person.name}</div>
  456. ${person.simplified_name && person.simplified_name !== person.name ? `<div class="node-name simplified">(${person.simplified_name})</div>` : ''}
  457. <div class="node-info">
  458. ${person.name_word ? `${person.name_word} · ` : ''}
  459. ${person.name_word_generation || ''}
  460. </div>
  461. </div>
  462. `;
  463. }
  464. // Render siblings with center person in the middle
  465. function renderSiblingsWithCenter(center, siblings) {
  466. const allSiblings = siblings || [];
  467. // Insert center person at the middle position
  468. const middleIndex = Math.floor(allSiblings.length / 2);
  469. const items = [];
  470. for (let i = 0; i < allSiblings.length; i++) {
  471. if (i === middleIndex) {
  472. items.push({ type: 'center', person: center });
  473. }
  474. items.push({ type: 'sibling', person: allSiblings[i] });
  475. }
  476. // If no siblings, just show center
  477. if (allSiblings.length === 0) {
  478. items.push({ type: 'center', person: center });
  479. }
  480. return `
  481. <div class="text-center mt-6 mb-4"><span class="generation-label">同辈兄弟姐妹</span></div>
  482. <div class="children-container">
  483. ${items.map(item => `
  484. <div class="child-group ${item.type === 'center' ? 'center-child' : ''}">
  485. <div class="connection-line horizontal"></div>
  486. ${renderTreeNode(item.person, item.type === 'center')}
  487. ${item.type !== 'center' && item.person.has_children ? `
  488. <button class="expand-btn" onclick="toggleChildren(this, ${item.person.id})">+</button>
  489. <div class="children-container" style="display: none;" data-parent-id="${item.person.id}">
  490. </div>
  491. ` : ''}
  492. </div>
  493. `).join('')}
  494. </div>
  495. `;
  496. }
  497. // Render children tree
  498. function renderChildrenTree(children) {
  499. if (!children || children.length === 0) return '';
  500. return `
  501. <div class="text-center mt-6 mb-4"><span class="generation-label">后代谱系</span></div>
  502. ${renderChildrenRecursive(children)}
  503. `;
  504. }
  505. // Render children recursively
  506. function renderChildrenRecursive(children, level = 0) {
  507. if (!children || children.length === 0) return '';
  508. return `
  509. <div class="children-container">
  510. ${children.map(child => `
  511. <div class="child-group">
  512. <div class="connection-line"></div>
  513. ${renderTreeNode(child, false)}
  514. ${child.has_children ? `
  515. <button class="expand-btn" onclick="toggleChildren(this, ${child.id})">+</button>
  516. <div class="children-container" style="display: none;" data-parent-id="${child.id}">
  517. </div>
  518. ` : ''}
  519. </div>
  520. `).join('')}
  521. </div>
  522. `;
  523. }
  524. // Render tree node
  525. function renderTreeNode(person, isCenter = false) {
  526. return `
  527. <div class="tree-node ${isCenter ? 'center' : ''}">
  528. <div class="node-name">${person.name}</div>
  529. ${person.simplified_name && person.simplified_name !== person.name ? `<div class="node-name simplified">(${person.simplified_name})</div>` : ''}
  530. <div class="node-info">
  531. ${person.name_word ? `${person.name_word} · ` : ''}
  532. ${person.name_word_generation || ''}
  533. </div>
  534. </div>
  535. `;
  536. }
  537. // Toggle children visibility with lazy loading
  538. async function toggleChildren(btn, parentId) {
  539. const container = btn.nextElementSibling;
  540. const isExpanded = container.style.display !== 'none';
  541. if (isExpanded) {
  542. container.style.display = 'none';
  543. btn.innerHTML = '+';
  544. } else {
  545. // Check if children are already loaded
  546. if (container.innerHTML.trim() === '') {
  547. // Load children lazily
  548. btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
  549. try {
  550. const response = await fetch(`/manager/api/get_descendants/${parentId}`, {
  551. credentials: 'include'
  552. });
  553. const result = await response.json();
  554. if (result.success && result.children) {
  555. // Render children
  556. container.innerHTML = renderChildrenRecursive(result.children);
  557. }
  558. } catch (error) {
  559. console.error('Failed to load children:', error);
  560. } finally {
  561. btn.innerHTML = '−';
  562. }
  563. }
  564. container.style.display = 'flex';
  565. btn.innerHTML = '−';
  566. }
  567. }
  568. // Enter key search
  569. document.getElementById('searchInput').addEventListener('keypress', function(e) {
  570. if (e.key === 'Enter') {
  571. searchMember();
  572. }
  573. });
  574. </script>
  575. {% endblock %}