visualize_how_results.py 126 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. How解构结果可视化脚本 V2
  5. 改进版:
  6. - 使用标签页展示多个帖子
  7. - 参考 visualize_inspiration_points.py 的帖子详情展示
  8. - 分层可折叠的匹配结果
  9. """
  10. import json
  11. from pathlib import Path
  12. from typing import Dict, List
  13. import sys
  14. import html as html_module
  15. # 添加项目根目录到路径
  16. project_root = Path(__file__).parent.parent.parent
  17. sys.path.insert(0, str(project_root))
  18. from script.data_processing.path_config import PathConfig
  19. # ============ 相似度阈值配置 ============
  20. SIMILARITY_THRESHOLD_SAME = 0.8 # >= 此值为"相同"
  21. SIMILARITY_THRESHOLD_SIMILAR = 0.5 # >= 此值为"相似",< SAME阈值
  22. # < SIMILAR阈值 为"无关"
  23. # 相似度对应的颜色
  24. SIMILARITY_COLOR_SAME = "#10b981" # 绿色
  25. SIMILARITY_COLOR_SIMILAR = "#f59e0b" # 橙色
  26. SIMILARITY_COLOR_UNRELATED = "#9ca3af" # 灰色
  27. def get_similarity_status(similarity: float) -> tuple:
  28. """根据相似度返回状态标签和颜色
  29. Returns:
  30. tuple: (label, color, css_class)
  31. """
  32. if similarity >= SIMILARITY_THRESHOLD_SAME:
  33. return ("相同", SIMILARITY_COLOR_SAME, "same")
  34. elif similarity >= SIMILARITY_THRESHOLD_SIMILAR:
  35. return ("相似", SIMILARITY_COLOR_SIMILAR, "similar")
  36. else:
  37. return ("无关", SIMILARITY_COLOR_UNRELATED, "unrelated")
  38. # 注意:已改用基于相似度的显示方式,不再使用关系类型
  39. # def get_relation_color(relation: str) -> str:
  40. # """根据关系类型返回对应的颜色"""
  41. # color_map = {
  42. # "same": "#10b981", # 绿色 - 同义
  43. # "contains": "#3b82f6", # 蓝色 - 包含
  44. # "contained_by": "#8b5cf6", # 紫色 - 被包含
  45. # "coordinate": "#f59e0b", # 橙色 - 同级
  46. # "overlap": "#ec4899", # 粉色 - 部分重叠
  47. # "related": "#6366f1", # 靛蓝 - 相关
  48. # "unrelated": "#9ca3af" # 灰色 - 无关
  49. # }
  50. # return color_map.get(relation, "#9ca3af")
  51. #
  52. #
  53. # def get_relation_label(relation: str) -> str:
  54. # """返回关系类型的中文标签"""
  55. # label_map = {
  56. # "same": "同义",
  57. # "contains": "包含",
  58. # "contained_by": "被包含",
  59. # "coordinate": "同级",
  60. # "overlap": "部分重叠",
  61. # "related": "相关",
  62. # "unrelated": "无关"
  63. # }
  64. # return label_map.get(relation, relation)
  65. def generate_historical_post_card_html(post_detail: Dict, inspiration_point: Dict) -> str:
  66. """生成历史帖子的紧凑卡片HTML"""
  67. title = post_detail.get("title", "无标题")
  68. body_text = post_detail.get("body_text", "")
  69. images = post_detail.get("images", [])
  70. like_count = post_detail.get("like_count", 0)
  71. collect_count = post_detail.get("collect_count", 0)
  72. comment_count = post_detail.get("comment_count", 0)
  73. author = post_detail.get("channel_account_name", "")
  74. link = post_detail.get("link", "#")
  75. publish_time = post_detail.get("publish_time", "")
  76. # 获取灵感点信息
  77. point_name = inspiration_point.get("点的名称", "")
  78. point_desc = inspiration_point.get("点的描述", "")
  79. # 准备详情数据(用于模态框)
  80. import json
  81. post_detail_data = {
  82. "title": title,
  83. "body_text": body_text,
  84. "images": images,
  85. "like_count": like_count,
  86. "comment_count": comment_count,
  87. "collect_count": collect_count,
  88. "author": author,
  89. "publish_time": publish_time,
  90. "link": link
  91. }
  92. post_data_json = json.dumps(post_detail_data, ensure_ascii=False)
  93. post_data_json_escaped = html_module.escape(post_data_json)
  94. # 截取正文预览(前80个字符)
  95. body_preview = body_text[:80] + "..." if len(body_text) > 80 else body_text
  96. # 生成缩略图
  97. thumbnail_html = ""
  98. if images:
  99. thumbnail_html = f'<img src="{images[0]}" alt="Post thumbnail" class="historical-post-thumbnail" loading="lazy">'
  100. html = f'''
  101. <div class="historical-post-card" data-post-data='{post_data_json_escaped}' onclick="showPostDetail(this)">
  102. <div class="historical-post-image">
  103. {thumbnail_html}
  104. {f'<div class="post-card-image-count">{len(images)}</div>' if len(images) > 1 else ''}
  105. </div>
  106. <div class="historical-post-content">
  107. <div class="historical-post-title">{html_module.escape(title)}</div>
  108. <div class="historical-inspiration-info">
  109. <span class="inspiration-type-badge-small">灵感点</span>
  110. <span class="inspiration-name-small">{html_module.escape(point_name)}</span>
  111. </div>
  112. <div class="historical-inspiration-desc">{html_module.escape(point_desc[:100])}{"..." if len(point_desc) > 100 else ""}</div>
  113. <div class="historical-post-meta">
  114. <span class="historical-post-time">📅 {publish_time}</span>
  115. </div>
  116. <div class="historical-post-stats">
  117. <span>❤ {like_count}</span>
  118. <span>⭐ {collect_count}</span>
  119. <a href="{link}" target="_blank" class="view-link" onclick="event.stopPropagation()">查看原帖 →</a>
  120. </div>
  121. </div>
  122. </div>
  123. '''
  124. return html
  125. def generate_post_detail_html(post_data: Dict, post_idx: int) -> str:
  126. """生成帖子详情HTML(紧凑的卡片样式,点击可展开)"""
  127. post_detail = post_data.get("帖子详情", {})
  128. title = post_detail.get("title", "无标题")
  129. body_text = post_detail.get("body_text", "")
  130. images = post_detail.get("images", [])
  131. like_count = post_detail.get("like_count", 0)
  132. comment_count = post_detail.get("comment_count", 0)
  133. collect_count = post_detail.get("collect_count", 0)
  134. author = post_detail.get("channel_account_name", "")
  135. publish_time = post_detail.get("publish_time", "")
  136. link = post_detail.get("link", "")
  137. post_id = post_data.get("帖子id", f"post-{post_idx}")
  138. # 准备详情数据(用于模态框)
  139. import json
  140. post_detail_data = {
  141. "title": title,
  142. "body_text": body_text,
  143. "images": images,
  144. "like_count": like_count,
  145. "comment_count": comment_count,
  146. "collect_count": collect_count,
  147. "author": author,
  148. "publish_time": publish_time,
  149. "link": link
  150. }
  151. post_data_json = json.dumps(post_detail_data, ensure_ascii=False)
  152. post_data_json_escaped = html_module.escape(post_data_json)
  153. # 生成缩略图HTML
  154. thumbnail_html = ""
  155. if images and len(images) > 0:
  156. # 使用第一张图片作为缩略图,添加懒加载
  157. thumbnail_html = f'<img src="{images[0]}" class="post-card-thumbnail" alt="缩略图" loading="lazy">'
  158. else:
  159. thumbnail_html = '<div class="post-card-thumbnail-placeholder">📄</div>'
  160. # 截断正文用于预览
  161. body_preview = body_text[:80] + "..." if len(body_text) > 80 else body_text
  162. html = f'''
  163. <div class="post-card-compact" data-post-data='{post_data_json_escaped}' onclick="showPostDetail(this)">
  164. <div class="post-card-image">
  165. {thumbnail_html}
  166. {f'<div class="post-card-image-count">📷 {len(images)}</div>' if len(images) > 1 else ''}
  167. </div>
  168. <div class="post-card-content">
  169. <div class="post-card-title">{html_module.escape(title)}</div>
  170. <div class="post-card-preview">{html_module.escape(body_preview) if body_preview else "暂无正文"}</div>
  171. <div class="post-card-meta">
  172. <span class="post-card-author">👤 {html_module.escape(author)}</span>
  173. <span class="post-card-time">📅 {publish_time}</span>
  174. </div>
  175. <div class="post-card-stats">
  176. <span>👍 {like_count}</span>
  177. <span>💬 {comment_count if comment_count else 0}</span>
  178. <span>⭐ {collect_count if collect_count else 0}</span>
  179. </div>
  180. </div>
  181. </div>
  182. '''
  183. return html
  184. def generate_inspiration_detail_html(point: Dict, point_type: str = "灵感点") -> str:
  185. """生成点详情HTML(简化版,适配新结构)
  186. Args:
  187. point: 点数据(包含匹配人设结果)
  188. point_type: 点类型(灵感点/关键点/目的点)
  189. """
  190. name = point.get("名称", "")
  191. desc = point.get("描述", "")
  192. match_results = point.get("匹配人设结果", [])
  193. # 根据最高相似度确定结论
  194. max_similarity = 0.0
  195. if match_results:
  196. max_similarity = max(m.get("相似度", 0) for m in match_results)
  197. status_label, status_color, status_suffix = get_similarity_status(max_similarity)
  198. insp_conclusion = status_label
  199. insp_conclusion_class = f"insp-conclusion-{status_suffix}"
  200. # 根据点类型设置图标
  201. point_icons = {
  202. "灵感点": "💡",
  203. "关键点": "🔑",
  204. "目的点": "🎯"
  205. }
  206. point_icon = point_icons.get(point_type, "💡")
  207. html = f'''
  208. <div class="inspiration-detail-card">
  209. <div class="inspiration-header">
  210. <span class="inspiration-type-badge">{point_icon} {point_type}</span>
  211. <h3 class="inspiration-name">{html_module.escape(name)}</h3>
  212. <span class="inspiration-conclusion {insp_conclusion_class}">{insp_conclusion}</span>
  213. </div>
  214. <div class="inspiration-description">
  215. <div class="desc-label">描述:</div>
  216. <div class="desc-text">{html_module.escape(desc)}</div>
  217. </div>
  218. <div class="inspiration-stats">
  219. <div class="stats-label">最高相似度: <span class="similarity-value">{max_similarity:.2f}</span></div>
  220. <div class="stats-label">匹配人设数: <span class="match-count">{len(match_results)}</span></div>
  221. </div>
  222. </div>
  223. '''
  224. return html
  225. def load_feature_category_mapping(config: PathConfig) -> Dict:
  226. """加载特征名称到分类的映射"""
  227. mapping_file = config.feature_category_mapping_file
  228. try:
  229. with open(mapping_file, "r", encoding="utf-8") as f:
  230. return json.load(f)
  231. except Exception as e:
  232. print(f"警告: 无法加载特征分类映射文件: {e}")
  233. return {}
  234. def load_feature_source_mapping(config: PathConfig) -> Dict:
  235. """加载特征名称到帖子来源的映射"""
  236. mapping_file = config.feature_source_mapping_file
  237. try:
  238. with open(mapping_file, "r", encoding="utf-8") as f:
  239. data = json.load(f)
  240. # 转换为便于查询的格式: {特征名称: [来源列表]}
  241. result = {}
  242. for feature_type in ["灵感点", "关键点", "目的点"]:
  243. if feature_type in data:
  244. for item in data[feature_type]:
  245. feature_name = item.get("特征名称")
  246. if feature_name:
  247. result[feature_name] = item.get("特征来源", [])
  248. return result
  249. except Exception as e:
  250. print(f"警告: 无法加载特征来源映射文件: {e}")
  251. return {}
  252. def generate_single_match_html(match: Dict, match_idx: int, post_idx: int, point_idx: int, category_mapping: Dict = None, source_mapping: Dict = None, point_type: str = "灵感点") -> str:
  253. """生成单个匹配项的HTML
  254. Args:
  255. match: 单个匹配数据
  256. match_idx: 匹配项索引
  257. post_idx: 帖子索引
  258. point_idx: 点索引
  259. category_mapping: 特征分类映射
  260. source_mapping: 特征来源映射
  261. point_type: 当前点的类型(灵感点/关键点/目的点)
  262. """
  263. persona_name = match.get("人设特征名称", "")
  264. feature_type = match.get("特征类型", "")
  265. feature_categories = match.get("特征分类", [])
  266. persona_level = match.get("人设特征层级", "")
  267. similarity = match.get("相似度", 0.0)
  268. explanation = match.get("说明", "")
  269. # 根据相似度确定颜色和标签
  270. label, color, _ = get_similarity_status(similarity)
  271. match_id = f"post-{post_idx}-{point_type}-{point_idx}-match-{match_idx}"
  272. # 判断是否同层级匹配
  273. is_same_level = (persona_level == point_type)
  274. same_level_class = "match-same-level" if is_same_level else ""
  275. # 生成合并的层级-类型标签
  276. combined_badge_html = ""
  277. if persona_level and feature_type:
  278. combined_text = f"{persona_level}-{feature_type}"
  279. # 同层级用特殊样式
  280. if is_same_level:
  281. combined_badge_html = f'<span class="feature-combined-badge combined-badge-same">[{html_module.escape(combined_text)}]</span>'
  282. else:
  283. combined_badge_html = f'<span class="feature-combined-badge">[{html_module.escape(combined_text)}]</span>'
  284. categories_badge_html = ""
  285. if feature_categories:
  286. categories_text = " / ".join(feature_categories)
  287. categories_badge_html = f'<span class="feature-category-badge">{html_module.escape(categories_text)}</span>'
  288. # 获取该人设特征的分类信息
  289. categories_html = ""
  290. if category_mapping and persona_name:
  291. found_categories = None
  292. # 依次在灵感点、关键点、目的点中查找
  293. for persona_type in ["灵感点", "关键点", "目的点"]:
  294. if persona_type in category_mapping:
  295. type_mapping = category_mapping[persona_type]
  296. if persona_name in type_mapping:
  297. found_categories = type_mapping[persona_name].get("所属分类", [])
  298. break
  299. if found_categories:
  300. # 简洁样式:[大类/中类/小类]
  301. categories_reversed = list(reversed(found_categories))
  302. categories_text = "/".join(categories_reversed)
  303. categories_html = f'<span class="category-simple">[{html_module.escape(categories_text)}]</span>'
  304. # 获取该人设特征的历史帖子来源
  305. historical_posts_html = ""
  306. if source_mapping and persona_name and persona_name in source_mapping:
  307. source_list = source_mapping[persona_name]
  308. if source_list:
  309. historical_cards = []
  310. for source_item in source_list:
  311. post_detail = source_item.get("帖子详情", {})
  312. if post_detail:
  313. card_html = generate_historical_post_card_html(post_detail, source_item)
  314. historical_cards.append(card_html)
  315. if historical_cards:
  316. historical_posts_html = f'''
  317. <div class="historical-posts-section">
  318. <h4 class="historical-posts-title">历史帖子来源</h4>
  319. <div class="historical-posts-grid">
  320. {"".join(historical_cards)}
  321. </div>
  322. </div>
  323. '''
  324. # 生成历史帖子HTML
  325. historical_posts_html = ""
  326. if source_mapping and persona_name and persona_name in source_mapping:
  327. source_list = source_mapping[persona_name]
  328. if source_list:
  329. for source_item in source_list[:5]: # 最多5个
  330. post_detail = source_item.get("帖子详情", {})
  331. if post_detail:
  332. card_html = generate_historical_post_card_html(post_detail, source_item)
  333. historical_posts_html += card_html
  334. # 将数据编码到data属性中
  335. import html as html_encode
  336. data_explanation = html_encode.escape(explanation)
  337. data_historical = html_encode.escape(historical_posts_html)
  338. # 生成紧凑的匹配项HTML(可点击,弹出模态框)
  339. html = f'''
  340. <div class="match-item-compact {same_level_class}"
  341. data-persona-name="{html_module.escape(persona_name)}"
  342. data-feature-type="{html_module.escape(feature_type)}"
  343. data-persona-level="{html_module.escape(persona_level)}"
  344. data-similarity="{similarity}"
  345. data-label="{label}"
  346. data-explanation="{data_explanation}"
  347. data-historical-posts="{data_historical}"
  348. onclick="showMatchDetail(this)">
  349. {combined_badge_html}
  350. <span class="persona-name">{html_module.escape(persona_name)}</span>
  351. <span class="score-badge">相似度: {similarity:.2f}</span>
  352. <span class="relation-badge" style="background: {color};">{label}</span>
  353. </div>
  354. '''
  355. return html
  356. def generate_match_results_html(point: Dict, point_idx: int, post_idx: int, category_mapping: Dict = None, source_mapping: Dict = None, point_type: str = "灵感点") -> str:
  357. """生成可折叠的匹配结果HTML(适配新结构)
  358. Args:
  359. point: 点数据(包含匹配人设结果)
  360. point_idx: 点索引
  361. post_idx: 帖子索引
  362. category_mapping: 特征分类映射
  363. source_mapping: 特征来源映射
  364. point_type: 点类型(灵感点/关键点/目的点)
  365. """
  366. point_name = point.get("名称", "")
  367. match_results = point.get("匹配人设结果", [])
  368. if not match_results:
  369. return ""
  370. if category_mapping is None:
  371. category_mapping = {}
  372. # 用于显示的序号(点索引+1)
  373. feature_number = point_idx + 1
  374. # 匹配结果已经按相似度降序排列(在match脚本中处理过)
  375. sorted_matches = match_results
  376. # 找出最高相似度,确定状态
  377. max_similarity = 0.0
  378. if match_results:
  379. max_similarity = max(match.get("相似度", 0) for match in match_results)
  380. # 根据最高相似度确定状态
  381. status, _, status_suffix = get_similarity_status(max_similarity)
  382. status_class = f"status-{status_suffix}"
  383. found_status_html = f'<span class="feature-match-status {status_class}">{status}</span>'
  384. # 统计相似度分布
  385. same_label = f"相同 (≥{SIMILARITY_THRESHOLD_SAME})"
  386. similar_label = f"相似 ({SIMILARITY_THRESHOLD_SIMILAR}-{SIMILARITY_THRESHOLD_SAME})"
  387. unrelated_label = f"无关 (<{SIMILARITY_THRESHOLD_SIMILAR})"
  388. similarity_ranges = {same_label: 0, similar_label: 0, unrelated_label: 0}
  389. for match in match_results:
  390. similarity = match.get("相似度", 0)
  391. status_label, _, _ = get_similarity_status(similarity)
  392. if status_label == "相同":
  393. similarity_ranges[same_label] += 1
  394. elif status_label == "相似":
  395. similarity_ranges[similar_label] += 1
  396. else:
  397. similarity_ranges[unrelated_label] += 1
  398. # 生成统计信息
  399. stats_items = []
  400. range_colors = {
  401. same_label: SIMILARITY_COLOR_SAME,
  402. similar_label: SIMILARITY_COLOR_SIMILAR,
  403. unrelated_label: SIMILARITY_COLOR_UNRELATED
  404. }
  405. for range_name, count in similarity_ranges.items():
  406. if count > 0:
  407. color = range_colors[range_name]
  408. stats_items.append(f'<span class="stat-badge" style="background: {color};">{range_name}: {count}</span>')
  409. stats_html = "".join(stats_items)
  410. # 按人设特征层级分组匹配项
  411. level_groups = {
  412. "灵感点": {"标签": [], "分类": []},
  413. "关键点": {"标签": [], "分类": []},
  414. "目的点": {"标签": [], "分类": []}
  415. }
  416. for i, match in enumerate(sorted_matches):
  417. persona_level = match.get("人设特征层级", "")
  418. feature_type = match.get("特征类型", "")
  419. if persona_level in level_groups and feature_type in ["标签", "分类"]:
  420. level_groups[persona_level][feature_type].append((i, match))
  421. # 生成分组的匹配项HTML
  422. matches_html = ""
  423. # 按层级顺序显示:灵感点 -> 关键点 -> 目的点
  424. level_index = 1
  425. for level_name in ["灵感点", "关键点", "目的点"]:
  426. level_data = level_groups[level_name]
  427. total_count = len(level_data["标签"]) + len(level_data["分类"])
  428. if total_count == 0:
  429. continue
  430. # 生成该层级的折叠区域
  431. level_section_id = f"post-{post_idx}-{point_type}-{point_idx}-level-{level_name}"
  432. # 找出该层级的最高分匹配
  433. all_level_matches = level_data["标签"] + level_data["分类"]
  434. top_match = None
  435. max_similarity = 0
  436. for _, match in all_level_matches:
  437. similarity = match.get("相似度", 0)
  438. if similarity > max_similarity:
  439. max_similarity = similarity
  440. top_match = match
  441. # 生成最高分特征信息
  442. top_match_html = ""
  443. if top_match:
  444. top_persona_name = top_match.get("人设特征名称", "")
  445. top_feature_type = top_match.get("特征类型", "")
  446. similarity_label, similarity_color, _ = get_similarity_status(max_similarity)
  447. top_match_html = f'''
  448. <div class="level-top-match">
  449. <span class="top-match-label">最高:</span>
  450. <span class="top-match-type">[{html_module.escape(top_feature_type)}]</span>
  451. <span class="top-match-name">{html_module.escape(top_persona_name)}</span>
  452. <span class="top-match-score" style="background: {similarity_color}; color: white;">{max_similarity:.2f} {similarity_label}</span>
  453. </div>
  454. '''
  455. # 计算该层级的相似度分布
  456. level_stats = {"相同": 0, "相似": 0, "无关": 0}
  457. for _, match in all_level_matches:
  458. similarity = match.get("相似度", 0)
  459. stat_label, _, _ = get_similarity_status(similarity)
  460. level_stats[stat_label] += 1
  461. # 生成统计标签
  462. level_stats_html = ""
  463. if level_stats["相同"] > 0:
  464. level_stats_html += f'<span class="stat-badge" style="background: #10b981;">相同: {level_stats["相同"]}</span>'
  465. if level_stats["相似"] > 0:
  466. level_stats_html += f'<span class="stat-badge" style="background: #f59e0b;">相似: {level_stats["相似"]}</span>'
  467. if level_stats["无关"] > 0:
  468. level_stats_html += f'<span class="stat-badge" style="background: #9ca3af;">无关: {level_stats["无关"]}</span>'
  469. matches_html += f'''
  470. <div class="level-group-section">
  471. <div class="level-group-header" onclick="toggleMatchGroup(event, '{level_section_id}')">
  472. <div class="level-header-left">
  473. <span class="expand-icon" id="{level_section_id}-icon">▶</span>
  474. <h4 class="level-group-title">{feature_number}.{level_index} 匹配人设{level_name} ({total_count})</h4>
  475. <div class="level-stats">{level_stats_html}</div>
  476. </div>
  477. {top_match_html}
  478. </div>
  479. <div class="level-group-content" id="{level_section_id}-content" style="display: none;">
  480. '''
  481. # 该层级下的标签分组
  482. subgroup_index = 1
  483. if level_data["标签"]:
  484. group_id = f"post-{post_idx}-{point_type}-{point_idx}-level-{level_name}-label"
  485. group_matches_html = ""
  486. # 找出标签中的最高分
  487. tag_top_match = None
  488. tag_max_similarity = 0
  489. for i, match in level_data["标签"]:
  490. similarity = match.get("相似度", 0)
  491. if similarity > tag_max_similarity:
  492. tag_max_similarity = similarity
  493. tag_top_match = match
  494. match_html = generate_single_match_html(
  495. match, i, post_idx, point_idx,
  496. category_mapping, source_mapping, point_type
  497. )
  498. group_matches_html += match_html
  499. # 生成标签最高分信息
  500. tag_top_html = ""
  501. if tag_top_match:
  502. tag_persona_name = tag_top_match.get("人设特征名称", "")
  503. tag_persona_level = tag_top_match.get("人设特征层级", "")
  504. tag_feature_type = tag_top_match.get("特征类型", "")
  505. tag_label, tag_color, _ = get_similarity_status(tag_max_similarity)
  506. # 生成层级-类型标签
  507. tag_combined = f"[{tag_persona_level}-{tag_feature_type}]" if tag_persona_level and tag_feature_type else ""
  508. tag_top_html = f'''
  509. <div class="subgroup-top-match">
  510. <span class="subgroup-top-label">最高:</span>
  511. <span class="subgroup-top-combined">{tag_combined}</span>
  512. <span class="subgroup-top-name">{html_module.escape(tag_persona_name)}</span>
  513. <span class="subgroup-top-score" style="background: {tag_color}; color: white;">{tag_max_similarity:.2f} {tag_label}</span>
  514. </div>
  515. '''
  516. matches_html += f'''
  517. <div class="match-subgroup-section">
  518. <div class="match-subgroup-header" onclick="toggleMatchGroup(event, '{group_id}')">
  519. <div class="subgroup-header-left">
  520. <span class="expand-icon" id="{group_id}-icon">▼</span>
  521. <h5 class="match-subgroup-title">{feature_number}.{level_index}.{subgroup_index} 标签 ({len(level_data["标签"])})</h5>
  522. </div>
  523. {tag_top_html}
  524. </div>
  525. <div class="match-subgroup-content" id="{group_id}-content">
  526. {group_matches_html}
  527. </div>
  528. </div>
  529. '''
  530. subgroup_index += 1
  531. # 该层级下的分类分组
  532. if level_data["分类"]:
  533. group_id = f"post-{post_idx}-{point_type}-{point_idx}-level-{level_name}-category"
  534. group_matches_html = ""
  535. # 找出分类中的最高分
  536. cat_top_match = None
  537. cat_max_similarity = 0
  538. for i, match in level_data["分类"]:
  539. similarity = match.get("相似度", 0)
  540. if similarity > cat_max_similarity:
  541. cat_max_similarity = similarity
  542. cat_top_match = match
  543. match_html = generate_single_match_html(
  544. match, i, post_idx, point_idx,
  545. category_mapping, source_mapping, point_type
  546. )
  547. group_matches_html += match_html
  548. # 生成分类最高分信息
  549. cat_top_html = ""
  550. if cat_top_match:
  551. cat_persona_name = cat_top_match.get("人设特征名称", "")
  552. cat_persona_level = cat_top_match.get("人设特征层级", "")
  553. cat_feature_type = cat_top_match.get("特征类型", "")
  554. cat_label, cat_color, _ = get_similarity_status(cat_max_similarity)
  555. # 生成层级-类型标签
  556. cat_combined = f"[{cat_persona_level}-{cat_feature_type}]" if cat_persona_level and cat_feature_type else ""
  557. cat_top_html = f'''
  558. <div class="subgroup-top-match">
  559. <span class="subgroup-top-label">最高:</span>
  560. <span class="subgroup-top-combined">{cat_combined}</span>
  561. <span class="subgroup-top-name">{html_module.escape(cat_persona_name)}</span>
  562. <span class="subgroup-top-score" style="background: {cat_color}; color: white;">{cat_max_similarity:.2f} {cat_label}</span>
  563. </div>
  564. '''
  565. matches_html += f'''
  566. <div class="match-subgroup-section">
  567. <div class="match-subgroup-header" onclick="toggleMatchGroup(event, '{group_id}')">
  568. <div class="subgroup-header-left">
  569. <span class="expand-icon" id="{group_id}-icon">▼</span>
  570. <h5 class="match-subgroup-title">{feature_number}.{level_index}.{subgroup_index} 分类 ({len(level_data["分类"])})</h5>
  571. </div>
  572. {cat_top_html}
  573. </div>
  574. <div class="match-subgroup-content" id="{group_id}-content">
  575. {group_matches_html}
  576. </div>
  577. </div>
  578. '''
  579. subgroup_index += 1
  580. matches_html += '''
  581. </div>
  582. </div>
  583. '''
  584. level_index += 1
  585. section_id = f"post-{post_idx}-{point_type}-{point_idx}-section"
  586. # 找出所有匹配中的最高分
  587. overall_top_match = None
  588. overall_max_similarity = 0
  589. for match in match_results:
  590. similarity = match.get("相似度", 0)
  591. if similarity > overall_max_similarity:
  592. overall_max_similarity = similarity
  593. overall_top_match = match
  594. # 生成最高分信息
  595. overall_top_html = ""
  596. if overall_top_match:
  597. top_persona_name = overall_top_match.get("人设特征名称", "")
  598. top_feature_type = overall_top_match.get("特征类型", "")
  599. top_persona_level = overall_top_match.get("人设特征层级", "")
  600. top_label, top_color, _ = get_similarity_status(overall_max_similarity)
  601. overall_top_html = f'''
  602. <div class="overall-top-match">
  603. <span class="overall-top-label">最高:</span>
  604. <span class="overall-top-combined">[{html_module.escape(top_persona_level)}-{html_module.escape(top_feature_type)}]</span>
  605. <span class="overall-top-name">{html_module.escape(top_persona_name)}</span>
  606. <span class="overall-top-score" style="background: {top_color}; color: white;">{overall_max_similarity:.2f} {top_label}</span>
  607. </div>
  608. '''
  609. html = f'''
  610. <div class="match-results-section">
  611. <div class="match-section-header collapsible-header" onclick="toggleFeatureSection(event, '{section_id}')">
  612. <div class="header-left">
  613. <span class="expand-icon" id="{section_id}-icon">▼</span>
  614. <h4>{feature_number}. 匹配结果: {html_module.escape(point_name)}</h4>
  615. {found_status_html}
  616. </div>
  617. <div class="match-stats">{stats_html}</div>
  618. {overall_top_html}
  619. </div>
  620. <div class="matches-list" id="{section_id}-content">
  621. {matches_html}
  622. </div>
  623. </div>
  624. '''
  625. return html
  626. def generate_toc_html(post_data: Dict, post_idx: int, feature_status_map: Dict[str, str] = None, overall_conclusion: str = "") -> str:
  627. """生成目录导航HTML
  628. Args:
  629. post_data: 帖子数据
  630. post_idx: 帖子索引
  631. feature_status_map: 特征名称到状态的映射 {特征名称: "相同"|"相似"|"无关"}
  632. overall_conclusion: 整体结论
  633. """
  634. how_result = post_data.get("解构结果", {})
  635. if feature_status_map is None:
  636. feature_status_map = {}
  637. toc_items = []
  638. # 帖子详情
  639. toc_items.append(f'<div class="toc-item toc-level-1" onclick="scrollToSection(\'post-{post_idx}-detail\')"><span class="toc-badge toc-badge-post">帖子详情</span> 帖子信息</div>')
  640. # 处理不同类型的点
  641. point_types = [
  642. ("灵感点列表", "灵感点", "toc-badge-inspiration", "💡"),
  643. ("目的点列表", "目的点", "toc-badge-purpose", "🎯"),
  644. ("关键点列表", "关键点", "toc-badge-key", "🔑")
  645. ]
  646. for list_key, point_name, badge_class, icon in point_types:
  647. point_list = how_result.get(list_key, [])
  648. if not point_list:
  649. continue
  650. # 点类型分组标题
  651. toc_items.append(f'<div class="toc-item toc-level-1 toc-group-header"><span class="toc-badge {badge_class}">{icon} {point_name}</span></div>')
  652. for point_idx, point in enumerate(point_list):
  653. name = point.get("名称", f"{point_name} {point_idx + 1}")
  654. name_short = name[:18] + "..." if len(name) > 18 else name
  655. # 计算该点的整体状态(根据匹配人设结果的最高相似度)
  656. match_results = point.get("匹配人设结果", [])
  657. max_similarity = 0.0
  658. if match_results:
  659. max_similarity = max(m.get("相似度", 0) for m in match_results)
  660. status_label, _, status_suffix = get_similarity_status(max_similarity)
  661. point_status = status_label
  662. point_status_class = f"toc-point-{status_suffix}"
  663. toc_items.append(f'<div class="toc-item toc-level-2 {point_status_class}" onclick="scrollToSection(\'post-{post_idx}-point-{list_key}-{point_idx}\')"><span class="toc-badge {badge_class}">{point_name}</span> {html_module.escape(name_short)} <span class="toc-point-status">[{point_status}]</span> <span class="toc-similarity">({max_similarity:.2f})</span></div>')
  664. # 整体结论HTML
  665. conclusion_html = ""
  666. if overall_conclusion:
  667. if overall_conclusion == "相同":
  668. conclusion_class = "conclusion-same"
  669. conclusion_icon = "✓"
  670. elif overall_conclusion == "相似":
  671. conclusion_class = "conclusion-similar"
  672. conclusion_icon = "~"
  673. else: # 无关
  674. conclusion_class = "conclusion-unrelated"
  675. conclusion_icon = "✗"
  676. conclusion_html = f'''
  677. <div class="toc-conclusion {conclusion_class}">
  678. <span class="conclusion-icon">{conclusion_icon}</span>
  679. <span class="conclusion-text">{overall_conclusion}</span>
  680. </div>
  681. '''
  682. return f'''
  683. <div class="toc-container">
  684. <div class="toc-header">目录导航</div>
  685. {conclusion_html}
  686. <div class="toc-content">
  687. {"".join(toc_items)}
  688. </div>
  689. </div>
  690. '''
  691. def generate_post_content_html(post_data: Dict, post_idx: int, category_mapping: Dict = None, source_mapping: Dict = None) -> str:
  692. """生成单个帖子的完整内容HTML(适配新结构:点直接包含匹配人设结果)"""
  693. how_result = post_data.get("解构结果", {})
  694. # 1. 帖子详情
  695. post_detail_html = generate_post_detail_html(post_data, post_idx)
  696. # 2. 生成所有点的详情HTML(灵感点、关键点、目的点)
  697. all_points_detail_html = ""
  698. point_types = [
  699. ("灵感点列表", "灵感点"),
  700. ("关键点列表", "关键点"),
  701. ("目的点列表", "目的点")
  702. ]
  703. for list_key, point_type in point_types:
  704. point_list = how_result.get(list_key, [])
  705. for point_idx, point in enumerate(point_list):
  706. point_detail = generate_inspiration_detail_html(point, point_type)
  707. all_points_detail_html += f'''
  708. <div id="post-{post_idx}-{point_type}-{point_idx}" class="inspiration-detail-item content-section">
  709. {point_detail}
  710. </div>
  711. '''
  712. # 3. 生成所有匹配结果HTML
  713. all_matches_html = ""
  714. for list_key, point_type in point_types:
  715. point_list = how_result.get(list_key, [])
  716. for point_idx, point in enumerate(point_list):
  717. match_html = generate_match_results_html(
  718. point, point_idx, post_idx,
  719. category_mapping, source_mapping, point_type
  720. )
  721. if match_html:
  722. all_matches_html += f'''
  723. <div id="post-{post_idx}-match-{point_type}-{point_idx}" class="point-match-wrapper">
  724. {match_html}
  725. </div>
  726. '''
  727. html = f'''
  728. <div class="post-content-wrapper">
  729. <!-- 第一个框:左右分栏(帖子详情 + 点详情) -->
  730. <div class="top-section-box">
  731. <div class="two-column-layout">
  732. <div class="left-column">
  733. <div id="post-{post_idx}-detail" class="post-detail-wrapper">
  734. {post_detail_html}
  735. </div>
  736. </div>
  737. <div class="right-column">
  738. <div class="inspirations-detail-wrapper">
  739. {all_points_detail_html}
  740. </div>
  741. </div>
  742. </div>
  743. </div>
  744. <!-- 下面:所有匹配结果 -->
  745. <div class="matches-section">
  746. {all_matches_html}
  747. </div>
  748. </div>
  749. '''
  750. return html
  751. def generate_combined_html(posts_data: List[Dict], category_mapping: Dict = None, source_mapping: Dict = None) -> str:
  752. """生成包含所有帖子的单一HTML(左边目录,右边内容)"""
  753. # 生成统一的目录(包含所有帖子)
  754. all_toc_items = []
  755. for post_idx, post in enumerate(posts_data):
  756. post_detail = post.get("帖子详情", {})
  757. title = post_detail.get("title", "无标题")
  758. post_id = post_detail.get("post_id", f"post_{post_idx}")
  759. # 获取发布时间并格式化
  760. publish_timestamp = post_detail.get("publish_timestamp", 0)
  761. if publish_timestamp:
  762. from datetime import datetime
  763. # publish_timestamp 是毫秒级时间戳,需要除以1000
  764. date_str = datetime.fromtimestamp(publish_timestamp / 1000).strftime("%Y-%m-%d")
  765. else:
  766. date_str = "未知日期"
  767. # 帖子标题作为一级目录(可折叠),在标题前显示日期
  768. all_toc_items.append(f'''
  769. <div class="toc-item toc-level-0 toc-post-header collapsed" data-post-id="{post_idx}" onclick="toggleTocPost(event, {post_idx})">
  770. <span class="toc-expand-icon">▼</span>
  771. <div class="toc-item-content">
  772. <span style="color: #666; font-size: 0.9em;">{date_str}</span> {html_module.escape(title[:30])}...
  773. </div>
  774. </div>
  775. <div class="toc-children hidden" id="toc-post-{post_idx}-children">
  776. ''')
  777. how_result = post.get("解构结果", {})
  778. # 生成点类型目录
  779. point_types = [
  780. ("灵感点列表", "灵感点", "toc-badge-inspiration", "💡"),
  781. ("目的点列表", "目的点", "toc-badge-purpose", "🎯"),
  782. ("关键点列表", "关键点", "toc-badge-key", "🔑")
  783. ]
  784. for list_key, point_name, badge_class, icon in point_types:
  785. point_list = how_result.get(list_key, [])
  786. if not point_list:
  787. continue
  788. # 点类型分组标题(可折叠)
  789. group_id = f"post-{post_idx}-{list_key}"
  790. all_toc_items.append(f'''
  791. <div class="toc-item toc-level-1 toc-group-header collapsed" onclick="toggleTocGroup(event, '{group_id}')">
  792. <span class="toc-expand-icon">▼</span>
  793. <div class="toc-item-content">
  794. {icon} {point_name}
  795. </div>
  796. </div>
  797. <div class="toc-children hidden" id="toc-{group_id}-children">
  798. ''')
  799. for point_idx, point in enumerate(point_list):
  800. name = point.get("名称", f"{point_name} {point_idx + 1}")
  801. name_short = name[:18] + "..." if len(name) > 18 else name
  802. # 计算该点的状态(基于匹配人设结果的最高相似度)
  803. match_results = point.get("匹配人设结果", [])
  804. max_similarity = 0.0
  805. if match_results:
  806. max_similarity = max(m.get("相似度", 0) for m in match_results)
  807. status_label, _, status_suffix = get_similarity_status(max_similarity)
  808. point_status = status_label
  809. point_status_class = f"toc-point-{status_suffix}"
  810. # 点项(点击切换到该点的视图)
  811. point_item_id = f"post-{post_idx}-{list_key}-point-{point_idx}"
  812. point_view_id = f"view-post-{post_idx}-point-{list_key}-{point_idx}"
  813. all_toc_items.append(f'''
  814. <div class="toc-item toc-level-2 {point_status_class}" onclick="showPointView(event, '{point_view_id}')">
  815. <div class="toc-item-content">
  816. [{point_name}] {html_module.escape(name_short)}
  817. <span class="toc-point-status">[{point_status}]</span>
  818. <span class="toc-similarity-score">{max_similarity:.2f}</span>
  819. </div>
  820. </div>
  821. ''')
  822. # 关闭点类型分组的children
  823. all_toc_items.append('</div>')
  824. # 关闭帖子的children
  825. all_toc_items.append('</div>')
  826. # 生成所有独立的内容视图
  827. all_content_views = []
  828. for post_idx, post in enumerate(posts_data):
  829. post_detail = post.get("帖子详情", {})
  830. how_result = post.get("解构结果", {})
  831. # 1. 生成"仅帖子详情"的视图
  832. post_detail_html = generate_post_detail_html(post, post_idx)
  833. post_only_view = f'''
  834. <div class="content-view" id="view-post-{post_idx}-detail" style="display: none;">
  835. <div class="post-content-wrapper">
  836. <div class="top-section-box">
  837. <div id="post-{post_idx}-detail" class="post-detail-wrapper">
  838. {post_detail_html}
  839. </div>
  840. </div>
  841. </div>
  842. </div>
  843. '''
  844. all_content_views.append(post_only_view)
  845. # 2. 为每个点生成独立视图(包含帖子详情+点详情+匹配结果)
  846. for point_list_key in ["灵感点列表", "目的点列表", "关键点列表"]:
  847. point_list = how_result.get(point_list_key, [])
  848. # 提取点类型(去掉"列表")
  849. point_type = point_list_key.replace("列表", "")
  850. for point_idx, point in enumerate(point_list):
  851. # 生成点的详情HTML
  852. point_detail_html = generate_inspiration_detail_html(point, point_type)
  853. # 生成该点的匹配结果
  854. matches_html = generate_match_results_html(
  855. point, point_idx, post_idx,
  856. category_mapping, source_mapping, point_type
  857. )
  858. # 组合成完整视图
  859. point_view_id = f"view-post-{post_idx}-point-{point_list_key}-{point_idx}"
  860. point_view = f'''
  861. <div class="content-view" id="{point_view_id}" style="display: none;">
  862. <div class="post-content-wrapper">
  863. <div class="top-section-box">
  864. <div class="two-column-layout">
  865. <div class="left-column">
  866. <div class="post-detail-wrapper">
  867. {post_detail_html}
  868. </div>
  869. </div>
  870. <div class="right-column">
  871. <div class="inspirations-detail-wrapper">
  872. <div id="post-{post_idx}-point-{point_list_key}-{point_idx}" class="inspiration-detail-item content-section">
  873. {point_detail_html}
  874. </div>
  875. </div>
  876. </div>
  877. </div>
  878. </div>
  879. <div class="matches-section">
  880. {matches_html}
  881. </div>
  882. </div>
  883. </div>
  884. '''
  885. all_content_views.append(point_view)
  886. # 组合所有内容视图
  887. all_contents_html = "\n".join(all_content_views)
  888. # 组合目录HTML
  889. toc_items_html = "\n".join(all_toc_items)
  890. html = f'''
  891. <!DOCTYPE html>
  892. <html lang="zh-CN">
  893. <head>
  894. <meta charset="UTF-8">
  895. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  896. <title>How解构结果可视化</title>
  897. <style>
  898. * {{
  899. margin: 0;
  900. padding: 0;
  901. box-sizing: border-box;
  902. }}
  903. body {{
  904. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  905. background: #f5f5f5;
  906. color: #333;
  907. line-height: 1.6;
  908. }}
  909. /* 两栏布局容器 */
  910. .page-container {{
  911. display: flex;
  912. height: 100vh;
  913. overflow: hidden;
  914. }}
  915. /* 左侧目录 */
  916. .left-sidebar {{
  917. width: 420px;
  918. background: white;
  919. border-right: 2px solid #e5e7eb;
  920. display: flex;
  921. flex-direction: column;
  922. overflow: hidden;
  923. }}
  924. /* 右侧内容区 */
  925. .right-content {{
  926. flex: 1;
  927. overflow-y: auto;
  928. background: #f5f5f5;
  929. padding: 0;
  930. position: relative;
  931. }}
  932. /* 吸顶面包屑导航 */
  933. .breadcrumb-nav {{
  934. position: sticky;
  935. top: 0;
  936. left: 0;
  937. right: 0;
  938. background: white;
  939. border-bottom: 2px solid #e5e7eb;
  940. padding: 12px 30px;
  941. display: flex;
  942. align-items: center;
  943. gap: 8px;
  944. font-size: 14px;
  945. z-index: 100;
  946. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  947. }}
  948. .breadcrumb-item {{
  949. color: #667eea;
  950. cursor: pointer;
  951. transition: all 0.2s;
  952. font-weight: 500;
  953. }}
  954. .breadcrumb-item:hover {{
  955. color: #5568d3;
  956. text-decoration: underline;
  957. }}
  958. .breadcrumb-separator {{
  959. color: #9ca3af;
  960. user-select: none;
  961. }}
  962. .content-view {{
  963. padding: 30px;
  964. width: 100%;
  965. height: 100%;
  966. }}
  967. .content-view.active {{
  968. display: block !important;
  969. }}
  970. .post-content-wrapper {{
  971. max-width: 1600px;
  972. margin: 0 auto;
  973. display: flex;
  974. flex-direction: column;
  975. gap: 30px;
  976. }}
  977. .container {{
  978. max-width: 1600px;
  979. margin: 0 auto;
  980. background: white;
  981. min-height: 100vh;
  982. }}
  983. .header {{
  984. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  985. color: white;
  986. padding: 20px 30px;
  987. text-align: center;
  988. }}
  989. .header h1 {{
  990. font-size: 28px;
  991. font-weight: bold;
  992. margin-bottom: 5px;
  993. }}
  994. .header p {{
  995. font-size: 14px;
  996. opacity: 0.9;
  997. }}
  998. .toc-header {{
  999. padding: 15px 20px;
  1000. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1001. color: white;
  1002. font-weight: 600;
  1003. font-size: 16px;
  1004. border-bottom: 2px solid #5a67d8;
  1005. }}
  1006. .toc-content {{
  1007. padding: 10px;
  1008. overflow-y: auto;
  1009. flex: 1;
  1010. }}
  1011. .toc-level-0 {{
  1012. color: #111827;
  1013. font-weight: 700;
  1014. font-size: 15px;
  1015. background: #f9fafb;
  1016. margin-top: 8px;
  1017. margin-bottom: 4px;
  1018. }}
  1019. .toc-post-header {{
  1020. font-weight: 700;
  1021. }}
  1022. .toc-badge-info {{
  1023. background: #dbeafe;
  1024. color: #1e40af;
  1025. }}
  1026. .toc-item {{
  1027. padding: 10px 15px;
  1028. margin: 4px 0;
  1029. cursor: pointer;
  1030. border-radius: 6px;
  1031. font-size: 14px;
  1032. transition: all 0.2s;
  1033. user-select: none;
  1034. display: flex;
  1035. align-items: center;
  1036. gap: 8px;
  1037. }}
  1038. .toc-item:hover {{
  1039. background: #f9fafb;
  1040. }}
  1041. .toc-item.active {{
  1042. color: #667eea;
  1043. font-weight: 600;
  1044. }}
  1045. .toc-item.active-ancestor {{
  1046. color: #667eea;
  1047. }}
  1048. .toc-expand-icon {{
  1049. font-size: 10px;
  1050. transition: transform 0.2s;
  1051. color: #9ca3af;
  1052. flex-shrink: 0;
  1053. }}
  1054. .toc-item.collapsed .toc-expand-icon {{
  1055. transform: rotate(-90deg);
  1056. }}
  1057. .toc-item-content {{
  1058. flex: 1;
  1059. min-width: 0;
  1060. }}
  1061. /* 子项隐藏 */
  1062. .toc-children {{
  1063. display: block;
  1064. }}
  1065. .toc-children.hidden {{
  1066. display: none;
  1067. }}
  1068. .toc-level-1 {{
  1069. color: #111827;
  1070. font-weight: 500;
  1071. padding-left: 20px;
  1072. }}
  1073. .toc-level-2 {{
  1074. color: #6b7280;
  1075. font-size: 13px;
  1076. padding-left: 40px;
  1077. }}
  1078. .toc-level-3 {{
  1079. padding-left: 60px;
  1080. font-size: 12px;
  1081. color: #9ca3af;
  1082. }}
  1083. .toc-level-4 {{
  1084. padding-left: 80px;
  1085. font-size: 11px;
  1086. color: #9ca3af;
  1087. }}
  1088. .toc-feature-status {{
  1089. font-weight: 500;
  1090. font-size: 10px;
  1091. padding: 2px 6px;
  1092. border-radius: 4px;
  1093. margin-left: auto;
  1094. background: #f3f4f6;
  1095. color: #6b7280;
  1096. }}
  1097. /* 目录整体结论 */
  1098. .toc-conclusion {{
  1099. padding: 15px 20px;
  1100. margin: 10px;
  1101. border-radius: 8px;
  1102. display: flex;
  1103. align-items: center;
  1104. gap: 10px;
  1105. font-weight: 600;
  1106. font-size: 14px;
  1107. }}
  1108. .toc-conclusion.conclusion-found {{
  1109. background: #d1fae5;
  1110. color: #065f46;
  1111. border: 2px solid #10b981;
  1112. }}
  1113. .toc-conclusion.conclusion-partial {{
  1114. background: #fed7aa;
  1115. color: #92400e;
  1116. border: 2px solid #f59e0b;
  1117. }}
  1118. .toc-conclusion.conclusion-not-found {{
  1119. background: #fee2e2;
  1120. color: #991b1b;
  1121. border: 2px solid #ef4444;
  1122. }}
  1123. .conclusion-icon {{
  1124. font-size: 18px;
  1125. font-weight: 700;
  1126. }}
  1127. .conclusion-text {{
  1128. flex: 1;
  1129. }}
  1130. .toc-badge {{
  1131. display: inline-block;
  1132. font-size: 12px;
  1133. margin-right: 6px;
  1134. opacity: 0.8;
  1135. }}
  1136. .toc-group-header {{
  1137. font-weight: 600;
  1138. cursor: pointer !important;
  1139. }}
  1140. .toc-point-status {{
  1141. font-size: 10px;
  1142. font-weight: 500;
  1143. padding: 2px 6px;
  1144. border-radius: 4px;
  1145. margin-left: auto;
  1146. background: #f3f4f6;
  1147. color: #6b7280;
  1148. }}
  1149. .main-content {{
  1150. min-width: 0;
  1151. display: flex;
  1152. flex-direction: column;
  1153. gap: 30px;
  1154. }}
  1155. /* 顶部框:包含帖子详情和灵感点详情 */
  1156. .top-section-box {{
  1157. background: white;
  1158. border: 1px solid #e5e7eb;
  1159. border-radius: 12px;
  1160. padding: 25px;
  1161. box-shadow: 0 2px 8px rgba(0,0,0,0.05);
  1162. }}
  1163. /* 顶部框内的两栏布局 */
  1164. .two-column-layout {{
  1165. display: grid;
  1166. grid-template-columns: 380px 1fr;
  1167. gap: 25px;
  1168. align-items: start;
  1169. }}
  1170. .left-column {{
  1171. position: relative;
  1172. }}
  1173. .post-detail-wrapper {{
  1174. /* 帖子详情区域 */
  1175. }}
  1176. .right-column {{
  1177. min-width: 0;
  1178. }}
  1179. .inspirations-detail-wrapper {{
  1180. display: flex;
  1181. flex-direction: column;
  1182. gap: 20px;
  1183. max-height: 600px;
  1184. overflow-y: auto;
  1185. padding-right: 10px;
  1186. }}
  1187. .inspiration-detail-item {{
  1188. /* 单个灵感点详情 */
  1189. }}
  1190. /* 匹配结果区域 */
  1191. .matches-section {{
  1192. display: flex;
  1193. flex-direction: column;
  1194. gap: 20px;
  1195. }}
  1196. /* 步骤区域 */
  1197. .step-section {{
  1198. background: white;
  1199. border: 2px solid #e5e7eb;
  1200. border-radius: 12px;
  1201. overflow: hidden;
  1202. box-shadow: 0 2px 8px rgba(0,0,0,0.05);
  1203. transition: all 0.3s ease;
  1204. }}
  1205. .step-section:hover {{
  1206. box-shadow: 0 4px 12px rgba(0,0,0,0.08);
  1207. border-color: #d1d5db;
  1208. }}
  1209. .step-header {{
  1210. padding: 18px 24px;
  1211. background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
  1212. border-bottom: 2px solid #e5e7eb;
  1213. display: flex;
  1214. justify-content: space-between;
  1215. align-items: center;
  1216. cursor: pointer;
  1217. user-select: none;
  1218. transition: all 0.3s ease;
  1219. }}
  1220. .step-header:hover {{
  1221. background: linear-gradient(135deg, #f3f4f6 0%, #f9fafb 100%);
  1222. }}
  1223. .step-name {{
  1224. font-size: 18px;
  1225. color: #111827;
  1226. font-weight: 600;
  1227. margin: 0;
  1228. }}
  1229. .step-inspiration-name {{
  1230. font-size: 14px;
  1231. color: #6b7280;
  1232. font-style: italic;
  1233. }}
  1234. .step-features-list {{
  1235. padding: 20px;
  1236. display: flex;
  1237. flex-direction: column;
  1238. gap: 15px;
  1239. }}
  1240. .feature-match-wrapper {{
  1241. /* 特征匹配容器 */
  1242. }}
  1243. .content-section {{
  1244. scroll-margin-top: 80px;
  1245. }}
  1246. /* 帖子详情卡片(紧凑样式) */
  1247. .post-card-compact {{
  1248. display: grid;
  1249. grid-template-columns: 200px 1fr;
  1250. gap: 20px;
  1251. background: white;
  1252. border: 1px solid #e5e7eb;
  1253. border-radius: 12px;
  1254. overflow: hidden;
  1255. padding: 20px;
  1256. cursor: pointer;
  1257. transition: all 0.3s;
  1258. margin-bottom: 30px;
  1259. }}
  1260. .post-card-compact:hover {{
  1261. box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  1262. transform: translateY(-2px);
  1263. border-color: #667eea;
  1264. }}
  1265. .post-card-image {{
  1266. position: relative;
  1267. display: flex;
  1268. align-items: center;
  1269. justify-content: center;
  1270. }}
  1271. .post-card-thumbnail {{
  1272. width: 100%;
  1273. height: 150px;
  1274. object-fit: contain;
  1275. border-radius: 8px;
  1276. background: #f9fafb;
  1277. }}
  1278. .post-card-thumbnail-placeholder {{
  1279. width: 100%;
  1280. height: 150px;
  1281. display: flex;
  1282. align-items: center;
  1283. justify-content: center;
  1284. background: #f3f4f6;
  1285. color: #9ca3af;
  1286. font-size: 48px;
  1287. border-radius: 8px;
  1288. }}
  1289. .post-card-image-count {{
  1290. position: absolute;
  1291. bottom: 8px;
  1292. right: 8px;
  1293. background: rgba(0, 0, 0, 0.7);
  1294. color: white;
  1295. padding: 4px 8px;
  1296. border-radius: 12px;
  1297. font-size: 11px;
  1298. font-weight: 600;
  1299. }}
  1300. .post-card-content {{
  1301. display: flex;
  1302. flex-direction: column;
  1303. gap: 12px;
  1304. }}
  1305. .post-card-title {{
  1306. font-size: 18px;
  1307. font-weight: 600;
  1308. color: #111827;
  1309. line-height: 1.4;
  1310. overflow: hidden;
  1311. text-overflow: ellipsis;
  1312. display: -webkit-box;
  1313. -webkit-line-clamp: 2;
  1314. -webkit-box-orient: vertical;
  1315. }}
  1316. .post-card-preview {{
  1317. font-size: 14px;
  1318. color: #6b7280;
  1319. line-height: 1.6;
  1320. overflow: hidden;
  1321. text-overflow: ellipsis;
  1322. display: -webkit-box;
  1323. -webkit-line-clamp: 2;
  1324. -webkit-box-orient: vertical;
  1325. }}
  1326. .post-card-meta {{
  1327. display: flex;
  1328. gap: 15px;
  1329. font-size: 13px;
  1330. color: #9ca3af;
  1331. }}
  1332. .post-card-stats {{
  1333. display: flex;
  1334. gap: 15px;
  1335. font-size: 13px;
  1336. color: #6b7280;
  1337. font-weight: 500;
  1338. }}
  1339. /* 灵感点详情卡片样式保持不变 */
  1340. .inspiration-detail-card {{
  1341. background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
  1342. padding: 25px;
  1343. border-bottom: 2px solid #e5e7eb;
  1344. }}
  1345. .inspiration-header {{
  1346. margin-bottom: 15px;
  1347. display: flex;
  1348. align-items: center;
  1349. gap: 10px;
  1350. flex-wrap: wrap;
  1351. }}
  1352. .inspiration-conclusion {{
  1353. padding: 6px 14px;
  1354. border-radius: 16px;
  1355. font-size: 13px;
  1356. font-weight: 700;
  1357. margin-left: auto;
  1358. }}
  1359. .inspiration-conclusion.insp-conclusion-found {{
  1360. background: #d1fae5;
  1361. color: #065f46;
  1362. border: 2px solid #10b981;
  1363. }}
  1364. .inspiration-conclusion.insp-conclusion-partial {{
  1365. background: #fed7aa;
  1366. color: #92400e;
  1367. border: 2px solid #f59e0b;
  1368. }}
  1369. .inspiration-conclusion.insp-conclusion-not-found {{
  1370. background: #fee2e2;
  1371. color: #991b1b;
  1372. border: 2px solid #ef4444;
  1373. }}
  1374. .inspiration-type-badge {{
  1375. display: inline-block;
  1376. padding: 4px 12px;
  1377. background: #fef3c7;
  1378. color: #92400e;
  1379. border-radius: 12px;
  1380. font-size: 12px;
  1381. font-weight: 600;
  1382. }}
  1383. .inspiration-name {{
  1384. font-size: 20px;
  1385. color: #111827;
  1386. font-weight: 600;
  1387. margin: 0;
  1388. }}
  1389. .inspiration-description {{
  1390. margin-bottom: 15px;
  1391. }}
  1392. .desc-label {{
  1393. font-weight: 600;
  1394. color: #6b7280;
  1395. font-size: 13px;
  1396. margin-bottom: 8px;
  1397. }}
  1398. .desc-text {{
  1399. color: #4b5563;
  1400. font-size: 14px;
  1401. line-height: 1.7;
  1402. }}
  1403. .inspiration-features {{
  1404. margin-top: 15px;
  1405. }}
  1406. .features-label {{
  1407. font-weight: 600;
  1408. color: #6b7280;
  1409. font-size: 13px;
  1410. margin-bottom: 8px;
  1411. }}
  1412. .features-tags {{
  1413. display: flex;
  1414. flex-wrap: wrap;
  1415. gap: 8px;
  1416. }}
  1417. .feature-tag {{
  1418. padding: 5px 12px;
  1419. background: #667eea;
  1420. color: white;
  1421. border-radius: 16px;
  1422. font-size: 13px;
  1423. font-weight: 500;
  1424. display: inline-flex;
  1425. align-items: center;
  1426. gap: 4px;
  1427. }}
  1428. .feature-tag.feature-same {{
  1429. background: #10b981;
  1430. }}
  1431. .feature-tag.feature-similar {{
  1432. background: #f59e0b;
  1433. }}
  1434. .feature-tag.feature-unrelated {{
  1435. background: #9ca3af;
  1436. }}
  1437. .feature-status-label {{
  1438. font-weight: 700;
  1439. font-size: 11px;
  1440. padding: 2px 6px;
  1441. background: rgba(255, 255, 255, 0.3);
  1442. border-radius: 4px;
  1443. }}
  1444. /* 匹配结果部分 */
  1445. .match-results-section {{
  1446. padding: 48px 25px;
  1447. border-bottom: none;
  1448. }}
  1449. .match-results-section:first-child {{
  1450. padding-top: 25px;
  1451. }}
  1452. .match-section-header {{
  1453. display: flex;
  1454. justify-content: space-between;
  1455. align-items: center;
  1456. padding: 10px 0 24px 0;
  1457. background: transparent;
  1458. border: none;
  1459. border-bottom: 1px solid #e5e7eb;
  1460. border-radius: 0;
  1461. gap: 12px;
  1462. cursor: pointer;
  1463. user-select: none;
  1464. transition: all 0.2s ease;
  1465. }}
  1466. .match-section-header:hover {{
  1467. border-bottom-color: #9ca3af;
  1468. }}
  1469. .header-left {{
  1470. display: flex;
  1471. align-items: center;
  1472. gap: 10px;
  1473. flex: 1;
  1474. }}
  1475. .collapsible-header {{
  1476. cursor: pointer;
  1477. user-select: none;
  1478. transition: background 0.2s;
  1479. padding: 10px;
  1480. margin: -10px;
  1481. border-radius: 6px;
  1482. }}
  1483. .collapsible-header:hover {{
  1484. background: #f9fafb;
  1485. }}
  1486. .overall-top-match {{
  1487. display: inline-flex;
  1488. align-items: center;
  1489. gap: 8px;
  1490. padding: 6px 12px;
  1491. background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
  1492. border-radius: 8px;
  1493. font-size: 13px;
  1494. border: 1px solid #bae6fd;
  1495. flex-shrink: 0;
  1496. }}
  1497. .overall-top-label {{
  1498. font-weight: 700;
  1499. color: #0369a1;
  1500. }}
  1501. .overall-top-combined {{
  1502. padding: 3px 8px;
  1503. background: linear-gradient(135deg, #e0e7ff 0%, #fef3c7 100%);
  1504. color: #4338ca;
  1505. border-radius: 6px;
  1506. font-size: 11px;
  1507. font-weight: 600;
  1508. border: 1px solid #c7d2fe;
  1509. }}
  1510. .overall-top-name {{
  1511. font-weight: 700;
  1512. color: #1e40af;
  1513. }}
  1514. .overall-top-score {{
  1515. padding: 4px 12px;
  1516. border-radius: 12px;
  1517. font-size: 13px;
  1518. font-weight: 700;
  1519. margin-left: auto;
  1520. }}
  1521. .match-section-header .header-left {{
  1522. display: flex;
  1523. align-items: center;
  1524. gap: 10px;
  1525. flex: 1;
  1526. }}
  1527. .match-section-header h4 {{
  1528. font-size: 18px;
  1529. color: #111827;
  1530. margin: 0;
  1531. }}
  1532. .match-stats {{
  1533. display: flex;
  1534. gap: 10px;
  1535. flex-wrap: wrap;
  1536. }}
  1537. .stat-badge {{
  1538. padding: 4px 10px;
  1539. border-radius: 12px;
  1540. color: white;
  1541. font-size: 12px;
  1542. font-weight: 600;
  1543. }}
  1544. .matches-list {{
  1545. display: flex;
  1546. flex-direction: column;
  1547. gap: 8px;
  1548. transition: all 0.3s ease;
  1549. overflow: hidden;
  1550. }}
  1551. .match-group-section {{
  1552. margin-bottom: 20px;
  1553. background: white;
  1554. border: 1px solid #e5e7eb;
  1555. border-radius: 8px;
  1556. overflow: hidden;
  1557. }}
  1558. .match-group-header {{
  1559. padding: 12px 18px;
  1560. background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
  1561. border-bottom: 2px solid #d1d5db;
  1562. cursor: pointer;
  1563. user-select: none;
  1564. display: flex;
  1565. align-items: center;
  1566. gap: 10px;
  1567. transition: background 0.2s;
  1568. }}
  1569. .match-group-header:hover {{
  1570. background: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%);
  1571. }}
  1572. .match-group-title {{
  1573. font-size: 15px;
  1574. font-weight: 600;
  1575. color: #374151;
  1576. margin: 0;
  1577. }}
  1578. .match-group-content {{
  1579. padding: 15px;
  1580. display: flex;
  1581. flex-direction: row;
  1582. flex-wrap: wrap;
  1583. gap: 10px;
  1584. }}
  1585. /* 层级分组样式 - 极简无边框 */
  1586. .level-group-section {{
  1587. background: transparent;
  1588. border: none;
  1589. border-radius: 0;
  1590. overflow: visible;
  1591. }}
  1592. .level-group-header {{
  1593. padding: 20px 0 16px 0;
  1594. background: transparent;
  1595. border-bottom: 1px solid #e5e7eb;
  1596. cursor: pointer;
  1597. user-select: none;
  1598. display: flex;
  1599. justify-content: space-between;
  1600. align-items: center;
  1601. transition: all 0.2s ease;
  1602. }}
  1603. .level-group-header:hover {{
  1604. border-bottom-color: #9ca3af;
  1605. }}
  1606. .level-group-header .expand-icon {{
  1607. color: #6b7280;
  1608. font-size: 13px;
  1609. }}
  1610. .level-header-left {{
  1611. display: flex;
  1612. align-items: center;
  1613. gap: 8px;
  1614. }}
  1615. .level-group-title {{
  1616. font-size: 15px;
  1617. font-weight: 600;
  1618. color: #111827;
  1619. margin: 0;
  1620. }}
  1621. .level-stats {{
  1622. display: inline-flex;
  1623. gap: 8px;
  1624. align-items: center;
  1625. }}
  1626. .level-top-match {{
  1627. display: inline-flex;
  1628. align-items: center;
  1629. gap: 6px;
  1630. font-size: 12px;
  1631. color: #6b7280;
  1632. }}
  1633. .top-match-label {{
  1634. font-weight: 500;
  1635. color: #9ca3af;
  1636. }}
  1637. .top-match-type {{
  1638. padding: 2px 6px;
  1639. background: #f3f4f6;
  1640. border-radius: 4px;
  1641. font-size: 11px;
  1642. font-weight: 500;
  1643. color: #6b7280;
  1644. }}
  1645. .top-match-name {{
  1646. font-weight: 500;
  1647. color: #374151;
  1648. }}
  1649. .top-match-score {{
  1650. padding: 3px 10px;
  1651. border-radius: 12px;
  1652. font-size: 12px;
  1653. font-weight: 700;
  1654. margin-left: auto;
  1655. }}
  1656. .level-group-content {{
  1657. padding: 0;
  1658. background: transparent;
  1659. transition: all 0.3s ease;
  1660. overflow: hidden;
  1661. }}
  1662. .level-group-content.collapsed {{
  1663. display: none;
  1664. }}
  1665. /* 子分组样式 - 极简 */
  1666. .match-subgroup-section {{
  1667. display: block;
  1668. width: 100%;
  1669. background: transparent;
  1670. border-radius: 0;
  1671. overflow: visible;
  1672. }}
  1673. .match-subgroup-header {{
  1674. padding: 12px 0 8px 0;
  1675. background: transparent;
  1676. cursor: pointer;
  1677. user-select: none;
  1678. display: flex;
  1679. justify-content: space-between;
  1680. align-items: center;
  1681. transition: all 0.2s ease;
  1682. border-bottom: 1px solid #f3f4f6;
  1683. }}
  1684. .match-subgroup-header:hover {{
  1685. border-bottom-color: #e5e7eb;
  1686. }}
  1687. .subgroup-header-left {{
  1688. display: flex;
  1689. align-items: center;
  1690. gap: 8px;
  1691. }}
  1692. .match-subgroup-title {{
  1693. font-size: 13px;
  1694. font-weight: 600;
  1695. color: #374151;
  1696. margin: 0;
  1697. }}
  1698. .subgroup-top-match {{
  1699. display: inline-flex;
  1700. align-items: center;
  1701. gap: 6px;
  1702. font-size: 11px;
  1703. color: #6b7280;
  1704. }}
  1705. .subgroup-top-label {{
  1706. font-weight: 500;
  1707. color: #9ca3af;
  1708. }}
  1709. .subgroup-top-combined {{
  1710. padding: 2px 6px;
  1711. background: #f3f4f6;
  1712. color: #6b7280;
  1713. border-radius: 4px;
  1714. font-size: 10px;
  1715. font-weight: 500;
  1716. }}
  1717. .subgroup-top-name {{
  1718. font-weight: 500;
  1719. color: #374151;
  1720. }}
  1721. .subgroup-top-score {{
  1722. padding: 2px 8px;
  1723. border-radius: 10px;
  1724. font-size: 11px;
  1725. font-weight: 600;
  1726. }}
  1727. .match-subgroup-content {{
  1728. padding: 0;
  1729. display: flex;
  1730. flex-direction: row;
  1731. flex-wrap: wrap;
  1732. gap: 8px;
  1733. transition: all 0.3s ease;
  1734. overflow: hidden;
  1735. background: transparent;
  1736. }}
  1737. .match-item-compact {{
  1738. display: inline-flex;
  1739. align-items: center;
  1740. gap: 6px;
  1741. padding: 8px 14px;
  1742. background: transparent;
  1743. border: 1.5px solid #e5e7eb;
  1744. border-radius: 8px;
  1745. font-size: 13px;
  1746. transition: all 0.3s ease;
  1747. cursor: pointer;
  1748. }}
  1749. .match-item-compact:hover {{
  1750. border-color: #667eea;
  1751. box-shadow: 0 4px 8px rgba(102, 126, 234, 0.15);
  1752. transform: translateY(-2px);
  1753. background: #fafbff;
  1754. }}
  1755. .match-item-compact:active {{
  1756. transform: translateY(0);
  1757. }}
  1758. /* 匹配项中的层级-类型标签 */
  1759. .match-item-compact .feature-combined-badge {{
  1760. color: #9ca3af;
  1761. }}
  1762. /* 匹配详情模态框 */
  1763. .match-modal {{
  1764. display: none;
  1765. position: fixed;
  1766. z-index: 3000;
  1767. left: 0;
  1768. top: 0;
  1769. width: 100%;
  1770. height: 100%;
  1771. background: rgba(0, 0, 0, 0.5);
  1772. animation: fadeIn 0.2s;
  1773. }}
  1774. .match-modal-content {{
  1775. position: relative;
  1776. background: white;
  1777. margin: 5% auto;
  1778. padding: 0;
  1779. width: 80%;
  1780. max-width: 900px;
  1781. max-height: 80vh;
  1782. border-radius: 12px;
  1783. box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
  1784. overflow: hidden;
  1785. animation: slideDown 0.3s;
  1786. }}
  1787. .match-modal-header {{
  1788. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1789. color: white;
  1790. padding: 20px 25px;
  1791. display: flex;
  1792. align-items: center;
  1793. justify-content: space-between;
  1794. }}
  1795. .match-modal-title {{
  1796. font-size: 18px;
  1797. font-weight: 600;
  1798. margin: 0;
  1799. }}
  1800. .match-modal-close {{
  1801. color: white;
  1802. font-size: 32px;
  1803. font-weight: 300;
  1804. cursor: pointer;
  1805. background: none;
  1806. border: none;
  1807. padding: 0;
  1808. line-height: 1;
  1809. transition: transform 0.2s;
  1810. }}
  1811. .match-modal-close:hover {{
  1812. transform: scale(1.2);
  1813. }}
  1814. .match-modal-body {{
  1815. padding: 25px;
  1816. max-height: calc(80vh - 80px);
  1817. overflow-y: auto;
  1818. }}
  1819. .match-detail-section {{
  1820. margin-bottom: 25px;
  1821. }}
  1822. .match-detail-section h3 {{
  1823. font-size: 16px;
  1824. font-weight: 600;
  1825. color: #374151;
  1826. margin-bottom: 12px;
  1827. padding-bottom: 8px;
  1828. border-bottom: 2px solid #e5e7eb;
  1829. }}
  1830. .match-explanation-text {{
  1831. line-height: 1.8;
  1832. color: #4b5563;
  1833. padding: 15px;
  1834. background: #f9fafb;
  1835. border-radius: 8px;
  1836. border-left: 4px solid #667eea;
  1837. }}
  1838. .match-item-collapsible {{
  1839. border: 1px solid #e5e7eb;
  1840. border-radius: 8px;
  1841. overflow: hidden;
  1842. background: white;
  1843. transition: box-shadow 0.2s;
  1844. }}
  1845. .match-item-collapsible:hover {{
  1846. box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  1847. }}
  1848. .match-header {{
  1849. padding: 12px 16px;
  1850. cursor: pointer;
  1851. user-select: none;
  1852. transition: background 0.2s;
  1853. }}
  1854. .match-header:hover {{
  1855. background: #f9fafb;
  1856. }}
  1857. .match-header-left {{
  1858. display: flex;
  1859. align-items: center;
  1860. gap: 10px;
  1861. }}
  1862. .expand-icon {{
  1863. color: #9ca3af;
  1864. font-size: 14px;
  1865. transition: transform 0.3s ease;
  1866. display: inline-flex;
  1867. align-items: center;
  1868. justify-content: center;
  1869. width: 20px;
  1870. height: 20px;
  1871. flex-shrink: 0;
  1872. }}
  1873. .expand-icon.expanded {{
  1874. transform: rotate(0deg);
  1875. }}
  1876. /* 折叠状态时旋转 */
  1877. .collapsed .expand-icon {{
  1878. transform: rotate(-90deg);
  1879. }}
  1880. .persona-name {{
  1881. font-weight: 600;
  1882. font-size: 14px;
  1883. color: #111827;
  1884. }}
  1885. .match-item-compact .persona-name {{
  1886. font-weight: 500;
  1887. font-size: 13px;
  1888. color: #374151;
  1889. }}
  1890. .relation-badge {{
  1891. padding: 3px 10px;
  1892. border-radius: 12px;
  1893. color: white;
  1894. font-size: 11px;
  1895. font-weight: 600;
  1896. }}
  1897. .score-badge {{
  1898. padding: 3px 10px;
  1899. border-radius: 12px;
  1900. background: #e5e7eb;
  1901. color: #374151;
  1902. font-size: 11px;
  1903. font-weight: 600;
  1904. }}
  1905. .feature-combined-badge {{
  1906. padding: 3px 8px;
  1907. border-radius: 10px;
  1908. background: transparent;
  1909. color: #9ca3af;
  1910. font-size: 10px;
  1911. font-weight: 500;
  1912. border: none;
  1913. }}
  1914. .feature-combined-badge.combined-badge-same {{
  1915. background: transparent;
  1916. color: #9ca3af;
  1917. border: none;
  1918. box-shadow: none;
  1919. font-weight: 500;
  1920. }}
  1921. .feature-category-badge {{
  1922. padding: 3px 8px;
  1923. border-radius: 10px;
  1924. background: #dbeafe;
  1925. color: #1e40af;
  1926. font-size: 10px;
  1927. font-weight: 500;
  1928. border: 1px solid #93c5fd;
  1929. }}
  1930. /* 同层级匹配 - 统一边框颜色 */
  1931. .match-item-compact.match-same-level {{
  1932. border: 1.5px solid #e5e7eb;
  1933. background: transparent;
  1934. box-shadow: none;
  1935. }}
  1936. .match-item-compact.match-same-level:hover {{
  1937. border-color: #667eea;
  1938. box-shadow: 0 4px 8px rgba(102, 126, 234, 0.15);
  1939. transform: translateY(-2px);
  1940. background: #fafbff;
  1941. }}
  1942. .level-badge {{
  1943. padding: 4px 10px;
  1944. border-radius: 12px;
  1945. font-size: 11px;
  1946. font-weight: 600;
  1947. margin-right: 6px;
  1948. }}
  1949. .level-badge-match {{
  1950. background: #e0e7ff;
  1951. color: #4338ca;
  1952. border: 1px solid #a5b4fc;
  1953. }}
  1954. .level-badge-category {{
  1955. background: #fef3c7;
  1956. color: #92400e;
  1957. border: 1px solid #fcd34d;
  1958. }}
  1959. .feature-match-status {{
  1960. padding: 5px 12px;
  1961. border-radius: 12px;
  1962. font-size: 12px;
  1963. font-weight: 700;
  1964. margin-left: 12px;
  1965. }}
  1966. .feature-match-status.status-same {{
  1967. background: #d1fae5;
  1968. color: #065f46;
  1969. border: 2px solid #10b981;
  1970. }}
  1971. .feature-match-status.status-similar {{
  1972. background: #fed7aa;
  1973. color: #92400e;
  1974. border: 2px solid #f59e0b;
  1975. }}
  1976. .feature-match-status.status-unrelated {{
  1977. background: #e5e7eb;
  1978. color: #4b5563;
  1979. border: 2px solid #9ca3af;
  1980. }}
  1981. .match-content {{
  1982. padding: 16px;
  1983. background: #f9fafb;
  1984. border-top: 1px solid #e5e7eb;
  1985. }}
  1986. .match-explanation {{
  1987. font-size: 13px;
  1988. color: #4b5563;
  1989. line-height: 1.7;
  1990. margin-bottom: 16px;
  1991. }}
  1992. /* 历史帖子来源区域 */
  1993. .historical-posts-section {{
  1994. margin-top: 20px;
  1995. padding-top: 20px;
  1996. border-top: 2px solid #e5e7eb;
  1997. }}
  1998. .historical-posts-title {{
  1999. font-size: 14px;
  2000. font-weight: 600;
  2001. color: #374151;
  2002. margin-bottom: 12px;
  2003. }}
  2004. .historical-posts-grid {{
  2005. display: grid;
  2006. grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  2007. gap: 16px;
  2008. }}
  2009. .historical-post-card {{
  2010. display: grid;
  2011. grid-template-columns: 120px 1fr;
  2012. gap: 12px;
  2013. background: white;
  2014. border: 1px solid #e5e7eb;
  2015. border-radius: 8px;
  2016. padding: 12px;
  2017. transition: all 0.2s;
  2018. cursor: pointer;
  2019. }}
  2020. .historical-post-card:hover {{
  2021. border-color: #667eea;
  2022. box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1);
  2023. transform: translateY(-2px);
  2024. }}
  2025. .historical-post-image {{
  2026. position: relative;
  2027. }}
  2028. .historical-post-thumbnail {{
  2029. width: 100%;
  2030. height: 100px;
  2031. object-fit: contain;
  2032. border-radius: 6px;
  2033. background: #f9fafb;
  2034. }}
  2035. .historical-post-content {{
  2036. display: flex;
  2037. flex-direction: column;
  2038. gap: 8px;
  2039. }}
  2040. .historical-post-title {{
  2041. font-size: 13px;
  2042. font-weight: 600;
  2043. color: #1f2937;
  2044. line-height: 1.4;
  2045. overflow: hidden;
  2046. display: -webkit-box;
  2047. -webkit-line-clamp: 2;
  2048. -webkit-box-orient: vertical;
  2049. }}
  2050. .historical-inspiration-info {{
  2051. display: flex;
  2052. align-items: center;
  2053. gap: 6px;
  2054. }}
  2055. .inspiration-type-badge-small {{
  2056. padding: 2px 6px;
  2057. border-radius: 4px;
  2058. background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
  2059. color: white;
  2060. font-size: 10px;
  2061. font-weight: 600;
  2062. }}
  2063. .inspiration-name-small {{
  2064. font-size: 11px;
  2065. color: #667eea;
  2066. font-weight: 500;
  2067. }}
  2068. .historical-inspiration-desc {{
  2069. font-size: 11px;
  2070. color: #6b7280;
  2071. line-height: 1.5;
  2072. overflow: hidden;
  2073. display: -webkit-box;
  2074. -webkit-line-clamp: 2;
  2075. -webkit-box-orient: vertical;
  2076. }}
  2077. .historical-post-meta {{
  2078. margin: 4px 0;
  2079. }}
  2080. .historical-post-time {{
  2081. font-size: 11px;
  2082. color: #9ca3af;
  2083. }}
  2084. .historical-post-stats {{
  2085. display: flex;
  2086. align-items: center;
  2087. gap: 10px;
  2088. font-size: 11px;
  2089. color: #9ca3af;
  2090. }}
  2091. .view-link {{
  2092. margin-left: auto;
  2093. color: #667eea;
  2094. text-decoration: none;
  2095. font-weight: 500;
  2096. }}
  2097. .view-link:hover {{
  2098. color: #764ba2;
  2099. }}
  2100. .category-simple {{
  2101. color: #9ca3af;
  2102. font-size: 12px;
  2103. font-weight: 400;
  2104. margin-right: 6px;
  2105. }}
  2106. /* 帖子详情模态框 */
  2107. .post-detail-modal {{
  2108. display: none;
  2109. position: fixed;
  2110. z-index: 4000;
  2111. left: 0;
  2112. top: 0;
  2113. width: 100%;
  2114. height: 100%;
  2115. background: rgba(0, 0, 0, 0.7);
  2116. overflow: auto;
  2117. animation: fadeIn 0.3s;
  2118. }}
  2119. .post-detail-modal.active {{
  2120. display: flex;
  2121. align-items: center;
  2122. justify-content: center;
  2123. padding: 40px 20px;
  2124. }}
  2125. .post-detail-content {{
  2126. position: relative;
  2127. background: white;
  2128. border-radius: 16px;
  2129. max-width: 900px;
  2130. width: 100%;
  2131. max-height: 90vh;
  2132. overflow-y: auto;
  2133. box-shadow: 0 20px 60px rgba(0,0,0,0.3);
  2134. }}
  2135. .post-detail-close {{
  2136. position: sticky;
  2137. top: 20px;
  2138. right: 20px;
  2139. float: right;
  2140. font-size: 36px;
  2141. font-weight: 300;
  2142. color: #9ca3af;
  2143. background: white;
  2144. border: none;
  2145. cursor: pointer;
  2146. width: 40px;
  2147. height: 40px;
  2148. border-radius: 50%;
  2149. display: flex;
  2150. align-items: center;
  2151. justify-content: center;
  2152. transition: all 0.2s;
  2153. z-index: 10;
  2154. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  2155. }}
  2156. .post-detail-close:hover {{
  2157. color: #ef4444;
  2158. background: #fee2e2;
  2159. transform: rotate(90deg);
  2160. }}
  2161. .post-detail-header {{
  2162. padding: 40px 40px 20px 40px;
  2163. border-bottom: 2px solid #e5e7eb;
  2164. }}
  2165. .post-detail-title {{
  2166. font-size: 28px;
  2167. font-weight: bold;
  2168. color: #111827;
  2169. line-height: 1.4;
  2170. margin-bottom: 15px;
  2171. }}
  2172. .post-detail-meta {{
  2173. display: flex;
  2174. justify-content: space-between;
  2175. align-items: center;
  2176. color: #6b7280;
  2177. font-size: 14px;
  2178. gap: 20px;
  2179. }}
  2180. .post-detail-author {{
  2181. font-weight: 500;
  2182. }}
  2183. .post-detail-time {{
  2184. color: #9ca3af;
  2185. }}
  2186. .post-detail-stats {{
  2187. display: flex;
  2188. gap: 15px;
  2189. font-weight: 500;
  2190. }}
  2191. .post-detail-body {{
  2192. padding: 30px 40px;
  2193. }}
  2194. .post-detail-desc {{
  2195. font-size: 15px;
  2196. color: #4b5563;
  2197. line-height: 1.8;
  2198. margin-bottom: 25px;
  2199. white-space: pre-wrap;
  2200. }}
  2201. .post-detail-images {{
  2202. display: grid;
  2203. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  2204. gap: 15px;
  2205. margin-top: 20px;
  2206. }}
  2207. .post-detail-image {{
  2208. width: 100%;
  2209. border-radius: 8px;
  2210. object-fit: cover;
  2211. cursor: pointer;
  2212. transition: transform 0.2s;
  2213. }}
  2214. .post-detail-image:hover {{
  2215. transform: scale(1.02);
  2216. }}
  2217. .post-detail-footer {{
  2218. padding: 20px 40px 30px 40px;
  2219. border-top: 1px solid #e5e7eb;
  2220. text-align: center;
  2221. }}
  2222. .post-detail-link {{
  2223. display: inline-block;
  2224. padding: 12px 30px;
  2225. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  2226. color: white;
  2227. text-decoration: none;
  2228. border-radius: 8px;
  2229. font-weight: 500;
  2230. transition: all 0.3s;
  2231. }}
  2232. .post-detail-link:hover {{
  2233. transform: translateY(-2px);
  2234. box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
  2235. }}
  2236. /* 响应式 */
  2237. @media (max-width: 1400px) {{
  2238. .content-with-toc {{
  2239. grid-template-columns: 240px 1fr;
  2240. }}
  2241. .two-column-layout {{
  2242. grid-template-columns: 320px 1fr;
  2243. }}
  2244. }}
  2245. @media (max-width: 1200px) {{
  2246. .content-with-toc {{
  2247. grid-template-columns: 200px 1fr;
  2248. }}
  2249. .two-column-layout {{
  2250. grid-template-columns: 280px 1fr;
  2251. }}
  2252. }}
  2253. @media (max-width: 1024px) {{
  2254. .page-container {{
  2255. flex-direction: column;
  2256. height: auto;
  2257. }}
  2258. .left-sidebar {{
  2259. width: 100%;
  2260. max-height: 300px;
  2261. border-right: none;
  2262. border-bottom: 2px solid #e5e7eb;
  2263. }}
  2264. .right-content {{
  2265. height: auto;
  2266. }}
  2267. .two-column-layout {{
  2268. grid-template-columns: 1fr;
  2269. }}
  2270. .inspirations-detail-wrapper {{
  2271. max-height: none;
  2272. }}
  2273. .post-card-compact {{
  2274. grid-template-columns: 150px 1fr;
  2275. }}
  2276. }}
  2277. @media (max-width: 768px) {{
  2278. .header {{
  2279. padding: 15px 20px;
  2280. }}
  2281. .header h1 {{
  2282. font-size: 24px;
  2283. }}
  2284. .left-sidebar {{
  2285. max-height: 250px;
  2286. }}
  2287. .post-info-section {{
  2288. padding: 20px;
  2289. }}
  2290. }}
  2291. </style>
  2292. </head>
  2293. <body>
  2294. <div class="page-container">
  2295. <!-- 左侧目录 -->
  2296. <div class="left-sidebar">
  2297. <div class="header">
  2298. <h1>How 解构结果</h1>
  2299. <p>灵感点特征匹配分析</p>
  2300. </div>
  2301. <div class="toc-content">
  2302. {toc_items_html}
  2303. </div>
  2304. </div>
  2305. <!-- 右侧内容 -->
  2306. <div class="right-content">
  2307. <!-- 吸顶面包屑导航 -->
  2308. <div class="breadcrumb-nav" id="breadcrumb-nav">
  2309. <span class="breadcrumb-item" id="breadcrumb-point">-</span>
  2310. <span class="breadcrumb-separator" id="sep-1" style="display:none;">></span>
  2311. <span class="breadcrumb-item" id="breadcrumb-step" style="display:none;">-</span>
  2312. <span class="breadcrumb-separator" id="sep-2" style="display:none;">></span>
  2313. <span class="breadcrumb-item" id="breadcrumb-feature" style="display:none;">-</span>
  2314. <span class="breadcrumb-separator" id="sep-3" style="display:none;">></span>
  2315. <span class="breadcrumb-item" id="breadcrumb-level" style="display:none;">-</span>
  2316. <span class="breadcrumb-separator" id="sep-4" style="display:none;">></span>
  2317. <span class="breadcrumb-item" id="breadcrumb-subgroup" style="display:none;">-</span>
  2318. </div>
  2319. {all_contents_html}
  2320. </div>
  2321. </div>
  2322. <!-- 匹配详情模态框 -->
  2323. <div id="matchModal" class="match-modal" onclick="closeMatchModal(event)">
  2324. <div class="match-modal-content" onclick="event.stopPropagation()">
  2325. <div class="match-modal-header">
  2326. <h2 class="match-modal-title" id="matchModalTitle"></h2>
  2327. <button class="match-modal-close" onclick="closeMatchModal()">&times;</button>
  2328. </div>
  2329. <div class="match-modal-body" id="matchModalBody"></div>
  2330. </div>
  2331. </div>
  2332. <!-- 帖子详情模态框 -->
  2333. <div id="postDetailModal" class="post-detail-modal" onclick="closePostDetail(event)"></div>
  2334. <script>
  2335. // 切换内容视图
  2336. function showContentView(viewId, clickedElement) {{
  2337. // 隐藏所有视图
  2338. var allViews = document.querySelectorAll('.content-view');
  2339. allViews.forEach(function(view) {{
  2340. view.style.display = 'none';
  2341. view.classList.remove('active');
  2342. }});
  2343. // 显示指定视图
  2344. var targetView = document.getElementById(viewId);
  2345. if (targetView) {{
  2346. targetView.style.display = 'block';
  2347. targetView.classList.add('active');
  2348. }}
  2349. // 更新目录项的active状态:清除所有,然后标记当前项和祖先
  2350. var tocItems = document.querySelectorAll('.toc-item');
  2351. tocItems.forEach(function(item) {{
  2352. item.classList.remove('active', 'active-ancestor');
  2353. }});
  2354. // 标记当前激活项
  2355. var activeItem = clickedElement || (event && event.currentTarget);
  2356. if (activeItem && activeItem.classList.contains('toc-item')) {{
  2357. activeItem.classList.add('active');
  2358. // 标记所有祖先节点
  2359. var parent = activeItem.parentElement;
  2360. while (parent) {{
  2361. // 查找上一个兄弟节点中的toc-item
  2362. var prevSibling = parent.previousElementSibling;
  2363. if (prevSibling && prevSibling.classList.contains('toc-item')) {{
  2364. prevSibling.classList.add('active-ancestor');
  2365. }}
  2366. parent = parent.parentElement;
  2367. // 到达toc-content就停止
  2368. if (parent && parent.classList.contains('toc-content')) {{
  2369. break;
  2370. }}
  2371. }}
  2372. }}
  2373. }}
  2374. // 折叠/展开帖子
  2375. function toggleTocPost(event, postIdx) {{
  2376. event.stopPropagation();
  2377. var item = event.currentTarget;
  2378. var children = document.getElementById('toc-post-' + postIdx + '-children');
  2379. item.classList.toggle('collapsed');
  2380. children.classList.toggle('hidden');
  2381. }}
  2382. // 折叠/展开点类型分组
  2383. function toggleTocGroup(event, groupId) {{
  2384. event.stopPropagation();
  2385. var item = event.currentTarget;
  2386. var children = document.getElementById('toc-' + groupId + '-children');
  2387. item.classList.toggle('collapsed');
  2388. children.classList.toggle('hidden');
  2389. }}
  2390. // 直接显示点的内容视图(新结构:点没有子级,直接跳转)
  2391. function showPointView(event, viewId) {{
  2392. event.stopPropagation();
  2393. // 切换到该点的视图
  2394. showContentView(viewId, event.currentTarget);
  2395. // 滚动到右侧内容区域的顶部
  2396. var rightContent = document.querySelector('.right-content');
  2397. if (rightContent) {{
  2398. rightContent.scrollTo({{ top: 0, behavior: 'smooth' }});
  2399. }}
  2400. }}
  2401. // 折叠/展开点项(同时切换到该点的内容视图)
  2402. function toggleTocPoint(event, pointItemId, viewId) {{
  2403. event.stopPropagation();
  2404. // 如果点击的是展开图标,只切换折叠状态
  2405. if (event.target.classList.contains('toc-expand-icon')) {{
  2406. event.preventDefault();
  2407. var item = event.currentTarget;
  2408. var children = document.getElementById('toc-' + pointItemId + '-children');
  2409. item.classList.toggle('collapsed');
  2410. children.classList.toggle('hidden');
  2411. }} else {{
  2412. // 如果点击的是其他部分,切换到该点的视图
  2413. showContentView(viewId, event.currentTarget);
  2414. // 滚动到右侧内容区域的顶部
  2415. var rightContent = document.querySelector('.right-content');
  2416. if (rightContent) {{
  2417. rightContent.scrollTo({{ top: 0, behavior: 'smooth' }});
  2418. }}
  2419. }}
  2420. }}
  2421. // 折叠/展开特征项(特征有子层级)
  2422. function toggleTocFeature(event, featureItemId, viewId, featureSectionId) {{
  2423. event.stopPropagation();
  2424. // 如果点击的是展开图标,只切换折叠状态
  2425. if (event.target.classList.contains('toc-expand-icon')) {{
  2426. event.preventDefault();
  2427. var item = event.currentTarget;
  2428. var children = document.getElementById('toc-' + featureItemId + '-children');
  2429. item.classList.toggle('collapsed');
  2430. children.classList.toggle('hidden');
  2431. }} else {{
  2432. // 如果点击的是其他部分,切换到该点的视图并滚动到特征
  2433. showContentView(viewId, event.currentTarget);
  2434. setTimeout(function() {{
  2435. var element = document.getElementById(featureSectionId);
  2436. if (element) {{
  2437. element.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
  2438. }}
  2439. }}, 100);
  2440. }}
  2441. }}
  2442. // 滚动到层级分组(在特征下的子层级)
  2443. function scrollToLevelSection(event, viewId, levelSectionId) {{
  2444. event.stopPropagation();
  2445. // 先切换到对应点的视图
  2446. showContentView(viewId, event.currentTarget);
  2447. // 等待视图切换完成后滚动到层级分组位置
  2448. setTimeout(function() {{
  2449. var element = document.getElementById(levelSectionId);
  2450. if (element) {{
  2451. element.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
  2452. }}
  2453. }}, 100);
  2454. }}
  2455. // 点击特征:先切换到对应点的视图,再滚动到特征位置
  2456. function clickFeature(event, pointViewId, featureSectionId) {{
  2457. event.stopPropagation();
  2458. // 先切换到对应点的视图
  2459. showContentView(pointViewId, event.currentTarget);
  2460. // 等待视图切换完成后滚动到特征位置
  2461. setTimeout(function() {{
  2462. var element = document.getElementById(featureSectionId);
  2463. if (element) {{
  2464. element.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
  2465. }}
  2466. }}, 100);
  2467. }}
  2468. function scrollToSection(sectionId) {{
  2469. var element = document.getElementById(sectionId);
  2470. if (element) {{
  2471. element.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
  2472. }}
  2473. }}
  2474. function toggleMatch(matchId) {{
  2475. var content = document.getElementById(matchId + '-content');
  2476. var icon = document.getElementById(matchId + '-icon');
  2477. if (content.style.display === 'none') {{
  2478. content.style.display = 'block';
  2479. icon.classList.add('expanded');
  2480. }} else {{
  2481. content.style.display = 'none';
  2482. icon.classList.remove('expanded');
  2483. }}
  2484. }}
  2485. function toggleFeatureSection(event, sectionId) {{
  2486. event.stopPropagation();
  2487. var content = document.getElementById(sectionId + '-content');
  2488. var icon = document.getElementById(sectionId + '-icon');
  2489. if (!content || !icon) {{
  2490. console.error('Element not found:', sectionId);
  2491. return;
  2492. }}
  2493. var isHidden = content.style.display === 'none' || !content.style.display || window.getComputedStyle(content).display === 'none';
  2494. if (isHidden) {{
  2495. content.style.display = 'flex';
  2496. icon.textContent = '▼';
  2497. }} else {{
  2498. content.style.display = 'none';
  2499. icon.textContent = '▶';
  2500. }}
  2501. }}
  2502. function toggleStepSection(sectionId) {{
  2503. var content = document.getElementById(sectionId + '-content');
  2504. var icon = document.getElementById(sectionId + '-icon');
  2505. if (content.style.display === 'none') {{
  2506. content.style.display = 'flex';
  2507. icon.textContent = '▼';
  2508. }} else {{
  2509. content.style.display = 'none';
  2510. icon.textContent = '▶';
  2511. }}
  2512. }}
  2513. function toggleMatchGroup(event, groupId) {{
  2514. event.stopPropagation();
  2515. var content = document.getElementById(groupId + '-content');
  2516. var icon = document.getElementById(groupId + '-icon');
  2517. if (!content || !icon) {{
  2518. console.error('Element not found:', groupId);
  2519. return;
  2520. }}
  2521. var isHidden = content.style.display === 'none' || !content.style.display || window.getComputedStyle(content).display === 'none';
  2522. if (isHidden) {{
  2523. // 根据class决定使用什么display值
  2524. if (content.classList.contains('match-subgroup-content')) {{
  2525. content.style.display = 'flex';
  2526. }} else {{
  2527. content.style.display = 'block';
  2528. }}
  2529. icon.textContent = '▼';
  2530. }} else {{
  2531. content.style.display = 'none';
  2532. icon.textContent = '▶';
  2533. }}
  2534. }}
  2535. function showMatchDetail(element) {{
  2536. var modal = document.getElementById('matchModal');
  2537. var title = document.getElementById('matchModalTitle');
  2538. var body = document.getElementById('matchModalBody');
  2539. // 从data属性读取数据
  2540. var personaName = element.dataset.personaName;
  2541. var featureType = element.dataset.featureType;
  2542. var personaLevel = element.dataset.personaLevel;
  2543. var similarity = parseFloat(element.dataset.similarity);
  2544. var label = element.dataset.label;
  2545. var explanation = element.dataset.explanation;
  2546. var historicalPosts = element.dataset.historicalPosts;
  2547. // 设置标题 - 合并显示层级和类型
  2548. var titleText = '';
  2549. if (personaLevel && featureType) {{
  2550. titleText = '[' + personaLevel + '-' + featureType + '] ';
  2551. }}
  2552. titleText += personaName + ' (相似度: ' + similarity.toFixed(2) + ' - ' + label + ')';
  2553. title.textContent = titleText;
  2554. // 生成内容
  2555. var bodyHTML = '<div class="match-detail-section">';
  2556. bodyHTML += '<h3>匹配说明</h3>';
  2557. bodyHTML += '<div class="match-explanation-text">' + explanation + '</div>';
  2558. bodyHTML += '</div>';
  2559. // 如果有历史帖子
  2560. if (historicalPosts && historicalPosts.trim().length > 0) {{
  2561. bodyHTML += '<div class="match-detail-section">';
  2562. bodyHTML += '<h3>历史帖子来源</h3>';
  2563. bodyHTML += '<div class="historical-posts-grid">';
  2564. bodyHTML += historicalPosts;
  2565. bodyHTML += '</div>';
  2566. bodyHTML += '</div>';
  2567. }}
  2568. body.innerHTML = bodyHTML;
  2569. modal.style.display = 'block';
  2570. }}
  2571. function closeMatchModal(event) {{
  2572. var modal = document.getElementById('matchModal');
  2573. if (!event || event.target === modal) {{
  2574. modal.style.display = 'none';
  2575. }}
  2576. }}
  2577. // ESC键关闭模态框
  2578. document.addEventListener('keydown', function(event) {{
  2579. if (event.key === 'Escape') {{
  2580. closeMatchModal();
  2581. }}
  2582. }});
  2583. function showPostDetail(element) {{
  2584. const postDataStr = element.dataset.postData;
  2585. if (!postDataStr) return;
  2586. try {{
  2587. const postData = JSON.parse(postDataStr);
  2588. // 生成图片HTML(添加懒加载)
  2589. let imagesHtml = '';
  2590. if (postData.images && postData.images.length > 0) {{
  2591. imagesHtml = postData.images.map(img =>
  2592. `<img src="${{img}}" class="post-detail-image" alt="图片" loading="lazy">`
  2593. ).join('');
  2594. }} else {{
  2595. imagesHtml = '<div style="text-align: center; color: #9ca3af; padding: 40px;">暂无图片</div>';
  2596. }}
  2597. const modalHtml = `
  2598. <div class="post-detail-content" onclick="event.stopPropagation()">
  2599. <button class="post-detail-close" onclick="closePostDetail()">×</button>
  2600. <div class="post-detail-header">
  2601. <div class="post-detail-title">${{postData.title || '无标题'}}</div>
  2602. <div class="post-detail-meta">
  2603. <div>
  2604. <span class="post-detail-author">👤 ${{postData.author}}</span>
  2605. <span class="post-detail-time"> · 📅 ${{postData.publish_time}}</span>
  2606. </div>
  2607. <div class="post-detail-stats">
  2608. <span>👍 ${{postData.like_count}}</span>
  2609. <span>💬 ${{postData.comment_count}}</span>
  2610. <span>⭐ ${{postData.collect_count}}</span>
  2611. </div>
  2612. </div>
  2613. </div>
  2614. <div class="post-detail-body">
  2615. ${{postData.body_text ? `<div class="post-detail-desc">${{postData.body_text}}</div>` : '<div style="color: #9ca3af;">暂无正文</div>'}}
  2616. <div class="post-detail-images">
  2617. ${{imagesHtml}}
  2618. </div>
  2619. </div>
  2620. <div class="post-detail-footer">
  2621. <a href="${{postData.link}}" target="_blank" class="post-detail-link">
  2622. 在小红书查看完整内容 →
  2623. </a>
  2624. </div>
  2625. </div>
  2626. `;
  2627. let modal = document.getElementById('postDetailModal');
  2628. if (!modal) {{
  2629. modal = document.createElement('div');
  2630. modal.id = 'postDetailModal';
  2631. modal.className = 'post-detail-modal';
  2632. modal.onclick = closePostDetail;
  2633. document.body.appendChild(modal);
  2634. }}
  2635. modal.innerHTML = modalHtml;
  2636. modal.classList.add('active');
  2637. document.body.style.overflow = 'hidden';
  2638. }} catch (e) {{
  2639. console.error('解析帖子数据失败:', e);
  2640. }}
  2641. }}
  2642. function closePostDetail(event) {{
  2643. if (event && event.target !== event.currentTarget) return;
  2644. const modal = document.getElementById('postDetailModal');
  2645. if (modal) {{
  2646. modal.classList.remove('active');
  2647. document.body.style.overflow = '';
  2648. }}
  2649. }}
  2650. function moveImage(postId, direction) {{
  2651. var carousel = document.querySelector(`[data-post-id="${{postId}}"]`);
  2652. var track = document.getElementById(postId + '-track');
  2653. var totalImages = parseInt(carousel.getAttribute('data-total-images'));
  2654. if (!carousel.currentIndex) {{
  2655. carousel.currentIndex = 0;
  2656. }}
  2657. carousel.currentIndex = (carousel.currentIndex + direction + totalImages) % totalImages;
  2658. track.style.transform = `translateX(-${{carousel.currentIndex * 100}}%)`;
  2659. updateIndicators(postId, carousel.currentIndex);
  2660. }}
  2661. function jumpToImage(postId, index) {{
  2662. var carousel = document.querySelector(`[data-post-id="${{postId}}"]`);
  2663. var track = document.getElementById(postId + '-track');
  2664. carousel.currentIndex = index;
  2665. track.style.transform = `translateX(-${{index * 100}}%)`;
  2666. updateIndicators(postId, index);
  2667. }}
  2668. function updateIndicators(postId, activeIndex) {{
  2669. var indicators = document.querySelectorAll(`#${{postId}}-indicators .indicator`);
  2670. indicators.forEach((indicator, i) => {{
  2671. if (i === activeIndex) {{
  2672. indicator.classList.add('active');
  2673. }} else {{
  2674. indicator.classList.remove('active');
  2675. }}
  2676. }});
  2677. }}
  2678. // 目录激活状态追踪
  2679. function updateTocActiveState() {{
  2680. const rightContent = document.querySelector('.right-content');
  2681. if (!rightContent) return;
  2682. // 找到当前激活的view
  2683. const activeView = document.querySelector('.content-view[style*="display: block"], .content-view:not([style*="display: none"])');
  2684. if (!activeView) return;
  2685. // 在激活的view中查找所有sections
  2686. const sections = activeView.querySelectorAll('[id]');
  2687. const tocItems = document.querySelectorAll('.toc-item');
  2688. let currentActive = null;
  2689. let maxVisibility = 0;
  2690. // 找到当前在视口中最visible的section
  2691. sections.forEach(section => {{
  2692. if (!section.id) return;
  2693. const rect = section.getBoundingClientRect();
  2694. const sectionTop = rect.top;
  2695. const sectionBottom = rect.bottom;
  2696. const windowHeight = window.innerHeight;
  2697. // 计算section在视口中的可见度
  2698. if (sectionTop < windowHeight && sectionBottom > 0) {{
  2699. const visibleTop = Math.max(sectionTop, 0);
  2700. const visibleBottom = Math.min(sectionBottom, windowHeight);
  2701. const visibility = visibleBottom - visibleTop;
  2702. // 优先选择顶部在视口上半部分的section
  2703. if (sectionTop < windowHeight / 2 && visibility > maxVisibility) {{
  2704. maxVisibility = visibility;
  2705. currentActive = section.id;
  2706. }}
  2707. }}
  2708. }});
  2709. // 更新目录项的激活状态
  2710. tocItems.forEach(item => {{
  2711. // 移除所有激活状态和祖先状态
  2712. item.classList.remove('active');
  2713. item.classList.remove('active-ancestor');
  2714. const onclick = item.getAttribute('onclick');
  2715. if (!onclick) return;
  2716. // 检查是否匹配当前活动section
  2717. if (currentActive && onclick.includes(currentActive)) {{
  2718. item.classList.add('active');
  2719. // 标记所有祖先节点
  2720. let parent = item.parentElement;
  2721. while (parent) {{
  2722. if (parent.classList && parent.classList.contains('toc-item')) {{
  2723. parent.classList.add('active-ancestor');
  2724. }}
  2725. parent = parent.parentElement;
  2726. }}
  2727. }}
  2728. }});
  2729. // 更新面包屑导航
  2730. updateBreadcrumb(currentActive);
  2731. }}
  2732. // 更新面包屑导航(只显示右侧内容层级,不显示帖子信息)
  2733. function updateBreadcrumb(currentSectionId) {{
  2734. const breadcrumbPoint = document.getElementById('breadcrumb-point');
  2735. const breadcrumbStep = document.getElementById('breadcrumb-step');
  2736. const breadcrumbFeature = document.getElementById('breadcrumb-feature');
  2737. const breadcrumbLevel = document.getElementById('breadcrumb-level');
  2738. const breadcrumbSubgroup = document.getElementById('breadcrumb-subgroup');
  2739. const sep1 = document.getElementById('sep-1');
  2740. const sep2 = document.getElementById('sep-2');
  2741. const sep3 = document.getElementById('sep-3');
  2742. const sep4 = document.getElementById('sep-4');
  2743. // 隐藏所有可选元素
  2744. [breadcrumbStep, breadcrumbFeature, breadcrumbLevel, breadcrumbSubgroup, sep1, sep2, sep3, sep4].forEach(el => {{
  2745. if (el) el.style.display = 'none';
  2746. }});
  2747. if (!currentSectionId) {{
  2748. breadcrumbPoint.textContent = '-';
  2749. return;
  2750. }}
  2751. // 从当前section找到所在的view
  2752. let currentView = null;
  2753. const allViews = document.querySelectorAll('.content-view');
  2754. allViews.forEach(view => {{
  2755. if (view.style.display !== 'none' && view.querySelector('#' + currentSectionId)) {{
  2756. currentView = view;
  2757. }}
  2758. }});
  2759. if (!currentView) return;
  2760. // 从view ID解析信息: view-post-0-point-灵感点列表-0
  2761. const viewId = currentView.id;
  2762. const viewMatch = viewId.match(/view-post-(\d+)-point-(.+?)-(\d+)/);
  2763. if (viewMatch) {{
  2764. const postIdx = viewMatch[1];
  2765. const pointType = viewMatch[2].replace('列表', '');
  2766. const pointIdx = parseInt(viewMatch[3]);
  2767. // 1. 点信息(总是显示)
  2768. const pointElement = currentView.querySelector('.inspiration-detail-card .inspiration-name');
  2769. const pointName = pointElement ? pointElement.textContent : `${{pointType}} ${{pointIdx + 1}}`;
  2770. breadcrumbPoint.textContent = `${{pointType}} ${{pointIdx + 1}}: ${{pointName.substring(0, 20)}}${{pointName.length > 20 ? '...' : ''}}`;
  2771. breadcrumbPoint.onclick = function() {{
  2772. showContentView(viewId, this);
  2773. const rightContent = document.querySelector('.right-content');
  2774. if (rightContent) rightContent.scrollTo({{ top: 0, behavior: 'smooth' }});
  2775. }};
  2776. // 2. 步骤信息(如果在步骤section中)
  2777. if (currentSectionId.includes('step')) {{
  2778. const stepMatch = currentSectionId.match(/step-\d+-(\d+)/);
  2779. if (stepMatch) {{
  2780. sep1.style.display = 'inline';
  2781. breadcrumbStep.style.display = 'inline';
  2782. breadcrumbStep.textContent = `步骤 ${{parseInt(stepMatch[1]) + 1}}`;
  2783. breadcrumbStep.onclick = function() {{
  2784. const el = document.getElementById(currentSectionId);
  2785. if (el) el.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
  2786. }};
  2787. }}
  2788. }}
  2789. // 3. 特征信息(如果在特征相关section中)
  2790. if (currentSectionId.includes('feat')) {{
  2791. const featMatch = currentSectionId.match(/feat-(\d+)-(\d+)/);
  2792. if (featMatch) {{
  2793. const featIdx = parseInt(featMatch[2]);
  2794. // 查找特征名称
  2795. const featureSection = document.getElementById('post-' + postIdx + '-feat-' + pointIdx + '-' + featIdx);
  2796. let featureName = `特征 ${{featIdx + 1}}`;
  2797. if (featureSection) {{
  2798. const header = featureSection.querySelector('.match-section-header h4');
  2799. if (header) {{
  2800. const match = header.textContent.match(/匹配结果:\s*(.+?)\s*\(/);
  2801. if (match) featureName = match[1].substring(0, 15) + (match[1].length > 15 ? '...' : '');
  2802. }}
  2803. }}
  2804. sep2.style.display = 'inline';
  2805. breadcrumbFeature.style.display = 'inline';
  2806. breadcrumbFeature.textContent = featureName;
  2807. breadcrumbFeature.onclick = function() {{
  2808. if (featureSection) featureSection.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
  2809. }};
  2810. // 4. 层级分组信息(如果在level section中)
  2811. if (currentSectionId.includes('level')) {{
  2812. const levelMatch = currentSectionId.match(/level-(.+?)(?:-|$)/);
  2813. if (levelMatch) {{
  2814. const levelName = levelMatch[1];
  2815. const levelElement = document.getElementById(currentSectionId);
  2816. let levelTitle = `匹配人设${{levelName}}`;
  2817. if (levelElement) {{
  2818. const header = levelElement.querySelector('.level-group-title');
  2819. if (header) levelTitle = header.textContent.trim();
  2820. }}
  2821. sep3.style.display = 'inline';
  2822. breadcrumbLevel.style.display = 'inline';
  2823. breadcrumbLevel.textContent = levelTitle.substring(0, 20);
  2824. breadcrumbLevel.onclick = function() {{
  2825. if (levelElement) levelElement.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
  2826. }};
  2827. }}
  2828. }}
  2829. }}
  2830. }}
  2831. }}
  2832. }}
  2833. // 监听滚动事件
  2834. document.addEventListener('DOMContentLoaded', function() {{
  2835. // 默认显示第一个视图
  2836. var allViews = document.querySelectorAll('.content-view');
  2837. if (allViews.length > 0) {{
  2838. // 找到第一个点视图(跳过帖子详情视图)
  2839. var firstPointView = null;
  2840. var firstPointViewId = null;
  2841. for (var i = 0; i < allViews.length; i++) {{
  2842. if (allViews[i].id.includes('point') && allViews[i].id.includes('灵感点列表')) {{
  2843. firstPointView = allViews[i];
  2844. firstPointViewId = allViews[i].id;
  2845. break;
  2846. }}
  2847. }}
  2848. // 如果有点视图,显示第一个点;否则显示第一个帖子详情
  2849. var defaultView = firstPointView || allViews[0];
  2850. defaultView.style.display = 'block';
  2851. defaultView.classList.add('active');
  2852. // 展开第一个帖子
  2853. var firstPost = document.querySelector('.toc-post-header');
  2854. if (firstPost) {{
  2855. firstPost.classList.remove('collapsed');
  2856. var postIdx = firstPost.getAttribute('data-post-id');
  2857. var postChildren = document.getElementById('toc-post-' + postIdx + '-children');
  2858. if (postChildren) {{
  2859. postChildren.classList.remove('hidden');
  2860. }}
  2861. }}
  2862. // 展开第一个灵感点分组
  2863. var firstInspGroup = document.querySelector('[onclick*="post-0-灵感点列表"]');
  2864. if (firstInspGroup && firstInspGroup.classList.contains('toc-group-header')) {{
  2865. firstInspGroup.classList.remove('collapsed');
  2866. var groupChildren = document.getElementById('toc-post-0-灵感点列表-children');
  2867. if (groupChildren) {{
  2868. groupChildren.classList.remove('hidden');
  2869. }}
  2870. }}
  2871. // 展开第一个灵感点
  2872. var firstPoint = document.querySelector('.toc-level-2[onclick*="view-post-0-point-灵感点列表-0"]');
  2873. if (firstPoint) {{
  2874. firstPoint.classList.remove('collapsed');
  2875. var pointId = firstPoint.getAttribute('onclick').match(/toggleTocPoint\(event,\s*'([^']+)'/);
  2876. if (pointId && pointId[1]) {{
  2877. var pointChildren = document.getElementById('toc-' + pointId[1] + '-children');
  2878. if (pointChildren) {{
  2879. pointChildren.classList.remove('hidden');
  2880. }}
  2881. }}
  2882. }}
  2883. // 激活对应的目录项
  2884. if (firstPointViewId) {{
  2885. // 找到对应的目录项并添加active类
  2886. var tocItems = document.querySelectorAll('.toc-item');
  2887. tocItems.forEach(function(item) {{
  2888. var onclick = item.getAttribute('onclick');
  2889. if (onclick && onclick.includes(firstPointViewId)) {{
  2890. item.classList.add('active');
  2891. // 标记祖先节点
  2892. var parent = item.parentElement;
  2893. while (parent) {{
  2894. if (parent.classList && parent.classList.contains('toc-item')) {{
  2895. parent.classList.add('active-ancestor');
  2896. }}
  2897. parent = parent.parentElement;
  2898. }}
  2899. }}
  2900. }});
  2901. }}
  2902. }}
  2903. const rightContent = document.querySelector('.right-content');
  2904. if (rightContent) {{
  2905. rightContent.addEventListener('scroll', function() {{
  2906. // 使用节流避免频繁触发
  2907. if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
  2908. this.scrollTimeout = setTimeout(updateTocActiveState, 50);
  2909. }});
  2910. // 初始化时更新一次
  2911. updateTocActiveState();
  2912. }}
  2913. }});
  2914. </script>
  2915. </body>
  2916. </html>
  2917. '''
  2918. return html
  2919. def minify_html(html: str) -> str:
  2920. """压缩HTML,去除多余空格和换行"""
  2921. import re
  2922. # 保护script和style标签内容
  2923. scripts = []
  2924. styles = []
  2925. def save_script(match):
  2926. scripts.append(match.group(0))
  2927. return f"___SCRIPT_{len(scripts)-1}___"
  2928. def save_style(match):
  2929. styles.append(match.group(0))
  2930. return f"___STYLE_{len(styles)-1}___"
  2931. # 保存script和style
  2932. html = re.sub(r'<script[^>]*>.*?</script>', save_script, html, flags=re.DOTALL)
  2933. html = re.sub(r'<style[^>]*>.*?</style>', save_style, html, flags=re.DOTALL)
  2934. # 去除HTML注释
  2935. html = re.sub(r'<!--.*?-->', '', html, flags=re.DOTALL)
  2936. # 去除多余空格和换行
  2937. html = re.sub(r'\s+', ' ', html)
  2938. html = re.sub(r'>\s+<', '><', html)
  2939. # 恢复script和style
  2940. for i, script in enumerate(scripts):
  2941. html = html.replace(f"___SCRIPT_{i}___", script)
  2942. for i, style in enumerate(styles):
  2943. html = html.replace(f"___STYLE_{i}___", style)
  2944. return html.strip()
  2945. def main():
  2946. """主函数"""
  2947. # 解析命令行参数
  2948. import sys
  2949. account_name = sys.argv[1] if len(sys.argv) > 1 else None
  2950. # 使用路径配置
  2951. config = PathConfig(account_name=account_name)
  2952. # 确保输出目录存在
  2953. config.ensure_dirs()
  2954. # 获取路径
  2955. input_dir = config.how_results_dir
  2956. output_file = config.visualization_file
  2957. print(f"账号: {config.account_name}")
  2958. print(f"输入目录: {input_dir}")
  2959. print(f"输出文件: {output_file}")
  2960. print()
  2961. print(f"读取 how 解构结果: {input_dir}")
  2962. # 加载特征分类映射
  2963. print(f"加载特征分类映射...")
  2964. category_mapping = load_feature_category_mapping(config)
  2965. print(f"已加载 {sum(len(v) for v in category_mapping.values())} 个特征分类")
  2966. # 加载特征来源映射
  2967. print(f"加载特征来源映射...")
  2968. source_mapping = load_feature_source_mapping(config)
  2969. print(f"已加载 {len(source_mapping)} 个特征的来源信息")
  2970. json_files = list(input_dir.glob("*_how.json"))
  2971. print(f"找到 {len(json_files)} 个文件\n")
  2972. posts_data = []
  2973. for i, file_path in enumerate(json_files, 1):
  2974. print(f"读取文件 [{i}/{len(json_files)}]: {file_path.name}")
  2975. with open(file_path, "r", encoding="utf-8") as f:
  2976. post_data = json.load(f)
  2977. posts_data.append(post_data)
  2978. # 按发布时间降序排序(最新的在前)
  2979. print(f"\n按发布时间排序...")
  2980. posts_data.sort(key=lambda x: x.get("帖子详情", {}).get("publish_timestamp", 0), reverse=True)
  2981. print(f"\n生成合并的 HTML...")
  2982. html_content = generate_combined_html(posts_data, category_mapping, source_mapping)
  2983. # 保存原始版本
  2984. print(f"保存原始HTML到: {output_file}")
  2985. with open(output_file, "w", encoding="utf-8") as f:
  2986. f.write(html_content)
  2987. original_size = len(html_content) / 1024 / 1024
  2988. print(f"原始HTML大小: {original_size:.1f} MB")
  2989. # 压缩HTML
  2990. print(f"\n压缩HTML...")
  2991. minified_html = minify_html(html_content)
  2992. minified_file = output_file.parent / "当前帖子_解构结果_可视化.min.html"
  2993. print(f"保存压缩HTML到: {minified_file}")
  2994. with open(minified_file, "w", encoding="utf-8") as f:
  2995. f.write(minified_html)
  2996. minified_size = len(minified_html) / 1024 / 1024
  2997. print(f"压缩HTML大小: {minified_size:.1f} MB (减少 {(1 - minified_size/original_size)*100:.1f}%)")
  2998. # Gzip压缩
  2999. import gzip
  3000. print(f"\n生成Gzip压缩版本...")
  3001. gzip_file = output_file.parent / "当前帖子_解构结果_可视化.html.gz"
  3002. with gzip.open(gzip_file, "wb") as f:
  3003. f.write(minified_html.encode('utf-8'))
  3004. gzip_size = gzip_file.stat().st_size / 1024 / 1024
  3005. print(f"Gzip压缩大小: {gzip_size:.1f} MB (比原始减少 {(1 - gzip_size/original_size)*100:.1f}%)")
  3006. print(f"\n完成! 生成了3个版本:")
  3007. print(f"1. 原始版本: {output_file} ({original_size:.1f} MB)")
  3008. print(f"2. 压缩版本: {minified_file} ({minified_size:.1f} MB)")
  3009. print(f"3. Gzip版本: {gzip_file} ({gzip_size:.1f} MB)")
  3010. print(f"\n建议分享: {gzip_file.name} (浏览器可直接打开)")
  3011. if __name__ == "__main__":
  3012. main()