visualize_inspiration_points.py 105 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415
  1. """
  2. 灵感点分析结果可视化脚本
  3. 读取 how/灵感点 目录下的分析结果,结合作者历史帖子详情,生成可视化HTML页面
  4. """
  5. import json
  6. from pathlib import Path
  7. from typing import Dict, Any, List, Optional
  8. from datetime import datetime
  9. import html as html_module
  10. def load_inspiration_points_data(inspiration_dir: str) -> List[Dict[str, Any]]:
  11. """
  12. 加载所有灵感点的分析结果
  13. Args:
  14. inspiration_dir: 灵感点目录路径
  15. Returns:
  16. 灵感点分析结果列表
  17. """
  18. inspiration_path = Path(inspiration_dir)
  19. results = []
  20. # 遍历所有子目录
  21. for subdir in inspiration_path.iterdir():
  22. if subdir.is_dir():
  23. # 查找 all_summary 文件
  24. summary_files = list(subdir.glob("all_summary_*.json"))
  25. if summary_files:
  26. summary_file = summary_files[0]
  27. try:
  28. with open(summary_file, 'r', encoding='utf-8') as f:
  29. data = json.load(f)
  30. # 加载完整的 step1 和 step2 数据
  31. step1_data = None
  32. step2_data = None
  33. # 直接从当前子目录查找 step1 和 step2 文件
  34. step1_files = list(subdir.glob("all_step1_*.json"))
  35. step2_files = list(subdir.glob("all_step2_*.json"))
  36. if step1_files:
  37. try:
  38. with open(step1_files[0], 'r', encoding='utf-8') as f:
  39. step1_data = json.load(f)
  40. except Exception as e:
  41. print(f"警告: 读取 {step1_files[0]} 失败: {e}")
  42. if step2_files:
  43. try:
  44. with open(step2_files[0], 'r', encoding='utf-8') as f:
  45. step2_data = json.load(f)
  46. except Exception as e:
  47. print(f"警告: 读取 {step2_files[0]} 失败: {e}")
  48. # 加载搜索结果
  49. search_results = {}
  50. search_dir = subdir / "search"
  51. if search_dir.exists() and search_dir.is_dir():
  52. search_files = list(search_dir.glob("all_search_*.json"))
  53. for search_file in search_files:
  54. # 从文件名提取关键词:all_search_关键词.json
  55. keyword = search_file.stem.replace("all_search_", "")
  56. try:
  57. with open(search_file, 'r', encoding='utf-8') as f:
  58. search_data = json.load(f)
  59. search_results[keyword] = search_data
  60. except Exception as e:
  61. print(f"警告: 读取 {search_file} 失败: {e}")
  62. results.append({
  63. "summary": data,
  64. "step1": step1_data,
  65. "step2": step2_data,
  66. "search_results": search_results,
  67. "inspiration_name": subdir.name
  68. })
  69. except Exception as e:
  70. print(f"警告: 读取 {summary_file} 失败: {e}")
  71. return results
  72. def load_posts_data(posts_dir: str) -> Dict[str, Dict[str, Any]]:
  73. """
  74. 加载所有帖子详情数据
  75. Args:
  76. posts_dir: 帖子目录路径
  77. Returns:
  78. 帖子ID到帖子详情的映射
  79. """
  80. posts_path = Path(posts_dir)
  81. posts_map = {}
  82. for post_file in posts_path.glob("*.json"):
  83. try:
  84. with open(post_file, 'r', encoding='utf-8') as f:
  85. post_data = json.load(f)
  86. post_id = post_data.get("channel_content_id")
  87. if post_id:
  88. posts_map[post_id] = post_data
  89. except Exception as e:
  90. print(f"警告: 读取 {post_file} 失败: {e}")
  91. return posts_map
  92. def generate_post_card_html(post: Dict[str, Any], note_id_prefix: str = "info-post", post_to_mapping_data: Dict[str, Any] = None) -> str:
  93. """
  94. 生成单个帖子卡片HTML(与搜索结果样式一致)
  95. Args:
  96. post: 帖子数据
  97. note_id_prefix: 帖子ID前缀,用于区分不同区域的帖子
  98. post_to_mapping_data: 帖子到分类和点映射数据
  99. Returns:
  100. HTML字符串
  101. """
  102. import html as html_module
  103. import random
  104. title = post.get("title", "")
  105. desc = post.get("body_text", "")
  106. images = post.get("images", [])
  107. like_count = post.get("like_count", 0)
  108. comment_count = post.get("comment_count", 0)
  109. link = post.get("link", "")
  110. author = post.get("channel_account_name", "")
  111. post_id = post.get("channel_content_id", "")
  112. # 生成唯一的note_id
  113. note_id = f"{note_id_prefix}-{random.randint(10000, 99999)}"
  114. # 生成图片轮播HTML
  115. images_html = ""
  116. if images and len(images) > 0:
  117. images_track = "".join([f'<img src="{img}" class="note-image" alt="图片{i+1}">' for i, img in enumerate(images)])
  118. # 图片导航按钮
  119. nav_buttons = ""
  120. if len(images) > 1:
  121. nav_buttons = f'''
  122. <button class="note-carousel-button prev" onclick="event.stopPropagation(); moveNoteImage('{note_id}', -1)">‹</button>
  123. <button class="note-carousel-button next" onclick="event.stopPropagation(); moveNoteImage('{note_id}', 1)">›</button>
  124. '''
  125. images_html = f'''
  126. <div class="note-image-carousel" data-note-id="{note_id}" data-total-images="{len(images)}">
  127. <div class="note-images-track" id="{note_id}-track">
  128. {images_track}
  129. </div>
  130. {nav_buttons}
  131. </div>
  132. '''
  133. else:
  134. # 无图片时显示占位符
  135. images_html = f'''
  136. <div class="note-image-carousel">
  137. <div class="note-images-track">
  138. <div class="note-image" style="display: flex; align-items: center; justify-content: center; background: #f3f4f6; color: #9ca3af;">
  139. 暂无图片
  140. </div>
  141. </div>
  142. </div>
  143. '''
  144. # 准备详情数据
  145. note_data = {
  146. "title": title,
  147. "desc": desc,
  148. "images": images,
  149. "link": link,
  150. "author": author,
  151. "like_count": like_count,
  152. "comment_count": comment_count
  153. }
  154. # 添加灵感点、关键点、目的点到note_data
  155. if post_to_mapping_data and post_id and post_id in post_to_mapping_data:
  156. mapping = post_to_mapping_data[post_id]
  157. note_data["inspiration_points"] = mapping.get("灵感点列表", [])
  158. note_data["key_points"] = mapping.get("关键点列表", [])
  159. note_data["purpose_points"] = mapping.get("目的点列表", [])
  160. import json
  161. note_data_json = json.dumps(note_data, ensure_ascii=False)
  162. note_data_json_escaped = html_module.escape(note_data_json)
  163. # 获取帖子的灵感点、关键点、目的点
  164. points_html = ""
  165. if post_to_mapping_data and post_id and post_id in post_to_mapping_data:
  166. mapping = post_to_mapping_data[post_id]
  167. inspiration_points = mapping.get("灵感点列表", [])
  168. key_points = mapping.get("关键点列表", [])
  169. purpose_points = mapping.get("目的点列表", [])
  170. points_sections = []
  171. # 灵感点
  172. if inspiration_points:
  173. insp_items = "".join([
  174. f'<div class="point-item"><span class="point-name">{html_module.escape(p.get("灵感点", ""))}</span></div>'
  175. for p in inspiration_points[:3] # 最多显示3个
  176. ])
  177. points_sections.append(f'<div class="points-section"><div class="points-label">灵感点</div><div class="points-list">{insp_items}</div></div>')
  178. # 关键点
  179. if key_points:
  180. key_items = "".join([
  181. f'<div class="point-item"><span class="point-name">{html_module.escape(k.get("关键点", ""))}</span></div>'
  182. for k in key_points[:3] # 最多显示3个
  183. ])
  184. points_sections.append(f'<div class="points-section"><div class="points-label">关键点</div><div class="points-list">{key_items}</div></div>')
  185. # 目的点
  186. if purpose_points:
  187. purpose_items = "".join([
  188. f'<div class="point-item"><span class="point-name">{html_module.escape(p.get("目的点", ""))}</span></div>'
  189. for p in purpose_points[:3] # 最多显示3个
  190. ])
  191. points_sections.append(f'<div class="points-section"><div class="points-label">目的点</div><div class="points-list">{purpose_items}</div></div>')
  192. if points_sections:
  193. points_html = f'<div class="note-points">{"".join(points_sections)}</div>'
  194. card_html = f'''
  195. <div class="search-note-item" data-note-data='{note_data_json_escaped}' onclick="showNoteDetail(this)">
  196. {images_html}
  197. <div class="note-info">
  198. <div class="note-title">{html_module.escape(title) if title else "无标题"}</div>
  199. <div class="note-stats">
  200. <span class="note-stat">❤️ {like_count if like_count else 0}</span>
  201. {f'<span class="note-stat">💬 {comment_count}</span>' if comment_count else ''}
  202. </div>
  203. <div class="note-author">{html_module.escape(author) if author else "匿名"}</div>
  204. </div>
  205. {f'<div class="note-points-hover">{points_html}</div>' if points_html else ''}
  206. </div>
  207. '''
  208. return card_html
  209. def generate_inspiration_card_html(
  210. inspiration_data: Dict[str, Any],
  211. inspiration_to_post_data: Dict[str, Any] = None,
  212. category_index_data: Dict[str, Any] = None,
  213. post_to_mapping_data: Dict[str, Any] = None
  214. ) -> str:
  215. """
  216. 生成单个灵感点的卡片HTML
  217. Args:
  218. inspiration_data: 灵感点数据
  219. inspiration_to_post_data: 点到帖子映射数据
  220. category_index_data: 分类索引数据
  221. Returns:
  222. HTML字符串
  223. """
  224. summary = inspiration_data.get("summary", {})
  225. step1 = inspiration_data.get("step1", {})
  226. inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
  227. search_results = inspiration_data.get("search_results", {})
  228. # 提取关键指标
  229. metrics = summary.get("关键指标", {})
  230. step1_score = metrics.get("step1_top1_score", 0)
  231. step1_match_element = metrics.get("step1_top1_匹配要素", "")
  232. # 确定卡片颜色(基于Step1分数)
  233. if step1_score >= 0.7:
  234. border_color = "#10b981"
  235. step1_color = "#10b981"
  236. elif step1_score >= 0.5:
  237. border_color = "#f59e0b"
  238. step1_color = "#f59e0b"
  239. elif step1_score >= 0.3:
  240. border_color = "#3b82f6"
  241. step1_color = "#3b82f6"
  242. else:
  243. border_color = "#ef4444"
  244. step1_color = "#ef4444"
  245. # 转义HTML
  246. inspiration_name_escaped = html_module.escape(inspiration_name)
  247. step1_match_element_escaped = html_module.escape(step1_match_element)
  248. # 获取Step1匹配结果(简要展示)
  249. step1_matches = step1.get("匹配结果列表", []) if step1 else []
  250. step1_match_preview = ""
  251. if step1_matches:
  252. top_match = step1_matches[0]
  253. # 从新的数据结构中提取信息
  254. input_info = top_match.get("输入信息", {})
  255. match_result = top_match.get("匹配结果", {})
  256. element_name = input_info.get("A", "")
  257. match_score = match_result.get("score", 0)
  258. same_parts = match_result.get("相同部分", {})
  259. increment_parts = match_result.get("增量部分", {})
  260. # 生成相同部分和增量部分的HTML
  261. parts_html = ""
  262. if same_parts:
  263. same_items = [f"{html_module.escape(k)}" for k in same_parts.keys()]
  264. parts_html += f'<div class="preview-parts same"><strong>相同:</strong> {", ".join(same_items)}</div>'
  265. if increment_parts:
  266. inc_items = [f"{html_module.escape(k)}" for k in increment_parts.keys()]
  267. parts_html += f'<div class="preview-parts increment"><strong>增量:</strong> {", ".join(inc_items)}</div>'
  268. step1_match_preview = f'''
  269. <div class="match-preview">
  270. <div class="match-preview-header">🎯 Step1 Top1匹配</div>
  271. <div class="match-preview-content">
  272. <span class="match-preview-name">{html_module.escape(element_name)}</span>
  273. <span class="match-preview-score" style="color: {step1_color};">{match_score:.2f}</span>
  274. </div>
  275. {parts_html}
  276. </div>
  277. '''
  278. # 提取 top3 匹配信息
  279. top3_matches = []
  280. if step1:
  281. matches = step1.get("匹配结果列表", [])
  282. for i, match in enumerate(matches[:3]):
  283. input_info = match.get("输入信息", {})
  284. match_result = match.get("匹配结果", {})
  285. element_name = input_info.get("A", "")
  286. match_score = match_result.get("score", 0)
  287. context = input_info.get("A_Context", "")
  288. # 解析层级关系
  289. hierarchy = []
  290. if context:
  291. lines = context.split("\n")
  292. for line in lines:
  293. if ":" in line:
  294. key, value = line.split(":", 1)
  295. key = key.strip()
  296. value = value.strip()
  297. if key in ["所属视角", "一级分类", "二级分类"]:
  298. hierarchy.append(value)
  299. top3_matches.append({
  300. "rank": i + 1,
  301. "name": element_name,
  302. "score": match_score,
  303. "hierarchy": hierarchy
  304. })
  305. # 准备详细数据用于弹窗
  306. detail_data_json = json.dumps(inspiration_data, ensure_ascii=False)
  307. detail_data_json_escaped = html_module.escape(detail_data_json)
  308. # top3 匹配数据
  309. top3_json = json.dumps(top3_matches, ensure_ascii=False)
  310. top3_json_escaped = html_module.escape(top3_json)
  311. # 生成详细HTML并进行HTML转义
  312. detail_html = generate_detail_html(inspiration_data)
  313. detail_html_escaped = html_module.escape(detail_html)
  314. # 生成匹配列表HTML
  315. matches_html = ""
  316. if step1:
  317. step1_matches = step1.get("匹配结果列表", [])
  318. for idx, match in enumerate(step1_matches):
  319. input_info = match.get("输入信息", {})
  320. match_result = match.get("匹配结果", {})
  321. element_name = input_info.get("A", "")
  322. context = input_info.get("A_Context", "")
  323. score = match_result.get("score", 0)
  324. score_explain = match_result.get("score说明", "")
  325. same_parts = match_result.get("相同部分", {})
  326. increment_parts = match_result.get("增量部分", {})
  327. # 解析层级
  328. hierarchy = []
  329. if context:
  330. lines = context.split("\n")
  331. for line in lines:
  332. if ":" in line:
  333. key, value = line.split(":", 1)
  334. key = key.strip()
  335. value = value.strip()
  336. if key in ["所属视角", "一级分类", "二级分类"]:
  337. hierarchy.append(value)
  338. hierarchy_html = " › ".join([html_module.escape(h) for h in hierarchy]) if hierarchy else ""
  339. rank_class = f"rank-{idx + 1}" if idx < 3 else ""
  340. # 相同部分HTML
  341. same_parts_html = ""
  342. if same_parts:
  343. same_items = "".join([f'<div class="part-item"><span class="part-key">{html_module.escape(k)}:</span><span class="part-value">{html_module.escape(v)}</span></div>' for k, v in same_parts.items()])
  344. same_parts_html = f'''
  345. <div class="match-parts same-parts">
  346. <div class="match-parts-title">✅ 相同部分</div>
  347. {same_items}
  348. </div>
  349. '''
  350. # 增量部分HTML
  351. increment_parts_html = ""
  352. if increment_parts:
  353. inc_items = "".join([f'<div class="part-item"><span class="part-key">{html_module.escape(k)}:</span><span class="part-value">{html_module.escape(v)}</span></div>' for k, v in increment_parts.items()])
  354. increment_parts_html = f'''
  355. <div class="match-parts increment-parts">
  356. <div class="match-parts-title">➕ 增量部分</div>
  357. {inc_items}
  358. </div>
  359. '''
  360. # 生成搜索结果HTML(网格展示,图片轮播)
  361. search_html = ""
  362. if element_name in search_results:
  363. search_data = search_results[element_name]
  364. notes = search_data.get("notes", [])
  365. notes_count = len(notes)
  366. if notes_count > 0:
  367. notes_items = ""
  368. for note_idx, note in enumerate(notes[:20]): # 显示前20条
  369. title = note.get("title", "")
  370. desc = note.get("desc", "")
  371. link = note.get("link", "")
  372. author = note.get("channel_account_name", "")
  373. like_count = note.get("like_count", 0)
  374. comment_count = note.get("comment_count", 0)
  375. images = note.get("images", [])
  376. note_id = f"note-{idx}-{note_idx}"
  377. # 生成图片轮播HTML
  378. images_html = ""
  379. if images and len(images) > 0:
  380. images_track = "".join([f'<img src="{img}" class="note-image" alt="图片{i+1}">' for i, img in enumerate(images)])
  381. # 图片导航按钮和指示点
  382. nav_buttons = ""
  383. dots_html = ""
  384. if len(images) > 1:
  385. nav_buttons = f'''
  386. <button class="note-carousel-button prev" onclick="event.stopPropagation(); moveNoteImage('{note_id}', -1)">‹</button>
  387. <button class="note-carousel-button next" onclick="event.stopPropagation(); moveNoteImage('{note_id}', 1)">›</button>
  388. '''
  389. dots = "".join([f'<div class="note-image-dot{" active" if i == 0 else ""}"></div>' for i in range(len(images))])
  390. dots_html = f'<div class="note-image-dots" id="{note_id}-dots">{dots}</div>'
  391. images_html = f'''
  392. <div class="note-image-carousel" data-note-id="{note_id}" data-total-images="{len(images)}">
  393. <div class="note-images-track" id="{note_id}-track">
  394. {images_track}
  395. </div>
  396. {nav_buttons}
  397. {dots_html}
  398. </div>
  399. '''
  400. else:
  401. # 无图片时显示占位符
  402. images_html = f'''
  403. <div class="note-image-carousel">
  404. <div class="note-images-track">
  405. <div class="note-image" style="display: flex; align-items: center; justify-content: center; background: #f3f4f6; color: #9ca3af;">
  406. 暂无图片
  407. </div>
  408. </div>
  409. </div>
  410. '''
  411. # 准备详情数据
  412. note_data = {
  413. "title": title,
  414. "desc": desc,
  415. "link": link,
  416. "author": author,
  417. "like_count": like_count,
  418. "comment_count": comment_count,
  419. "images": images
  420. }
  421. note_data_json = json.dumps(note_data, ensure_ascii=False)
  422. note_data_escaped = html_module.escape(note_data_json)
  423. notes_items += f'''
  424. <div class="search-note-item" data-note-data='{note_data_escaped}' onclick="showNoteDetail(this)">
  425. {images_html}
  426. <div class="note-content">
  427. <div class="note-title">{html_module.escape(title) if title else "无标题"}</div>
  428. <div class="note-desc">{html_module.escape(desc) if desc else "暂无描述"}</div>
  429. <div class="note-footer">
  430. <div class="note-author">@{html_module.escape(author)}</div>
  431. <div class="note-stats">
  432. <span>👍 {like_count}</span>
  433. <span>💬 {comment_count}</span>
  434. </div>
  435. </div>
  436. <a href="{link}" target="_blank" class="note-link" onclick="event.stopPropagation()">在小红书查看</a>
  437. </div>
  438. </div>
  439. '''
  440. search_html = f'''
  441. <div class="search-results-section">
  442. <div class="search-notes-list">
  443. {notes_items}
  444. </div>
  445. </div>
  446. '''
  447. # 默认全部展开
  448. expanded_class = " expanded"
  449. # 生成当前匹配项的灵感点和灵感分类详情
  450. match_info_section = ""
  451. if inspiration_to_post_data and category_index_data:
  452. # 获取灵感点数据
  453. inspiration_points = inspiration_to_post_data.get("点到帖子映射", {}).get("灵感点", {})
  454. inspiration_info = inspiration_points.get(inspiration_name, {})
  455. # 获取当前匹配的灵感分类数据
  456. categories = category_index_data.get("灵感分类", {})
  457. category_info = categories.get(element_name, {})
  458. # 生成灵感点详情HTML(左栏)
  459. insp_detail_html = ""
  460. if inspiration_info:
  461. insp_dimension = inspiration_info.get("维度", "")
  462. insp_desc = inspiration_info.get("描述", "")
  463. insp_posts = inspiration_info.get("帖子详情列表", [])
  464. # 生成帖子卡片
  465. insp_posts_html = ""
  466. for post in insp_posts[:6]: # 最多显示6个
  467. insp_posts_html += generate_post_card_html(post, f"match-{idx}-insp-post", post_to_mapping_data)
  468. insp_detail_html = f'''
  469. <div class="info-detail-column">
  470. <div class="info-header">
  471. <span class="info-label">[灵感点]</span>
  472. <span class="info-name">{html_module.escape(inspiration_name)}</span>
  473. </div>
  474. <div class="info-content">
  475. {f'<div class="info-field"><span class="info-field-label">维度:</span><span class="info-field-value">{html_module.escape(insp_dimension)}</span></div>' if insp_dimension else ''}
  476. {f'<div class="info-field"><span class="info-field-label">描述:</span><span class="info-field-value">{html_module.escape(insp_desc)}</span></div>' if insp_desc else ''}
  477. {f'<div class="info-posts"><div class="info-posts-title">相关帖子</div><div class="info-posts-grid">{insp_posts_html}</div></div>' if insp_posts_html else ''}
  478. </div>
  479. </div>
  480. '''
  481. # 生成灵感分类详情HTML(右栏)
  482. cat_detail_html = ""
  483. if category_info:
  484. cat_level = category_info.get("分类层级", "")
  485. cat_definition = category_info.get("分类定义", "")
  486. cat_posts = category_info.get("帖子详情列表", [])
  487. # 生成帖子卡片
  488. cat_posts_html = ""
  489. for post in cat_posts[:6]: # 最多显示6个
  490. cat_posts_html += generate_post_card_html(post, f"match-{idx}-cat-post", post_to_mapping_data)
  491. cat_detail_html = f'''
  492. <div class="info-detail-column">
  493. <div class="info-header">
  494. <span class="info-label">[灵感分类]</span>
  495. <span class="info-name">{html_module.escape(element_name)}</span>
  496. </div>
  497. <div class="info-content">
  498. {f'<div class="info-field"><span class="info-field-label">层级:</span><span class="info-field-value">{html_module.escape(cat_level)}</span></div>' if cat_level else ''}
  499. {f'<div class="info-field"><span class="info-field-label">定义:</span><span class="info-field-value">{html_module.escape(cat_definition)}</span></div>' if cat_definition else ''}
  500. {f'<div class="info-posts"><div class="info-posts-title">相关帖子</div><div class="info-posts-grid">{cat_posts_html}</div></div>' if cat_posts_html else ''}
  501. </div>
  502. </div>
  503. '''
  504. if insp_detail_html or cat_detail_html:
  505. match_info_section = f'''
  506. <div class="inspiration-category-section">
  507. {insp_detail_html}
  508. {cat_detail_html}
  509. </div>
  510. '''
  511. # 步骤1:灵感点匹配灵感分类
  512. step1_html = f'''
  513. <div class="step-section-wrapper step-1-wrapper">
  514. <div class="step-header">
  515. <span class="step-number-badge">步骤 1</span>
  516. <span class="step-title">灵感点匹配灵感分类</span>
  517. </div>
  518. <div class="match-analysis-section" id="match-{idx}-step1" data-step-name="灵感点匹配灵感分类">
  519. <div class="match-parts-container">
  520. <div class="match-parts-column">
  521. {same_parts_html}
  522. </div>
  523. <div class="match-parts-column">
  524. {increment_parts_html}
  525. </div>
  526. </div>
  527. {f'<div class="match-explain"><div class="match-explain-title">💡 分数说明</div><div class="match-explain-text">{html_module.escape(score_explain)}</div></div>' if score_explain else ''}
  528. </div>
  529. </div>
  530. '''
  531. # 步骤2:搜索结果(如果有)
  532. step2_html = ""
  533. if search_html:
  534. step2_html = f'''
  535. <div class="step-section expanded" data-step="2" id="match-{idx}-step2" data-step-name="灵感分类搜索">
  536. <div class="step-section-header" onclick="toggleStep(this)">
  537. <div class="step-section-title">
  538. <span class="step-number">2</span>
  539. <span>灵感分类搜索</span>
  540. </div>
  541. <div class="step-toggle">▼</div>
  542. </div>
  543. <div class="step-section-content">
  544. {search_html}
  545. </div>
  546. </div>
  547. '''
  548. # 创建安全的ID(移除特殊字符)
  549. safe_element_id = ''.join(c if c.isalnum() or c in '_-' else '_' for c in element_name)
  550. matches_html += f'''
  551. <div class="match-item{expanded_class}" data-index="{idx}" id="match-{idx}" data-match-name="{html_module.escape(element_name)}">
  552. <div class="match-main-header" onclick="toggleMainMatch(this)">
  553. <div class="match-header-row">
  554. <div class="match-header-left">
  555. <span class="match-rank {rank_class}">Top {idx + 1}</span>
  556. <span class="detail-label">[灵感点]</span>
  557. <span class="match-title">{html_module.escape(inspiration_name)}</span>
  558. </div>
  559. <div class="match-header-center">
  560. <span class="match-score-label">匹配分数</span>
  561. <span class="match-score-value">{score:.2f}</span>
  562. </div>
  563. <div class="match-header-right">
  564. <span class="detail-label">[灵感分类]</span>
  565. <span class="match-category">{html_module.escape(element_name)}</span>
  566. <span class="match-hierarchy">({hierarchy_html})</span>
  567. </div>
  568. </div>
  569. <div class="match-toggle-main">▼</div>
  570. </div>
  571. <div class="match-main-content">
  572. {match_info_section}
  573. {step1_html}
  574. {step2_html}
  575. </div>
  576. </div>
  577. '''
  578. # 获取top1匹配的灵感分类名称(用于顶部标题)
  579. top1_category_name = ""
  580. if step1:
  581. step1_matches = step1.get("匹配结果列表", [])
  582. if step1_matches:
  583. top_match = step1_matches[0]
  584. input_info = top_match.get("输入信息", {})
  585. top1_category_name = input_info.get("A", "")
  586. html = f'''
  587. <div class="inspiration-detail"
  588. data-inspiration-name="{inspiration_name_escaped}"
  589. data-step1-score="{step1_score}"
  590. data-top3-matches="{top3_json_escaped}">
  591. <div class="breadcrumb-container">
  592. <div class="breadcrumb" id="dynamicBreadcrumb">
  593. <span class="breadcrumb-item"><span class="breadcrumb-label">[灵感点]</span> {inspiration_name_escaped}</span>
  594. </div>
  595. </div>
  596. <div class="inspiration-content-wrapper">
  597. <div class="matches-list">
  598. {matches_html}
  599. </div>
  600. </div>
  601. </div>
  602. '''
  603. return html
  604. def generate_detail_html(inspiration_data: Dict[str, Any]) -> str:
  605. """
  606. 生成灵感点的详细信息HTML
  607. Args:
  608. inspiration_data: 灵感点数据
  609. Returns:
  610. 详细信息的HTML字符串
  611. """
  612. import html as html_module
  613. summary = inspiration_data.get("summary", {})
  614. step1 = inspiration_data.get("step1", {})
  615. step2 = inspiration_data.get("step2", {})
  616. inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
  617. content = f'''
  618. <div class="modal-header">
  619. <h2 class="modal-title">{html_module.escape(inspiration_name)}</h2>
  620. </div>
  621. '''
  622. # 获取元数据,用于后面的日志链接
  623. metadata = summary.get("元数据", {})
  624. # Step1 详细信息
  625. if step1 and step1.get("灵感"):
  626. inspiration = step1.get("灵感", "")
  627. matches = step1.get("匹配结果列表", [])
  628. content += f'''
  629. <div class="modal-section">
  630. <h3>🎯 Step1: 灵感人设匹配</h3>
  631. <div class="step-content">
  632. <div class="step-field">
  633. <span class="step-field-label">灵感内容:</span>
  634. <span class="step-field-value">{html_module.escape(inspiration)}</span>
  635. </div>
  636. '''
  637. # 显示匹配结果(只显示Top1)
  638. if matches:
  639. content += f'''
  640. <div class="step-field">
  641. <span class="step-field-label">Top1匹配结果:</span>
  642. <div class="matches-list">
  643. '''
  644. for index, match in enumerate(matches[:1]):
  645. input_info = match.get("输入信息", {})
  646. match_result = match.get("匹配结果", {})
  647. element_a = input_info.get("A", "")
  648. context_a = input_info.get("A_Context", "")
  649. score = match_result.get("score", 0)
  650. score_explain = match_result.get("score说明", "")
  651. same_parts = match_result.get("相同部分", {})
  652. increment_parts = match_result.get("增量部分", {})
  653. content += f'''
  654. <div class="match-item">
  655. <div class="match-header">
  656. <span class="match-element-name">{html_module.escape(element_a)}</span>
  657. <span class="match-score">{score:.2f}</span>
  658. </div>
  659. '''
  660. if context_a:
  661. content += f'<div class="match-context"><strong>📍 所属分类:</strong> {html_module.escape(context_a).replace(chr(10), "<br>")}</div>'
  662. if score_explain:
  663. content += f'<div class="match-explain"><strong>💡 分数说明:</strong> {html_module.escape(score_explain)}</div>'
  664. # 相同部分
  665. if same_parts:
  666. content += '''
  667. <div class="match-parts same-parts">
  668. <div class="parts-header">✅ 相同部分</div>
  669. <div class="parts-content">
  670. '''
  671. for key, value in same_parts.items():
  672. content += f'''
  673. <div class="part-item">
  674. <span class="part-key">{html_module.escape(key)}:</span>
  675. <span class="part-value">{html_module.escape(value)}</span>
  676. </div>
  677. '''
  678. content += '''
  679. </div>
  680. </div>
  681. '''
  682. # 增量部分
  683. if increment_parts:
  684. content += '''
  685. <div class="match-parts increment-parts">
  686. <div class="parts-header">➕ 增量部分</div>
  687. <div class="parts-content">
  688. '''
  689. for key, value in increment_parts.items():
  690. content += f'''
  691. <div class="part-item">
  692. <span class="part-key">{html_module.escape(key)}:</span>
  693. <span class="part-value">{html_module.escape(value)}</span>
  694. </div>
  695. '''
  696. content += '''
  697. </div>
  698. </div>
  699. '''
  700. content += '''
  701. </div>
  702. '''
  703. content += '''
  704. </div>
  705. </div>
  706. '''
  707. content += '''
  708. </div>
  709. </div>
  710. '''
  711. # 日志链接
  712. if metadata.get("log_url"):
  713. content += f'''
  714. <div class="modal-link">
  715. <a href="{metadata["log_url"]}" target="_blank" class="modal-link-btn">
  716. 🔗 查看详细日志
  717. </a>
  718. </div>
  719. '''
  720. return content
  721. def generate_detail_modal_content_js() -> str:
  722. """
  723. 生成详情弹窗内容的JavaScript函数
  724. Returns:
  725. JavaScript代码字符串
  726. """
  727. return '''
  728. // 笔记图片当前索引管理
  729. const noteImageStates = {};
  730. // 移动笔记图片
  731. function moveNoteImage(noteId, direction) {
  732. if (!noteImageStates[noteId]) {
  733. noteImageStates[noteId] = 0;
  734. }
  735. const carousel = document.querySelector(`[data-note-id="${noteId}"]`);
  736. if (!carousel) return;
  737. const totalImages = parseInt(carousel.dataset.totalImages);
  738. const track = document.getElementById(noteId + '-track');
  739. if (!track) return;
  740. let newIndex = noteImageStates[noteId] + direction;
  741. if (newIndex < 0) newIndex = 0;
  742. if (newIndex >= totalImages) newIndex = totalImages - 1;
  743. noteImageStates[noteId] = newIndex;
  744. // 移动轨道
  745. track.style.transform = `translateX(-${newIndex * 100}%)`;
  746. // 更新指示点
  747. const dots = document.querySelectorAll(`#${noteId}-dots .note-image-dot`);
  748. dots.forEach((dot, i) => {
  749. dot.classList.toggle('active', i === newIndex);
  750. });
  751. // 更新按钮状态
  752. const prevBtn = carousel.querySelector('.note-carousel-button.prev');
  753. const nextBtn = carousel.querySelector('.note-carousel-button.next');
  754. if (prevBtn) {
  755. prevBtn.classList.toggle('disabled', newIndex === 0);
  756. }
  757. if (nextBtn) {
  758. nextBtn.classList.toggle('disabled', newIndex >= totalImages - 1);
  759. }
  760. }
  761. // 显示笔记详情
  762. function showNoteDetail(element) {
  763. const noteDataStr = element.dataset.noteData;
  764. if (!noteDataStr) return;
  765. try {
  766. const noteData = JSON.parse(noteDataStr);
  767. // 生成图片HTML
  768. let imagesHtml = '';
  769. if (noteData.images && noteData.images.length > 0) {
  770. imagesHtml = noteData.images.map(img =>
  771. `<img src="${img}" class="note-detail-image" alt="图片">`
  772. ).join('');
  773. } else {
  774. imagesHtml = '<div style="text-align: center; color: #9ca3af; padding: 40px;">暂无图片</div>';
  775. }
  776. // 生成灵感点、关键点、目的点HTML
  777. let pointsDetailHtml = '';
  778. const hasPoints = (noteData.inspiration_points && noteData.inspiration_points.length > 0) ||
  779. (noteData.key_points && noteData.key_points.length > 0) ||
  780. (noteData.purpose_points && noteData.purpose_points.length > 0);
  781. if (hasPoints) {
  782. let sections = [];
  783. // 灵感点
  784. if (noteData.inspiration_points && noteData.inspiration_points.length > 0) {
  785. const items = noteData.inspiration_points.map(p =>
  786. `<div class="detail-point-item">
  787. <div class="detail-point-name">${p.灵感点 || ''}</div>
  788. ${p.描述 ? `<div class="detail-point-desc">${p.描述}</div>` : ''}
  789. </div>`
  790. ).join('');
  791. sections.push(`<div class="detail-points-section">
  792. <div class="detail-points-title">💡 灵感点</div>
  793. ${items}
  794. </div>`);
  795. }
  796. // 关键点
  797. if (noteData.key_points && noteData.key_points.length > 0) {
  798. const items = noteData.key_points.map(k =>
  799. `<div class="detail-point-item">
  800. <div class="detail-point-name">${k.关键点 || ''}</div>
  801. ${k.描述 ? `<div class="detail-point-desc">${k.描述}</div>` : ''}
  802. </div>`
  803. ).join('');
  804. sections.push(`<div class="detail-points-section">
  805. <div class="detail-points-title">🔑 关键点</div>
  806. ${items}
  807. </div>`);
  808. }
  809. // 目的点
  810. if (noteData.purpose_points && noteData.purpose_points.length > 0) {
  811. const items = noteData.purpose_points.map(p =>
  812. `<div class="detail-point-item">
  813. <div class="detail-point-name">${p.目的点 || ''}</div>
  814. ${p.描述 ? `<div class="detail-point-desc">${p.描述}</div>` : ''}
  815. </div>`
  816. ).join('');
  817. sections.push(`<div class="detail-points-section">
  818. <div class="detail-points-title">🎯 目的点</div>
  819. ${items}
  820. </div>`);
  821. }
  822. pointsDetailHtml = `<div class="note-detail-points">${sections.join('')}</div>`;
  823. }
  824. const modalHtml = `
  825. <div class="note-detail-content">
  826. <button class="note-detail-close" onclick="closeNoteDetail()">×</button>
  827. <div class="note-detail-header">
  828. <div class="note-detail-title">${noteData.title || '无标题'}</div>
  829. <div class="note-detail-meta">
  830. <span class="note-detail-author">@${noteData.author}</span>
  831. <div class="note-detail-stats">
  832. <span>👍 ${noteData.like_count}</span>
  833. <span>💬 ${noteData.comment_count}</span>
  834. </div>
  835. </div>
  836. </div>
  837. <div class="note-detail-body">
  838. ${noteData.desc ? `<div class="note-detail-desc">${noteData.desc}</div>` : ''}
  839. ${pointsDetailHtml}
  840. <div class="note-detail-images">
  841. ${imagesHtml}
  842. </div>
  843. </div>
  844. <div class="note-detail-footer">
  845. <a href="${noteData.link}" target="_blank" class="note-detail-link">
  846. 在小红书查看完整内容 →
  847. </a>
  848. </div>
  849. </div>
  850. `;
  851. let modal = document.getElementById('noteDetailModal');
  852. if (!modal) {
  853. modal = document.createElement('div');
  854. modal.id = 'noteDetailModal';
  855. modal.className = 'note-detail-modal';
  856. modal.onclick = (e) => {
  857. if (e.target === modal) closeNoteDetail();
  858. };
  859. document.body.appendChild(modal);
  860. }
  861. modal.innerHTML = modalHtml;
  862. modal.classList.add('active');
  863. document.body.style.overflow = 'hidden';
  864. } catch (e) {
  865. console.error('Error parsing note data:', e);
  866. }
  867. }
  868. // 关闭笔记详情
  869. function closeNoteDetail() {
  870. const modal = document.getElementById('noteDetailModal');
  871. if (modal) {
  872. modal.classList.remove('active');
  873. document.body.style.overflow = '';
  874. }
  875. }
  876. // ESC键关闭详情
  877. document.addEventListener('keydown', function(event) {
  878. if (event.key === 'Escape') {
  879. closeNoteDetail();
  880. }
  881. });
  882. // 切换主匹配项的展开/折叠
  883. function toggleMainMatch(element) {
  884. const matchItem = element.closest('.match-item');
  885. matchItem.classList.toggle('expanded');
  886. }
  887. // 切换步骤的展开/折叠
  888. function toggleStep(element) {
  889. const stepSection = element.closest('.step-section');
  890. stepSection.classList.toggle('expanded');
  891. }
  892. // 切换匹配详情的展开/折叠
  893. function toggleMatchSection(element) {
  894. const matchSection = element.closest('.match-section');
  895. matchSection.classList.toggle('expanded');
  896. }
  897. // 显示指定的灵感详情
  898. function showDetail(index) {
  899. const details = document.querySelectorAll('.inspiration-detail');
  900. details.forEach((detail, i) => {
  901. if (i === index) {
  902. detail.classList.add('active');
  903. // 滚动到顶部
  904. const section = document.querySelector('.inspirations-section');
  905. if (section) {
  906. section.scrollTop = 0;
  907. }
  908. } else {
  909. detail.classList.remove('active');
  910. }
  911. });
  912. }
  913. // 生成导航目录
  914. function generateNavigation() {
  915. const details = document.querySelectorAll('.inspiration-detail');
  916. const navList = document.getElementById('navList');
  917. navList.innerHTML = '';
  918. details.forEach((detail, index) => {
  919. const name = detail.dataset.inspirationName;
  920. const score = parseFloat(detail.dataset.step1Score) || 0;
  921. const top3MatchesStr = detail.dataset.top3Matches;
  922. let top3Matches = [];
  923. try {
  924. top3Matches = JSON.parse(top3MatchesStr);
  925. } catch(e) {
  926. console.error('Error parsing top3 matches:', e);
  927. }
  928. const navItem = document.createElement('div');
  929. navItem.className = 'nav-item';
  930. if (index === 0) navItem.classList.add('active');
  931. navItem.dataset.cardIndex = index;
  932. // 生成匹配列表HTML
  933. let matchesHtml = '';
  934. if (top3Matches && top3Matches.length > 0) {
  935. matchesHtml = '<div class="nav-item-matches">';
  936. top3Matches.forEach((match, i) => {
  937. // 生成层级路径
  938. let hierarchyHtml = '';
  939. if (match.hierarchy && match.hierarchy.length > 0) {
  940. hierarchyHtml = '<div class="nav-match-hierarchy">';
  941. match.hierarchy.forEach((level, idx) => {
  942. if (idx > 0) {
  943. hierarchyHtml += '<span class="hierarchy-separator">›</span>';
  944. }
  945. hierarchyHtml += `<span class="hierarchy-item">${level}</span>`;
  946. });
  947. hierarchyHtml += '</div>';
  948. }
  949. matchesHtml += `
  950. <div class="nav-match-item">
  951. <span class="nav-match-rank rank-${match.rank}">Top${match.rank}</span>
  952. <div class="nav-match-content">
  953. <span class="nav-match-name">${match.name}</span>
  954. ${hierarchyHtml}
  955. <span class="nav-match-score">${match.score.toFixed(2)}</span>
  956. </div>
  957. </div>
  958. `;
  959. });
  960. matchesHtml += '</div>';
  961. }
  962. navItem.innerHTML = `
  963. <div class="nav-item-header">
  964. <span class="nav-item-name" title="${name}">
  965. <span class="nav-label">[灵感点]</span>${name}
  966. </span>
  967. <span class="nav-item-score">${score.toFixed(2)}</span>
  968. </div>
  969. ${matchesHtml}
  970. `;
  971. navItem.addEventListener('click', () => {
  972. // 移除所有active状态
  973. document.querySelectorAll('.nav-item').forEach(item => {
  974. item.classList.remove('active');
  975. });
  976. // 添加当前active状态
  977. navItem.classList.add('active');
  978. // 显示对应详情
  979. showDetail(index);
  980. });
  981. navList.appendChild(navItem);
  982. });
  983. // 默认显示第一个详情
  984. if (details.length > 0) {
  985. showDetail(0);
  986. }
  987. }
  988. // 更新面包屑
  989. function updateBreadcrumb(matchName, stepName) {
  990. const breadcrumb = document.querySelector('.inspiration-detail.active #dynamicBreadcrumb');
  991. if (!breadcrumb) return;
  992. const inspirationName = document.querySelector('.inspiration-detail.active').dataset.inspirationName;
  993. let breadcrumbHtml = `
  994. <span class="breadcrumb-item"><span class="breadcrumb-label">[灵感点]</span> ${inspirationName}</span>
  995. `;
  996. if (matchName) {
  997. breadcrumbHtml += `
  998. <span class="breadcrumb-separator">›</span>
  999. <span class="breadcrumb-item"><span class="breadcrumb-label">[灵感分类]</span> ${matchName}</span>
  1000. `;
  1001. }
  1002. if (stepName) {
  1003. breadcrumbHtml += `
  1004. <span class="breadcrumb-separator">›</span>
  1005. <span class="breadcrumb-item breadcrumb-current">${stepName}</span>
  1006. `;
  1007. }
  1008. breadcrumb.innerHTML = breadcrumbHtml;
  1009. }
  1010. // 监听滚动,更新面包屑
  1011. function setupBreadcrumbObserver() {
  1012. const activeDetail = document.querySelector('.inspiration-detail.active');
  1013. if (!activeDetail) return;
  1014. const contentWrapper = activeDetail.querySelector('.inspiration-content-wrapper');
  1015. if (!contentWrapper) return;
  1016. // 获取所有需要监听的section
  1017. const sections = activeDetail.querySelectorAll('.step-section');
  1018. if (sections.length === 0) return;
  1019. // 创建Intersection Observer
  1020. const observerOptions = {
  1021. root: null,
  1022. rootMargin: '-100px 0px -50% 0px',
  1023. threshold: 0
  1024. };
  1025. const observer = new IntersectionObserver((entries) => {
  1026. entries.forEach(entry => {
  1027. if (entry.isIntersecting) {
  1028. const section = entry.target;
  1029. const matchItem = section.closest('.match-item');
  1030. const matchName = matchItem ? matchItem.dataset.matchName : '';
  1031. const stepName = section.dataset.stepName || '';
  1032. updateBreadcrumb(matchName, stepName);
  1033. }
  1034. });
  1035. }, observerOptions);
  1036. // 观察所有section
  1037. sections.forEach(section => observer.observe(section));
  1038. // 存储observer以便清理
  1039. if (!window.breadcrumbObservers) {
  1040. window.breadcrumbObservers = [];
  1041. }
  1042. window.breadcrumbObservers.push(observer);
  1043. }
  1044. // 页面加载时生成导航
  1045. document.addEventListener('DOMContentLoaded', () => {
  1046. generateNavigation();
  1047. setupBreadcrumbObserver();
  1048. });
  1049. // 当切换灵感点时,重新设置observer
  1050. const originalShowDetail = showDetail;
  1051. showDetail = function(index) {
  1052. // 清理旧的observers
  1053. if (window.breadcrumbObservers) {
  1054. window.breadcrumbObservers.forEach(obs => obs.disconnect());
  1055. window.breadcrumbObservers = [];
  1056. }
  1057. // 调用原始函数
  1058. originalShowDetail(index);
  1059. // 设置新的observer
  1060. setTimeout(() => {
  1061. setupBreadcrumbObserver();
  1062. }, 100);
  1063. };
  1064. '''
  1065. def generate_persona_structure_html(persona_data: Dict[str, Any]) -> str:
  1066. """
  1067. 生成人设结构的树状HTML
  1068. Args:
  1069. persona_data: 人设数据
  1070. Returns:
  1071. 人设结构的HTML字符串
  1072. """
  1073. if not persona_data:
  1074. return '<div class="empty-state">暂无人设数据</div>'
  1075. inspiration_list = persona_data.get("灵感点列表", [])
  1076. if not inspiration_list:
  1077. return '<div class="empty-state">暂无灵感点列表数据</div>'
  1078. html_parts = ['<div class="tree">']
  1079. for perspective_idx, perspective in enumerate(inspiration_list):
  1080. perspective_name = perspective.get("视角名称", "未知视角")
  1081. perspective_desc = perspective.get("视角描述", "")
  1082. pattern_list = perspective.get("模式列表", [])
  1083. # 一级节点:视角
  1084. html_parts.append(f'''
  1085. <ul>
  1086. <li>
  1087. <div class="tree-node level-1">
  1088. <span class="node-icon">📁</span>
  1089. <span class="node-name">{html_module.escape(perspective_name)}</span>
  1090. <span class="node-count">{len(pattern_list)}个分类</span>
  1091. </div>
  1092. ''')
  1093. if perspective_desc:
  1094. html_parts.append(f'''
  1095. <div class="node-desc">{html_module.escape(perspective_desc)}</div>
  1096. ''')
  1097. # 二级节点:分类
  1098. if pattern_list:
  1099. html_parts.append('<ul>')
  1100. for pattern in pattern_list:
  1101. category_name = pattern.get("分类名称", "未知分类")
  1102. core_definition = pattern.get("核心定义", "")
  1103. subcategories = pattern.get("二级细分", [])
  1104. total_posts = sum(len(sub.get("帖子ID列表", [])) for sub in subcategories)
  1105. html_parts.append(f'''
  1106. <li>
  1107. <div class="tree-node level-2">
  1108. <span class="node-icon">📂</span>
  1109. <span class="node-name">{html_module.escape(category_name)}</span>
  1110. <span class="node-count">{total_posts}个帖子</span>
  1111. </div>
  1112. ''')
  1113. if core_definition:
  1114. html_parts.append(f'''
  1115. <div class="node-desc">{html_module.escape(core_definition)}</div>
  1116. ''')
  1117. # 三级节点:细分
  1118. if subcategories:
  1119. html_parts.append('<ul>')
  1120. for subcategory in subcategories:
  1121. sub_name = subcategory.get("分类名称", "未知细分")
  1122. sub_definition = subcategory.get("分类定义", "")
  1123. post_ids = subcategory.get("帖子ID列表", [])
  1124. html_parts.append(f'''
  1125. <li>
  1126. <div class="tree-node level-3">
  1127. <span class="node-icon">📄</span>
  1128. <span class="node-name">{html_module.escape(sub_name)}</span>
  1129. <span class="node-count">{len(post_ids)}个帖子</span>
  1130. </div>
  1131. ''')
  1132. if sub_definition:
  1133. html_parts.append(f'''
  1134. <div class="node-desc">{html_module.escape(sub_definition)}</div>
  1135. ''')
  1136. if post_ids:
  1137. html_parts.append(f'''
  1138. <div class="node-posts">
  1139. <span class="posts-label">📋 帖子ID:</span>
  1140. <span class="posts-ids">{", ".join([html_module.escape(str(pid)) for pid in post_ids[:5]])}</span>
  1141. {f'<span class="posts-more">... 等{len(post_ids)}个</span>' if len(post_ids) > 5 else ''}
  1142. </div>
  1143. ''')
  1144. html_parts.append('</li>')
  1145. html_parts.append('</ul>')
  1146. html_parts.append('</li>')
  1147. html_parts.append('</ul>')
  1148. html_parts.append('</li>')
  1149. html_parts.append('</ul>')
  1150. html_parts.append('</div>')
  1151. return ''.join(html_parts)
  1152. def generate_html(
  1153. inspirations_data: List[Dict[str, Any]],
  1154. posts_map: Dict[str, Dict[str, Any]],
  1155. persona_data: Dict[str, Any],
  1156. output_path: str,
  1157. inspiration_to_post_data: Dict[str, Any] = None,
  1158. category_index_data: Dict[str, Any] = None,
  1159. post_to_mapping_data: Dict[str, Any] = None
  1160. ) -> str:
  1161. """
  1162. 生成完整的可视化HTML
  1163. Args:
  1164. inspirations_data: 灵感点数据列表
  1165. posts_map: 帖子数据映射
  1166. persona_data: 人设数据
  1167. output_path: 输出文件路径
  1168. Returns:
  1169. 输出文件路径
  1170. """
  1171. timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  1172. # 统计信息
  1173. total_count = len(inspirations_data)
  1174. # Step1 统计
  1175. step1_excellent_count = sum(1 for d in inspirations_data
  1176. if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) >= 0.7)
  1177. step1_good_count = sum(1 for d in inspirations_data
  1178. if 0.5 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.7)
  1179. step1_normal_count = sum(1 for d in inspirations_data
  1180. if 0.3 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.5)
  1181. step1_need_opt_count = sum(1 for d in inspirations_data
  1182. if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.3)
  1183. # 平均分数
  1184. total_step1_score = sum(d["summary"].get("关键指标", {}).get("step1_top1_score", 0)
  1185. for d in inspirations_data)
  1186. avg_step1_score = total_step1_score / total_count if total_count > 0 else 0
  1187. # 按Step1分数排序
  1188. inspirations_data_sorted = sorted(
  1189. inspirations_data,
  1190. key=lambda x: x["summary"].get("关键指标", {}).get("step1_top1_score", 0),
  1191. reverse=True
  1192. )
  1193. # 生成卡片HTML
  1194. cards_html = [
  1195. generate_inspiration_card_html(data, inspiration_to_post_data, category_index_data, post_to_mapping_data)
  1196. for data in inspirations_data_sorted
  1197. ]
  1198. cards_html_str = '\n'.join(cards_html)
  1199. # 生成人设结构HTML
  1200. persona_structure_html = generate_persona_structure_html(persona_data)
  1201. # 生成JavaScript
  1202. detail_modal_js = generate_detail_modal_content_js()
  1203. # 完整HTML
  1204. html_content = f'''<!DOCTYPE html>
  1205. <html lang="zh-CN">
  1206. <head>
  1207. <meta charset="UTF-8">
  1208. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  1209. <title>灵感点分析可视化</title>
  1210. <style>
  1211. * {{
  1212. margin: 0;
  1213. padding: 0;
  1214. box-sizing: border-box;
  1215. }}
  1216. body {{
  1217. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  1218. background: #f5f7fa;
  1219. color: #333;
  1220. line-height: 1.6;
  1221. margin: 0;
  1222. padding: 0;
  1223. overflow: hidden;
  1224. }}
  1225. .container {{
  1226. max-width: 100%;
  1227. height: 100vh;
  1228. margin: 0;
  1229. padding: 0;
  1230. display: flex;
  1231. }}
  1232. .main-content-layout {{
  1233. display: flex;
  1234. width: 100%;
  1235. height: 100vh;
  1236. overflow: hidden;
  1237. }}
  1238. .sidebar-nav {{
  1239. width: 400px;
  1240. background: white;
  1241. padding: 30px 20px;
  1242. box-shadow: 2px 0 10px rgba(0,0,0,0.1);
  1243. overflow-y: auto;
  1244. flex-shrink: 0;
  1245. height: 100vh;
  1246. }}
  1247. .nav-title {{
  1248. font-size: 18px;
  1249. font-weight: 700;
  1250. color: #1a1a1a;
  1251. margin-bottom: 15px;
  1252. padding-bottom: 10px;
  1253. border-bottom: 2px solid #e5e7eb;
  1254. }}
  1255. .nav-list {{
  1256. display: flex;
  1257. flex-direction: column;
  1258. gap: 12px;
  1259. }}
  1260. .nav-item {{
  1261. border-radius: 10px;
  1262. cursor: pointer;
  1263. transition: all 0.3s;
  1264. border: 2px solid #e5e7eb;
  1265. overflow: hidden;
  1266. background: white;
  1267. }}
  1268. .nav-item:hover {{
  1269. border-color: #667eea;
  1270. box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
  1271. }}
  1272. .nav-item.active {{
  1273. border-color: #667eea;
  1274. box-shadow: 0 4px 16px rgba(102, 126, 234, 0.25);
  1275. }}
  1276. .nav-item-header {{
  1277. padding: 12px 14px;
  1278. background: #f9fafb;
  1279. display: flex;
  1280. justify-content: space-between;
  1281. align-items: center;
  1282. gap: 8px;
  1283. }}
  1284. .nav-item.active .nav-item-header {{
  1285. background: rgba(102, 126, 234, 0.1);
  1286. }}
  1287. .nav-item-name {{
  1288. flex: 1;
  1289. font-weight: 600;
  1290. font-size: 14px;
  1291. color: #1f2937;
  1292. overflow: hidden;
  1293. text-overflow: ellipsis;
  1294. white-space: nowrap;
  1295. display: flex;
  1296. align-items: center;
  1297. gap: 6px;
  1298. }}
  1299. .nav-item.active .nav-item-name {{
  1300. color: #667eea;
  1301. }}
  1302. .nav-label {{
  1303. display: inline-block;
  1304. font-size: 11px;
  1305. color: #8b5cf6;
  1306. background: #f5f3ff;
  1307. padding: 2px 6px;
  1308. border-radius: 3px;
  1309. font-weight: 500;
  1310. white-space: nowrap;
  1311. flex-shrink: 0;
  1312. }}
  1313. .nav-item-score {{
  1314. font-size: 13px;
  1315. font-weight: 700;
  1316. padding: 4px 10px;
  1317. border-radius: 6px;
  1318. background: #e5e7eb;
  1319. color: #6b7280;
  1320. }}
  1321. .nav-item.active .nav-item-score {{
  1322. background: #667eea;
  1323. color: white;
  1324. }}
  1325. .nav-item-matches {{
  1326. padding: 10px 14px;
  1327. background: white;
  1328. border-top: 1px solid #e5e7eb;
  1329. }}
  1330. .nav-match-item {{
  1331. padding: 8px 0;
  1332. font-size: 12px;
  1333. color: #6b7280;
  1334. display: flex;
  1335. align-items: flex-start;
  1336. gap: 6px;
  1337. }}
  1338. .nav-match-item:not(:last-child) {{
  1339. border-bottom: 1px dashed #e5e7eb;
  1340. }}
  1341. .nav-match-rank {{
  1342. font-weight: 700;
  1343. color: #9ca3af;
  1344. min-width: 20px;
  1345. flex-shrink: 0;
  1346. }}
  1347. .nav-match-rank.rank-1 {{
  1348. color: #f59e0b;
  1349. }}
  1350. .nav-match-rank.rank-2 {{
  1351. color: #c0c0c0;
  1352. }}
  1353. .nav-match-rank.rank-3 {{
  1354. color: #cd7f32;
  1355. }}
  1356. .nav-match-content {{
  1357. flex: 1;
  1358. line-height: 1.6;
  1359. min-width: 0;
  1360. }}
  1361. .nav-match-name {{
  1362. color: #1f2937;
  1363. font-weight: 600;
  1364. display: block;
  1365. margin-bottom: 4px;
  1366. }}
  1367. .nav-match-hierarchy {{
  1368. font-size: 11px;
  1369. color: #9ca3af;
  1370. display: flex;
  1371. align-items: center;
  1372. gap: 4px;
  1373. flex-wrap: wrap;
  1374. margin-bottom: 3px;
  1375. }}
  1376. .hierarchy-item {{
  1377. display: inline-flex;
  1378. align-items: center;
  1379. gap: 4px;
  1380. }}
  1381. .hierarchy-separator {{
  1382. color: #d1d5db;
  1383. }}
  1384. .nav-match-score {{
  1385. color: #667eea;
  1386. font-weight: 700;
  1387. font-size: 13px;
  1388. }}
  1389. .inspirations-section {{
  1390. flex: 1;
  1391. height: 100vh;
  1392. overflow-y: auto;
  1393. background: #f5f7fa;
  1394. position: relative;
  1395. }}
  1396. .breadcrumb-container {{
  1397. position: sticky;
  1398. top: 0;
  1399. left: 0;
  1400. right: 0;
  1401. background: white;
  1402. border-bottom: 1px solid #e5e7eb;
  1403. padding: 15px 30px;
  1404. z-index: 100;
  1405. box-shadow: 0 1px 3px rgba(0,0,0,0.05);
  1406. }}
  1407. .breadcrumb {{
  1408. display: flex;
  1409. align-items: center;
  1410. gap: 8px;
  1411. font-size: 14px;
  1412. color: #6b7280;
  1413. }}
  1414. .breadcrumb-item {{
  1415. display: flex;
  1416. align-items: center;
  1417. gap: 8px;
  1418. }}
  1419. .breadcrumb-separator {{
  1420. color: #d1d5db;
  1421. }}
  1422. .breadcrumb-current {{
  1423. color: #111827;
  1424. font-weight: 600;
  1425. }}
  1426. .breadcrumb-label {{
  1427. display: inline-block;
  1428. font-size: 12px;
  1429. color: #6366f1;
  1430. background: #eef2ff;
  1431. padding: 2px 8px;
  1432. border-radius: 4px;
  1433. font-weight: 500;
  1434. margin-right: 4px;
  1435. }}
  1436. .inspiration-content-wrapper {{
  1437. padding: 30px 40px;
  1438. }}
  1439. .inspirations-grid {{
  1440. display: none;
  1441. }}
  1442. .inspiration-display {{
  1443. width: 100%;
  1444. }}
  1445. .inspiration-detail {{
  1446. display: none;
  1447. }}
  1448. .inspiration-detail.active {{
  1449. display: block;
  1450. }}
  1451. .detail-header {{
  1452. background: white;
  1453. border-radius: 12px;
  1454. padding: 25px 30px;
  1455. margin-bottom: 15px;
  1456. box-shadow: 0 1px 3px rgba(0,0,0,0.06);
  1457. border: 1px solid #e5e7eb;
  1458. }}
  1459. .detail-title-row {{
  1460. display: flex;
  1461. justify-content: space-between;
  1462. align-items: center;
  1463. margin-bottom: 15px;
  1464. }}
  1465. .detail-title-left, .detail-title-right {{
  1466. display: flex;
  1467. align-items: center;
  1468. gap: 10px;
  1469. }}
  1470. .detail-label {{
  1471. font-size: 12px;
  1472. color: #6366f1;
  1473. background: #eef2ff;
  1474. padding: 3px 10px;
  1475. border-radius: 4px;
  1476. font-weight: 500;
  1477. }}
  1478. .detail-category {{
  1479. font-size: 18px;
  1480. font-weight: 600;
  1481. color: #374151;
  1482. }}
  1483. .detail-title {{
  1484. font-size: 24px;
  1485. font-weight: 700;
  1486. color: #111827;
  1487. margin-bottom: 12px;
  1488. }}
  1489. .detail-score-section {{
  1490. display: flex;
  1491. align-items: center;
  1492. gap: 12px;
  1493. }}
  1494. .detail-score-label {{
  1495. font-size: 13px;
  1496. color: #6b7280;
  1497. font-weight: 500;
  1498. }}
  1499. .detail-score-value {{
  1500. font-size: 20px;
  1501. font-weight: 700;
  1502. color: #374151;
  1503. }}
  1504. .card-header {{
  1505. display: flex;
  1506. justify-content: space-between;
  1507. align-items: flex-start;
  1508. margin-bottom: 20px;
  1509. gap: 12px;
  1510. }}
  1511. .inspiration-name {{
  1512. font-size: 28px;
  1513. font-weight: 800;
  1514. color: #1a1a1a;
  1515. line-height: 1.4;
  1516. flex: 1;
  1517. }}
  1518. .grade-badge {{
  1519. background: #10b981;
  1520. color: white;
  1521. padding: 6px 14px;
  1522. border-radius: 20px;
  1523. font-size: 12px;
  1524. font-weight: 700;
  1525. white-space: nowrap;
  1526. }}
  1527. .score-section {{
  1528. display: flex;
  1529. align-items: center;
  1530. gap: 25px;
  1531. margin-bottom: 20px;
  1532. padding: 20px;
  1533. background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
  1534. border-radius: 12px;
  1535. }}
  1536. .score-item {{
  1537. display: flex;
  1538. flex-direction: column;
  1539. align-items: center;
  1540. gap: 8px;
  1541. flex: 1;
  1542. }}
  1543. .main-score {{
  1544. display: flex;
  1545. flex-direction: column;
  1546. align-items: center;
  1547. gap: 8px;
  1548. }}
  1549. .score-circle {{
  1550. width: 90px;
  1551. height: 90px;
  1552. border-radius: 50%;
  1553. border: 6px solid #10b981;
  1554. display: flex;
  1555. align-items: center;
  1556. justify-content: center;
  1557. background: white;
  1558. }}
  1559. .score-value {{
  1560. font-size: 26px;
  1561. font-weight: 800;
  1562. color: #10b981;
  1563. }}
  1564. .score-label {{
  1565. font-size: 12px;
  1566. color: #6b7280;
  1567. font-weight: 600;
  1568. }}
  1569. .sub-scores {{
  1570. flex: 1;
  1571. display: flex;
  1572. flex-direction: column;
  1573. gap: 12px;
  1574. }}
  1575. .sub-score-item {{
  1576. display: flex;
  1577. justify-content: space-between;
  1578. align-items: center;
  1579. padding: 10px 15px;
  1580. background: white;
  1581. border-radius: 8px;
  1582. }}
  1583. .sub-score-label {{
  1584. font-size: 13px;
  1585. color: #6b7280;
  1586. font-weight: 600;
  1587. }}
  1588. .sub-score-value {{
  1589. font-size: 18px;
  1590. font-weight: 700;
  1591. color: #2563eb;
  1592. }}
  1593. .metrics-section {{
  1594. display: flex;
  1595. flex-direction: column;
  1596. gap: 10px;
  1597. margin-bottom: 15px;
  1598. }}
  1599. .metric-item {{
  1600. display: flex;
  1601. align-items: center;
  1602. gap: 8px;
  1603. font-size: 13px;
  1604. color: #4b5563;
  1605. }}
  1606. .metric-icon {{
  1607. font-size: 16px;
  1608. }}
  1609. .metric-label {{
  1610. font-weight: 600;
  1611. }}
  1612. .metric-value {{
  1613. color: #1f2937;
  1614. }}
  1615. .match-preview {{
  1616. background: #f9fafb;
  1617. padding: 12px;
  1618. border-radius: 8px;
  1619. margin-bottom: 10px;
  1620. border-left: 3px solid #8b5cf6;
  1621. }}
  1622. .match-preview-header {{
  1623. font-size: 12px;
  1624. font-weight: 600;
  1625. color: #6b7280;
  1626. margin-bottom: 6px;
  1627. }}
  1628. .match-preview-content {{
  1629. display: flex;
  1630. justify-content: space-between;
  1631. align-items: center;
  1632. }}
  1633. .match-preview-name {{
  1634. font-size: 13px;
  1635. color: #1f2937;
  1636. flex: 1;
  1637. }}
  1638. .match-preview-score {{
  1639. font-size: 16px;
  1640. font-weight: 700;
  1641. }}
  1642. .preview-parts {{
  1643. margin-top: 8px;
  1644. padding: 8px 10px;
  1645. border-radius: 6px;
  1646. font-size: 12px;
  1647. line-height: 1.6;
  1648. }}
  1649. .preview-parts.same {{
  1650. background: #f0fdf4;
  1651. color: #15803d;
  1652. border-left: 3px solid #10b981;
  1653. }}
  1654. .preview-parts.increment {{
  1655. background: #fff7ed;
  1656. color: #92400e;
  1657. border-left: 3px solid #f59e0b;
  1658. margin-top: 6px;
  1659. }}
  1660. .preview-parts strong {{
  1661. font-weight: 700;
  1662. margin-right: 6px;
  1663. }}
  1664. .score-divider {{
  1665. width: 1px;
  1666. height: 40px;
  1667. background: #e5e7eb;
  1668. }}
  1669. .inspiration-category-section {{
  1670. display: grid;
  1671. grid-template-columns: 1fr 1fr;
  1672. gap: 25px;
  1673. margin-bottom: 0;
  1674. }}
  1675. .info-detail-column {{
  1676. background: #fafbfc;
  1677. border-radius: 10px;
  1678. border: 1px solid #e5e7eb;
  1679. overflow: hidden;
  1680. }}
  1681. .info-header {{
  1682. padding: 15px 20px;
  1683. border-bottom: 1px solid #e5e7eb;
  1684. background: #f9fafb;
  1685. display: flex;
  1686. align-items: center;
  1687. gap: 10px;
  1688. }}
  1689. .info-label {{
  1690. font-size: 11px;
  1691. color: #8b5cf6;
  1692. background: #f5f3ff;
  1693. padding: 2px 6px;
  1694. border-radius: 3px;
  1695. font-weight: 500;
  1696. }}
  1697. .info-name {{
  1698. font-size: 16px;
  1699. font-weight: 600;
  1700. color: #111827;
  1701. }}
  1702. .info-content {{
  1703. padding: 20px;
  1704. }}
  1705. .info-field {{
  1706. margin-bottom: 12px;
  1707. line-height: 1.6;
  1708. }}
  1709. .info-field-label {{
  1710. font-weight: 600;
  1711. color: #374151;
  1712. margin-right: 6px;
  1713. }}
  1714. .info-field-value {{
  1715. color: #6b7280;
  1716. }}
  1717. .info-posts {{
  1718. margin-top: 20px;
  1719. }}
  1720. .info-posts-title {{
  1721. font-size: 14px;
  1722. font-weight: 600;
  1723. color: #374151;
  1724. margin-bottom: 12px;
  1725. }}
  1726. .info-posts-grid {{
  1727. display: flex;
  1728. gap: 15px;
  1729. overflow-x: auto;
  1730. overflow-y: hidden;
  1731. padding-bottom: 10px;
  1732. scroll-behavior: smooth;
  1733. }}
  1734. .info-posts-grid::-webkit-scrollbar {{
  1735. height: 6px;
  1736. }}
  1737. .info-posts-grid::-webkit-scrollbar-track {{
  1738. background: #f3f4f6;
  1739. border-radius: 3px;
  1740. }}
  1741. .info-posts-grid::-webkit-scrollbar-thumb {{
  1742. background: #d1d5db;
  1743. border-radius: 3px;
  1744. }}
  1745. .info-posts-grid::-webkit-scrollbar-thumb:hover {{
  1746. background: #9ca3af;
  1747. }}
  1748. .info-posts-grid .search-note-item {{
  1749. flex: 0 0 280px;
  1750. min-width: 280px;
  1751. }}
  1752. .matches-list {{
  1753. display: flex;
  1754. flex-direction: column;
  1755. gap: 15px;
  1756. }}
  1757. .match-item {{
  1758. background: white;
  1759. border-radius: 12px;
  1760. overflow: hidden;
  1761. box-shadow: 0 2px 8px rgba(0,0,0,0.06);
  1762. transition: all 0.3s;
  1763. margin-bottom: 20px;
  1764. }}
  1765. .match-item:hover {{
  1766. box-shadow: 0 4px 16px rgba(0,0,0,0.1);
  1767. }}
  1768. .match-main-header {{
  1769. background: #ffffff;
  1770. border-bottom: 1px solid #e5e7eb;
  1771. padding: 16px 20px;
  1772. cursor: pointer;
  1773. display: flex;
  1774. justify-content: space-between;
  1775. align-items: center;
  1776. }}
  1777. .match-main-header:hover {{
  1778. background: #f9fafb;
  1779. }}
  1780. .match-header-row {{
  1781. flex: 1;
  1782. display: flex;
  1783. justify-content: space-between;
  1784. align-items: center;
  1785. gap: 20px;
  1786. }}
  1787. .match-header-left, .match-header-right {{
  1788. display: flex;
  1789. align-items: center;
  1790. gap: 10px;
  1791. flex: 1;
  1792. }}
  1793. .match-header-center {{
  1794. display: flex;
  1795. flex-direction: column;
  1796. align-items: center;
  1797. gap: 4px;
  1798. padding: 0 20px;
  1799. }}
  1800. .match-score-label {{
  1801. font-size: 11px;
  1802. color: #6b7280;
  1803. text-transform: uppercase;
  1804. }}
  1805. .match-score-value {{
  1806. font-size: 20px;
  1807. font-weight: 700;
  1808. color: #6366f1;
  1809. }}
  1810. .match-title {{
  1811. color: #111827;
  1812. font-size: 16px;
  1813. font-weight: 700;
  1814. }}
  1815. .match-category {{
  1816. color: #111827;
  1817. font-size: 16px;
  1818. font-weight: 700;
  1819. }}
  1820. .match-hierarchy {{
  1821. color: #6b7280;
  1822. font-size: 13px;
  1823. }}
  1824. .match-toggle-main {{
  1825. color: #9ca3af;
  1826. font-size: 20px;
  1827. transition: transform 0.3s;
  1828. flex-shrink: 0;
  1829. }}
  1830. .match-item.expanded .match-toggle-main {{
  1831. transform: rotate(180deg);
  1832. }}
  1833. .match-main-content {{
  1834. max-height: 0;
  1835. overflow: hidden;
  1836. transition: max-height 0.3s ease;
  1837. }}
  1838. .match-item.expanded .match-main-content {{
  1839. max-height: 10000px;
  1840. }}
  1841. .match-main-content .inspiration-category-section {{
  1842. margin: 25px;
  1843. margin-bottom: 0;
  1844. }}
  1845. .match-analysis-section {{
  1846. padding: 25px;
  1847. border-top: 2px solid #e5e7eb;
  1848. margin-top: 15px;
  1849. }}
  1850. .match-header {{
  1851. padding: 20px;
  1852. cursor: pointer;
  1853. display: flex;
  1854. justify-content: space-between;
  1855. align-items: center;
  1856. gap: 15px;
  1857. background: #fafafa;
  1858. transition: background 0.3s;
  1859. border-bottom: 1px solid #e5e7eb;
  1860. }}
  1861. .match-header:hover {{
  1862. background: #f5f5f5;
  1863. }}
  1864. .match-section.expanded .match-header {{
  1865. background: rgba(102, 126, 234, 0.05);
  1866. }}
  1867. .match-header-left {{
  1868. flex: 1;
  1869. min-width: 0;
  1870. }}
  1871. .match-rank {{
  1872. display: inline-block;
  1873. padding: 3px 10px;
  1874. border-radius: 4px;
  1875. font-size: 11px;
  1876. font-weight: 600;
  1877. background: #f3f4f6;
  1878. color: #6b7280;
  1879. }}
  1880. .match-element-name {{
  1881. font-size: 18px;
  1882. font-weight: 700;
  1883. color: #1f2937;
  1884. display: block;
  1885. margin-bottom: 6px;
  1886. }}
  1887. .match-context {{
  1888. font-size: 13px;
  1889. color: #6b7280;
  1890. display: flex;
  1891. align-items: center;
  1892. gap: 6px;
  1893. }}
  1894. .match-context-separator {{
  1895. color: #d1d5db;
  1896. }}
  1897. .match-header-right {{
  1898. display: flex;
  1899. align-items: center;
  1900. gap: 15px;
  1901. flex-shrink: 0;
  1902. }}
  1903. .match-score {{
  1904. font-size: 24px;
  1905. font-weight: 800;
  1906. color: #667eea;
  1907. }}
  1908. .match-toggle {{
  1909. width: 32px;
  1910. height: 32px;
  1911. border-radius: 8px;
  1912. background: #e5e7eb;
  1913. display: flex;
  1914. align-items: center;
  1915. justify-content: center;
  1916. transition: all 0.3s;
  1917. font-size: 18px;
  1918. color: #6b7280;
  1919. }}
  1920. .match-item.expanded .match-toggle {{
  1921. background: #667eea;
  1922. color: white;
  1923. transform: rotate(180deg);
  1924. }}
  1925. .match-section {{
  1926. border-bottom: 1px solid #e5e7eb;
  1927. }}
  1928. .match-section:last-child {{
  1929. border-bottom: none;
  1930. }}
  1931. .step-section-header {{
  1932. padding: 15px 20px;
  1933. background: #f9fafb;
  1934. cursor: pointer;
  1935. display: flex;
  1936. justify-content: space-between;
  1937. align-items: center;
  1938. border-bottom: 2px solid #e5e7eb;
  1939. }}
  1940. .step-section-header:hover {{
  1941. background: #f3f4f6;
  1942. }}
  1943. .step-section-title {{
  1944. font-size: 15px;
  1945. font-weight: 700;
  1946. color: #1f2937;
  1947. display: flex;
  1948. align-items: center;
  1949. gap: 8px;
  1950. }}
  1951. .step-number {{
  1952. display: inline-flex;
  1953. align-items: center;
  1954. justify-content: center;
  1955. width: 20px;
  1956. height: 20px;
  1957. background: #e5e7eb;
  1958. color: #6b7280;
  1959. border-radius: 50%;
  1960. font-size: 11px;
  1961. font-weight: 600;
  1962. }}
  1963. .step-toggle {{
  1964. font-size: 18px;
  1965. color: #6b7280;
  1966. transition: transform 0.3s;
  1967. }}
  1968. .step-section.expanded .step-toggle {{
  1969. transform: rotate(180deg);
  1970. }}
  1971. .step-section-content {{
  1972. max-height: 0;
  1973. overflow: hidden;
  1974. transition: max-height 0.3s ease;
  1975. }}
  1976. .step-section.expanded .step-section-content {{
  1977. max-height: 5000px;
  1978. }}
  1979. .match-content {{
  1980. max-height: 0;
  1981. overflow: hidden;
  1982. transition: max-height 0.3s ease;
  1983. }}
  1984. .match-section.expanded .match-content {{
  1985. max-height: 2000px;
  1986. }}
  1987. .match-content-inner {{
  1988. padding: 20px;
  1989. }}
  1990. .match-explain {{
  1991. background: #fffbeb;
  1992. padding: 16px 20px;
  1993. border-radius: 8px;
  1994. margin-top: 20px;
  1995. border-left: 3px solid #f59e0b;
  1996. }}
  1997. .match-explain-title {{
  1998. font-weight: 600;
  1999. color: #374151;
  2000. margin-bottom: 6px;
  2001. font-size: 13px;
  2002. }}
  2003. .match-explain-text {{
  2004. color: #6b7280;
  2005. font-size: 14px;
  2006. line-height: 1.7;
  2007. }}
  2008. .match-parts-container {{
  2009. display: grid;
  2010. grid-template-columns: 1fr 1fr;
  2011. gap: 25px;
  2012. margin-bottom: 20px;
  2013. }}
  2014. .match-parts-column {{
  2015. min-width: 0;
  2016. }}
  2017. .match-parts {{
  2018. margin-bottom: 0;
  2019. height: 100%;
  2020. background: #f9fafb;
  2021. padding: 16px;
  2022. border-radius: 8px;
  2023. border: 1px solid #e5e7eb;
  2024. }}
  2025. .match-parts-title {{
  2026. font-weight: 700;
  2027. font-size: 14px;
  2028. margin-bottom: 16px;
  2029. color: #374151;
  2030. text-transform: uppercase;
  2031. letter-spacing: 0.5px;
  2032. }}
  2033. .part-item {{
  2034. padding: 10px 0;
  2035. margin-bottom: 8px;
  2036. font-size: 14px;
  2037. border-bottom: 1px solid #e5e7eb;
  2038. }}
  2039. .part-item:last-child {{
  2040. border-bottom: none;
  2041. }}
  2042. .part-key {{
  2043. font-weight: 500;
  2044. color: #374151;
  2045. margin-right: 8px;
  2046. }}
  2047. .part-value {{
  2048. color: #6b7280;
  2049. }}
  2050. .same-parts {{
  2051. background: #ecfdf5;
  2052. border-color: #10b981;
  2053. }}
  2054. .increment-parts {{
  2055. background: #fef3c7;
  2056. border-color: #f59e0b;
  2057. }}
  2058. .search-results-section {{
  2059. }}
  2060. .search-notes-list {{
  2061. display: grid;
  2062. grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  2063. gap: 20px;
  2064. }}
  2065. .search-note-item {{
  2066. background: white;
  2067. border-radius: 12px;
  2068. overflow: hidden;
  2069. box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  2070. border: 1px solid #e5e7eb;
  2071. transition: all 0.3s;
  2072. }}
  2073. .search-note-item:hover {{
  2074. box-shadow: 0 4px 16px rgba(102, 126, 234, 0.2);
  2075. border-color: #667eea;
  2076. }}
  2077. .note-image-carousel {{
  2078. position: relative;
  2079. width: 100%;
  2080. min-height: 200px;
  2081. max-height: 400px;
  2082. background: #f3f4f6;
  2083. overflow: hidden;
  2084. display: flex;
  2085. align-items: center;
  2086. justify-content: center;
  2087. }}
  2088. .note-images-track {{
  2089. display: flex;
  2090. width: 100%;
  2091. height: 100%;
  2092. transition: transform 0.4s ease;
  2093. }}
  2094. .note-image {{
  2095. flex: 0 0 100%;
  2096. width: 100%;
  2097. height: auto;
  2098. max-height: 400px;
  2099. object-fit: contain;
  2100. }}
  2101. .note-carousel-button {{
  2102. position: absolute;
  2103. top: 50%;
  2104. transform: translateY(-50%);
  2105. width: 32px;
  2106. height: 32px;
  2107. background: rgba(255, 255, 255, 0.9);
  2108. border: none;
  2109. border-radius: 50%;
  2110. display: flex;
  2111. align-items: center;
  2112. justify-content: center;
  2113. cursor: pointer;
  2114. transition: all 0.3s;
  2115. z-index: 10;
  2116. font-size: 16px;
  2117. color: #6b7280;
  2118. opacity: 0;
  2119. }}
  2120. .search-note-item:hover .note-carousel-button {{
  2121. opacity: 1;
  2122. }}
  2123. .note-carousel-button:hover {{
  2124. background: #667eea;
  2125. color: white;
  2126. }}
  2127. .note-carousel-button.disabled {{
  2128. opacity: 0 !important;
  2129. }}
  2130. .note-carousel-button.prev {{
  2131. left: 10px;
  2132. }}
  2133. .note-carousel-button.next {{
  2134. right: 10px;
  2135. }}
  2136. .note-image-dots {{
  2137. position: absolute;
  2138. bottom: 10px;
  2139. left: 50%;
  2140. transform: translateX(-50%);
  2141. display: flex;
  2142. gap: 6px;
  2143. z-index: 10;
  2144. }}
  2145. .note-image-dot {{
  2146. width: 6px;
  2147. height: 6px;
  2148. border-radius: 50%;
  2149. background: rgba(255, 255, 255, 0.6);
  2150. transition: all 0.3s;
  2151. }}
  2152. .note-image-dot.active {{
  2153. background: white;
  2154. width: 20px;
  2155. border-radius: 3px;
  2156. }}
  2157. .note-content {{
  2158. padding: 15px;
  2159. }}
  2160. .note-title {{
  2161. font-size: 15px;
  2162. font-weight: 600;
  2163. color: #1f2937;
  2164. margin-bottom: 8px;
  2165. overflow: hidden;
  2166. text-overflow: ellipsis;
  2167. display: -webkit-box;
  2168. -webkit-line-clamp: 2;
  2169. -webkit-box-orient: vertical;
  2170. line-height: 1.4;
  2171. }}
  2172. .note-desc {{
  2173. font-size: 13px;
  2174. color: #6b7280;
  2175. line-height: 1.6;
  2176. margin-bottom: 12px;
  2177. overflow: hidden;
  2178. text-overflow: ellipsis;
  2179. display: -webkit-box;
  2180. -webkit-line-clamp: 2;
  2181. -webkit-box-orient: vertical;
  2182. }}
  2183. .note-footer {{
  2184. display: flex;
  2185. justify-content: space-between;
  2186. align-items: center;
  2187. padding-top: 12px;
  2188. border-top: 1px solid #f3f4f6;
  2189. }}
  2190. .note-author {{
  2191. font-size: 12px;
  2192. font-weight: 600;
  2193. color: #6b7280;
  2194. overflow: hidden;
  2195. text-overflow: ellipsis;
  2196. white-space: nowrap;
  2197. max-width: 150px;
  2198. }}
  2199. .note-points-hover {{
  2200. position: absolute;
  2201. bottom: 100%;
  2202. left: 0;
  2203. right: 0;
  2204. background: white;
  2205. border: 1px solid #e5e7eb;
  2206. border-radius: 8px;
  2207. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  2208. padding: 12px;
  2209. margin-bottom: 8px;
  2210. opacity: 0;
  2211. visibility: hidden;
  2212. transition: opacity 0.2s, visibility 0.2s;
  2213. z-index: 10;
  2214. max-height: 300px;
  2215. overflow-y: auto;
  2216. }}
  2217. .search-note-item {{
  2218. position: relative;
  2219. }}
  2220. .search-note-item:hover .note-points-hover {{
  2221. opacity: 1;
  2222. visibility: visible;
  2223. }}
  2224. .note-points {{
  2225. /* 用于详情弹窗中的样式 */
  2226. }}
  2227. .points-section {{
  2228. margin-bottom: 8px;
  2229. }}
  2230. .points-section:last-child {{
  2231. margin-bottom: 0;
  2232. }}
  2233. .points-label {{
  2234. font-size: 11px;
  2235. font-weight: 600;
  2236. color: #6b7280;
  2237. margin-bottom: 4px;
  2238. }}
  2239. .points-list {{
  2240. display: flex;
  2241. flex-wrap: wrap;
  2242. gap: 4px;
  2243. }}
  2244. .point-item {{
  2245. display: inline-block;
  2246. }}
  2247. .point-name {{
  2248. display: inline-block;
  2249. font-size: 11px;
  2250. color: #4b5563;
  2251. background: #f3f4f6;
  2252. padding: 2px 6px;
  2253. border-radius: 3px;
  2254. }}
  2255. .note-stats {{
  2256. display: flex;
  2257. gap: 10px;
  2258. font-size: 12px;
  2259. color: #9ca3af;
  2260. }}
  2261. .note-link {{
  2262. display: block;
  2263. text-align: center;
  2264. padding: 8px;
  2265. margin-top: 10px;
  2266. background: rgba(102, 126, 234, 0.1);
  2267. color: #667eea;
  2268. text-decoration: none;
  2269. border-radius: 6px;
  2270. font-size: 13px;
  2271. font-weight: 600;
  2272. transition: all 0.3s;
  2273. }}
  2274. .note-link:hover {{
  2275. background: #667eea;
  2276. color: white;
  2277. }}
  2278. /* 笔记详情Modal样式 */
  2279. .note-detail-modal {{
  2280. display: none;
  2281. position: fixed;
  2282. top: 0;
  2283. left: 0;
  2284. width: 100%;
  2285. height: 100%;
  2286. background: rgba(0, 0, 0, 0.8);
  2287. z-index: 9999;
  2288. overflow-y: auto;
  2289. padding: 40px 20px;
  2290. }}
  2291. .note-detail-modal.active {{
  2292. display: flex;
  2293. align-items: flex-start;
  2294. justify-content: center;
  2295. }}
  2296. .note-detail-content {{
  2297. background: white;
  2298. border-radius: 16px;
  2299. max-width: 1200px;
  2300. width: 100%;
  2301. position: relative;
  2302. margin: auto;
  2303. }}
  2304. .note-detail-close {{
  2305. position: absolute;
  2306. top: 20px;
  2307. right: 20px;
  2308. width: 40px;
  2309. height: 40px;
  2310. background: white;
  2311. border: none;
  2312. border-radius: 50%;
  2313. font-size: 24px;
  2314. cursor: pointer;
  2315. display: flex;
  2316. align-items: center;
  2317. justify-content: center;
  2318. box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  2319. transition: all 0.3s;
  2320. z-index: 10;
  2321. }}
  2322. .note-detail-close:hover {{
  2323. background: #ef4444;
  2324. color: white;
  2325. transform: rotate(90deg);
  2326. }}
  2327. .note-detail-header {{
  2328. padding: 30px;
  2329. border-bottom: 1px solid #e5e7eb;
  2330. }}
  2331. .note-detail-title {{
  2332. font-size: 24px;
  2333. font-weight: 800;
  2334. color: #1f2937;
  2335. margin-bottom: 15px;
  2336. }}
  2337. .note-detail-meta {{
  2338. display: flex;
  2339. align-items: center;
  2340. gap: 20px;
  2341. font-size: 14px;
  2342. color: #6b7280;
  2343. }}
  2344. .note-detail-author {{
  2345. font-weight: 600;
  2346. color: #1f2937;
  2347. }}
  2348. .note-detail-stats {{
  2349. display: flex;
  2350. gap: 15px;
  2351. }}
  2352. .note-detail-body {{
  2353. padding: 30px;
  2354. }}
  2355. .note-detail-desc {{
  2356. font-size: 15px;
  2357. line-height: 1.8;
  2358. color: #4b5563;
  2359. margin-bottom: 30px;
  2360. }}
  2361. .note-detail-points {{
  2362. margin-bottom: 30px;
  2363. background: #f9fafb;
  2364. border-radius: 8px;
  2365. padding: 20px;
  2366. }}
  2367. .detail-points-section {{
  2368. margin-bottom: 20px;
  2369. }}
  2370. .detail-points-section:last-child {{
  2371. margin-bottom: 0;
  2372. }}
  2373. .detail-points-title {{
  2374. font-size: 16px;
  2375. font-weight: 600;
  2376. color: #111827;
  2377. margin-bottom: 12px;
  2378. }}
  2379. .detail-point-item {{
  2380. background: white;
  2381. padding: 12px 15px;
  2382. border-radius: 6px;
  2383. margin-bottom: 10px;
  2384. border-left: 3px solid #e5e7eb;
  2385. }}
  2386. .detail-point-item:last-child {{
  2387. margin-bottom: 0;
  2388. }}
  2389. .detail-point-name {{
  2390. font-size: 14px;
  2391. font-weight: 600;
  2392. color: #374151;
  2393. margin-bottom: 4px;
  2394. }}
  2395. .detail-point-desc {{
  2396. font-size: 13px;
  2397. line-height: 1.6;
  2398. color: #6b7280;
  2399. }}
  2400. .note-detail-images {{
  2401. display: grid;
  2402. grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  2403. gap: 15px;
  2404. }}
  2405. .note-detail-image {{
  2406. width: 100%;
  2407. border-radius: 8px;
  2408. cursor: pointer;
  2409. transition: all 0.3s;
  2410. }}
  2411. .note-detail-image:hover {{
  2412. transform: scale(1.05);
  2413. box-shadow: 0 8px 24px rgba(0,0,0,0.2);
  2414. }}
  2415. .note-detail-footer {{
  2416. padding: 20px 30px;
  2417. border-top: 1px solid #e5e7eb;
  2418. display: flex;
  2419. justify-content: center;
  2420. }}
  2421. .note-detail-link {{
  2422. display: inline-block;
  2423. padding: 12px 32px;
  2424. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  2425. color: white;
  2426. text-decoration: none;
  2427. border-radius: 10px;
  2428. font-weight: 600;
  2429. transition: all 0.3s;
  2430. }}
  2431. .note-detail-link:hover {{
  2432. transform: translateY(-2px);
  2433. box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
  2434. }}
  2435. /* Modal样式 */
  2436. .modal-overlay {{
  2437. display: none;
  2438. position: fixed;
  2439. top: 0;
  2440. left: 0;
  2441. right: 0;
  2442. bottom: 0;
  2443. background: rgba(0, 0, 0, 0.8);
  2444. z-index: 1000;
  2445. align-items: center;
  2446. justify-content: center;
  2447. padding: 20px;
  2448. overflow-y: auto;
  2449. }}
  2450. .modal-overlay.active {{
  2451. display: flex;
  2452. }}
  2453. .modal-content {{
  2454. background: white;
  2455. border-radius: 16px;
  2456. max-width: 1200px;
  2457. width: 100%;
  2458. max-height: 90vh;
  2459. overflow-y: auto;
  2460. position: relative;
  2461. }}
  2462. .modal-close {{
  2463. position: sticky;
  2464. top: 0;
  2465. right: 0;
  2466. background: white;
  2467. border: none;
  2468. font-size: 32px;
  2469. color: #6b7280;
  2470. cursor: pointer;
  2471. padding: 15px 20px;
  2472. z-index: 10;
  2473. text-align: right;
  2474. border-bottom: 1px solid #e5e7eb;
  2475. }}
  2476. .modal-close:hover {{
  2477. color: #1f2937;
  2478. }}
  2479. .modal-body {{
  2480. padding: 30px;
  2481. }}
  2482. .modal-header {{
  2483. margin-bottom: 25px;
  2484. padding-bottom: 20px;
  2485. border-bottom: 2px solid #e5e7eb;
  2486. }}
  2487. .modal-title {{
  2488. font-size: 28px;
  2489. font-weight: 800;
  2490. color: #1a1a1a;
  2491. }}
  2492. .modal-section {{
  2493. margin-bottom: 30px;
  2494. }}
  2495. .modal-section h3 {{
  2496. font-size: 20px;
  2497. font-weight: 700;
  2498. color: #374151;
  2499. margin-bottom: 15px;
  2500. padding-bottom: 10px;
  2501. border-bottom: 2px solid #f3f4f6;
  2502. }}
  2503. .info-grid {{
  2504. display: grid;
  2505. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  2506. gap: 15px;
  2507. }}
  2508. .info-item {{
  2509. background: #f9fafb;
  2510. padding: 12px 16px;
  2511. border-radius: 8px;
  2512. border-left: 3px solid #8b5cf6;
  2513. }}
  2514. .info-label {{
  2515. font-weight: 600;
  2516. color: #6b7280;
  2517. font-size: 13px;
  2518. margin-right: 8px;
  2519. }}
  2520. .info-value {{
  2521. color: #1f2937;
  2522. font-size: 14px;
  2523. }}
  2524. .metrics-grid {{
  2525. display: grid;
  2526. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  2527. gap: 15px;
  2528. }}
  2529. .metric-box {{
  2530. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  2531. padding: 20px;
  2532. border-radius: 12px;
  2533. text-align: center;
  2534. color: white;
  2535. }}
  2536. .metric-box.wide {{
  2537. grid-column: span 2;
  2538. }}
  2539. .metric-box-label {{
  2540. font-size: 13px;
  2541. opacity: 0.9;
  2542. margin-bottom: 8px;
  2543. font-weight: 600;
  2544. }}
  2545. .metric-box-value {{
  2546. font-size: 28px;
  2547. font-weight: 700;
  2548. }}
  2549. .metric-box-value.small {{
  2550. font-size: 16px;
  2551. }}
  2552. .step-content {{
  2553. background: #f9fafb;
  2554. padding: 20px;
  2555. border-radius: 12px;
  2556. }}
  2557. .step-field {{
  2558. margin-bottom: 20px;
  2559. }}
  2560. .step-field-label {{
  2561. font-weight: 700;
  2562. color: #374151;
  2563. font-size: 14px;
  2564. margin-bottom: 8px;
  2565. display: block;
  2566. }}
  2567. .step-field-value {{
  2568. color: #1f2937;
  2569. font-size: 15px;
  2570. line-height: 1.7;
  2571. }}
  2572. .matches-list {{
  2573. display: flex;
  2574. flex-direction: column;
  2575. gap: 15px;
  2576. margin-top: 10px;
  2577. }}
  2578. .match-item {{
  2579. background: white;
  2580. padding: 18px;
  2581. border-radius: 10px;
  2582. border-left: 5px solid #3b82f6;
  2583. }}
  2584. .match-item.top1 {{
  2585. border-left-color: #fbbf24;
  2586. background: linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, white 100%);
  2587. }}
  2588. .match-item.top2 {{
  2589. border-left-color: #c0c0c0;
  2590. background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 50%, white 100%);
  2591. }}
  2592. .match-item.top3 {{
  2593. border-left-color: #cd7f32;
  2594. background: linear-gradient(135deg, #fef3c7 0%, #fed7aa 50%, white 100%);
  2595. }}
  2596. .match-header {{
  2597. display: flex;
  2598. justify-content: space-between;
  2599. align-items: center;
  2600. margin-bottom: 12px;
  2601. gap: 10px;
  2602. }}
  2603. .match-rank {{
  2604. font-size: 18px;
  2605. font-weight: 800;
  2606. color: #6b7280;
  2607. }}
  2608. .match-element-name {{
  2609. flex: 1;
  2610. font-size: 16px;
  2611. font-weight: 700;
  2612. color: #1f2937;
  2613. }}
  2614. .match-score {{
  2615. font-size: 22px;
  2616. font-weight: 800;
  2617. color: #2563eb;
  2618. background: white;
  2619. padding: 6px 14px;
  2620. border-radius: 8px;
  2621. }}
  2622. .match-detail {{
  2623. background: rgba(255, 255, 255, 0.7);
  2624. padding: 10px;
  2625. border-radius: 6px;
  2626. margin-bottom: 10px;
  2627. font-size: 13px;
  2628. color: #4b5563;
  2629. }}
  2630. .match-reason {{
  2631. color: #1f2937;
  2632. font-size: 14px;
  2633. line-height: 1.7;
  2634. }}
  2635. .increment-matches {{
  2636. display: flex;
  2637. flex-direction: column;
  2638. gap: 12px;
  2639. margin-top: 10px;
  2640. }}
  2641. .increment-item {{
  2642. background: white;
  2643. padding: 15px;
  2644. border-radius: 8px;
  2645. border-left: 4px solid #10b981;
  2646. }}
  2647. .increment-header {{
  2648. display: flex;
  2649. justify-content: space-between;
  2650. align-items: center;
  2651. margin-bottom: 10px;
  2652. }}
  2653. .increment-words {{
  2654. font-weight: 700;
  2655. color: #1f2937;
  2656. font-size: 15px;
  2657. }}
  2658. .increment-score {{
  2659. font-size: 20px;
  2660. font-weight: 800;
  2661. color: #10b981;
  2662. }}
  2663. .increment-reason {{
  2664. color: #4b5563;
  2665. font-size: 13px;
  2666. line-height: 1.6;
  2667. }}
  2668. .empty-state {{
  2669. text-align: center;
  2670. padding: 40px;
  2671. color: #9ca3af;
  2672. font-size: 14px;
  2673. }}
  2674. .modal-link {{
  2675. margin-top: 25px;
  2676. padding-top: 20px;
  2677. border-top: 2px solid #e5e7eb;
  2678. text-align: center;
  2679. }}
  2680. .modal-link-btn {{
  2681. display: inline-flex;
  2682. align-items: center;
  2683. gap: 10px;
  2684. padding: 12px 24px;
  2685. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  2686. color: white;
  2687. text-decoration: none;
  2688. border-radius: 10px;
  2689. font-size: 15px;
  2690. font-weight: 600;
  2691. transition: all 0.3s;
  2692. }}
  2693. .modal-link-btn:hover {{
  2694. transform: translateY(-2px);
  2695. box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
  2696. }}
  2697. .timestamp {{
  2698. text-align: center;
  2699. color: white;
  2700. font-size: 13px;
  2701. margin-top: 30px;
  2702. opacity: 0.8;
  2703. }}
  2704. .match-context {{
  2705. background: #f3f4f6;
  2706. padding: 8px 12px;
  2707. border-radius: 6px;
  2708. margin: 8px 0;
  2709. font-size: 12px;
  2710. color: #6b7280;
  2711. line-height: 1.6;
  2712. }}
  2713. .match-explain {{
  2714. background: #fef3c7;
  2715. padding: 10px 12px;
  2716. border-radius: 6px;
  2717. margin: 10px 0;
  2718. font-size: 13px;
  2719. color: #92400e;
  2720. line-height: 1.7;
  2721. border-left: 3px solid #f59e0b;
  2722. }}
  2723. .match-parts {{
  2724. margin: 12px 0;
  2725. border-radius: 8px;
  2726. overflow: hidden;
  2727. }}
  2728. .match-parts.same-parts {{
  2729. background: #f0fdf4;
  2730. border: 2px solid #10b981;
  2731. }}
  2732. .match-parts.increment-parts {{
  2733. background: #fff7ed;
  2734. border: 2px solid #f59e0b;
  2735. }}
  2736. .parts-header {{
  2737. font-weight: 700;
  2738. padding: 10px 12px;
  2739. font-size: 13px;
  2740. }}
  2741. .same-parts .parts-header {{
  2742. background: #dcfce7;
  2743. color: #15803d;
  2744. }}
  2745. .increment-parts .parts-header {{
  2746. background: #fed7aa;
  2747. color: #92400e;
  2748. }}
  2749. .parts-content {{
  2750. padding: 8px 12px;
  2751. }}
  2752. .part-item {{
  2753. padding: 6px 0;
  2754. border-bottom: 1px solid rgba(0,0,0,0.05);
  2755. font-size: 13px;
  2756. line-height: 1.6;
  2757. }}
  2758. .part-item:last-child {{
  2759. border-bottom: none;
  2760. }}
  2761. .part-key {{
  2762. font-weight: 600;
  2763. color: #374151;
  2764. margin-right: 6px;
  2765. }}
  2766. .part-value {{
  2767. color: #1f2937;
  2768. }}
  2769. .increment-context {{
  2770. background: #fef3c7;
  2771. padding: 10px 12px;
  2772. border-radius: 6px;
  2773. margin: 10px 0;
  2774. font-size: 12px;
  2775. color: #92400e;
  2776. line-height: 1.6;
  2777. border-left: 3px solid #f59e0b;
  2778. }}
  2779. @media (max-width: 768px) {{
  2780. .main-content-layout {{
  2781. flex-direction: column;
  2782. }}
  2783. .sidebar-nav {{
  2784. position: relative;
  2785. width: 100%;
  2786. max-height: 300px;
  2787. }}
  2788. .inspirations-grid {{
  2789. grid-template-columns: 1fr;
  2790. }}
  2791. .header h1 {{
  2792. font-size: 32px;
  2793. }}
  2794. .stats-overview {{
  2795. grid-template-columns: repeat(2, 1fr);
  2796. }}
  2797. }}
  2798. </style>
  2799. </head>
  2800. <body>
  2801. <div class="container">
  2802. <div class="main-content-layout">
  2803. <div class="sidebar-nav">
  2804. <div class="nav-title">📑 灵感点列表</div>
  2805. <div class="nav-list" id="navList">
  2806. <!-- 目录将通过JS动态生成 -->
  2807. </div>
  2808. </div>
  2809. <div class="inspirations-section">
  2810. <div class="inspiration-display">
  2811. {cards_html_str}
  2812. </div>
  2813. </div>
  2814. </div>
  2815. <!-- Modal -->
  2816. <div id="detailModal" class="modal-overlay" onclick="closeModalOnOverlay(event)">
  2817. <div class="modal-content">
  2818. <button class="modal-close" onclick="closeModal()">&times;</button>
  2819. <div class="modal-body" id="modalBody">
  2820. <!-- Content will be inserted here -->
  2821. </div>
  2822. </div>
  2823. </div>
  2824. </div>
  2825. <script>
  2826. {detail_modal_js}
  2827. </script>
  2828. </body>
  2829. </html>'''
  2830. # 写入文件
  2831. output_file = Path(output_path)
  2832. output_file.parent.mkdir(parents=True, exist_ok=True)
  2833. with open(output_file, 'w', encoding='utf-8') as f:
  2834. f.write(html_content)
  2835. return str(output_file.absolute())
  2836. def load_persona_data(persona_path: str) -> Dict[str, Any]:
  2837. """
  2838. 加载人设数据
  2839. Args:
  2840. persona_path: 人设JSON文件路径
  2841. Returns:
  2842. 人设数据字典
  2843. """
  2844. try:
  2845. with open(persona_path, 'r', encoding='utf-8') as f:
  2846. return json.load(f)
  2847. except Exception as e:
  2848. print(f"警告: 读取人设文件失败: {e}")
  2849. return {}
  2850. def main():
  2851. """主函数"""
  2852. import sys
  2853. import os
  2854. # 配置路径(使用当前脚本的相对路径)
  2855. script_dir = os.path.dirname(os.path.abspath(__file__))
  2856. base_dir = os.path.join(script_dir, "data/阿里多多酱")
  2857. inspiration_dir = os.path.join(base_dir, "out/人设_1110/how/灵感点")
  2858. posts_dir = os.path.join(base_dir, "作者历史帖子")
  2859. persona_path = os.path.join(base_dir, "out/人设_1110/人设.json")
  2860. inspiration_to_post_path = os.path.join(base_dir, "out/人设_1110/点到帖子映射.json")
  2861. category_index_path = os.path.join(base_dir, "out/人设_1110/分类索引_完整.json")
  2862. post_to_mapping_path = os.path.join(base_dir, "out/人设_1110/帖子到分类和点映射.json")
  2863. output_path = os.path.join(base_dir, "out/人设_1110/how/灵感点可视化.html")
  2864. print("=" * 60)
  2865. print("灵感点分析可视化脚本")
  2866. print("=" * 60)
  2867. # 加载数据
  2868. print("\n📂 正在加载灵感点数据...")
  2869. inspirations_data = load_inspiration_points_data(inspiration_dir)
  2870. print(f"✅ 成功加载 {len(inspirations_data)} 个灵感点")
  2871. print("\n📂 正在加载帖子数据...")
  2872. posts_map = load_posts_data(posts_dir)
  2873. print(f"✅ 成功加载 {len(posts_map)} 个帖子")
  2874. print("\n📂 正在加载人设数据...")
  2875. persona_data = load_persona_data(persona_path)
  2876. print(f"✅ 成功加载人设数据")
  2877. print("\n📂 正在加载点到帖子映射数据...")
  2878. with open(inspiration_to_post_path, 'r', encoding='utf-8') as f:
  2879. inspiration_to_post_data = json.load(f)
  2880. print(f"✅ 成功加载点到帖子映射数据")
  2881. print("\n📂 正在加载分类索引数据...")
  2882. with open(category_index_path, 'r', encoding='utf-8') as f:
  2883. category_index_data = json.load(f)
  2884. print(f"✅ 成功加载分类索引数据")
  2885. print("\n📂 正在加载帖子到分类和点映射数据...")
  2886. with open(post_to_mapping_path, 'r', encoding='utf-8') as f:
  2887. post_to_mapping_data = json.load(f)
  2888. print(f"✅ 成功加载帖子到分类和点映射数据")
  2889. # 生成HTML
  2890. print("\n🎨 正在生成可视化HTML...")
  2891. result_path = generate_html(
  2892. inspirations_data,
  2893. posts_map,
  2894. persona_data,
  2895. output_path,
  2896. inspiration_to_post_data,
  2897. category_index_data,
  2898. post_to_mapping_data
  2899. )
  2900. print(f"\n✅ 可视化文件已生成!")
  2901. print(f"📄 文件路径: {result_path}")
  2902. print(f"\n💡 在浏览器中打开该文件即可查看可视化结果")
  2903. print("=" * 60)
  2904. if __name__ == "__main__":
  2905. main()