| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- How解构结果可视化脚本 V2
- 改进版:
- - 使用标签页展示多个帖子
- - 参考 visualize_inspiration_points.py 的帖子详情展示
- - 分层可折叠的匹配结果
- """
- import json
- from pathlib import Path
- from typing import Dict, List
- import sys
- import html as html_module
- # 添加项目根目录到路径
- project_root = Path(__file__).parent.parent.parent
- sys.path.insert(0, str(project_root))
- # 注意:已改用基于相似度的显示方式,不再使用关系类型
- # def get_relation_color(relation: str) -> str:
- # """根据关系类型返回对应的颜色"""
- # color_map = {
- # "same": "#10b981", # 绿色 - 同义
- # "contains": "#3b82f6", # 蓝色 - 包含
- # "contained_by": "#8b5cf6", # 紫色 - 被包含
- # "coordinate": "#f59e0b", # 橙色 - 同级
- # "overlap": "#ec4899", # 粉色 - 部分重叠
- # "related": "#6366f1", # 靛蓝 - 相关
- # "unrelated": "#9ca3af" # 灰色 - 无关
- # }
- # return color_map.get(relation, "#9ca3af")
- #
- #
- # def get_relation_label(relation: str) -> str:
- # """返回关系类型的中文标签"""
- # label_map = {
- # "same": "同义",
- # "contains": "包含",
- # "contained_by": "被包含",
- # "coordinate": "同级",
- # "overlap": "部分重叠",
- # "related": "相关",
- # "unrelated": "无关"
- # }
- # return label_map.get(relation, relation)
- def generate_historical_post_card_html(post_detail: Dict, inspiration_point: Dict) -> str:
- """生成历史帖子的紧凑卡片HTML"""
- title = post_detail.get("title", "无标题")
- body_text = post_detail.get("body_text", "")
- images = post_detail.get("images", [])
- like_count = post_detail.get("like_count", 0)
- collect_count = post_detail.get("collect_count", 0)
- comment_count = post_detail.get("comment_count", 0)
- author = post_detail.get("channel_account_name", "")
- link = post_detail.get("link", "#")
- publish_time = post_detail.get("publish_time", "")
- # 获取灵感点信息
- point_name = inspiration_point.get("点的名称", "")
- point_desc = inspiration_point.get("点的描述", "")
- # 准备详情数据(用于模态框)
- import json
- post_detail_data = {
- "title": title,
- "body_text": body_text,
- "images": images,
- "like_count": like_count,
- "comment_count": comment_count,
- "collect_count": collect_count,
- "author": author,
- "publish_time": publish_time,
- "link": link
- }
- post_data_json = json.dumps(post_detail_data, ensure_ascii=False)
- post_data_json_escaped = html_module.escape(post_data_json)
- # 截取正文预览(前80个字符)
- body_preview = body_text[:80] + "..." if len(body_text) > 80 else body_text
- # 生成缩略图
- thumbnail_html = ""
- if images:
- thumbnail_html = f'<img src="{images[0]}" alt="Post thumbnail" class="historical-post-thumbnail" loading="lazy">'
- html = f'''
- <div class="historical-post-card" data-post-data='{post_data_json_escaped}' onclick="showPostDetail(this)">
- <div class="historical-post-image">
- {thumbnail_html}
- {f'<div class="post-card-image-count">{len(images)}</div>' if len(images) > 1 else ''}
- </div>
- <div class="historical-post-content">
- <div class="historical-post-title">{html_module.escape(title)}</div>
- <div class="historical-inspiration-info">
- <span class="inspiration-type-badge-small">灵感点</span>
- <span class="inspiration-name-small">{html_module.escape(point_name)}</span>
- </div>
- <div class="historical-inspiration-desc">{html_module.escape(point_desc[:100])}{"..." if len(point_desc) > 100 else ""}</div>
- <div class="historical-post-meta">
- <span class="historical-post-time">📅 {publish_time}</span>
- </div>
- <div class="historical-post-stats">
- <span>❤ {like_count}</span>
- <span>⭐ {collect_count}</span>
- <a href="{link}" target="_blank" class="view-link" onclick="event.stopPropagation()">查看原帖 →</a>
- </div>
- </div>
- </div>
- '''
- return html
- def generate_post_detail_html(post_data: Dict, post_idx: int) -> str:
- """生成帖子详情HTML(紧凑的卡片样式,点击可展开)"""
- post_detail = post_data.get("帖子详情", {})
- title = post_detail.get("title", "无标题")
- body_text = post_detail.get("body_text", "")
- images = post_detail.get("images", [])
- like_count = post_detail.get("like_count", 0)
- comment_count = post_detail.get("comment_count", 0)
- collect_count = post_detail.get("collect_count", 0)
- author = post_detail.get("channel_account_name", "")
- publish_time = post_detail.get("publish_time", "")
- link = post_detail.get("link", "")
- post_id = post_data.get("帖子id", f"post-{post_idx}")
- # 准备详情数据(用于模态框)
- import json
- post_detail_data = {
- "title": title,
- "body_text": body_text,
- "images": images,
- "like_count": like_count,
- "comment_count": comment_count,
- "collect_count": collect_count,
- "author": author,
- "publish_time": publish_time,
- "link": link
- }
- post_data_json = json.dumps(post_detail_data, ensure_ascii=False)
- post_data_json_escaped = html_module.escape(post_data_json)
- # 生成缩略图HTML
- thumbnail_html = ""
- if images and len(images) > 0:
- # 使用第一张图片作为缩略图
- thumbnail_html = f'<img src="{images[0]}" class="post-card-thumbnail" alt="缩略图">'
- else:
- thumbnail_html = '<div class="post-card-thumbnail-placeholder">📄</div>'
- # 截断正文用于预览
- body_preview = body_text[:80] + "..." if len(body_text) > 80 else body_text
- html = f'''
- <div class="post-card-compact" data-post-data='{post_data_json_escaped}' onclick="showPostDetail(this)">
- <div class="post-card-image">
- {thumbnail_html}
- {f'<div class="post-card-image-count">📷 {len(images)}</div>' if len(images) > 1 else ''}
- </div>
- <div class="post-card-content">
- <div class="post-card-title">{html_module.escape(title)}</div>
- <div class="post-card-preview">{html_module.escape(body_preview) if body_preview else "暂无正文"}</div>
- <div class="post-card-meta">
- <span class="post-card-author">👤 {html_module.escape(author)}</span>
- <span class="post-card-time">📅 {publish_time}</span>
- </div>
- <div class="post-card-stats">
- <span>👍 {like_count}</span>
- <span>💬 {comment_count if comment_count else 0}</span>
- <span>⭐ {collect_count if collect_count else 0}</span>
- </div>
- </div>
- </div>
- '''
- return html
- def generate_inspiration_detail_html(inspiration_point: Dict) -> str:
- """生成灵感点详情HTML"""
- name = inspiration_point.get("名称", "")
- desc = inspiration_point.get("描述", "")
- features = inspiration_point.get("特征列表", [])
- features_html = "".join([
- f'<span class="feature-tag">{html_module.escape(f)}</span>'
- for f in features
- ])
- html = f'''
- <div class="inspiration-detail-card">
- <div class="inspiration-header">
- <span class="inspiration-type-badge">灵感点</span>
- <h3 class="inspiration-name">{html_module.escape(name)}</h3>
- </div>
- <div class="inspiration-description">
- <div class="desc-label">描述:</div>
- <div class="desc-text">{html_module.escape(desc)}</div>
- </div>
- <div class="inspiration-features">
- <div class="features-label">特征列表:</div>
- <div class="features-tags">{features_html}</div>
- </div>
- </div>
- '''
- return html
- def load_feature_category_mapping() -> Dict:
- """加载特征名称到分类的映射"""
- script_dir = Path(__file__).parent
- project_root = script_dir.parent.parent
- mapping_file = project_root / "data" / "data_1118" / "特征名称_分类映射.json"
- try:
- with open(mapping_file, "r", encoding="utf-8") as f:
- return json.load(f)
- except Exception as e:
- print(f"警告: 无法加载特征分类映射文件: {e}")
- return {}
- def load_feature_source_mapping() -> Dict:
- """加载特征名称到帖子来源的映射"""
- script_dir = Path(__file__).parent
- project_root = script_dir.parent.parent
- mapping_file = project_root / "data" / "data_1118" / "特征名称_帖子来源.json"
- try:
- with open(mapping_file, "r", encoding="utf-8") as f:
- data = json.load(f)
- # 转换为便于查询的格式: {特征名称: [来源列表]}
- result = {}
- for feature_type in ["灵感点", "关键点", "目的点"]:
- if feature_type in data:
- for item in data[feature_type]:
- feature_name = item.get("特征名称")
- if feature_name:
- result[feature_name] = item.get("特征来源", [])
- return result
- except Exception as e:
- print(f"警告: 无法加载特征来源映射文件: {e}")
- return {}
- def generate_match_results_html(how_steps: List[Dict], feature_idx: int, insp_idx: int, post_idx: int, category_mapping: Dict = None, source_mapping: Dict = None) -> str:
- """生成可折叠的匹配结果HTML"""
- if not how_steps or len(how_steps) == 0:
- return ""
- step = how_steps[0]
- features = step.get("特征列表", [])
- if feature_idx >= len(features):
- return ""
- feature_data = features[feature_idx]
- feature_name = feature_data.get("特征名称", "")
- match_results = feature_data.get("匹配结果", [])
- if category_mapping is None:
- category_mapping = {}
- # 按相似度排序
- sorted_matches = sorted(match_results, key=lambda x: x.get("匹配结果", {}).get("相似度", 0), reverse=True)
- # 统计相似度分布(按区间统计)
- similarity_ranges = {
- "高相似 (≥0.7)": 0,
- "中相似 (0.4-0.7)": 0,
- "低相似 (<0.4)": 0
- }
- for match in match_results:
- similarity = match.get("匹配结果", {}).get("相似度", 0)
- if similarity >= 0.7:
- similarity_ranges["高相似 (≥0.7)"] += 1
- elif similarity >= 0.4:
- similarity_ranges["中相似 (0.4-0.7)"] += 1
- else:
- similarity_ranges["低相似 (<0.4)"] += 1
- # 生成统计信息
- stats_items = []
- range_colors = {
- "高相似 (≥0.7)": "#10b981",
- "中相似 (0.4-0.7)": "#f59e0b",
- "低相似 (<0.4)": "#9ca3af"
- }
- for range_name, count in similarity_ranges.items():
- if count > 0:
- color = range_colors[range_name]
- stats_items.append(f'<span class="stat-badge" style="background: {color};">{range_name}: {count}</span>')
- stats_html = "".join(stats_items)
- # 生成匹配项
- matches_html = ""
- for i, match in enumerate(sorted_matches):
- persona_name = match.get("人设特征名称", "")
- match_result = match.get("匹配结果", {})
- similarity = match_result.get("相似度", 0.0)
- explanation = match_result.get("说明", "")
- # 根据相似度确定颜色
- if similarity >= 0.7:
- color = "#10b981" # 绿色 - 高相似
- label = "高相似"
- elif similarity >= 0.4:
- color = "#f59e0b" # 橙色 - 中相似
- label = "中相似"
- else:
- color = "#9ca3af" # 灰色 - 低相似
- label = "低相似"
- match_id = f"post-{post_idx}-insp-{insp_idx}-feat-{feature_idx}-match-{i}"
- # 获取该人设特征的分类信息
- # 需要在三个类型中查找该特征
- categories_html = ""
- if category_mapping and persona_name:
- found_categories = None
- # 依次在灵感点、关键点、目的点中查找
- for persona_type in ["灵感点", "关键点", "目的点"]:
- if persona_type in category_mapping:
- type_mapping = category_mapping[persona_type]
- if persona_name in type_mapping:
- found_categories = type_mapping[persona_name].get("所属分类", [])
- break
- if found_categories:
- # 简洁样式:[大类/中类/小类]
- categories_reversed = list(reversed(found_categories))
- categories_text = "/".join(categories_reversed)
- categories_html = f'<span class="category-simple">[{html_module.escape(categories_text)}]</span>'
- # 获取该人设特征的历史帖子来源
- historical_posts_html = ""
- if source_mapping and persona_name and persona_name in source_mapping:
- source_list = source_mapping[persona_name]
- if source_list:
- historical_cards = []
- for source_item in source_list:
- post_detail = source_item.get("帖子详情", {})
- if post_detail:
- card_html = generate_historical_post_card_html(post_detail, source_item)
- historical_cards.append(card_html)
- if historical_cards:
- historical_posts_html = f'''
- <div class="historical-posts-section">
- <h4 class="historical-posts-title">历史帖子来源</h4>
- <div class="historical-posts-grid">
- {"".join(historical_cards)}
- </div>
- </div>
- '''
- matches_html += f'''
- <div class="match-item-collapsible">
- <div class="match-header" onclick="toggleMatch('{match_id}')">
- <div class="match-header-left">
- <span class="expand-icon" id="{match_id}-icon">▶</span>
- <span class="persona-name">{categories_html} {html_module.escape(persona_name)}</span>
- <span class="relation-badge" style="background: {color};">{label}</span>
- <span class="score-badge">相似度: {similarity:.2f}</span>
- </div>
- </div>
- <div class="match-content" id="{match_id}-content" style="display: none;">
- <div class="match-explanation">{html_module.escape(explanation)}</div>
- {historical_posts_html}
- </div>
- </div>
- '''
- section_id = f"post-{post_idx}-insp-{insp_idx}-feat-{feature_idx}-section"
- html = f'''
- <div class="match-results-section">
- <div class="match-section-header collapsible-header" onclick="toggleFeatureSection('{section_id}')">
- <div class="header-left">
- <span class="expand-icon" id="{section_id}-icon">▼</span>
- <h4>匹配结果: {html_module.escape(feature_name)}</h4>
- </div>
- <div class="match-stats">{stats_html}</div>
- </div>
- <div class="matches-list" id="{section_id}-content">
- {matches_html}
- </div>
- </div>
- '''
- return html
- def generate_toc_html(post_data: Dict, post_idx: int) -> str:
- """生成目录导航HTML"""
- how_result = post_data.get("how解构结果", {})
- inspiration_list = how_result.get("灵感点列表", [])
- toc_items = []
- # 帖子详情
- 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>')
- # 灵感点
- for insp_idx, inspiration_point in enumerate(inspiration_list):
- name = inspiration_point.get("名称", f"灵感点 {insp_idx + 1}")
- name_short = name[:18] + "..." if len(name) > 18 else name
- toc_items.append(f'<div class="toc-item toc-level-1" onclick="scrollToSection(\'post-{post_idx}-insp-{insp_idx}\')"><span class="toc-badge toc-badge-inspiration">灵感点</span> {html_module.escape(name_short)}</div>')
- # 特征列表
- how_steps = inspiration_point.get("how步骤列表", [])
- if how_steps:
- features = how_steps[0].get("特征列表", [])
- for feat_idx, feature_data in enumerate(features):
- feature_name = feature_data.get("特征名称", f"特征 {feat_idx + 1}")
- toc_items.append(f'<div class="toc-item toc-level-2" onclick="scrollToSection(\'post-{post_idx}-feat-{insp_idx}-{feat_idx}\')"><span class="toc-badge toc-badge-feature">特征</span> {html_module.escape(feature_name)}</div>')
- return f'''
- <div class="toc-container">
- <div class="toc-header">目录导航</div>
- <div class="toc-content">
- {"".join(toc_items)}
- </div>
- </div>
- '''
- def generate_post_content_html(post_data: Dict, post_idx: int, category_mapping: Dict = None, source_mapping: Dict = None) -> str:
- """生成单个帖子的完整内容HTML"""
- # 生成目录
- toc_html = generate_toc_html(post_data, post_idx)
- # 1. 帖子详情
- post_detail_html = generate_post_detail_html(post_data, post_idx)
- # 2. 灵感点详情和匹配结果
- how_result = post_data.get("how解构结果", {})
- inspiration_list = how_result.get("灵感点列表", [])
- # 生成所有灵感点的详情HTML(只包含灵感点详情,不包含匹配结果)
- inspirations_detail_html = ""
- for insp_idx, inspiration_point in enumerate(inspiration_list):
- inspiration_detail = generate_inspiration_detail_html(inspiration_point)
- inspirations_detail_html += f'''
- <div id="post-{post_idx}-insp-{insp_idx}" class="inspiration-detail-item content-section">
- {inspiration_detail}
- </div>
- '''
- # 生成所有匹配结果HTML,按照how步骤分组
- all_matches_html = ""
- for insp_idx, inspiration_point in enumerate(inspiration_list):
- inspiration_name = inspiration_point.get("名称", f"灵感点 {insp_idx + 1}")
- how_steps = inspiration_point.get("how步骤列表", [])
- if how_steps:
- # 为每个灵感点创建一个区域
- for step_idx, step in enumerate(how_steps):
- step_name = step.get("步骤名称", f"步骤 {step_idx + 1}")
- features = step.get("特征列表", [])
- # 生成该步骤下所有特征的匹配结果
- features_html = ""
- for feat_idx, feature_data in enumerate(features):
- match_html = generate_match_results_html([step], feat_idx, insp_idx, post_idx, category_mapping, source_mapping)
- features_html += f'<div id="post-{post_idx}-feat-{insp_idx}-{feat_idx}" class="feature-match-wrapper">{match_html}</div>'
- # 生成步骤区域(可折叠)
- step_section_id = f"post-{post_idx}-step-{insp_idx}-{step_idx}"
- all_matches_html += f'''
- <div class="step-section">
- <div class="step-header collapsible-header" onclick="toggleStepSection('{step_section_id}')">
- <div class="header-left">
- <span class="expand-icon" id="{step_section_id}-icon">▼</span>
- <h3 class="step-name">{html_module.escape(step_name)}</h3>
- </div>
- <span class="step-inspiration-name">来自: {html_module.escape(inspiration_name)}</span>
- </div>
- <div class="step-features-list" id="{step_section_id}-content">
- {features_html}
- </div>
- </div>
- '''
- html = f'''
- <div class="content-with-toc">
- {toc_html}
- <div class="main-content">
- <!-- 第一个框:左右分栏(帖子详情 + 灵感点详情) -->
- <div class="top-section-box">
- <div class="two-column-layout">
- <div class="left-column">
- <div id="post-{post_idx}-detail" class="post-detail-wrapper">
- {post_detail_html}
- </div>
- </div>
- <div class="right-column">
- <div class="inspirations-detail-wrapper">
- {inspirations_detail_html}
- </div>
- </div>
- </div>
- </div>
- <!-- 下面:所有匹配结果 -->
- <div class="matches-section">
- {all_matches_html}
- </div>
- </div>
- </div>
- '''
- return html
- def generate_combined_html(posts_data: List[Dict], category_mapping: Dict = None, source_mapping: Dict = None) -> str:
- """生成包含所有帖子的单一HTML(带标签页)"""
- # 生成标签页按钮
- tabs_html = ""
- for i, post in enumerate(posts_data):
- post_detail = post.get("帖子详情", {})
- title = post_detail.get("title", "无标题")
- active_class = "active" if i == 0 else ""
- tabs_html += f'<button class="tab-button {active_class}" onclick="openTab(event, \'post-{i}\')">{html_module.escape(title)}</button>\n'
- # 生成标签页内容
- contents_html = ""
- for i, post in enumerate(posts_data):
- active_class = "active" if i == 0 else ""
- content = generate_post_content_html(post, i, category_mapping, source_mapping)
- contents_html += f'''
- <div id="post-{i}" class="tab-content {active_class}">
- {content}
- </div>
- '''
- html = f'''
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>How解构结果可视化</title>
- <style>
- * {{
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }}
- body {{
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- background: #f5f5f5;
- color: #333;
- line-height: 1.6;
- }}
- .container {{
- max-width: 1600px;
- margin: 0 auto;
- background: white;
- min-height: 100vh;
- }}
- .header {{
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- padding: 30px;
- text-align: center;
- }}
- .header h1 {{
- font-size: 32px;
- font-weight: bold;
- margin-bottom: 10px;
- }}
- .header p {{
- font-size: 16px;
- opacity: 0.9;
- }}
- /* 标签页样式 */
- .tabs-container {{
- display: flex;
- background: #f9fafb;
- border-bottom: 2px solid #e5e7eb;
- overflow-x: auto;
- position: sticky;
- top: 0;
- z-index: 100;
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
- }}
- .tab-button {{
- flex: 1;
- min-width: 200px;
- padding: 18px 30px;
- background: transparent;
- border: none;
- border-bottom: 3px solid transparent;
- cursor: pointer;
- font-size: 15px;
- font-weight: 500;
- color: #6b7280;
- transition: all 0.3s;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }}
- .tab-button:hover {{
- background: #f3f4f6;
- color: #374151;
- }}
- .tab-button.active {{
- color: #667eea;
- border-bottom-color: #667eea;
- background: white;
- }}
- .tab-content {{
- display: none;
- padding: 30px;
- }}
- .tab-content.active {{
- display: block;
- animation: fadeIn 0.3s;
- }}
- @keyframes fadeIn {{
- from {{ opacity: 0; transform: translateY(10px); }}
- to {{ opacity: 1; transform: translateY(0); }}
- }}
- /* 目录和主内容布局 */
- .content-with-toc {{
- display: grid;
- grid-template-columns: 280px 1fr;
- gap: 30px;
- align-items: start;
- }}
- .toc-container {{
- position: sticky;
- top: 80px;
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- overflow: hidden;
- max-height: calc(100vh - 100px);
- display: flex;
- flex-direction: column;
- }}
- .toc-header {{
- padding: 15px 20px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- font-weight: 600;
- font-size: 16px;
- }}
- .toc-content {{
- padding: 10px;
- overflow-y: auto;
- flex: 1;
- }}
- .toc-item {{
- padding: 10px 15px;
- margin: 4px 0;
- cursor: pointer;
- border-radius: 6px;
- font-size: 14px;
- transition: all 0.2s;
- user-select: none;
- }}
- .toc-item:hover {{
- background: #f3f4f6;
- }}
- .toc-item.active {{
- background: #e0e7ff;
- color: #667eea;
- font-weight: 600;
- }}
- .toc-level-1 {{
- color: #111827;
- font-weight: 500;
- }}
- .toc-level-2 {{
- color: #6b7280;
- font-size: 13px;
- padding-left: 30px;
- }}
- .toc-badge {{
- display: inline-block;
- padding: 2px 8px;
- border-radius: 10px;
- font-size: 11px;
- font-weight: 600;
- margin-right: 6px;
- }}
- .toc-badge-post {{
- background: #dbeafe;
- color: #1e40af;
- }}
- .toc-badge-inspiration {{
- background: #fef3c7;
- color: #92400e;
- }}
- .toc-badge-feature {{
- background: #e0e7ff;
- color: #3730a3;
- }}
- .main-content {{
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 30px;
- }}
- /* 顶部框:包含帖子详情和灵感点详情 */
- .top-section-box {{
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- padding: 25px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
- }}
- /* 顶部框内的两栏布局 */
- .two-column-layout {{
- display: grid;
- grid-template-columns: 380px 1fr;
- gap: 25px;
- align-items: start;
- }}
- .left-column {{
- position: relative;
- }}
- .post-detail-wrapper {{
- /* 帖子详情区域 */
- }}
- .right-column {{
- min-width: 0;
- }}
- .inspirations-detail-wrapper {{
- display: flex;
- flex-direction: column;
- gap: 20px;
- max-height: 600px;
- overflow-y: auto;
- padding-right: 10px;
- }}
- .inspiration-detail-item {{
- /* 单个灵感点详情 */
- }}
- /* 匹配结果区域 */
- .matches-section {{
- display: flex;
- flex-direction: column;
- gap: 20px;
- }}
- /* 步骤区域 */
- .step-section {{
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- overflow: hidden;
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
- }}
- .step-header {{
- padding: 20px 25px;
- background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
- border-bottom: 2px solid #e5e7eb;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }}
- .step-name {{
- font-size: 18px;
- color: #111827;
- font-weight: 600;
- margin: 0;
- }}
- .step-inspiration-name {{
- font-size: 14px;
- color: #6b7280;
- font-style: italic;
- }}
- .step-features-list {{
- padding: 20px;
- display: flex;
- flex-direction: column;
- gap: 15px;
- }}
- .feature-match-wrapper {{
- /* 特征匹配容器 */
- }}
- .content-section {{
- scroll-margin-top: 80px;
- }}
- /* 帖子详情卡片(紧凑样式) */
- .post-card-compact {{
- display: grid;
- grid-template-columns: 200px 1fr;
- gap: 20px;
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- overflow: hidden;
- padding: 20px;
- cursor: pointer;
- transition: all 0.3s;
- margin-bottom: 30px;
- }}
- .post-card-compact:hover {{
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
- transform: translateY(-2px);
- border-color: #667eea;
- }}
- .post-card-image {{
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- }}
- .post-card-thumbnail {{
- width: 100%;
- height: 150px;
- object-fit: contain;
- border-radius: 8px;
- background: #f9fafb;
- }}
- .post-card-thumbnail-placeholder {{
- width: 100%;
- height: 150px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #f3f4f6;
- color: #9ca3af;
- font-size: 48px;
- border-radius: 8px;
- }}
- .post-card-image-count {{
- position: absolute;
- bottom: 8px;
- right: 8px;
- background: rgba(0, 0, 0, 0.7);
- color: white;
- padding: 4px 8px;
- border-radius: 12px;
- font-size: 11px;
- font-weight: 600;
- }}
- .post-card-content {{
- display: flex;
- flex-direction: column;
- gap: 12px;
- }}
- .post-card-title {{
- font-size: 18px;
- font-weight: 600;
- color: #111827;
- line-height: 1.4;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- }}
- .post-card-preview {{
- font-size: 14px;
- color: #6b7280;
- line-height: 1.6;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- }}
- .post-card-meta {{
- display: flex;
- gap: 15px;
- font-size: 13px;
- color: #9ca3af;
- }}
- .post-card-stats {{
- display: flex;
- gap: 15px;
- font-size: 13px;
- color: #6b7280;
- font-weight: 500;
- }}
- /* 灵感点详情卡片样式保持不变 */
- .inspiration-detail-card {{
- background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
- padding: 25px;
- border-bottom: 2px solid #e5e7eb;
- }}
- .inspiration-header {{
- margin-bottom: 15px;
- display: flex;
- align-items: center;
- gap: 10px;
- }}
- .inspiration-type-badge {{
- display: inline-block;
- padding: 4px 12px;
- background: #fef3c7;
- color: #92400e;
- border-radius: 12px;
- font-size: 12px;
- font-weight: 600;
- }}
- .inspiration-name {{
- font-size: 20px;
- color: #111827;
- font-weight: 600;
- margin: 0;
- }}
- .inspiration-description {{
- margin-bottom: 15px;
- }}
- .desc-label {{
- font-weight: 600;
- color: #6b7280;
- font-size: 13px;
- margin-bottom: 8px;
- }}
- .desc-text {{
- color: #4b5563;
- font-size: 14px;
- line-height: 1.7;
- }}
- .inspiration-features {{
- margin-top: 15px;
- }}
- .features-label {{
- font-weight: 600;
- color: #6b7280;
- font-size: 13px;
- margin-bottom: 8px;
- }}
- .features-tags {{
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }}
- .feature-tag {{
- padding: 5px 12px;
- background: #667eea;
- color: white;
- border-radius: 16px;
- font-size: 13px;
- font-weight: 500;
- }}
- /* 匹配结果部分 */
- .match-results-section {{
- padding: 25px;
- border-bottom: 1px solid #e5e7eb;
- }}
- .match-results-section:last-child {{
- border-bottom: none;
- }}
- .match-section-header {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
- padding-bottom: 15px;
- border-bottom: 2px solid #e5e7eb;
- }}
- .collapsible-header {{
- cursor: pointer;
- user-select: none;
- transition: background 0.2s;
- padding: 10px;
- margin: -10px;
- border-radius: 6px;
- }}
- .collapsible-header:hover {{
- background: #f9fafb;
- }}
- .header-left {{
- display: flex;
- align-items: center;
- gap: 10px;
- }}
- .match-section-header h4 {{
- font-size: 18px;
- color: #111827;
- margin: 0;
- }}
- .match-stats {{
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- }}
- .stat-badge {{
- padding: 4px 10px;
- border-radius: 12px;
- color: white;
- font-size: 12px;
- font-weight: 600;
- }}
- .matches-list {{
- display: flex;
- flex-direction: column;
- gap: 8px;
- }}
- .match-item-collapsible {{
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- overflow: hidden;
- background: white;
- transition: box-shadow 0.2s;
- }}
- .match-item-collapsible:hover {{
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
- }}
- .match-header {{
- padding: 12px 16px;
- cursor: pointer;
- user-select: none;
- transition: background 0.2s;
- }}
- .match-header:hover {{
- background: #f9fafb;
- }}
- .match-header-left {{
- display: flex;
- align-items: center;
- gap: 10px;
- }}
- .expand-icon {{
- color: #9ca3af;
- font-size: 12px;
- transition: transform 0.2s;
- }}
- .expand-icon.expanded {{
- transform: rotate(90deg);
- }}
- .persona-name {{
- font-weight: 600;
- font-size: 14px;
- color: #111827;
- }}
- .relation-badge {{
- padding: 3px 10px;
- border-radius: 12px;
- color: white;
- font-size: 11px;
- font-weight: 600;
- }}
- .score-badge {{
- padding: 3px 10px;
- border-radius: 12px;
- background: #e5e7eb;
- color: #374151;
- font-size: 11px;
- font-weight: 600;
- }}
- .match-content {{
- padding: 16px;
- background: #f9fafb;
- border-top: 1px solid #e5e7eb;
- }}
- .match-explanation {{
- font-size: 13px;
- color: #4b5563;
- line-height: 1.7;
- margin-bottom: 16px;
- }}
- /* 历史帖子来源区域 */
- .historical-posts-section {{
- margin-top: 20px;
- padding-top: 20px;
- border-top: 2px solid #e5e7eb;
- }}
- .historical-posts-title {{
- font-size: 14px;
- font-weight: 600;
- color: #374151;
- margin-bottom: 12px;
- }}
- .historical-posts-grid {{
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
- gap: 16px;
- }}
- .historical-post-card {{
- display: grid;
- grid-template-columns: 120px 1fr;
- gap: 12px;
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- padding: 12px;
- transition: all 0.2s;
- cursor: pointer;
- }}
- .historical-post-card:hover {{
- border-color: #667eea;
- box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1);
- transform: translateY(-2px);
- }}
- .historical-post-image {{
- position: relative;
- }}
- .historical-post-thumbnail {{
- width: 100%;
- height: 100px;
- object-fit: contain;
- border-radius: 6px;
- background: #f9fafb;
- }}
- .historical-post-content {{
- display: flex;
- flex-direction: column;
- gap: 8px;
- }}
- .historical-post-title {{
- font-size: 13px;
- font-weight: 600;
- color: #1f2937;
- line-height: 1.4;
- overflow: hidden;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- }}
- .historical-inspiration-info {{
- display: flex;
- align-items: center;
- gap: 6px;
- }}
- .inspiration-type-badge-small {{
- padding: 2px 6px;
- border-radius: 4px;
- background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
- color: white;
- font-size: 10px;
- font-weight: 600;
- }}
- .inspiration-name-small {{
- font-size: 11px;
- color: #667eea;
- font-weight: 500;
- }}
- .historical-inspiration-desc {{
- font-size: 11px;
- color: #6b7280;
- line-height: 1.5;
- overflow: hidden;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- }}
- .historical-post-meta {{
- margin: 4px 0;
- }}
- .historical-post-time {{
- font-size: 11px;
- color: #9ca3af;
- }}
- .historical-post-stats {{
- display: flex;
- align-items: center;
- gap: 10px;
- font-size: 11px;
- color: #9ca3af;
- }}
- .view-link {{
- margin-left: auto;
- color: #667eea;
- text-decoration: none;
- font-weight: 500;
- }}
- .view-link:hover {{
- color: #764ba2;
- }}
- .category-simple {{
- color: #9ca3af;
- font-size: 12px;
- font-weight: 400;
- margin-right: 6px;
- }}
- /* 帖子详情模态框 */
- .post-detail-modal {{
- display: none;
- position: fixed;
- z-index: 1000;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.7);
- overflow: auto;
- animation: fadeIn 0.3s;
- }}
- .post-detail-modal.active {{
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 40px 20px;
- }}
- .post-detail-content {{
- position: relative;
- background: white;
- border-radius: 16px;
- max-width: 900px;
- width: 100%;
- max-height: 90vh;
- overflow-y: auto;
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
- }}
- .post-detail-close {{
- position: sticky;
- top: 20px;
- right: 20px;
- float: right;
- font-size: 36px;
- font-weight: 300;
- color: #9ca3af;
- background: white;
- border: none;
- cursor: pointer;
- width: 40px;
- height: 40px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
- z-index: 10;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }}
- .post-detail-close:hover {{
- color: #ef4444;
- background: #fee2e2;
- transform: rotate(90deg);
- }}
- .post-detail-header {{
- padding: 40px 40px 20px 40px;
- border-bottom: 2px solid #e5e7eb;
- }}
- .post-detail-title {{
- font-size: 28px;
- font-weight: bold;
- color: #111827;
- line-height: 1.4;
- margin-bottom: 15px;
- }}
- .post-detail-meta {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- color: #6b7280;
- font-size: 14px;
- gap: 20px;
- }}
- .post-detail-author {{
- font-weight: 500;
- }}
- .post-detail-time {{
- color: #9ca3af;
- }}
- .post-detail-stats {{
- display: flex;
- gap: 15px;
- font-weight: 500;
- }}
- .post-detail-body {{
- padding: 30px 40px;
- }}
- .post-detail-desc {{
- font-size: 15px;
- color: #4b5563;
- line-height: 1.8;
- margin-bottom: 25px;
- white-space: pre-wrap;
- }}
- .post-detail-images {{
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 15px;
- margin-top: 20px;
- }}
- .post-detail-image {{
- width: 100%;
- border-radius: 8px;
- object-fit: cover;
- cursor: pointer;
- transition: transform 0.2s;
- }}
- .post-detail-image:hover {{
- transform: scale(1.02);
- }}
- .post-detail-footer {{
- padding: 20px 40px 30px 40px;
- border-top: 1px solid #e5e7eb;
- text-align: center;
- }}
- .post-detail-link {{
- display: inline-block;
- padding: 12px 30px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- text-decoration: none;
- border-radius: 8px;
- font-weight: 500;
- transition: all 0.3s;
- }}
- .post-detail-link:hover {{
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
- }}
- /* 响应式 */
- @media (max-width: 1400px) {{
- .content-with-toc {{
- grid-template-columns: 240px 1fr;
- }}
- .two-column-layout {{
- grid-template-columns: 320px 1fr;
- }}
- }}
- @media (max-width: 1200px) {{
- .content-with-toc {{
- grid-template-columns: 200px 1fr;
- }}
- .two-column-layout {{
- grid-template-columns: 280px 1fr;
- }}
- }}
- @media (max-width: 1024px) {{
- .content-with-toc {{
- grid-template-columns: 1fr;
- }}
- .toc-container {{
- position: relative;
- top: 0;
- max-height: 300px;
- }}
- .two-column-layout {{
- grid-template-columns: 1fr;
- }}
- .inspirations-detail-wrapper {{
- max-height: none;
- }}
- .post-card-compact {{
- grid-template-columns: 150px 1fr;
- }}
- }}
- @media (max-width: 768px) {{
- .header {{
- padding: 20px;
- }}
- .header h1 {{
- font-size: 24px;
- }}
- .tab-button {{
- min-width: 150px;
- padding: 15px 20px;
- }}
- .tab-content {{
- padding: 15px;
- }}
- .post-info-section {{
- padding: 20px;
- }}
- }}
- </style>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <h1>How 解构结果可视化</h1>
- <p>灵感点特征匹配分析</p>
- </div>
- <div class="tabs-container">
- {tabs_html}
- </div>
- {contents_html}
- </div>
- <!-- 帖子详情模态框 -->
- <div id="postDetailModal" class="post-detail-modal" onclick="closePostDetail(event)"></div>
- <script>
- function openTab(evt, tabId) {{
- var tabContents = document.getElementsByClassName("tab-content");
- for (var i = 0; i < tabContents.length; i++) {{
- tabContents[i].classList.remove("active");
- }}
- var tabButtons = document.getElementsByClassName("tab-button");
- for (var i = 0; i < tabButtons.length; i++) {{
- tabButtons[i].classList.remove("active");
- }}
- document.getElementById(tabId).classList.add("active");
- evt.currentTarget.classList.add("active");
- }}
- function scrollToSection(sectionId) {{
- var element = document.getElementById(sectionId);
- if (element) {{
- element.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
- // 更新目录项的active状态
- var tocItems = document.querySelectorAll('.toc-item');
- tocItems.forEach(function(item) {{
- item.classList.remove('active');
- }});
- event.currentTarget.classList.add('active');
- }}
- }}
- function toggleMatch(matchId) {{
- var content = document.getElementById(matchId + '-content');
- var icon = document.getElementById(matchId + '-icon');
- if (content.style.display === 'none') {{
- content.style.display = 'block';
- icon.classList.add('expanded');
- }} else {{
- content.style.display = 'none';
- icon.classList.remove('expanded');
- }}
- }}
- function toggleFeatureSection(sectionId) {{
- var content = document.getElementById(sectionId + '-content');
- var icon = document.getElementById(sectionId + '-icon');
- if (content.style.display === 'none') {{
- content.style.display = 'flex';
- icon.textContent = '▼';
- }} else {{
- content.style.display = 'none';
- icon.textContent = '▶';
- }}
- }}
- function toggleStepSection(sectionId) {{
- var content = document.getElementById(sectionId + '-content');
- var icon = document.getElementById(sectionId + '-icon');
- if (content.style.display === 'none') {{
- content.style.display = 'flex';
- icon.textContent = '▼';
- }} else {{
- content.style.display = 'none';
- icon.textContent = '▶';
- }}
- }}
- function showPostDetail(element) {{
- const postDataStr = element.dataset.postData;
- if (!postDataStr) return;
- try {{
- const postData = JSON.parse(postDataStr);
- // 生成图片HTML
- let imagesHtml = '';
- if (postData.images && postData.images.length > 0) {{
- imagesHtml = postData.images.map(img =>
- `<img src="${{img}}" class="post-detail-image" alt="图片">`
- ).join('');
- }} else {{
- imagesHtml = '<div style="text-align: center; color: #9ca3af; padding: 40px;">暂无图片</div>';
- }}
- const modalHtml = `
- <div class="post-detail-content" onclick="event.stopPropagation()">
- <button class="post-detail-close" onclick="closePostDetail()">×</button>
- <div class="post-detail-header">
- <div class="post-detail-title">${{postData.title || '无标题'}}</div>
- <div class="post-detail-meta">
- <div>
- <span class="post-detail-author">👤 ${{postData.author}}</span>
- <span class="post-detail-time"> · 📅 ${{postData.publish_time}}</span>
- </div>
- <div class="post-detail-stats">
- <span>👍 ${{postData.like_count}}</span>
- <span>💬 ${{postData.comment_count}}</span>
- <span>⭐ ${{postData.collect_count}}</span>
- </div>
- </div>
- </div>
- <div class="post-detail-body">
- ${{postData.body_text ? `<div class="post-detail-desc">${{postData.body_text}}</div>` : '<div style="color: #9ca3af;">暂无正文</div>'}}
- <div class="post-detail-images">
- ${{imagesHtml}}
- </div>
- </div>
- <div class="post-detail-footer">
- <a href="${{postData.link}}" target="_blank" class="post-detail-link">
- 在小红书查看完整内容 →
- </a>
- </div>
- </div>
- `;
- let modal = document.getElementById('postDetailModal');
- if (!modal) {{
- modal = document.createElement('div');
- modal.id = 'postDetailModal';
- modal.className = 'post-detail-modal';
- modal.onclick = closePostDetail;
- document.body.appendChild(modal);
- }}
- modal.innerHTML = modalHtml;
- modal.classList.add('active');
- document.body.style.overflow = 'hidden';
- }} catch (e) {{
- console.error('解析帖子数据失败:', e);
- }}
- }}
- function closePostDetail(event) {{
- if (event && event.target !== event.currentTarget) return;
- const modal = document.getElementById('postDetailModal');
- if (modal) {{
- modal.classList.remove('active');
- document.body.style.overflow = '';
- }}
- }}
- function moveImage(postId, direction) {{
- var carousel = document.querySelector(`[data-post-id="${{postId}}"]`);
- var track = document.getElementById(postId + '-track');
- var totalImages = parseInt(carousel.getAttribute('data-total-images'));
- if (!carousel.currentIndex) {{
- carousel.currentIndex = 0;
- }}
- carousel.currentIndex = (carousel.currentIndex + direction + totalImages) % totalImages;
- track.style.transform = `translateX(-${{carousel.currentIndex * 100}}%)`;
- updateIndicators(postId, carousel.currentIndex);
- }}
- function jumpToImage(postId, index) {{
- var carousel = document.querySelector(`[data-post-id="${{postId}}"]`);
- var track = document.getElementById(postId + '-track');
- carousel.currentIndex = index;
- track.style.transform = `translateX(-${{index * 100}}%)`;
- updateIndicators(postId, index);
- }}
- function updateIndicators(postId, activeIndex) {{
- var indicators = document.querySelectorAll(`#${{postId}}-indicators .indicator`);
- indicators.forEach((indicator, i) => {{
- if (i === activeIndex) {{
- indicator.classList.add('active');
- }} else {{
- indicator.classList.remove('active');
- }}
- }});
- }}
- // 目录激活状态追踪
- function updateTocActiveState() {{
- const mainContent = document.querySelector('.main-content');
- if (!mainContent) return;
- const sections = document.querySelectorAll('.post-detail-wrapper, .inspiration-detail-card, .step-section');
- const tocItems = document.querySelectorAll('.toc-item');
- let currentActive = null;
- const scrollTop = mainContent.scrollTop;
- const windowHeight = window.innerHeight;
- // 找到当前在视口中的section
- sections.forEach(section => {{
- const rect = section.getBoundingClientRect();
- const sectionTop = rect.top;
- const sectionBottom = rect.bottom;
- // 如果section的顶部在视口上半部分,认为它是当前激活的
- if (sectionTop < windowHeight / 2 && sectionBottom > 0) {{
- currentActive = section.id;
- }}
- }});
- // 更新目录项的激活状态
- tocItems.forEach(item => {{
- const targetId = item.getAttribute('onclick')?.match(/'([^']+)'/)?.[1];
- if (targetId === currentActive) {{
- item.classList.add('active');
- }} else {{
- item.classList.remove('active');
- }}
- }});
- }}
- // 监听滚动事件
- document.addEventListener('DOMContentLoaded', function() {{
- const mainContent = document.querySelector('.main-content');
- if (mainContent) {{
- mainContent.addEventListener('scroll', function() {{
- // 使用节流避免频繁触发
- if (this.scrollTimeout) clearTimeout(this.scrollTimeout);
- this.scrollTimeout = setTimeout(updateTocActiveState, 50);
- }});
- // 初始化时更新一次
- updateTocActiveState();
- }}
- }});
- </script>
- </body>
- </html>
- '''
- return html
- def main():
- """主函数"""
- script_dir = Path(__file__).parent
- project_root = script_dir.parent.parent
- data_dir = project_root / "data" / "data_1118"
- input_dir = data_dir / "当前帖子_how解构结果"
- output_file = data_dir / "当前帖子_how解构结果_可视化.html"
- print(f"读取 how 解构结果: {input_dir}")
- # 加载特征分类映射
- print(f"加载特征分类映射...")
- category_mapping = load_feature_category_mapping()
- print(f"已加载 {sum(len(v) for v in category_mapping.values())} 个特征分类")
- # 加载特征来源映射
- print(f"加载特征来源映射...")
- source_mapping = load_feature_source_mapping()
- print(f"已加载 {len(source_mapping)} 个特征的来源信息")
- json_files = list(input_dir.glob("*_how.json"))
- print(f"找到 {len(json_files)} 个文件\n")
- posts_data = []
- for i, file_path in enumerate(json_files, 1):
- print(f"读取文件 [{i}/{len(json_files)}]: {file_path.name}")
- with open(file_path, "r", encoding="utf-8") as f:
- post_data = json.load(f)
- posts_data.append(post_data)
- print(f"\n生成合并的 HTML...")
- html_content = generate_combined_html(posts_data, category_mapping, source_mapping)
- print(f"保存到: {output_file}")
- with open(output_file, "w", encoding="utf-8") as f:
- f.write(html_content)
- print(f"\n完成! 可视化文件已保存")
- print(f"请在浏览器中打开: {output_file}")
- if __name__ == "__main__":
- main()
|