lineage_query.html 50 KB

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