script.js 65 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858
  1. // 全局元素索引(将由Python动态注入)
  2. // const elementIndex = {};
  3. // 显示元素详情模态框
  4. function showElementDetail(elementId) {
  5. const elem = elementIndex[elementId];
  6. if (!elem) return;
  7. // 创建模态框
  8. const modal = document.createElement('div');
  9. modal.className = 'element-modal-backdrop';
  10. modal.onclick = function(e) {
  11. if (e.target === modal) {
  12. document.body.removeChild(modal);
  13. }
  14. };
  15. // 创建模态框内容
  16. const modalContent = document.createElement('div');
  17. modalContent.className = 'element-modal-content';
  18. modalContent.onclick = function(e) {
  19. e.stopPropagation();
  20. };
  21. // 构建内容
  22. let html = '<div class="modal-header">';
  23. html += '<h3>' + elem.name + '</h3>';
  24. html += '<button class="modal-close" onclick="this.closest(\'.element-modal-backdrop\').remove()">×</button>';
  25. html += '</div>';
  26. html += '<div class="modal-body">';
  27. // 描述
  28. if (elem.description) {
  29. html += '<div class="modal-section">';
  30. html += '<strong>描述:</strong>';
  31. html += '<p>' + elem.description + '</p>';
  32. html += '</div>';
  33. }
  34. // 类型和维度
  35. html += '<div class="modal-section">';
  36. html += '<strong>类型:</strong> ' + elem.type;
  37. if (elem.dimension && elem.dimension.一级) {
  38. html += ' / ' + elem.dimension.一级;
  39. if (elem.dimension.二级) {
  40. html += ' / ' + elem.dimension.二级;
  41. }
  42. }
  43. html += '</div>';
  44. // 分类
  45. if (elem.category) {
  46. html += '<div class="modal-section">';
  47. html += '<strong>分类:</strong>';
  48. if (typeof elem.category === 'object') {
  49. html += ' ' + (elem.category.一级分类 || '');
  50. if (elem.category.二级分类) {
  51. html += ' / ' + elem.category.二级分类;
  52. }
  53. } else {
  54. html += ' ' + elem.category;
  55. }
  56. html += '</div>';
  57. }
  58. // 跳转到Tab3查看完整信息
  59. html += '<div class="modal-footer">';
  60. html += '<button class="modal-btn" onclick="jumpToElement(\'' + elem.id + '\')">查看完整信息</button>';
  61. html += '</div>';
  62. html += '</div>';
  63. modalContent.innerHTML = html;
  64. modal.appendChild(modalContent);
  65. document.body.appendChild(modal);
  66. }
  67. // 跳转到元素详情(Tab3)
  68. function jumpToElement(elementId) {
  69. // 关闭模态框
  70. const modal = document.querySelector('.element-modal-backdrop');
  71. if (modal) {
  72. modal.remove();
  73. }
  74. // 切换到Tab3
  75. switchTab('tab3');
  76. // 等待DOM更新后滚动到元素
  77. setTimeout(function() {
  78. const elemItem = document.querySelector('[data-elem-id="' + elementId + '"]');
  79. if (elemItem) {
  80. // 展开所有父级容器
  81. let parent = elemItem.parentElement;
  82. while (parent) {
  83. if (parent.classList.contains('collapsed')) {
  84. parent.classList.remove('collapsed');
  85. }
  86. parent = parent.parentElement;
  87. }
  88. // 滚动到元素并高亮
  89. elemItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
  90. elemItem.classList.add('highlight-pulse');
  91. setTimeout(function() {
  92. elemItem.classList.remove('highlight-pulse');
  93. }, 2000);
  94. }
  95. }, 100);
  96. }
  97. // Tab切换功能
  98. function switchTab(tabId) {
  99. // 隐藏所有tab内容
  100. const allTabs = document.querySelectorAll('.tab-content');
  101. allTabs.forEach(tab => {
  102. tab.style.display = 'none';
  103. });
  104. // 移除所有tab的active类
  105. const allTabButtons = document.querySelectorAll('.tab');
  106. allTabButtons.forEach(btn => {
  107. btn.classList.remove('active');
  108. });
  109. // 显示选中的tab内容
  110. document.getElementById(tabId).style.display = 'block';
  111. // 给选中的tab按钮添加active类
  112. if (event && event.target) {
  113. event.target.classList.add('active');
  114. } else {
  115. // 程序化切换时,手动添加active类
  116. document.querySelectorAll('.tab').forEach((btn, idx) => {
  117. if ((tabId === 'tab1' && idx === 0) ||
  118. (tabId === 'tab2' && idx === 1) ||
  119. (tabId === 'tab3' && idx === 2) ||
  120. (tabId === 'tab4' && idx === 3) ||
  121. (tabId === 'tab5' && idx === 4)) {
  122. btn.classList.add('active');
  123. }
  124. });
  125. }
  126. // 如果切换到tab4,重新绘制连线
  127. if (tabId === 'tab4') {
  128. setTimeout(() => {
  129. drawAllConnections();
  130. }, 100);
  131. }
  132. // Tab5 不在切换时自动绘制连线,只在点击卡片时绘制
  133. }
  134. // 展开/收起段落功能
  135. function toggleCollapse(element) {
  136. const listItem = element.closest('.paragraph-item');
  137. if (!listItem.classList.contains('collapsible')) {
  138. return;
  139. }
  140. listItem.classList.toggle('collapsed');
  141. }
  142. // 展开/收起段落详细内容
  143. function toggleDetails(element) {
  144. const detailsContainer = element.closest('.paragraph-item')
  145. .querySelector('.paragraph-details');
  146. if (detailsContainer) {
  147. detailsContainer.classList.toggle('collapsed');
  148. const toggleBtn = element.closest('.paragraph-header')
  149. .querySelector('.details-toggle-btn');
  150. if (detailsContainer.classList.contains('collapsed')) {
  151. toggleBtn.innerHTML = '<span class="details-icon">▶</span> 查看详细内容';
  152. } else {
  153. toggleBtn.innerHTML = '<span class="details-icon">▶</span> 隐藏详细内容';
  154. }
  155. }
  156. }
  157. // Tab3: 展开/收起第一层级(实质/形式)
  158. function toggleLevel1(element) {
  159. element.classList.toggle('collapsed');
  160. }
  161. // Tab3: 展开/收起第二层级(具体元素/具体概念/抽象概念)
  162. function toggleLevel2(element) {
  163. element.classList.toggle('collapsed');
  164. }
  165. // Tab3: 展开/收起分类组
  166. function toggleCategoryGroup(element) {
  167. const categoryGroup = element.closest('.category-group');
  168. if (categoryGroup && categoryGroup.classList.contains('collapsible')) {
  169. categoryGroup.classList.toggle('collapsed');
  170. }
  171. }
  172. // Tab3: 展开/收起二级分类组
  173. function toggleSubcategoryGroup(element) {
  174. const subcategoryGroup = element.closest('.subcategory-group');
  175. if (subcategoryGroup && subcategoryGroup.classList.contains('collapsible')) {
  176. subcategoryGroup.classList.toggle('collapsed');
  177. }
  178. }
  179. // Tab3: 展开/收起元素详情
  180. function toggleElementDetails(element) {
  181. const elementItem = element.closest('.element-item');
  182. const detailsContainer = elementItem.querySelector('.element-details');
  183. if (detailsContainer) {
  184. event.stopPropagation();
  185. elementItem.classList.toggle('expanded');
  186. detailsContainer.classList.toggle('collapsed');
  187. }
  188. }
  189. // Tab3: 全部展开/收起所有层级
  190. function toggleAllLevels(expand) {
  191. // 第一层级
  192. const level1Headers = document.querySelectorAll('.level1-header');
  193. level1Headers.forEach(header => {
  194. if (expand) {
  195. header.classList.remove('collapsed');
  196. } else {
  197. header.classList.add('collapsed');
  198. }
  199. });
  200. // 第二层级
  201. const level2Headers = document.querySelectorAll('.level2-header');
  202. level2Headers.forEach(header => {
  203. if (expand) {
  204. header.classList.remove('collapsed');
  205. } else {
  206. header.classList.add('collapsed');
  207. }
  208. });
  209. // 分类组
  210. const categoryGroups = document.querySelectorAll('.category-group.collapsible');
  211. categoryGroups.forEach(group => {
  212. if (expand) {
  213. group.classList.remove('collapsed');
  214. } else {
  215. group.classList.add('collapsed');
  216. }
  217. });
  218. // 二级分类组
  219. const subcategoryGroups = document.querySelectorAll('.subcategory-group.collapsible');
  220. subcategoryGroups.forEach(group => {
  221. if (expand) {
  222. group.classList.remove('collapsed');
  223. } else {
  224. group.classList.add('collapsed');
  225. }
  226. });
  227. }
  228. // 旧版本兼容:保持toggleAllCategories函数
  229. function toggleAllCategories(expand) {
  230. toggleAllLevels(expand);
  231. }
  232. // 页面加载完成后初始化
  233. document.addEventListener('DOMContentLoaded', function() {
  234. console.log('脚本结果可视化页面已加载');
  235. // 初始化Tab4连线图
  236. if (document.getElementById('tab4')) {
  237. initializeRelationshipGraph();
  238. }
  239. });
  240. // ===== Tab4 关系图功能 =====
  241. // 初始化关系图
  242. function initializeRelationshipGraph() {
  243. // 延迟执行以确保DOM完全加载
  244. setTimeout(() => {
  245. // 初始状态:隐藏所有右侧节点
  246. resetRelationshipView();
  247. // 监听窗口大小变化,重新绘制连线
  248. window.addEventListener('resize', debounce(() => {
  249. if (selectedSubstanceId && relationshipData[selectedSubstanceId]) {
  250. drawSelectedSubstanceConnections(selectedSubstanceId, relationshipData[selectedSubstanceId]);
  251. }
  252. }, 300));
  253. }, 100);
  254. }
  255. // 绘制所有连线
  256. function drawAllConnections() {
  257. if (typeof relationshipData === 'undefined') {
  258. return;
  259. }
  260. const svg = document.getElementById('relationship-svg');
  261. if (!svg) return;
  262. // 清空现有连线
  263. svg.innerHTML = '';
  264. // 遍历所有实质点
  265. Object.keys(relationshipData).forEach(substanceId => {
  266. const relations = relationshipData[substanceId];
  267. // 绘制到灵感点的连线
  268. relations.inspiration.forEach(rel => {
  269. drawConnection(substanceId, rel.target, 'inspiration', rel.avg_score);
  270. });
  271. // 绘制到目的点的连线
  272. relations.purpose.forEach(rel => {
  273. drawConnection(substanceId, rel.target, 'purpose', rel.avg_score);
  274. });
  275. // 绘制到关键点的连线
  276. relations.keypoint.forEach(rel => {
  277. drawConnection(substanceId, rel.target, 'keypoint', rel.avg_score);
  278. });
  279. });
  280. }
  281. // 绘制单条连线
  282. function drawConnection(sourceId, targetId, type, score) {
  283. const svg = document.getElementById('relationship-svg');
  284. if (!svg) return;
  285. const sourceNode = document.querySelector(`.substance-node[data-id="${sourceId}"]`);
  286. const targetNode = document.querySelector(`.target-node[data-id="${targetId}"]`);
  287. if (!sourceNode || !targetNode) return;
  288. // 获取节点位置
  289. const sourceRect = sourceNode.getBoundingClientRect();
  290. const targetRect = targetNode.getBoundingClientRect();
  291. const containerRect = svg.parentElement.getBoundingClientRect();
  292. // 计算相对于SVG容器的位置
  293. const x1 = sourceRect.right - containerRect.left;
  294. const y1 = sourceRect.top + sourceRect.height / 2 - containerRect.top;
  295. const x2 = targetRect.left - containerRect.left;
  296. const y2 = targetRect.top + targetRect.height / 2 - containerRect.top;
  297. // 创建贝塞尔曲线路径
  298. const midX = (x1 + x2) / 2;
  299. const path = `M ${x1} ${y1} Q ${midX} ${y1}, ${midX} ${(y1 + y2) / 2} T ${x2} ${y2}`;
  300. // 创建path元素
  301. const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  302. pathElement.setAttribute('d', path);
  303. pathElement.setAttribute('class', `connection-line ${type}`);
  304. pathElement.setAttribute('fill', 'none');
  305. pathElement.setAttribute('data-source', sourceId);
  306. pathElement.setAttribute('data-target', targetId);
  307. pathElement.setAttribute('data-type', type);
  308. pathElement.setAttribute('data-score', score);
  309. svg.appendChild(pathElement);
  310. }
  311. // 高亮相关关系
  312. function highlightRelationships(substanceId) {
  313. if (typeof relationshipData === 'undefined') {
  314. return;
  315. }
  316. // 清除之前的高亮
  317. clearHighlights();
  318. // 高亮当前实质点
  319. const substanceNode = document.querySelector(`.substance-node[data-id="${substanceId}"]`);
  320. if (substanceNode) {
  321. substanceNode.classList.add('highlighted');
  322. }
  323. // 获取相关关系
  324. const relations = relationshipData[substanceId];
  325. if (!relations) return;
  326. // 高亮相关的目标点
  327. [...relations.inspiration, ...relations.purpose, ...relations.keypoint].forEach(rel => {
  328. const targetNode = document.querySelector(`.target-node[data-id="${rel.target}"]`);
  329. if (targetNode) {
  330. targetNode.classList.add('highlighted');
  331. }
  332. });
  333. // 高亮相关的连线
  334. const lines = document.querySelectorAll(`.connection-line[data-source="${substanceId}"]`);
  335. lines.forEach(line => {
  336. line.classList.add('highlighted');
  337. });
  338. }
  339. // 清除所有高亮
  340. function clearHighlights() {
  341. // 清除节点高亮
  342. document.querySelectorAll('.node.highlighted').forEach(node => {
  343. node.classList.remove('highlighted');
  344. });
  345. // 清除连线高亮
  346. document.querySelectorAll('.connection-line.highlighted').forEach(line => {
  347. line.classList.remove('highlighted');
  348. });
  349. }
  350. // 防抖函数
  351. function debounce(func, wait) {
  352. let timeout;
  353. return function executedFunction(...args) {
  354. const later = () => {
  355. clearTimeout(timeout);
  356. func(...args);
  357. };
  358. clearTimeout(timeout);
  359. timeout = setTimeout(later, wait);
  360. };
  361. }
  362. // ===== Tab4 新增:点击选择实质点功能 =====
  363. let selectedSubstanceId = null;
  364. // 选择实质点,展示关联关系
  365. function selectSubstance(substanceId) {
  366. if (typeof relationshipData === 'undefined') {
  367. return;
  368. }
  369. // 如果点击已选中的实质点,不做处理
  370. if (selectedSubstanceId === substanceId) {
  371. return;
  372. }
  373. selectedSubstanceId = substanceId;
  374. // 显示重置按钮
  375. const resetBtn = document.querySelector('.reset-btn');
  376. if (resetBtn) {
  377. resetBtn.style.display = 'block';
  378. }
  379. // 获取关系数据
  380. const relations = relationshipData[substanceId];
  381. if (!relations) return;
  382. // 收集所有关联的节点ID
  383. const relatedNodeIds = new Set();
  384. // 添加灵感点
  385. relations.inspiration.forEach(rel => {
  386. relatedNodeIds.add(rel.target);
  387. });
  388. // 添加目的点
  389. relations.purpose.forEach(rel => {
  390. relatedNodeIds.add(rel.target);
  391. });
  392. // 添加关键点
  393. relations.keypoint.forEach(rel => {
  394. relatedNodeIds.add(rel.target);
  395. });
  396. // 添加关联的实质点
  397. relations.substance.forEach(rel => {
  398. relatedNodeIds.add(rel.target);
  399. });
  400. // 高亮左侧选中的实质点
  401. document.querySelectorAll('.substance-node').forEach(node => {
  402. if (node.getAttribute('data-id') === substanceId) {
  403. node.classList.add('selected');
  404. node.classList.remove('dimmed');
  405. } else {
  406. node.classList.remove('selected');
  407. node.classList.add('dimmed');
  408. }
  409. });
  410. // 右侧节点:只显示关联的节点
  411. document.querySelectorAll('.target-node').forEach(node => {
  412. const nodeId = node.getAttribute('data-id');
  413. if (relatedNodeIds.has(nodeId)) {
  414. node.classList.add('visible');
  415. node.classList.remove('hidden');
  416. } else {
  417. node.classList.remove('visible');
  418. node.classList.add('hidden');
  419. }
  420. });
  421. // 根据右侧节点的可见性,控制section的显示
  422. document.querySelectorAll('.target-section').forEach(section => {
  423. const visibleNodes = section.querySelectorAll('.target-node.visible');
  424. if (visibleNodes.length > 0) {
  425. section.classList.add('has-visible-nodes');
  426. section.classList.remove('all-hidden');
  427. } else {
  428. section.classList.remove('has-visible-nodes');
  429. section.classList.add('all-hidden');
  430. }
  431. });
  432. // 重新绘制连线(只绘制选中实质点的连线)
  433. drawSelectedSubstanceConnections(substanceId, relations);
  434. }
  435. // 重置视图
  436. function resetRelationshipView() {
  437. selectedSubstanceId = null;
  438. // 隐藏重置按钮
  439. const resetBtn = document.querySelector('.reset-btn');
  440. if (resetBtn) {
  441. resetBtn.style.display = 'none';
  442. }
  443. // 左侧实质点:移除所有高亮和变暗
  444. document.querySelectorAll('.substance-node').forEach(node => {
  445. node.classList.remove('selected', 'dimmed');
  446. });
  447. // 右侧节点:隐藏所有节点
  448. document.querySelectorAll('.target-node').forEach(node => {
  449. node.classList.remove('visible');
  450. node.classList.add('hidden');
  451. });
  452. // 隐藏所有section
  453. document.querySelectorAll('.target-section').forEach(section => {
  454. section.classList.remove('has-visible-nodes');
  455. section.classList.add('all-hidden');
  456. });
  457. // 清空连线
  458. const svg = document.getElementById('relationship-svg');
  459. if (svg) {
  460. svg.innerHTML = '';
  461. }
  462. }
  463. // 绘制选中实质点的连线
  464. function drawSelectedSubstanceConnections(substanceId, relations) {
  465. const svg = document.getElementById('relationship-svg');
  466. if (!svg) return;
  467. // 清空现有连线
  468. svg.innerHTML = '';
  469. // 绘制到灵感点的连线
  470. relations.inspiration.forEach(rel => {
  471. drawConnection(substanceId, rel.target, 'inspiration', rel.avg_score);
  472. });
  473. // 绘制到目的点的连线
  474. relations.purpose.forEach(rel => {
  475. drawConnection(substanceId, rel.target, 'purpose', rel.avg_score);
  476. });
  477. // 绘制到关键点的连线
  478. relations.keypoint.forEach(rel => {
  479. drawConnection(substanceId, rel.target, 'keypoint', rel.avg_score);
  480. });
  481. // 绘制到其他实质点的连线
  482. relations.substance.forEach(rel => {
  483. drawConnection(substanceId, rel.target, 'substance', rel.common_count / 10);
  484. });
  485. }
  486. // ===== Tab4 新版支撑关系图功能 =====
  487. let selectedSupportSubstanceId = null;
  488. let selectedSupportTargetId = null;
  489. // 选择实质点(新版)
  490. function selectSubstance(substanceId) {
  491. if (typeof supportRelationships === 'undefined') {
  492. console.error('supportRelationships not defined');
  493. return;
  494. }
  495. // 如果点击已选中的实质点,不做处理
  496. if (selectedSupportSubstanceId === substanceId) {
  497. return;
  498. }
  499. selectedSupportSubstanceId = substanceId;
  500. selectedSupportTargetId = null;
  501. // 清空画布
  502. clearSupportCanvas();
  503. // 获取关系数据
  504. const relations = supportRelationships.substance_to_target[substanceId];
  505. if (!relations) {
  506. console.error('No relations found for substance:', substanceId);
  507. return;
  508. }
  509. // 高亮选中的实质点,其他变暗
  510. document.querySelectorAll('.substance-card').forEach(card => {
  511. if (card.getAttribute('data-id') === substanceId) {
  512. card.classList.add('selected');
  513. card.classList.remove('dimmed');
  514. } else {
  515. card.classList.remove('selected');
  516. card.classList.add('dimmed');
  517. }
  518. });
  519. // 清除所有目标点的状态
  520. document.querySelectorAll('.target-card').forEach(card => {
  521. card.classList.remove('selected', 'dimmed', 'highlighted');
  522. });
  523. // 根据关系数据高亮相关的目标点
  524. const relatedTargets = new Set();
  525. relations.inspiration.forEach(rel => {
  526. relatedTargets.add(rel.target_id);
  527. });
  528. relations.purpose.forEach(rel => {
  529. relatedTargets.add(rel.target_id);
  530. });
  531. relations.keypoint.forEach(rel => {
  532. relatedTargets.add(rel.target_id);
  533. });
  534. // 高亮相关的目标点,其他变暗
  535. document.querySelectorAll('.target-card').forEach(card => {
  536. const cardId = card.getAttribute('data-id');
  537. if (relatedTargets.has(cardId)) {
  538. card.classList.add('highlighted');
  539. card.classList.remove('dimmed');
  540. } else {
  541. card.classList.add('dimmed');
  542. card.classList.remove('highlighted');
  543. }
  544. });
  545. // 绘制支撑关系(使用Sankey风格或树状结构)
  546. drawSupportRelationships(substanceId, relations);
  547. }
  548. // 选择目标点(反向查看)
  549. function selectTarget(targetType, targetIndex) {
  550. if (typeof supportRelationships === 'undefined') {
  551. console.error('supportRelationships not defined');
  552. return;
  553. }
  554. const targetId = `${targetType}-${targetIndex}`;
  555. // 如果点击已选中的目标点,不做处理
  556. if (selectedSupportTargetId === targetId) {
  557. return;
  558. }
  559. selectedSupportSubstanceId = null;
  560. selectedSupportTargetId = targetId;
  561. // 清空画布
  562. clearSupportCanvas();
  563. // 获取反向关系数据
  564. const relations = supportRelationships.target_to_substance[targetId];
  565. if (!relations) {
  566. console.error('No relations found for target:', targetId);
  567. return;
  568. }
  569. // 清除所有实质点的状态
  570. document.querySelectorAll('.substance-card').forEach(card => {
  571. card.classList.remove('selected', 'dimmed', 'highlighted');
  572. });
  573. // 高亮选中的目标点,其他变暗
  574. document.querySelectorAll('.target-card').forEach(card => {
  575. if (card.getAttribute('data-id') === targetId) {
  576. card.classList.add('selected');
  577. card.classList.remove('dimmed');
  578. } else {
  579. card.classList.remove('selected');
  580. card.classList.add('dimmed');
  581. }
  582. });
  583. // 收集所有支撑该目标点的实质点ID
  584. const supportingSubstances = new Set();
  585. relations.concrete_elements.forEach(item => supportingSubstances.add(item.substance_id));
  586. relations.concrete_concepts.forEach(item => supportingSubstances.add(item.substance_id));
  587. relations.abstract_concepts.forEach(item => supportingSubstances.add(item.substance_id));
  588. // 高亮支撑的实质点,其他变暗
  589. document.querySelectorAll('.substance-card').forEach(card => {
  590. const cardId = card.getAttribute('data-id');
  591. if (supportingSubstances.has(cardId)) {
  592. card.classList.add('highlighted');
  593. card.classList.remove('dimmed');
  594. } else {
  595. card.classList.add('dimmed');
  596. card.classList.remove('highlighted');
  597. }
  598. });
  599. // 绘制反向支撑关系
  600. drawReverseSupportRelationships(targetId, relations);
  601. }
  602. // 绘制支撑关系(实质点 → 目标点)
  603. function drawSupportRelationships(substanceId, relations) {
  604. const canvas = document.getElementById('support-canvas');
  605. if (!canvas) return;
  606. // 清空并移除占位文本
  607. const placeholder = canvas.querySelector('.placeholder-text');
  608. if (placeholder) {
  609. placeholder.style.display = 'none';
  610. }
  611. // 创建可视化内容
  612. let html = '<div class="support-flow">\n';
  613. html += '<div class="flow-title">支撑关系</div>\n';
  614. // 显示来源实质点
  615. const substanceData = supportRelationships.substance_to_target[substanceId];
  616. html += '<div class="flow-source">\n';
  617. html += '<div class="flow-node substance-flow-node">\n';
  618. html += `<div class="flow-node-label">${substanceData.type || '实质点'}</div>\n`;
  619. html += `<div class="flow-node-name">${substanceData.name || substanceId}</div>\n`;
  620. html += '</div>\n';
  621. html += '</div>\n';
  622. // 显示支撑的目标点(分类)
  623. html += '<div class="flow-targets">\n';
  624. if (relations.inspiration && relations.inspiration.length > 0) {
  625. html += '<div class="flow-target-group">\n';
  626. html += '<div class="flow-target-label">灵感点</div>\n';
  627. relations.inspiration.forEach(rel => {
  628. const score = (rel.avg_score * 100).toFixed(0);
  629. html += `<div class="flow-target-item inspiration-flow">\n`;
  630. html += `<div class="flow-target-text">${rel.point}</div>\n`;
  631. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  632. html += '</div>\n';
  633. });
  634. html += '</div>\n';
  635. }
  636. if (relations.purpose && relations.purpose.length > 0) {
  637. html += '<div class="flow-target-group">\n';
  638. html += '<div class="flow-target-label">目的点</div>\n';
  639. relations.purpose.forEach(rel => {
  640. const score = (rel.avg_score * 100).toFixed(0);
  641. html += `<div class="flow-target-item purpose-flow">\n`;
  642. html += `<div class="flow-target-text">${rel.point}</div>\n`;
  643. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  644. html += '</div>\n';
  645. });
  646. html += '</div>\n';
  647. }
  648. if (relations.keypoint && relations.keypoint.length > 0) {
  649. html += '<div class="flow-target-group">\n';
  650. html += '<div class="flow-target-label">关键点</div>\n';
  651. relations.keypoint.forEach(rel => {
  652. const score = (rel.avg_score * 100).toFixed(0);
  653. html += `<div class="flow-target-item keypoint-flow">\n`;
  654. html += `<div class="flow-target-text">${rel.point}</div>\n`;
  655. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  656. html += '</div>\n';
  657. });
  658. html += '</div>\n';
  659. }
  660. html += '</div>\n';
  661. html += '</div>\n';
  662. // 插入到画布
  663. canvas.insertAdjacentHTML('beforeend', html);
  664. }
  665. // 绘制反向支撑关系(目标点 ← 实质点)
  666. function drawReverseSupportRelationships(targetId, relations) {
  667. const canvas = document.getElementById('support-canvas');
  668. if (!canvas) return;
  669. // 清空并移除占位文本
  670. const placeholder = canvas.querySelector('.placeholder-text');
  671. if (placeholder) {
  672. placeholder.style.display = 'none';
  673. }
  674. // 创建可视化内容
  675. let html = '<div class="support-flow reverse">\n';
  676. html += '<div class="flow-title">被以下实质点支撑</div>\n';
  677. // 显示目标点
  678. html += '<div class="flow-source">\n';
  679. html += '<div class="flow-node target-flow-node">\n';
  680. html += `<div class="flow-node-label">目标点</div>\n`;
  681. html += `<div class="flow-node-name">${targetId}</div>\n`;
  682. html += '</div>\n';
  683. html += '</div>\n';
  684. // 显示支撑的实质点(分类)
  685. html += '<div class="flow-targets">\n';
  686. if (relations.concrete_elements && relations.concrete_elements.length > 0) {
  687. html += '<div class="flow-target-group">\n';
  688. html += '<div class="flow-target-label">具体元素</div>\n';
  689. relations.concrete_elements.forEach(item => {
  690. const score = (item.score * 100).toFixed(0);
  691. html += `<div class="flow-target-item concrete-element-flow">\n`;
  692. html += `<div class="flow-target-text">${item.name}</div>\n`;
  693. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  694. html += '</div>\n';
  695. });
  696. html += '</div>\n';
  697. }
  698. if (relations.concrete_concepts && relations.concrete_concepts.length > 0) {
  699. html += '<div class="flow-target-group">\n';
  700. html += '<div class="flow-target-label">具体概念</div>\n';
  701. relations.concrete_concepts.forEach(item => {
  702. const score = (item.score * 100).toFixed(0);
  703. html += `<div class="flow-target-item concrete-concept-flow">\n`;
  704. html += `<div class="flow-target-text">${item.name}</div>\n`;
  705. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  706. html += '</div>\n';
  707. });
  708. html += '</div>\n';
  709. }
  710. if (relations.abstract_concepts && relations.abstract_concepts.length > 0) {
  711. html += '<div class="flow-target-group">\n';
  712. html += '<div class="flow-target-label">抽象概念</div>\n';
  713. relations.abstract_concepts.forEach(item => {
  714. const score = (item.score * 100).toFixed(0);
  715. html += `<div class="flow-target-item abstract-concept-flow">\n`;
  716. html += `<div class="flow-target-text">${item.name}</div>\n`;
  717. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  718. html += '</div>\n';
  719. });
  720. html += '</div>\n';
  721. }
  722. html += '</div>\n';
  723. html += '</div>\n';
  724. // 插入到画布
  725. canvas.insertAdjacentHTML('beforeend', html);
  726. }
  727. // 清空支撑关系画布
  728. function clearSupportCanvas() {
  729. const canvas = document.getElementById('support-canvas');
  730. if (!canvas) return;
  731. // 移除所有flow相关元素
  732. const flows = canvas.querySelectorAll('.support-flow');
  733. flows.forEach(flow => flow.remove());
  734. // 重新显示占位文本
  735. const placeholder = canvas.querySelector('.placeholder-text');
  736. if (placeholder) {
  737. placeholder.style.display = 'flex';
  738. }
  739. }
  740. // ===== Tab5 实质与形式双向支撑关系图功能 =====
  741. let selectedLeftTargetType = null; // 左侧选题点类型:inspiration/keypoint/purpose
  742. let selectedLeftTargetIdx = null; // 左侧选题点索引
  743. let selectedTab5SubstanceId = null; // 选中的实质点ID (Tab5专用)
  744. let selectedFormId = null; // 选中的形式点ID
  745. let selectedRightTargetType = null; // 右侧选题点类型:inspiration/keypoint/purpose
  746. let selectedRightTargetIdx = null; // 右侧选题点索引
  747. // 选择左侧选题点(来自实质点评分)
  748. function selectLeftTarget(targetType, targetIdx) {
  749. if (typeof tab5Relationships === 'undefined') {
  750. console.error('tab5Relationships not defined');
  751. return;
  752. }
  753. // 如果点击已选中的目标点,重置显示所有元素
  754. if (selectedLeftTargetType === targetType && selectedLeftTargetIdx === targetIdx) {
  755. resetTab5Selection();
  756. return;
  757. }
  758. // 重置其他选择
  759. resetTab5Selection();
  760. selectedLeftTargetType = targetType;
  761. selectedLeftTargetIdx = targetIdx;
  762. // 高亮选中的目标点
  763. document.querySelectorAll('.tab5-left-targets .target-card').forEach(card => {
  764. const dataType = card.getAttribute('data-type');
  765. const dataId = card.getAttribute('data-id');
  766. const fullId = `${targetType}-${targetIdx}`;
  767. if (dataId === fullId) {
  768. card.classList.add('selected');
  769. card.classList.remove('dimmed', 'hidden');
  770. } else {
  771. card.classList.remove('selected');
  772. card.classList.add('hidden');
  773. card.classList.remove('dimmed');
  774. }
  775. });
  776. // 形式点全部隐藏
  777. document.querySelectorAll('.tab5-forms .form-card').forEach(card => {
  778. card.classList.add('hidden');
  779. card.classList.remove('selected', 'highlighted', 'dimmed');
  780. });
  781. // 右侧选题点全部隐藏
  782. document.querySelectorAll('.tab5-right-targets .target-card').forEach(card => {
  783. card.classList.add('hidden');
  784. card.classList.remove('selected', 'highlighted', 'dimmed');
  785. });
  786. // 获取反向关系:哪些实质点支撑这个目标点(直接使用完整的targetIdx)
  787. const targetId = `${targetType}-${targetIdx}`;
  788. const supportingSubstances = tab5Relationships.target_from_substance[targetId];
  789. if (supportingSubstances && supportingSubstances.length > 0) {
  790. // 高亮支撑的实质点
  791. const substanceIds = new Set(supportingSubstances.map(s => s.substance_id));
  792. document.querySelectorAll('.tab5-substances .substance-card').forEach(card => {
  793. const cardId = card.getAttribute('data-id');
  794. if (substanceIds.has(cardId)) {
  795. card.classList.add('highlighted');
  796. card.classList.remove('dimmed', 'hidden');
  797. } else {
  798. card.classList.remove('highlighted');
  799. card.classList.add('hidden');
  800. card.classList.remove('dimmed');
  801. }
  802. });
  803. }
  804. // 绘制连线(只连接当前显示的卡片)
  805. setTimeout(() => {
  806. drawFormAllConnections();
  807. }, 50);
  808. }
  809. // 选择实质点
  810. function selectSubstance(substanceId) {
  811. if (typeof tab5Relationships === 'undefined') {
  812. console.error('tab5Relationships not defined');
  813. return;
  814. }
  815. // 如果点击已选中的实质点,重置显示所有元素
  816. if (selectedTab5SubstanceId === substanceId) {
  817. resetTab5Selection();
  818. return;
  819. }
  820. // 重置其他选择
  821. resetTab5Selection();
  822. selectedTab5SubstanceId = substanceId;
  823. // 高亮选中的实质点
  824. document.querySelectorAll('.tab5-substances .substance-card').forEach(card => {
  825. const cardId = card.getAttribute('data-id');
  826. if (cardId === substanceId) {
  827. card.classList.add('selected');
  828. card.classList.remove('dimmed', 'hidden');
  829. } else {
  830. card.classList.remove('selected');
  831. card.classList.add('hidden');
  832. card.classList.remove('dimmed');
  833. }
  834. });
  835. // 形式点全部隐藏(先全部隐藏,后面再显示有关联的)
  836. document.querySelectorAll('.tab5-forms .form-card').forEach(card => {
  837. card.classList.add('hidden');
  838. card.classList.remove('selected', 'highlighted', 'dimmed');
  839. });
  840. // 右侧选题点全部隐藏
  841. document.querySelectorAll('.tab5-right-targets .target-card').forEach(card => {
  842. card.classList.add('hidden');
  843. card.classList.remove('selected', 'highlighted', 'dimmed');
  844. });
  845. // 获取这个实质点支撑的选题点
  846. const relations = tab5Relationships.substance_to_target[substanceId];
  847. if (relations) {
  848. const leftTargets = new Set();
  849. relations.inspiration.forEach(rel => leftTargets.add(rel.target_id));
  850. relations.purpose.forEach(rel => leftTargets.add(rel.target_id));
  851. relations.keypoint.forEach(rel => leftTargets.add(rel.target_id));
  852. // 高亮左侧相关选题点
  853. document.querySelectorAll('.tab5-left-targets .target-card').forEach(card => {
  854. const cardId = card.getAttribute('data-id');
  855. if (leftTargets.has(cardId)) {
  856. card.classList.add('highlighted');
  857. card.classList.remove('dimmed', 'hidden');
  858. } else {
  859. card.classList.remove('highlighted');
  860. card.classList.add('hidden');
  861. card.classList.remove('dimmed');
  862. }
  863. });
  864. }
  865. // 高亮支撑这个实质点的形式点(反向关系)
  866. const supportingForms = tab5Relationships.substance_from_form[substanceId];
  867. if (supportingForms && supportingForms.length > 0) {
  868. const formIds = new Set(supportingForms.map(f => f.form_id));
  869. document.querySelectorAll('.tab5-forms .form-card').forEach(card => {
  870. const cardId = card.getAttribute('data-id');
  871. if (formIds.has(cardId)) {
  872. card.classList.add('highlighted');
  873. card.classList.remove('dimmed', 'hidden');
  874. }
  875. // 注意:这里不对其他形式点再次隐藏,因为已经在前面全部隐藏了
  876. });
  877. }
  878. // 绘制连线(只连接当前显示的卡片)
  879. setTimeout(() => {
  880. drawFormAllConnections();
  881. }, 50);
  882. }
  883. // 选择形式点
  884. function selectForm(formId) {
  885. if (typeof tab5Relationships === 'undefined') {
  886. console.error('tab5Relationships not defined');
  887. return;
  888. }
  889. // 如果点击已选中的形式点,重置显示所有元素
  890. if (selectedFormId === formId) {
  891. resetTab5Selection();
  892. return;
  893. }
  894. // 重置其他选择
  895. resetTab5Selection();
  896. selectedFormId = formId;
  897. // 高亮选中的形式点
  898. document.querySelectorAll('.tab5-forms .form-card').forEach(card => {
  899. const cardId = card.getAttribute('data-id');
  900. if (cardId === formId) {
  901. card.classList.add('selected');
  902. card.classList.remove('dimmed', 'hidden');
  903. } else {
  904. card.classList.remove('selected');
  905. card.classList.add('hidden');
  906. card.classList.remove('dimmed');
  907. }
  908. });
  909. // 实质点全部隐藏(先全部隐藏,后面再显示有关联的)
  910. document.querySelectorAll('.tab5-substances .substance-card').forEach(card => {
  911. card.classList.add('hidden');
  912. card.classList.remove('selected', 'highlighted', 'dimmed');
  913. });
  914. // 左侧选题点全部隐藏
  915. document.querySelectorAll('.tab5-left-targets .target-card').forEach(card => {
  916. card.classList.add('hidden');
  917. card.classList.remove('selected', 'highlighted', 'dimmed');
  918. });
  919. // 获取这个形式点支撑的选题点
  920. const relations = tab5Relationships.form_to_target[formId];
  921. if (relations) {
  922. const rightTargets = new Set();
  923. relations.inspiration.forEach(rel => rightTargets.add(rel.target_id));
  924. relations.purpose.forEach(rel => rightTargets.add(rel.target_id));
  925. relations.keypoint.forEach(rel => rightTargets.add(rel.target_id));
  926. // 高亮右侧相关选题点
  927. document.querySelectorAll('.tab5-right-targets .target-card').forEach(card => {
  928. const cardId = card.getAttribute('data-id');
  929. if (rightTargets.has(cardId)) {
  930. card.classList.add('highlighted');
  931. card.classList.remove('dimmed', 'hidden');
  932. } else {
  933. card.classList.remove('highlighted');
  934. card.classList.add('hidden');
  935. card.classList.remove('dimmed');
  936. }
  937. });
  938. }
  939. // 高亮这个形式点支撑的实质点
  940. const supportedSubstances = tab5Relationships.form_to_substance[formId];
  941. if (supportedSubstances && supportedSubstances.length > 0) {
  942. const substanceIds = new Set(supportedSubstances.map(s => s.substance_id));
  943. document.querySelectorAll('.tab5-substances .substance-card').forEach(card => {
  944. const cardId = card.getAttribute('data-id');
  945. if (substanceIds.has(cardId)) {
  946. card.classList.add('highlighted');
  947. card.classList.remove('dimmed', 'hidden');
  948. }
  949. // 注意:这里不对其他实质点再次隐藏,因为已经在前面全部隐藏了
  950. });
  951. }
  952. // 绘制连线(只连接当前显示的卡片)
  953. setTimeout(() => {
  954. drawFormAllConnections();
  955. }, 50);
  956. }
  957. // 选择右侧选题点(来自形式点评分)
  958. function selectRightTarget(targetType, targetIdx) {
  959. if (typeof tab5Relationships === 'undefined') {
  960. console.error('tab5Relationships not defined');
  961. return;
  962. }
  963. // 如果点击已选中的目标点,重置显示所有元素
  964. if (selectedRightTargetType === targetType && selectedRightTargetIdx === targetIdx) {
  965. resetTab5Selection();
  966. return;
  967. }
  968. // 重置其他选择
  969. resetTab5Selection();
  970. selectedRightTargetType = targetType;
  971. selectedRightTargetIdx = targetIdx;
  972. // 高亮选中的目标点
  973. document.querySelectorAll('.tab5-right-targets .target-card').forEach(card => {
  974. const dataType = card.getAttribute('data-type');
  975. const dataId = card.getAttribute('data-id');
  976. const fullId = `${targetType}-${targetIdx}`;
  977. if (dataId === fullId) {
  978. card.classList.add('selected');
  979. card.classList.remove('dimmed', 'hidden');
  980. } else {
  981. card.classList.remove('selected');
  982. card.classList.add('hidden');
  983. card.classList.remove('dimmed');
  984. }
  985. });
  986. // 实质点全部隐藏
  987. document.querySelectorAll('.tab5-substances .substance-card').forEach(card => {
  988. card.classList.add('hidden');
  989. card.classList.remove('selected', 'highlighted', 'dimmed');
  990. });
  991. // 左侧选题点全部隐藏
  992. document.querySelectorAll('.tab5-left-targets .target-card').forEach(card => {
  993. card.classList.add('hidden');
  994. card.classList.remove('selected', 'highlighted', 'dimmed');
  995. });
  996. // 获取反向关系:哪些形式点支撑这个目标点(直接使用完整的targetIdx)
  997. const targetId = `${targetType}-${targetIdx}`;
  998. const supportingForms = tab5Relationships.target_from_form[targetId];
  999. if (supportingForms && supportingForms.length > 0) {
  1000. // 高亮支撑的形式点
  1001. const formIds = new Set(supportingForms.map(f => f.form_id));
  1002. document.querySelectorAll('.tab5-forms .form-card').forEach(card => {
  1003. const cardId = card.getAttribute('data-id');
  1004. if (formIds.has(cardId)) {
  1005. card.classList.add('highlighted');
  1006. card.classList.remove('dimmed', 'hidden');
  1007. } else {
  1008. card.classList.remove('highlighted');
  1009. card.classList.add('hidden');
  1010. card.classList.remove('dimmed');
  1011. }
  1012. });
  1013. }
  1014. // 绘制连线(只连接当前显示的卡片)
  1015. setTimeout(() => {
  1016. drawFormAllConnections();
  1017. }, 50);
  1018. }
  1019. // 重置Tab5所有选择
  1020. function resetTab5Selection() {
  1021. selectedLeftTargetType = null;
  1022. selectedLeftTargetIdx = null;
  1023. selectedSubstanceId = null;
  1024. selectedFormId = null;
  1025. selectedRightTargetType = null;
  1026. selectedRightTargetIdx = null;
  1027. // 清除所有高亮、选中和隐藏状态
  1028. document.querySelectorAll('#tab5 .target-card, #tab5 .substance-card, #tab5 .form-card').forEach(card => {
  1029. card.classList.remove('selected', 'dimmed', 'highlighted', 'hidden');
  1030. });
  1031. // 清除所有连线
  1032. const svg = document.getElementById('tab5-connection-svg');
  1033. if (svg) {
  1034. svg.innerHTML = '';
  1035. }
  1036. }
  1037. // 选择目标点(反向查看)
  1038. function selectFormTarget(targetType, targetId) {
  1039. if (typeof formSupportRelationships === 'undefined') {
  1040. console.error('formSupportRelationships not defined');
  1041. return;
  1042. }
  1043. let fullTargetId;
  1044. if (targetType === 'substance') {
  1045. fullTargetId = `substance-${targetId}`;
  1046. } else {
  1047. fullTargetId = `${targetType}-${targetId}`;
  1048. }
  1049. // 如果点击已选中的目标点,不做处理
  1050. if (selectedFormTargetId === fullTargetId) {
  1051. return;
  1052. }
  1053. selectedFormId = null;
  1054. selectedFormTargetId = fullTargetId;
  1055. // 清空画布
  1056. clearFormSupportCanvas();
  1057. // 获取反向关系数据
  1058. const relations = formSupportRelationships.target_to_form[fullTargetId];
  1059. if (!relations) {
  1060. console.error('No relations found for target:', fullTargetId);
  1061. return;
  1062. }
  1063. // 清除所有形式点的状态
  1064. document.querySelectorAll('.form-card').forEach(card => {
  1065. card.classList.remove('selected', 'dimmed', 'highlighted');
  1066. });
  1067. // 高亮选中的目标点,其他变暗
  1068. document.querySelectorAll('#tab5 .target-card').forEach(card => {
  1069. if (card.getAttribute('data-id') === fullTargetId) {
  1070. card.classList.add('selected');
  1071. card.classList.remove('dimmed');
  1072. } else {
  1073. card.classList.remove('selected');
  1074. card.classList.add('dimmed');
  1075. }
  1076. });
  1077. // 收集所有支撑该目标点的形式点ID
  1078. const supportingForms = new Set();
  1079. if (relations.concrete_element_forms) {
  1080. relations.concrete_element_forms.forEach(item => supportingForms.add(item.form_id));
  1081. }
  1082. if (relations.concrete_concept_forms) {
  1083. relations.concrete_concept_forms.forEach(item => supportingForms.add(item.form_id));
  1084. }
  1085. if (relations.overall_forms) {
  1086. relations.overall_forms.forEach(item => supportingForms.add(item.form_id));
  1087. }
  1088. if (relations.forms) {
  1089. relations.forms.forEach(item => supportingForms.add(item.form_id));
  1090. }
  1091. // 高亮支撑的形式点,其他变暗
  1092. document.querySelectorAll('.form-card').forEach(card => {
  1093. const cardId = card.getAttribute('data-id');
  1094. if (supportingForms.has(cardId)) {
  1095. card.classList.add('highlighted');
  1096. card.classList.remove('dimmed');
  1097. } else {
  1098. card.classList.add('dimmed');
  1099. card.classList.remove('highlighted');
  1100. }
  1101. });
  1102. // 绘制反向支撑关系
  1103. drawReverseFormSupportRelationships(fullTargetId, relations);
  1104. }
  1105. // 绘制形式点支撑关系(形式点 → 目标点)
  1106. function drawFormSupportRelationships(formId, relations) {
  1107. const canvas = document.getElementById('form-support-canvas');
  1108. if (!canvas) return;
  1109. // 清空并移除占位文本
  1110. const placeholder = canvas.querySelector('.placeholder-text');
  1111. if (placeholder) {
  1112. placeholder.style.display = 'none';
  1113. }
  1114. // 创建可视化内容
  1115. let html = '<div class="support-flow">\n';
  1116. html += '<div class="flow-title">支撑关系</div>\n';
  1117. // 显示来源形式点
  1118. html += '<div class="flow-source">\n';
  1119. html += '<div class="flow-node form-flow-node">\n';
  1120. html += `<div class="flow-node-label">${relations.type || '形式点'}</div>\n`;
  1121. html += `<div class="flow-node-name">${relations.name || formId}</div>\n`;
  1122. html += '</div>\n';
  1123. html += '</div>\n';
  1124. // 显示支撑的目标点(分类)
  1125. html += '<div class="flow-targets">\n';
  1126. if (relations.inspiration && relations.inspiration.length > 0) {
  1127. html += '<div class="flow-target-group">\n';
  1128. html += '<div class="flow-target-label">灵感点</div>\n';
  1129. relations.inspiration.forEach(rel => {
  1130. const score = (rel.avg_score * 100).toFixed(0);
  1131. html += `<div class="flow-target-item inspiration-flow">\n`;
  1132. html += `<div class="flow-target-text">${rel.point}</div>\n`;
  1133. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  1134. html += '</div>\n';
  1135. });
  1136. html += '</div>\n';
  1137. }
  1138. if (relations.purpose && relations.purpose.length > 0) {
  1139. html += '<div class="flow-target-group">\n';
  1140. html += '<div class="flow-target-label">目的点</div>\n';
  1141. relations.purpose.forEach(rel => {
  1142. const score = (rel.avg_score * 100).toFixed(0);
  1143. html += `<div class="flow-target-item purpose-flow">\n`;
  1144. html += `<div class="flow-target-text">${rel.point}</div>\n`;
  1145. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  1146. html += '</div>\n';
  1147. });
  1148. html += '</div>\n';
  1149. }
  1150. if (relations.keypoint && relations.keypoint.length > 0) {
  1151. html += '<div class="flow-target-group">\n';
  1152. html += '<div class="flow-target-label">关键点</div>\n';
  1153. relations.keypoint.forEach(rel => {
  1154. const score = (rel.avg_score * 100).toFixed(0);
  1155. html += `<div class="flow-target-item keypoint-flow">\n`;
  1156. html += `<div class="flow-target-text">${rel.point}</div>\n`;
  1157. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  1158. html += '</div>\n';
  1159. });
  1160. html += '</div>\n';
  1161. }
  1162. if (relations.substance && relations.substance.length > 0) {
  1163. html += '<div class="flow-target-group">\n';
  1164. html += '<div class="flow-target-label">实质点</div>\n';
  1165. relations.substance.forEach(rel => {
  1166. const score = (rel.avg_score * 100).toFixed(0);
  1167. let flowClass = '';
  1168. if (rel.type === '具体元素') {
  1169. flowClass = 'concrete-element-flow';
  1170. } else if (rel.type === '具体概念') {
  1171. flowClass = 'concrete-concept-flow';
  1172. } else if (rel.type === '抽象概念') {
  1173. flowClass = 'abstract-concept-flow';
  1174. }
  1175. html += `<div class="flow-target-item ${flowClass}">\n`;
  1176. html += `<div class="flow-target-text">${rel.name} <span style="font-size:0.8em;color:#6c757d;">(${rel.type})</span></div>\n`;
  1177. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  1178. html += '</div>\n';
  1179. });
  1180. html += '</div>\n';
  1181. }
  1182. html += '</div>\n';
  1183. html += '</div>\n';
  1184. // 插入到画布
  1185. canvas.insertAdjacentHTML('beforeend', html);
  1186. }
  1187. // 绘制反向形式点支撑关系(目标点 ← 形式点)
  1188. function drawReverseFormSupportRelationships(targetId, relations) {
  1189. const canvas = document.getElementById('form-support-canvas');
  1190. if (!canvas) return;
  1191. // 清空并移除占位文本
  1192. const placeholder = canvas.querySelector('.placeholder-text');
  1193. if (placeholder) {
  1194. placeholder.style.display = 'none';
  1195. }
  1196. // 创建可视化内容
  1197. let html = '<div class="support-flow reverse">\n';
  1198. html += '<div class="flow-title">被以下形式点支撑</div>\n';
  1199. // 显示目标点
  1200. html += '<div class="flow-source">\n';
  1201. html += '<div class="flow-node target-flow-node">\n';
  1202. html += `<div class="flow-node-label">目标点</div>\n`;
  1203. html += `<div class="flow-node-name">${targetId}</div>\n`;
  1204. html += '</div>\n';
  1205. html += '</div>\n';
  1206. // 显示支撑的形式点(分类)
  1207. html += '<div class="flow-targets">\n';
  1208. if (relations.concrete_element_forms && relations.concrete_element_forms.length > 0) {
  1209. html += '<div class="flow-target-group">\n';
  1210. html += '<div class="flow-target-label">具体元素形式</div>\n';
  1211. relations.concrete_element_forms.forEach(item => {
  1212. const score = (item.score * 100).toFixed(0);
  1213. html += `<div class="flow-target-item concrete-element-form-flow">\n`;
  1214. html += `<div class="flow-target-text">${item.name}</div>\n`;
  1215. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  1216. html += '</div>\n';
  1217. });
  1218. html += '</div>\n';
  1219. }
  1220. if (relations.concrete_concept_forms && relations.concrete_concept_forms.length > 0) {
  1221. html += '<div class="flow-target-group">\n';
  1222. html += '<div class="flow-target-label">具体概念形式</div>\n';
  1223. relations.concrete_concept_forms.forEach(item => {
  1224. const score = (item.score * 100).toFixed(0);
  1225. html += `<div class="flow-target-item concrete-concept-form-flow">\n`;
  1226. html += `<div class="flow-target-text">${item.name}</div>\n`;
  1227. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  1228. html += '</div>\n';
  1229. });
  1230. html += '</div>\n';
  1231. }
  1232. if (relations.overall_forms && relations.overall_forms.length > 0) {
  1233. html += '<div class="flow-target-group">\n';
  1234. html += '<div class="flow-target-label">整体形式</div>\n';
  1235. relations.overall_forms.forEach(item => {
  1236. const score = (item.score * 100).toFixed(0);
  1237. html += `<div class="flow-target-item overall-form-flow">\n`;
  1238. html += `<div class="flow-target-text">${item.name}</div>\n`;
  1239. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  1240. html += '</div>\n';
  1241. });
  1242. html += '</div>\n';
  1243. }
  1244. if (relations.forms && relations.forms.length > 0) {
  1245. html += '<div class="flow-target-group">\n';
  1246. html += '<div class="flow-target-label">形式点</div>\n';
  1247. relations.forms.forEach(item => {
  1248. const score = (item.score * 100).toFixed(0);
  1249. html += `<div class="flow-target-item form-flow">\n`;
  1250. html += `<div class="flow-target-text">${item.name} <span style="font-size:0.8em;color:#6c757d;">(${item.type})</span></div>\n`;
  1251. html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
  1252. html += '</div>\n';
  1253. });
  1254. html += '</div>\n';
  1255. }
  1256. html += '</div>\n';
  1257. html += '</div>\n';
  1258. // 插入到画布
  1259. canvas.insertAdjacentHTML('beforeend', html);
  1260. }
  1261. // 清空形式点支撑关系画布
  1262. function clearFormSupportCanvas() {
  1263. const canvas = document.getElementById('form-support-canvas');
  1264. if (!canvas) return;
  1265. // 移除所有flow相关元素
  1266. const flows = canvas.querySelectorAll('.support-flow');
  1267. flows.forEach(flow => flow.remove());
  1268. // 重新显示占位文本
  1269. const placeholder = canvas.querySelector('.placeholder-text');
  1270. if (placeholder) {
  1271. placeholder.style.display = 'flex';
  1272. }
  1273. }
  1274. // ===== Tab5 连线绘制功能 =====
  1275. // 绘制Tab5所有连线
  1276. function drawFormAllConnections() {
  1277. if (typeof tab5Relationships === 'undefined') {
  1278. console.log('tab5Relationships not defined, skipping connection drawing');
  1279. return;
  1280. }
  1281. const svg = document.getElementById('tab5-connection-svg');
  1282. if (!svg) {
  1283. console.log('SVG container not found');
  1284. return;
  1285. }
  1286. // 清空现有连线
  1287. svg.innerHTML = '';
  1288. // 1. 绘制左侧选题点到实质点的连线(反向关系)
  1289. drawLeftTargetToSubstanceConnections(svg);
  1290. // 2. 绘制实质点到形式点的连线(支撑关系)
  1291. drawSubstanceToFormConnections(svg);
  1292. // 3. 绘制形式点到右侧选题点的连线
  1293. drawFormToRightTargetConnections(svg);
  1294. }
  1295. // 绘制左侧选题点到实质点的连线
  1296. function drawLeftTargetToSubstanceConnections(svg) {
  1297. const targetFromSubstance = tab5Relationships.target_from_substance;
  1298. Object.keys(targetFromSubstance).forEach(targetId => {
  1299. const substances = targetFromSubstance[targetId];
  1300. substances.forEach(substanceData => {
  1301. const sourceCard = document.querySelector(`.tab5-left-targets .target-card[data-id="${targetId}"]`);
  1302. const targetCard = document.querySelector(`.tab5-substances .substance-card[data-id="${substanceData.substance_id}"]`);
  1303. if (sourceCard && targetCard && !sourceCard.classList.contains('hidden') && !targetCard.classList.contains('hidden')) {
  1304. drawTab5Connection(
  1305. svg,
  1306. sourceCard,
  1307. targetCard,
  1308. 'left-target-substance',
  1309. targetId,
  1310. substanceData.substance_id,
  1311. {
  1312. score: substanceData.score,
  1313. substanceName: substanceData.name,
  1314. type: substanceData.type
  1315. }
  1316. );
  1317. }
  1318. });
  1319. });
  1320. }
  1321. // 绘制实质点到形式点的连线
  1322. function drawSubstanceToFormConnections(svg) {
  1323. const formToSubstance = tab5Relationships.form_to_substance;
  1324. Object.keys(formToSubstance).forEach(formId => {
  1325. const substances = formToSubstance[formId];
  1326. substances.forEach(substanceData => {
  1327. const sourceCard = document.querySelector(`.tab5-substances .substance-card[data-id="${substanceData.substance_id}"]`);
  1328. const targetCard = document.querySelector(`.tab5-forms .form-card[data-id="${formId}"]`);
  1329. if (sourceCard && targetCard && !sourceCard.classList.contains('hidden') && !targetCard.classList.contains('hidden')) {
  1330. drawTab5Connection(
  1331. svg,
  1332. sourceCard,
  1333. targetCard,
  1334. 'substance-form',
  1335. substanceData.substance_id,
  1336. formId,
  1337. {
  1338. substanceName: substanceData.name,
  1339. formName: tab5Relationships.form_to_target[formId]?.name || ''
  1340. }
  1341. );
  1342. }
  1343. });
  1344. });
  1345. }
  1346. // 绘制形式点到右侧选题点的连线
  1347. function drawFormToRightTargetConnections(svg) {
  1348. const formToTarget = tab5Relationships.form_to_target;
  1349. Object.keys(formToTarget).forEach(formId => {
  1350. const relations = formToTarget[formId];
  1351. // 绘制到灵感点的连线
  1352. relations.inspiration.forEach(rel => {
  1353. const sourceCard = document.querySelector(`.tab5-forms .form-card[data-id="${formId}"]`);
  1354. const targetCard = document.querySelector(`.tab5-right-targets .target-card[data-id="${rel.target_id}"]`);
  1355. if (sourceCard && targetCard && !sourceCard.classList.contains('hidden') && !targetCard.classList.contains('hidden')) {
  1356. drawTab5Connection(
  1357. svg,
  1358. sourceCard,
  1359. targetCard,
  1360. 'form-inspiration',
  1361. formId,
  1362. rel.target_id,
  1363. {
  1364. score: rel.avg_score,
  1365. point: rel.point,
  1366. formName: relations.name
  1367. }
  1368. );
  1369. }
  1370. });
  1371. // 绘制到目的点的连线
  1372. relations.purpose.forEach(rel => {
  1373. const sourceCard = document.querySelector(`.tab5-forms .form-card[data-id="${formId}"]`);
  1374. const targetCard = document.querySelector(`.tab5-right-targets .target-card[data-id="${rel.target_id}"]`);
  1375. if (sourceCard && targetCard && !sourceCard.classList.contains('hidden') && !targetCard.classList.contains('hidden')) {
  1376. drawTab5Connection(
  1377. svg,
  1378. sourceCard,
  1379. targetCard,
  1380. 'form-purpose',
  1381. formId,
  1382. rel.target_id,
  1383. {
  1384. score: rel.avg_score,
  1385. point: rel.point,
  1386. formName: relations.name
  1387. }
  1388. );
  1389. }
  1390. });
  1391. // 绘制到关键点的连线
  1392. relations.keypoint.forEach(rel => {
  1393. const sourceCard = document.querySelector(`.tab5-forms .form-card[data-id="${formId}"]`);
  1394. const targetCard = document.querySelector(`.tab5-right-targets .target-card[data-id="${rel.target_id}"]`);
  1395. if (sourceCard && targetCard && !sourceCard.classList.contains('hidden') && !targetCard.classList.contains('hidden')) {
  1396. drawTab5Connection(
  1397. svg,
  1398. sourceCard,
  1399. targetCard,
  1400. 'form-keypoint',
  1401. formId,
  1402. rel.target_id,
  1403. {
  1404. score: rel.avg_score,
  1405. point: rel.point,
  1406. formName: relations.name
  1407. }
  1408. );
  1409. }
  1410. });
  1411. });
  1412. }
  1413. // 绘制单条Tab5连线
  1414. function drawTab5Connection(svg, sourceCard, targetCard, connectionType, sourceId, targetId, metadata) {
  1415. const svgContainer = svg.parentElement;
  1416. const containerRect = svgContainer.getBoundingClientRect();
  1417. const sourceRect = sourceCard.getBoundingClientRect();
  1418. const targetRect = targetCard.getBoundingClientRect();
  1419. // 计算连线起点和终点
  1420. let x1, y1, x2, y2;
  1421. if (connectionType === 'left-target-substance') {
  1422. // 左侧选题点到实质点:从右边连到左边
  1423. x1 = sourceRect.right - containerRect.left;
  1424. y1 = sourceRect.top + sourceRect.height / 2 - containerRect.top;
  1425. x2 = targetRect.left - containerRect.left;
  1426. y2 = targetRect.top + targetRect.height / 2 - containerRect.top;
  1427. } else if (connectionType === 'substance-form') {
  1428. // 实质点到形式点:从右边连到左边
  1429. x1 = sourceRect.right - containerRect.left;
  1430. y1 = sourceRect.top + sourceRect.height / 2 - containerRect.top;
  1431. x2 = targetRect.left - containerRect.left;
  1432. y2 = targetRect.top + targetRect.height / 2 - containerRect.top;
  1433. } else {
  1434. // 形式点到右侧选题点:从右边连到左边
  1435. x1 = sourceRect.right - containerRect.left;
  1436. y1 = sourceRect.top + sourceRect.height / 2 - containerRect.top;
  1437. x2 = targetRect.left - containerRect.left;
  1438. y2 = targetRect.top + targetRect.height / 2 - containerRect.top;
  1439. }
  1440. // 创建贝塞尔曲线路径
  1441. const midX = (x1 + x2) / 2;
  1442. const path = `M ${x1} ${y1} Q ${midX} ${y1}, ${midX} ${(y1 + y2) / 2} T ${x2} ${y2}`;
  1443. // 创建path元素
  1444. const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  1445. pathElement.setAttribute('d', path);
  1446. pathElement.setAttribute('class', `tab5-connection-line ${connectionType}`);
  1447. pathElement.setAttribute('fill', 'none');
  1448. pathElement.setAttribute('stroke-width', '2');
  1449. pathElement.setAttribute('data-source-id', sourceId);
  1450. pathElement.setAttribute('data-target-id', targetId);
  1451. pathElement.setAttribute('data-connection-type', connectionType);
  1452. // 保存元数据以供点击时使用
  1453. pathElement.setAttribute('data-metadata', JSON.stringify(metadata));
  1454. // 添加点击事件
  1455. pathElement.style.cursor = 'pointer';
  1456. pathElement.addEventListener('click', function(e) {
  1457. e.stopPropagation();
  1458. showConnectionDetail(connectionType, sourceId, targetId, metadata);
  1459. });
  1460. // 添加hover效果
  1461. pathElement.addEventListener('mouseenter', function() {
  1462. this.setAttribute('stroke-width', '4');
  1463. this.style.filter = 'drop-shadow(0 0 6px rgba(0,0,0,0.3))';
  1464. });
  1465. pathElement.addEventListener('mouseleave', function() {
  1466. this.setAttribute('stroke-width', '2');
  1467. this.style.filter = 'none';
  1468. });
  1469. svg.appendChild(pathElement);
  1470. }
  1471. // 显示连线详情模态框
  1472. function showConnectionDetail(connectionType, sourceId, targetId, metadata) {
  1473. // 创建模态框
  1474. const modal = document.createElement('div');
  1475. modal.className = 'connection-modal-backdrop';
  1476. modal.onclick = function(e) {
  1477. if (e.target === modal) {
  1478. document.body.removeChild(modal);
  1479. }
  1480. };
  1481. // 创建模态框内容
  1482. const modalContent = document.createElement('div');
  1483. modalContent.className = 'connection-modal-content';
  1484. modalContent.onclick = function(e) {
  1485. e.stopPropagation();
  1486. };
  1487. // 构建内容
  1488. let html = '<div class="modal-header">';
  1489. html += '<h3>连线关系详情</h3>';
  1490. html += '<button class="modal-close" onclick="this.closest(\'.connection-modal-backdrop\').remove()">×</button>';
  1491. html += '</div>';
  1492. html += '<div class="modal-body">';
  1493. // 根据连接类型显示不同内容
  1494. if (connectionType === 'left-target-substance') {
  1495. html += '<div class="connection-detail-section">';
  1496. html += '<div class="connection-label">连接类型:</div>';
  1497. html += '<div class="connection-value">选题点 → 实质点</div>';
  1498. html += '</div>';
  1499. html += '<div class="connection-detail-section">';
  1500. html += '<div class="connection-label">实质点:</div>';
  1501. html += '<div class="connection-value">' + (metadata.substanceName || sourceId) + '</div>';
  1502. html += '</div>';
  1503. html += '<div class="connection-detail-section">';
  1504. html += '<div class="connection-label">实质点类型:</div>';
  1505. html += '<div class="connection-value">' + (metadata.type || '-') + '</div>';
  1506. html += '</div>';
  1507. if (metadata.score !== undefined) {
  1508. const scorePercent = (metadata.score * 100).toFixed(1);
  1509. html += '<div class="connection-detail-section">';
  1510. html += '<div class="connection-label">相似度评分:</div>';
  1511. html += '<div class="connection-value score-value">' + scorePercent + '%</div>';
  1512. html += '</div>';
  1513. }
  1514. html += '<div class="connection-detail-section">';
  1515. html += '<div class="connection-label">说明:</div>';
  1516. html += '<div class="connection-value">该实质点支撑了这个选题点的内容,相似度越高表示关联性越强</div>';
  1517. html += '</div>';
  1518. } else if (connectionType === 'substance-form') {
  1519. html += '<div class="connection-detail-section">';
  1520. html += '<div class="connection-label">连接类型:</div>';
  1521. html += '<div class="connection-value">实质点 → 形式点</div>';
  1522. html += '</div>';
  1523. html += '<div class="connection-detail-section">';
  1524. html += '<div class="connection-label">实质点:</div>';
  1525. html += '<div class="connection-value">' + (metadata.substanceName || sourceId) + '</div>';
  1526. html += '</div>';
  1527. html += '<div class="connection-detail-section">';
  1528. html += '<div class="connection-label">形式点:</div>';
  1529. html += '<div class="connection-value">' + (metadata.formName || targetId) + '</div>';
  1530. html += '</div>';
  1531. html += '<div class="connection-detail-section">';
  1532. html += '<div class="connection-label">说明:</div>';
  1533. html += '<div class="connection-value">该形式点是对实质点的具体表现形式</div>';
  1534. html += '</div>';
  1535. } else if (connectionType.startsWith('form-')) {
  1536. const targetType = connectionType.split('-')[1]; // inspiration, purpose, keypoint
  1537. const targetTypeLabel = {
  1538. 'inspiration': '灵感点',
  1539. 'purpose': '目的点',
  1540. 'keypoint': '关键点'
  1541. }[targetType] || targetType;
  1542. html += '<div class="connection-detail-section">';
  1543. html += '<div class="connection-label">连接类型:</div>';
  1544. html += '<div class="connection-value">形式点 → ' + targetTypeLabel + '</div>';
  1545. html += '</div>';
  1546. html += '<div class="connection-detail-section">';
  1547. html += '<div class="connection-label">形式点:</div>';
  1548. html += '<div class="connection-value">' + (metadata.formName || sourceId) + '</div>';
  1549. html += '</div>';
  1550. if (metadata.point) {
  1551. html += '<div class="connection-detail-section">';
  1552. html += '<div class="connection-label">' + targetTypeLabel + ':</div>';
  1553. html += '<div class="connection-value">' + metadata.point + '</div>';
  1554. html += '</div>';
  1555. }
  1556. if (metadata.score !== undefined) {
  1557. const scorePercent = (metadata.score * 100).toFixed(1);
  1558. html += '<div class="connection-detail-section">';
  1559. html += '<div class="connection-label">相似度评分:</div>';
  1560. html += '<div class="connection-value score-value">' + scorePercent + '%</div>';
  1561. html += '</div>';
  1562. }
  1563. html += '<div class="connection-detail-section">';
  1564. html += '<div class="connection-label">说明:</div>';
  1565. html += '<div class="connection-value">该形式点支撑了这个选题点的呈现,相似度越高表示形式与选题的契合度越高</div>';
  1566. html += '</div>';
  1567. }
  1568. html += '</div>';
  1569. modalContent.innerHTML = html;
  1570. modal.appendChild(modalContent);
  1571. document.body.appendChild(modal);
  1572. }