lineage_query.html 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223
  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: calc(100vh - 155px);
  11. min-height: 400px;
  12. color: rgba(255,255,255,0.5);
  13. }
  14. /* ── 新竖列布局 ── */
  15. .lineage-view {
  16. display: flex;
  17. flex-direction: column;
  18. align-items: flex-start;
  19. min-width: max-content;
  20. padding: 10px 60px 60px;
  21. }
  22. /* 每一行容器 */
  23. .lin-row {
  24. display: flex;
  25. flex-direction: row;
  26. align-items: flex-start;
  27. min-width: fit-content;
  28. justify-content: flex-start;
  29. }
  30. /* 同代横排(祖先/中心 + 兄弟按 child_order 合并排序) */
  31. .gen-peer-row {
  32. display: flex;
  33. flex-direction: row;
  34. flex-wrap: nowrap;
  35. gap: 12px;
  36. align-items: flex-start;
  37. padding: 0 6px;
  38. }
  39. /* 每个人的列:排行徽章 + 节点卡 */
  40. .gen-peer-col {
  41. display: flex;
  42. flex-direction: column;
  43. align-items: center;
  44. flex-shrink: 0;
  45. }
  46. /* 直系祖先/中心所在列:加竖线指示器 */
  47. .gen-peer-col.direct-col {
  48. position: relative;
  49. }
  50. /* 竖连接线:居中于 lin-row */
  51. .lin-center {
  52. min-width: 200px;
  53. display: flex;
  54. flex-direction: column;
  55. align-items: center;
  56. flex-shrink: 0;
  57. }
  58. .lin-vline {
  59. width: 3px;
  60. min-height: 36px;
  61. background: linear-gradient(to bottom, rgba(74,144,217,0.9), rgba(74,144,217,0.3));
  62. border-radius: 2px;
  63. margin: 0 auto;
  64. }
  65. /* 连接线行(居中) */
  66. .lin-vline-row {
  67. display: flex;
  68. flex-direction: row;
  69. min-width: fit-content;
  70. padding: 0 6px;
  71. }
  72. /* 子女排列区:由 JS alignAndCenter 动态设置 margin-left 居中 */
  73. .lin-children {
  74. display: flex;
  75. flex-direction: row;
  76. align-items: flex-start;
  77. flex-wrap: nowrap;
  78. gap: 14px;
  79. margin-left: 0;
  80. }
  81. .lin-child-col {
  82. display: flex;
  83. flex-direction: column;
  84. align-items: center;
  85. flex-shrink: 0;
  86. }
  87. .child-order-badge {
  88. background: rgba(255,215,0,0.12);
  89. border: 1px solid rgba(255,215,0,0.35);
  90. color: #ffd700;
  91. font-size: 11px;
  92. font-weight: 600;
  93. padding: 2px 10px;
  94. border-radius: 10px;
  95. margin-bottom: 5px;
  96. white-space: nowrap;
  97. }
  98. /* 分区标题 */
  99. .section-divider {
  100. display: flex;
  101. align-items: center;
  102. gap: 12px;
  103. margin: 18px 0 10px;
  104. color: rgba(255,215,0,0.6);
  105. font-size: 12px;
  106. font-weight: 500;
  107. white-space: nowrap;
  108. }
  109. .section-divider::after {
  110. content: '';
  111. flex: 1;
  112. height: 1px;
  113. background: rgba(255,215,0,0.15);
  114. min-width: 40px;
  115. }
  116. .load-more-ancestors-btn {
  117. display: flex;
  118. align-items: center;
  119. gap: 8px;
  120. background: linear-gradient(135deg, rgba(74,105,189,0.15), rgba(74,105,189,0.05));
  121. border: 1px dashed rgba(74,144,217,0.5);
  122. border-radius: 8px;
  123. color: rgba(74,144,217,0.9);
  124. font-size: 13px;
  125. padding: 10px 20px;
  126. cursor: pointer;
  127. margin: 8px auto 4px;
  128. transition: all 0.2s;
  129. }
  130. .load-more-ancestors-btn:hover {
  131. background: linear-gradient(135deg, rgba(74,105,189,0.3), rgba(74,105,189,0.15));
  132. border-color: #4a90d9;
  133. color: #4a90d9;
  134. }
  135. .load-more-ancestors-btn:disabled {
  136. opacity: 0.5;
  137. cursor: not-allowed;
  138. }
  139. .tree-container {
  140. padding: 20px;
  141. height: calc(100vh - 155px);
  142. min-height: 500px;
  143. background: #1a1a2e;
  144. border-radius: 12px;
  145. border: 1px solid rgba(255,215,0,0.2);
  146. overflow: auto;
  147. position: relative;
  148. }
  149. /* Tree node styles */
  150. .tree-wrapper {
  151. display: flex;
  152. flex-direction: column;
  153. align-items: center;
  154. padding: 20px 0;
  155. }
  156. .tree-node {
  157. background: linear-gradient(135deg, #2d3436, #1e272e);
  158. border: 2px solid #4a69bd;
  159. border-radius: 8px;
  160. padding: 12px 24px;
  161. margin: 10px 0;
  162. text-align: center;
  163. min-width: 160px;
  164. transition: all 0.3s;
  165. cursor: pointer;
  166. box-shadow: 0 4px 15px rgba(0,0,0,0.3);
  167. }
  168. .tree-node:hover {
  169. background: linear-gradient(135deg, #4a69bd, #2d3436);
  170. transform: scale(1.05);
  171. box-shadow: 0 6px 20px rgba(74, 105, 189, 0.4);
  172. }
  173. .tree-node.center {
  174. background: linear-gradient(135deg, #ffd700, #ff8c00);
  175. border-color: #ffd700;
  176. box-shadow: 0 0 30px rgba(255,215,0,0.5);
  177. }
  178. .tree-node.center .node-name {
  179. color: #1a1a2e !important;
  180. }
  181. .tree-node.center .node-name.simplified {
  182. color: rgba(26,26,46,0.8) !important;
  183. }
  184. .tree-node.center .node-info {
  185. color: rgba(26,26,46,0.8) !important;
  186. }
  187. .tree-node.direct-ancestor {
  188. background: linear-gradient(135deg, #4a90d9, #2d5a87);
  189. border-color: #4a90d9;
  190. box-shadow: 0 0 20px rgba(74, 144, 217, 0.4);
  191. }
  192. .tree-node.direct-ancestor .node-name {
  193. color: #fff !important;
  194. }
  195. .tree-node.direct-ancestor .node-name.simplified {
  196. color: rgba(255,255,255,0.8) !important;
  197. }
  198. .tree-node.direct-ancestor .node-info {
  199. color: rgba(255,255,255,0.9) !important;
  200. }
  201. .tree-node.clickable {
  202. cursor: pointer;
  203. transition: transform 0.2s, box-shadow 0.2s;
  204. }
  205. .tree-node.clickable:hover {
  206. transform: translateY(-3px);
  207. box-shadow: 0 8px 25px rgba(255, 255, 255, 0.15);
  208. }
  209. .node-name {
  210. font-size: 18px;
  211. font-weight: 700;
  212. color: #fff;
  213. margin-bottom: 4px;
  214. }
  215. .node-name.simplified {
  216. font-size: 13px;
  217. color: rgba(255,255,255,0.7);
  218. }
  219. .node-info {
  220. font-size: 12px;
  221. color: rgba(255,255,255,0.8);
  222. font-weight: 500;
  223. }
  224. /* Connection lines */
  225. .connection-line {
  226. width: 4px;
  227. height: 40px;
  228. background: linear-gradient(to bottom, #4a90d9, #2d3436);
  229. margin: 0 auto;
  230. border-radius: 2px;
  231. }
  232. .connection-line.vertical-line {
  233. width: 4px;
  234. height: 40px;
  235. background: linear-gradient(to bottom, #4a90d9, #2d3436);
  236. margin: 0 auto;
  237. border-radius: 2px;
  238. }
  239. .connection-line.horizontal {
  240. width: 60px;
  241. height: 4px;
  242. margin: 8px auto;
  243. background: linear-gradient(to right, #4a69bd, #2d3436);
  244. }
  245. .connection-line.horizontal.main-line {
  246. width: 60px;
  247. height: 4px;
  248. margin: 8px auto;
  249. background: linear-gradient(to right, #4a90d9, #4a90d9);
  250. }
  251. /* Children container */
  252. .children-container {
  253. display: flex;
  254. flex-wrap: nowrap;
  255. justify-content: flex-start;
  256. gap: 30px;
  257. margin-top: 20px;
  258. align-items: flex-start;
  259. min-width: max-content;
  260. }
  261. .child-group {
  262. display: flex;
  263. flex-direction: column;
  264. align-items: center;
  265. flex-shrink: 0;
  266. }
  267. .child-group.direct-line {
  268. display: flex;
  269. flex-direction: column;
  270. align-items: center;
  271. flex-shrink: 0;
  272. position: relative;
  273. }
  274. /* Expand/Collapse button */
  275. .expand-btn {
  276. background: linear-gradient(135deg, #4a69bd, #2d3436);
  277. border: 2px solid #4a69bd;
  278. border-radius: 50%;
  279. width: 36px;
  280. height: 36px;
  281. color: #fff;
  282. font-size: 18px;
  283. font-weight: bold;
  284. cursor: pointer;
  285. display: flex;
  286. align-items: center;
  287. justify-content: center;
  288. margin: 15px auto;
  289. transition: all 0.3s;
  290. box-shadow: 0 2px 10px rgba(0,0,0,0.3);
  291. }
  292. .expand-btn:hover {
  293. background: linear-gradient(135deg, #ffd700, #ff8c00);
  294. border-color: #ffd700;
  295. transform: rotate(90deg);
  296. box-shadow: 0 4px 15px rgba(255,215,0,0.4);
  297. }
  298. /* Generation label */
  299. .generation-label {
  300. color: #ffd700;
  301. font-size: 16px;
  302. font-weight: 700;
  303. margin-bottom: 20px;
  304. text-align: center;
  305. padding: 10px 20px;
  306. background: rgba(255,215,0,0.1);
  307. border: 1px solid rgba(255,215,0,0.3);
  308. border-radius: 25px;
  309. display: inline-block;
  310. }
  311. /* Search box */
  312. .search-box {
  313. max-width: 350px;
  314. }
  315. /* Section styling */
  316. .ancestors-section, .children-section {
  317. margin: 30px 0;
  318. }
  319. /* Header styling */
  320. .page-header {
  321. color: #ffd700;
  322. font-size: 24px;
  323. font-weight: 700;
  324. text-shadow: 0 0 10px rgba(255,215,0,0.3);
  325. }
  326. /* Tree container with scrollable area */
  327. .tree-container {
  328. background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
  329. border-radius: 16px;
  330. padding: 20px;
  331. box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  332. }
  333. .tree-container::-webkit-scrollbar {
  334. width: 8px;
  335. height: 8px;
  336. }
  337. .tree-container::-webkit-scrollbar-track {
  338. background: rgba(255, 255, 255, 0.1);
  339. border-radius: 4px;
  340. }
  341. .tree-container::-webkit-scrollbar-thumb {
  342. background: rgba(255, 215, 0, 0.5);
  343. border-radius: 4px;
  344. }
  345. .tree-container::-webkit-scrollbar-thumb:hover {
  346. background: rgba(255, 215, 0, 0.7);
  347. }
  348. /* Tree wrapper for horizontal scrolling */
  349. .tree-wrapper {
  350. width: 100%;
  351. overflow-x: auto;
  352. padding-bottom: 10px;
  353. scrollbar-width: thin;
  354. scrollbar-color: rgba(255,215,0,0.5) rgba(255,255,255,0.1);
  355. }
  356. .tree-wrapper::-webkit-scrollbar {
  357. width: 8px;
  358. height: 8px;
  359. }
  360. .tree-wrapper::-webkit-scrollbar-track {
  361. background: rgba(255, 255, 255, 0.1);
  362. border-radius: 4px;
  363. }
  364. .tree-wrapper::-webkit-scrollbar-thumb {
  365. background: rgba(255, 215, 0, 0.5);
  366. border-radius: 4px;
  367. }
  368. /* Generation row wrapper */
  369. .generation-row {
  370. display: flex;
  371. justify-content: center;
  372. width: 100%;
  373. overflow-x: auto;
  374. }
  375. /* Adoption styles */
  376. .tree-node.adopted-out {
  377. border: 2px dashed #ff6b6b !important;
  378. background: rgba(255, 107, 107, 0.1) !important;
  379. }
  380. /* 入继节点:橙黄色虚线框,区别于实线 */
  381. .tree-node.adopted-in {
  382. border: 2px dashed #f59e0b !important;
  383. background: rgba(245,158,11,0.08) !important;
  384. }
  385. .adoption-label {
  386. font-size: 10px;
  387. color: #ff6b6b;
  388. font-weight: bold;
  389. margin-top: 4px;
  390. text-align: center;
  391. }
  392. .adoption-label.adopted-in-label {
  393. color: #f59e0b;
  394. }
  395. /* 兄弟节点:稍小,低调 */
  396. .tree-node.sibling-node {
  397. background: rgba(74,105,189,0.2);
  398. border-color: rgba(74,105,189,0.4);
  399. min-width: 110px;
  400. padding: 8px 14px;
  401. }
  402. .tree-node.sibling-node .node-name {
  403. font-size: 14px;
  404. }
  405. .tree-node.sibling-node .node-info {
  406. font-size: 11px;
  407. }
  408. /* 世系模式开关:所有样式已内联到HTML元素上 */
  409. </style>
  410. {% endblock %}
  411. {% block content %}
  412. <div class="container-fluid">
  413. <!-- Header -->
  414. <div class="row mb-6">
  415. <div class="col-md-12">
  416. <div class="d-flex justify-content-between align-items-center">
  417. <h2 class="page-header">世系查询</h2>
  418. <div class="d-flex align-items-center" style="gap:10px;">
  419. <!-- 世系追溯模式:左右胶囊开关 -->
  420. <div id="lineageModeSwitch" style="display:inline-flex;align-items:center;background:#e5e7eb;border-radius:24px;padding:3px;gap:2px;flex-shrink:0;">
  421. <button id="modeIncense"
  422. onclick="setLineageMode('incense')"
  423. title="入继人员以养父为上辈,沿宗族香火追溯"
  424. style="padding:6px 20px;border:none;border-radius:20px;font-size:13px;font-weight:700;cursor:pointer;white-space:nowrap;background:#f59e0b;color:#1a1a2e;box-shadow:0 2px 8px rgba(245,158,11,0.5);">香火传承</button>
  425. <button id="modeBlood"
  426. onclick="setLineageMode('blood')"
  427. title="出继人员以亲生父为上辈,沿血缘源流追溯"
  428. style="padding:6px 20px;border:none;border-radius:20px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;background:transparent;color:#6b7280;">血缘传承</button>
  429. </div>
  430. <!-- 搜索框 -->
  431. <div class="search-box">
  432. <div class="input-group">
  433. <input type="text" id="searchInput" class="form-control" placeholder="搜索成员姓名(支持简繁)" />
  434. <button class="btn btn-primary" onclick="searchMember()">
  435. <i class="bi bi-search"></i>
  436. </button>
  437. </div>
  438. </div>
  439. </div>
  440. </div>
  441. </div>
  442. </div>
  443. <!-- Main Content -->
  444. <div class="row">
  445. <div class="col-md-12">
  446. <div id="treeContent">
  447. <!-- Empty State -->
  448. <div class="empty-state" id="emptyState">
  449. <i class="bi bi-search text-6xl mb-4"></i>
  450. <p class="text-lg">请在右上角搜索框中输入成员姓名</p>
  451. <p class="text-sm mt-2">支持简体和繁体姓名搜索</p>
  452. </div>
  453. <!-- Tree View -->
  454. <div id="treeView" style="display: none;" class="tree-container">
  455. <!-- Ancestors Section -->
  456. <div class="ancestors-section" id="ancestorsTree"></div>
  457. <!-- Siblings and Center Person Section -->
  458. <div class="siblings-section" id="siblingsTree"></div>
  459. <!-- Children Section -->
  460. <div class="children-section" id="childrenTree"></div>
  461. </div>
  462. </div>
  463. </div>
  464. </div>
  465. </div>
  466. <script>
  467. // Search member
  468. async function searchMember() {
  469. const keyword = document.getElementById('searchInput').value.trim();
  470. if (!keyword) {
  471. alert('请输入搜索关键词');
  472. return;
  473. }
  474. // Show loading state
  475. const searchBtn = document.querySelector('.search-box button');
  476. const originalBtnHtml = searchBtn.innerHTML;
  477. searchBtn.disabled = true;
  478. searchBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
  479. try {
  480. const response = await fetch('/manager/api/search_member', {
  481. method: 'POST',
  482. headers: { 'Content-Type': 'application/json' },
  483. body: JSON.stringify({ keyword: keyword }),
  484. credentials: 'include'
  485. });
  486. const result = await response.json();
  487. if (result.success && result.members) {
  488. if (result.members.length === 1) {
  489. // Only one match, load directly
  490. document.getElementById('emptyState').style.display = 'none';
  491. document.getElementById('treeView').style.display = 'block';
  492. await loadLineage(result.members[0].id);
  493. } else {
  494. // Multiple matches, show selection dialog
  495. showMemberSelection(result.members);
  496. }
  497. } else {
  498. alert('未找到匹配的成员');
  499. }
  500. } finally {
  501. // Restore button
  502. searchBtn.disabled = false;
  503. searchBtn.innerHTML = originalBtnHtml;
  504. }
  505. }
  506. // Show member selection dialog
  507. function showMemberSelection(members) {
  508. // Remove existing dialog
  509. const existingDialog = document.getElementById('memberSelectionDialog');
  510. if (existingDialog) {
  511. existingDialog.remove();
  512. }
  513. // Store members in sessionStorage
  514. sessionStorage.setItem('searchMembers', JSON.stringify(members));
  515. // Build member list rows
  516. const memberRows = members.map((member, index) => {
  517. const simplifiedPart = member.simplified_name && member.simplified_name !== member.name
  518. ? `<span style="color: rgba(255,255,255,0.6); font-size: 13px;">(${member.simplified_name})</span>` : '';
  519. const genPart = member.name_word_generation
  520. ? `<span style="background: rgba(74,105,189,0.3); color: #8ab4f8; border-radius: 4px; padding: 1px 6px; font-size: 12px; margin-left: 6px;">第${member.name_word_generation}世</span>` : '';
  521. const fatherPart = member.father_name
  522. ? `<span style="color: rgba(255,255,255,0.5); font-size: 12px; margin-left: 6px;">父: ${member.father_name}${member.father_simplified_name && member.father_simplified_name !== member.father_name ? '(' + member.father_simplified_name + ')' : ''}</span>` : '';
  523. const idPart = `<span style="color: rgba(255,200,100,0.6); font-size: 11px; margin-left: 6px;">ID:${member.id}</span>`;
  524. const adoptionPart = member.adoption_note
  525. ? `<div style="margin-top: 3px; margin-left: 2px;">
  526. <span style="display:inline-block; background: rgba(220,80,30,0.18); border: 1px solid rgba(220,100,30,0.45);
  527. color: #f4a460; font-size: 11px; border-radius: 4px; padding: 1px 7px; white-space: nowrap;">
  528. 🔄 ${member.adoption_note}
  529. </span>
  530. </div>` : '';
  531. // 子女信息
  532. let childrenPart = '';
  533. if (member.children && member.children.length > 0) {
  534. const kids = member.children;
  535. const MAX_SHOW = 3;
  536. const shown = kids.slice(0, MAX_SHOW);
  537. const extra = kids.length - MAX_SHOW;
  538. const kidLinks = shown.map(k =>
  539. `<a href="/manager/member_detail/${k.id}" target="_blank"
  540. onclick="event.stopPropagation()"
  541. style="color:#86efac; text-decoration:none; font-size:11px;"
  542. onmouseover="this.style.textDecoration='underline'"
  543. onmouseout="this.style.textDecoration='none'">${k.name}</a>`
  544. ).join('<span style="color:rgba(255,255,255,0.3);margin:0 2px;">·</span>');
  545. const extraBadge = extra > 0
  546. ? `<span style="color:rgba(255,255,255,0.35);font-size:11px;margin-left:3px;">+${extra}子</span>` : '';
  547. childrenPart = `<div style="margin-top:3px;margin-left:2px;">
  548. <span style="color:rgba(255,255,255,0.35);font-size:11px;">子:</span>${kidLinks}${extraBadge}
  549. </div>`;
  550. }
  551. return `
  552. <div class="member-select-row"
  553. onclick="selectMemberByIndex(${index})"
  554. style="padding: 10px 12px; border-bottom: 1px solid rgba(255,255,255,0.08);
  555. cursor: pointer; display: flex; justify-content: space-between;
  556. align-items: center; transition: background 0.15s; border-radius: 6px;"
  557. onmouseover="this.style.background='rgba(74,105,189,0.2)'"
  558. onmouseout="this.style.background=''">
  559. <div style="flex: 1; min-width: 0;">
  560. <div>
  561. <span style="color: #ffd700; font-size: 13px; margin-right: 8px; flex-shrink: 0;">${index + 1}.</span>
  562. <span style="color: #fff; font-weight: 600;">${member.name}</span>
  563. ${simplifiedPart}${genPart}${fatherPart}${idPart}
  564. </div>
  565. ${adoptionPart}${childrenPart}
  566. </div>
  567. <a href="/manager/member_detail/${member.id}" target="_blank"
  568. onclick="event.stopPropagation()"
  569. title="新标签页查看详情"
  570. style="flex-shrink: 0; margin-left: 12px; padding: 4px 10px;
  571. background: rgba(74,105,189,0.4); border: 1px solid #4a69bd;
  572. border-radius: 6px; color: #8ab4f8; font-size: 12px;
  573. text-decoration: none; white-space: nowrap;">
  574. 查看详情 ↗
  575. </a>
  576. </div>`;
  577. }).join('');
  578. // Create dialog
  579. const dialog = document.createElement('div');
  580. dialog.id = 'memberSelectionDialog';
  581. dialog.style.cssText = `
  582. position: fixed;
  583. top: 50%;
  584. left: 50%;
  585. transform: translate(-50%, -50%);
  586. background: #1a1a2e;
  587. border: 2px solid #4a69bd;
  588. border-radius: 12px;
  589. padding: 24px;
  590. z-index: 10000;
  591. min-width: 480px;
  592. max-width: 680px;
  593. width: 90vw;
  594. box-shadow: 0 10px 40px rgba(0,0,0,0.6);
  595. `;
  596. dialog.innerHTML = `
  597. <h3 style="color: #ffd700; margin-bottom: 4px; text-align: center; font-size: 16px;">找到多个匹配成员</h3>
  598. <p style="color: rgba(255,255,255,0.5); text-align: center; font-size: 12px; margin-bottom: 14px;">点击条目加载世系;点击「查看详情」在新标签页中查看</p>
  599. <div style="margin-bottom: 16px; max-height: 320px; overflow-y: auto; padding-right: 2px;">
  600. ${memberRows}
  601. </div>
  602. <div style="display: flex; justify-content: center;">
  603. <button onclick="closeSelectionDialog()"
  604. style="padding: 8px 30px; background: #2d3436;
  605. border: 2px solid #666; border-radius: 8px; color: #aaa;
  606. cursor: pointer;">取消</button>
  607. </div>
  608. `;
  609. // Add overlay
  610. const overlay = document.createElement('div');
  611. overlay.id = 'dialogOverlay';
  612. overlay.style.cssText = `
  613. position: fixed;
  614. top: 0;
  615. left: 0;
  616. width: 100%;
  617. height: 100%;
  618. background: rgba(0,0,0,0.7);
  619. z-index: 9999;
  620. `;
  621. overlay.onclick = closeSelectionDialog;
  622. document.body.appendChild(overlay);
  623. document.body.appendChild(dialog);
  624. }
  625. // Close selection dialog
  626. function closeSelectionDialog() {
  627. document.getElementById('memberSelectionDialog')?.remove();
  628. document.getElementById('dialogOverlay')?.remove();
  629. }
  630. // Select member by row click
  631. function selectMemberByIndex(index) {
  632. const membersStr = sessionStorage.getItem('searchMembers');
  633. if (!membersStr) {
  634. alert('数据错误,请重新搜索');
  635. closeSelectionDialog();
  636. return;
  637. }
  638. const members = JSON.parse(membersStr);
  639. if (index >= 0 && index < members.length) {
  640. document.getElementById('emptyState').style.display = 'none';
  641. document.getElementById('treeView').style.display = 'block';
  642. loadLineage(members[index].id);
  643. closeSelectionDialog();
  644. }
  645. }
  646. // Load lineage data
  647. async function loadLineage(memberId) {
  648. _currentMemberId = memberId;
  649. // Show loading state - preserve container elements
  650. document.getElementById('ancestorsTree').innerHTML = `
  651. <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px;">
  652. <div class="spinner-border text-yellow-400" style="width: 3rem; height: 3rem;" role="status">
  653. <span class="visually-hidden">Loading...</span>
  654. </div>
  655. <p class="mt-4 text-white text-lg">正在加载祖先数据...</p>
  656. </div>
  657. `;
  658. document.getElementById('siblingsTree').innerHTML = `
  659. <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px;">
  660. <div class="spinner-border text-yellow-400" style="width: 3rem; height: 3rem;" role="status">
  661. <span class="visually-hidden">Loading...</span>
  662. </div>
  663. <p class="mt-4 text-white text-lg">正在加载同辈数据...</p>
  664. </div>
  665. `;
  666. document.getElementById('childrenTree').innerHTML = `
  667. <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px;">
  668. <div class="spinner-border text-yellow-400" style="width: 3rem; height: 3rem;" role="status">
  669. <span class="visually-hidden">Loading...</span>
  670. </div>
  671. <p class="mt-4 text-white text-lg">正在加载后代数据...</p>
  672. </div>
  673. `;
  674. try {
  675. const response = await fetch(`/manager/api/get_lineage/${memberId}?mode=${_lineageMode}`, {
  676. credentials: 'include'
  677. });
  678. const result = await response.json();
  679. if (result.success) {
  680. console.log('Lineage data received:', result.data);
  681. // Use requestAnimationFrame for smoother rendering
  682. requestAnimationFrame(() => {
  683. renderLineage(result.data);
  684. });
  685. } else {
  686. console.error('API error:', result.message);
  687. document.getElementById('ancestorsTree').innerHTML = '';
  688. document.getElementById('siblingsTree').innerHTML = `
  689. <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 200px;">
  690. <i class="bi bi-x-circle text-red-500 text-6xl mb-4"></i>
  691. <p class="text-white text-lg">加载失败</p>
  692. <p class="text-gray-400 text-sm">${result.message || '服务器错误,请稍后重试'}</p>
  693. </div>
  694. `;
  695. document.getElementById('childrenTree').innerHTML = '';
  696. }
  697. } catch (error) {
  698. console.error('Fetch error:', error);
  699. document.getElementById('ancestorsTree').innerHTML = '';
  700. document.getElementById('siblingsTree').innerHTML = `
  701. <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 200px;">
  702. <i class="bi bi-x-circle text-red-500 text-6xl mb-4"></i>
  703. <p class="text-white text-lg">加载失败</p>
  704. <p class="text-gray-400 text-sm">网络错误: ${error.message}</p>
  705. </div>
  706. `;
  707. document.getElementById('childrenTree').innerHTML = '';
  708. }
  709. }
  710. // ── 工具函数 ─────────────────────────────────────────────────────────────────
  711. // 根据 child_order 生成"长子/次子/三子..."标签
  712. function getChildOrderLabel(childOrder, fallbackIndex) {
  713. const ord = childOrder != null ? childOrder : (fallbackIndex + 1);
  714. const labels = ['长', '次', '三', '四', '五', '六', '七', '八', '九', '十'];
  715. if (ord >= 1 && ord <= 10) return labels[ord - 1] + '子';
  716. return `第${ord}子`;
  717. }
  718. // 渲染单个节点 HTML
  719. // type: 'center' | 'ancestor' | 'sibling' | 'child'
  720. function renderNode(person, type) {
  721. if (!person) return '';
  722. let cls = 'tree-node clickable';
  723. if (type === 'center') cls += ' center';
  724. else if (type === 'ancestor') cls += ' direct-ancestor';
  725. // siblings / children use default style (can be slightly smaller)
  726. if (type === 'sibling') cls += ' sibling-node';
  727. // adoption_label:后端直接标注在当事人身上(如"从xx出继"),优先使用
  728. // sub_relation_type 仅用于子女/入继节点的补充显示
  729. if (person.adoption_label) {
  730. cls += ' adopted-out';
  731. } else if (person.sub_relation_type === 2) {
  732. cls += ' adopted-out';
  733. } else if (person.sub_relation_type === 3) {
  734. cls += ' adopted-in';
  735. }
  736. let adoptLabel = '';
  737. if (person.adoption_label) {
  738. // 后端已计算好的标注(用于出继/入继当事人本人卡片)
  739. adoptLabel = `<div class="adoption-label">${person.adoption_label}</div>`;
  740. } else if (person.sub_relation_type === 2) {
  741. adoptLabel = `<div class="adoption-label">${person.adoptive_parent_name ? '出继给 ' + person.adoptive_parent_name : '出继'}</div>`;
  742. } else if (person.sub_relation_type === 3) {
  743. // 入继(养父母侧子女记录):优先显示完整说明
  744. const adoptText = person.adopt_info || (person.bio_parent_name ? `入继自 ${person.bio_parent_name}` : '入继');
  745. adoptLabel = `<div class="adoption-label adopted-in-label">${adoptText}</div>`;
  746. }
  747. return `
  748. <div class="${cls}" data-id="${person.id}" onclick="openPersonDetail(${person.id})">
  749. <div class="node-name">${person.name}</div>
  750. ${person.simplified_name && person.simplified_name !== person.name
  751. ? `<div class="node-name simplified">(${person.simplified_name})</div>` : ''}
  752. <div class="node-info">
  753. ${person.name_word ? person.name_word + ' · ' : ''}${person.name_word_generation || ''}
  754. </div>
  755. ${adoptLabel}
  756. </div>`;
  757. }
  758. // 渲染同代横排:direct(祖先/中心)+ siblings 合并按 child_order 排序
  759. // mainType: 'ancestor' | 'center'
  760. function renderPeerRow(main, siblings, mainType) {
  761. if (!main) return '';
  762. const all = [
  763. { ...main, _isDirect: true },
  764. ...(siblings || [])
  765. ].sort((a, b) => {
  766. const oa = (a.child_order != null ? a.child_order : 9999);
  767. const ob = (b.child_order != null ? b.child_order : 9999);
  768. return oa !== ob ? oa - ob : (a.id - b.id);
  769. });
  770. const MAX_SHOW = 12;
  771. const shown = all.slice(0, MAX_SHOW);
  772. const more = all.length - MAX_SHOW;
  773. let cols = shown.map((p, i) => {
  774. const badge = getChildOrderLabel(p.child_order, i);
  775. const type = p._isDirect ? mainType : 'sibling';
  776. const directCls = p._isDirect ? ' direct-col' : '';
  777. return `<div class="gen-peer-col${directCls}">
  778. <div class="child-order-badge">${badge}</div>
  779. ${renderNode(p, type)}
  780. </div>`;
  781. }).join('');
  782. if (more > 0) {
  783. cols += `<div class="gen-peer-col">
  784. <div class="child-order-badge" style="visibility:hidden">-</div>
  785. <div class="tree-node sibling-node" style="opacity:.6;min-width:80px;font-size:13px;">+${more} 人</div>
  786. </div>`;
  787. }
  788. return `<div class="gen-peer-row">${cols}</div>`;
  789. }
  790. // ── 主渲染函数 ────────────────────────────────────────────────────────────────
  791. // 全局状态:当前展示的 generations(用于"继续向上"追加)
  792. let _currentGenerations = [];
  793. let _currentCenter = null;
  794. let _currentSiblings = [];
  795. let _currentChildren = [];
  796. let _currentMemberId = null; // 记录当前查询人员,切换模式时重新加载
  797. let _lineageMode = 'incense'; // 'incense':香火传承(养父为上辈) | 'blood':血脉追溯(亲生父为上辈)
  798. // 切换世系追溯模式(element.style 直接控制,最高优先级,不受CSS影响)
  799. function setLineageMode(mode) {
  800. const changed = (_lineageMode !== mode);
  801. _lineageMode = mode;
  802. const btnI = document.getElementById('modeIncense');
  803. const btnB = document.getElementById('modeBlood');
  804. // 页面 header 背景是浅色,用深色方案确保可见
  805. if (mode === 'incense') {
  806. if (btnI) {
  807. btnI.style.cssText = 'padding:6px 20px;border:none;border-radius:20px;font-size:13px;font-weight:700;cursor:pointer;white-space:nowrap;background:#f59e0b;color:#1a1a2e;box-shadow:0 2px 8px rgba(245,158,11,0.5);';
  808. }
  809. if (btnB) {
  810. btnB.style.cssText = 'padding:6px 20px;border:none;border-radius:20px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;background:transparent;color:#6b7280;box-shadow:none;';
  811. }
  812. } else {
  813. if (btnI) {
  814. btnI.style.cssText = 'padding:6px 20px;border:none;border-radius:20px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;background:transparent;color:#6b7280;box-shadow:none;';
  815. }
  816. if (btnB) {
  817. btnB.style.cssText = 'padding:6px 20px;border:none;border-radius:20px;font-size:13px;font-weight:700;cursor:pointer;white-space:nowrap;background:#ef4444;color:#ffffff;box-shadow:0 2px 8px rgba(239,68,68,0.45);';
  818. }
  819. }
  820. // 模式变化且已有查询人员时,重新加载世系
  821. if (changed && _currentMemberId) {
  822. loadLineage(_currentMemberId);
  823. }
  824. }
  825. function renderLineage(data) {
  826. const { center, generations, siblings, children } = data;
  827. _currentGenerations = [...generations];
  828. _currentCenter = center;
  829. _currentSiblings = siblings || [];
  830. _currentChildren = children || [];
  831. _renderAncestorView(center, _currentGenerations, _currentSiblings, _currentChildren,
  832. data.has_more_ancestors, data.topmost_ancestor_id);
  833. }
  834. function _renderAncestorView(center, generations, siblings, children, hasMore, topmostId) {
  835. const ancestorGens = [...generations].reverse(); // 从最远祖先 → 父亲
  836. let html = '<div class="lineage-view">';
  837. // ── 1. 祖先竖列(最远→父亲)────────────────────────────────────────────
  838. if (ancestorGens.length > 0) {
  839. // "继续向上追溯"按钮(如果还有更高祖先)
  840. if (hasMore && topmostId) {
  841. html += `<div class="lin-row"><div class="lin-center">
  842. <button class="load-more-ancestors-btn" id="loadMoreAncestorsBtn"
  843. onclick="loadMoreAncestors(${topmostId}, this)">
  844. <i class="bi bi-arrow-up-circle"></i> 继续向上追溯(仍有更早的祖先)
  845. </button>
  846. </div></div>`;
  847. } else if (ancestorGens.length > 0) {
  848. html += `<div class="lin-row"><div class="lin-center">
  849. <div style="font-size:12px;color:rgba(255,255,255,0.3);text-align:center;padding:6px 0;">
  850. ↑ 已到达最上辈先祖
  851. </div>
  852. </div></div>`;
  853. }
  854. html += `<div class="section-divider">祖先世系</div>`;
  855. }
  856. ancestorGens.forEach((gen, idx) => {
  857. // 竖连接线(第一个不加顶部线)
  858. if (idx > 0) {
  859. html += `<div class="lin-vline-row"><div class="lin-vline" style="margin-left:6px;"></div></div>`;
  860. }
  861. html += `<div class="lin-row">`;
  862. html += renderPeerRow(gen.ancestor, gen.siblings || [], 'ancestor');
  863. html += `</div>`;
  864. });
  865. // ── 2. 中心人物 ──────────────────────────────────────────────────────────
  866. if (ancestorGens.length > 0) {
  867. html += `<div class="lin-vline-row"><div class="lin-vline" style="margin-left:6px;"></div></div>`;
  868. }
  869. html += `<div class="section-divider">查询人物</div>`;
  870. html += `<div class="lin-row">`;
  871. html += renderPeerRow(center, siblings || [], 'center');
  872. html += `</div>`;
  873. // ── 3. 子女横排(按 child_order 排序)────────────────────────────────────
  874. if (children && children.length > 0) {
  875. html += `<div class="lin-vline-row"><div class="lin-vline" style="margin-left:6px;"></div></div>`;
  876. html += `<div class="section-divider">子女</div>`;
  877. html += `<div class="lin-row">`;
  878. html += ` <div class="lin-children">`;
  879. children.forEach((child, idx) => {
  880. const badge = getChildOrderLabel(child.child_order, idx);
  881. html += `<div class="lin-child-col">`;
  882. html += ` <div class="child-order-badge">${badge}</div>`;
  883. html += renderNode(child, 'child');
  884. if (child.has_children) {
  885. html += `<button class="expand-btn" onclick="toggleChildren(this,${child.id})">+</button>`;
  886. html += `<div class="children-container" style="display:none;" data-parent-id="${child.id}"></div>`;
  887. }
  888. html += `</div>`;
  889. });
  890. html += ` </div>`;
  891. html += `</div>`;
  892. }
  893. html += '</div>'; // lineage-view
  894. // 写入容器(ancestorsTree 承载全部内容,其余清空)
  895. document.getElementById('ancestorsTree').innerHTML = html;
  896. document.getElementById('siblingsTree').innerHTML = '';
  897. document.getElementById('childrenTree').innerHTML = '';
  898. // 渲染后对齐直系列并居中显示
  899. requestAnimationFrame(() => alignAndCenter());
  900. }
  901. // 对齐直系列并将中心人物滚动至视口中央
  902. function alignAndCenter() {
  903. const treeView = document.getElementById('treeView');
  904. const lineageView = document.querySelector('#ancestorsTree .lineage-view');
  905. if (!treeView || !lineageView) return;
  906. // ── Step 1: 让每代行的 direct-col 对齐到同一 x 坐标 ──────────────────────
  907. const directCols = Array.from(lineageView.querySelectorAll('.gen-peer-col.direct-col'));
  908. if (directCols.length > 0) {
  909. const lineageLeft = lineageView.getBoundingClientRect().left;
  910. // 记录每个 direct-col 相对于 lineageView 的左偏移
  911. const offsets = directCols.map(col =>
  912. col.getBoundingClientRect().left - lineageLeft
  913. );
  914. const maxOffset = Math.max(...offsets);
  915. // 给偏移不足的那一行在其 gen-peer-row 上补 padding-left
  916. directCols.forEach((col, i) => {
  917. const diff = maxOffset - offsets[i];
  918. if (diff > 0) {
  919. const row = col.closest('.gen-peer-row');
  920. if (row) {
  921. const cur = parseFloat(row.style.paddingLeft) || 6;
  922. row.style.paddingLeft = (cur + diff) + 'px';
  923. }
  924. }
  925. });
  926. }
  927. // ── Step 2: 布局刷新后,子女居中 + 滚动至中心人物居中 ───────────────────
  928. requestAnimationFrame(() => {
  929. const centerNode = lineageView.querySelector('.tree-node.center');
  930. if (!centerNode) return;
  931. const lvRect = lineageView.getBoundingClientRect();
  932. const cnRect = centerNode.getBoundingClientRect();
  933. // 中心人物的水平中心(相对于 lineageView 左边缘)
  934. const centerX = cnRect.left + cnRect.width / 2 - lvRect.left;
  935. // 将 .lin-children 居中对齐到中心人物
  936. const linChildren = lineageView.querySelector('.lin-children');
  937. if (linChildren) {
  938. const childrenW = linChildren.scrollWidth;
  939. const desiredMargin = Math.max(0, centerX - childrenW / 2);
  940. linChildren.style.marginLeft = desiredMargin + 'px';
  941. }
  942. // 布局再次刷新后滚动视口
  943. requestAnimationFrame(() => {
  944. const tvRect = treeView.getBoundingClientRect();
  945. const cnRect2 = centerNode.getBoundingClientRect();
  946. // 水平居中
  947. const hDelta = (cnRect2.left + cnRect2.width / 2) - (tvRect.left + tvRect.width / 2);
  948. treeView.scrollLeft += hDelta;
  949. // 垂直居中:让中心人物处于视口中部偏上(1/3 处),上方留给祖先
  950. const targetY = tvRect.top + tvRect.height * 0.4;
  951. const vDelta = (cnRect2.top + cnRect2.height / 2) - targetY;
  952. treeView.scrollTop += vDelta;
  953. });
  954. });
  955. }
  956. // 继续向上追溯:加载 ancestor_id 以上的祖先链,并前插到当前列表
  957. async function loadMoreAncestors(topmostAncestorId, btn) {
  958. if (btn) {
  959. btn.disabled = true;
  960. btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> 追溯中...';
  961. }
  962. try {
  963. const resp = await fetch(`/manager/api/get_ancestors_above/${topmostAncestorId}?mode=${_lineageMode}`, {
  964. credentials: 'include'
  965. });
  966. const result = await resp.json();
  967. if (!result.success) {
  968. alert('加载失败:' + result.message);
  969. if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-arrow-up-circle"></i> 继续向上追溯'; }
  970. return;
  971. }
  972. const newGens = result.data.generations; // 顺序:从 topmostAncestorId 的父亲 → 更高祖先
  973. if (!newGens || newGens.length === 0) {
  974. if (btn) btn.outerHTML = '<div style="font-size:12px;color:rgba(255,255,255,0.3);text-align:center;padding:6px 0;">↑ 已到达最上辈先祖</div>';
  975. return;
  976. }
  977. // 若 anchor 节点本身有出继标注,更新其在 _currentGenerations 中的记录
  978. if (result.data.anchor_adoption_label) {
  979. const anchorEntry = _currentGenerations.find(g => g.ancestor && g.ancestor.id === topmostAncestorId);
  980. if (anchorEntry) {
  981. anchorEntry.ancestor.adoption_label = result.data.anchor_adoption_label;
  982. }
  983. }
  984. // 将新祖先追加到全局 _currentGenerations 末尾(末尾 = 更远的祖先)
  985. _currentGenerations = _currentGenerations.concat(newGens);
  986. // 重新渲染整个视图(中心人物、兄弟、子女使用缓存数据)
  987. _renderAncestorView(
  988. _currentCenter,
  989. _currentGenerations,
  990. _currentSiblings,
  991. _currentChildren,
  992. result.data.has_more_ancestors,
  993. result.data.topmost_ancestor_id
  994. );
  995. } catch (e) {
  996. alert('网络错误:' + e.message);
  997. if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-arrow-up-circle"></i> 继续向上追溯'; }
  998. }
  999. }
  1000. // 展开子孙(按钮旁的懒加载容器)
  1001. function renderChildrenRecursive(children) {
  1002. if (!children || children.length === 0) return '';
  1003. return `<div class="lin-children" style="flex-wrap:wrap;gap:12px;">
  1004. ${children.map((child, idx) => {
  1005. const badge = getChildOrderLabel(child.child_order, idx);
  1006. return `<div class="lin-child-col">
  1007. <div class="child-order-badge">${badge}</div>
  1008. ${renderNode(child, 'child')}
  1009. ${child.has_children
  1010. ? `<button class="expand-btn" onclick="toggleChildren(this,${child.id})">+</button>
  1011. <div class="children-container" style="display:none;" data-parent-id="${child.id}"></div>`
  1012. : ''}
  1013. </div>`;
  1014. }).join('')}
  1015. </div>`;
  1016. }
  1017. // Open person detail in new tab
  1018. function openPersonDetail(personId) {
  1019. window.open(`/manager/member_detail/${personId}`, '_blank');
  1020. }
  1021. // Toggle children visibility with lazy loading
  1022. async function toggleChildren(btn, parentId) {
  1023. const container = btn.nextElementSibling;
  1024. const isExpanded = container.style.display !== 'none';
  1025. if (isExpanded) {
  1026. container.style.display = 'none';
  1027. btn.innerHTML = '+';
  1028. } else {
  1029. // Check if children are already loaded
  1030. if (container.innerHTML.trim() === '') {
  1031. // Load children lazily
  1032. btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
  1033. // Get already displayed descendant IDs to exclude
  1034. const excludedIds = getExcludedDescendantIds(btn);
  1035. const excludeParam = excludedIds.length > 0 ? `?exclude=${excludedIds.join(',')}` : '';
  1036. console.log(`[ToggleChildren] Parent ID: ${parentId}, Excluded IDs: ${excludedIds}, URL: /manager/api/get_descendants/${parentId}${excludeParam}`);
  1037. try {
  1038. const response = await fetch(`/manager/api/get_descendants/${parentId}${excludeParam}`, {
  1039. credentials: 'include'
  1040. });
  1041. const result = await response.json();
  1042. if (result.success && result.children) {
  1043. // Render children
  1044. container.innerHTML = renderChildrenRecursive(result.children);
  1045. }
  1046. } catch (error) {
  1047. console.error('Failed to load children:', error);
  1048. } finally {
  1049. btn.innerHTML = '−';
  1050. }
  1051. }
  1052. container.style.display = 'flex';
  1053. btn.innerHTML = '−';
  1054. }
  1055. }
  1056. // Get descendant IDs that are already displayed in the tree (to avoid duplicates)
  1057. function getExcludedDescendantIds(btn) {
  1058. const excluded = new Set();
  1059. // Helper function to extract IDs from tree nodes
  1060. const extractIdsFromNodes = (container) => {
  1061. if (!container) return;
  1062. const nodes = container.querySelectorAll('.tree-node');
  1063. nodes.forEach(node => {
  1064. const id = node.getAttribute('data-id');
  1065. if (id && !isNaN(parseInt(id))) {
  1066. excluded.add(parseInt(id));
  1067. }
  1068. });
  1069. };
  1070. // Get IDs from ancestors tree
  1071. const ancestorsTree = document.getElementById('ancestorsTree');
  1072. extractIdsFromNodes(ancestorsTree);
  1073. // Get IDs from siblings tree (center person and siblings)
  1074. const siblingsTree = document.getElementById('siblingsTree');
  1075. extractIdsFromNodes(siblingsTree);
  1076. // Get IDs from children tree
  1077. const childrenTree = document.getElementById('childrenTree');
  1078. extractIdsFromNodes(childrenTree);
  1079. console.log('Excluded IDs:', Array.from(excluded));
  1080. return Array.from(excluded);
  1081. }
  1082. // Enter key search
  1083. document.getElementById('searchInput').addEventListener('keypress', function(e) {
  1084. if (e.key === 'Enter') {
  1085. searchMember();
  1086. }
  1087. });
  1088. </script>
  1089. {% endblock %}