|
@@ -0,0 +1,2068 @@
|
|
|
|
|
+"""
|
|
|
|
|
+灵感点分析结果可视化脚本
|
|
|
|
|
+
|
|
|
|
|
+读取 how/灵感点 目录下的分析结果,结合作者历史帖子详情,生成可视化HTML页面
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+import json
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+from typing import Dict, Any, List, Optional
|
|
|
|
|
+from datetime import datetime
|
|
|
|
|
+import html as html_module
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def load_inspiration_points_data(inspiration_dir: str) -> List[Dict[str, Any]]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 加载所有灵感点的分析结果
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ inspiration_dir: 灵感点目录路径
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 灵感点分析结果列表
|
|
|
|
|
+ """
|
|
|
|
|
+ inspiration_path = Path(inspiration_dir)
|
|
|
|
|
+ results = []
|
|
|
|
|
+
|
|
|
|
|
+ # 遍历所有子目录
|
|
|
|
|
+ for subdir in inspiration_path.iterdir():
|
|
|
|
|
+ if subdir.is_dir():
|
|
|
|
|
+ # 查找 all_summary 文件
|
|
|
|
|
+ summary_files = list(subdir.glob("all_summary_*.json"))
|
|
|
|
|
+ if summary_files:
|
|
|
|
|
+ summary_file = summary_files[0]
|
|
|
|
|
+ try:
|
|
|
|
|
+ with open(summary_file, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ data = json.load(f)
|
|
|
|
|
+
|
|
|
|
|
+ # 加载完整的 step1 和 step2 数据
|
|
|
|
|
+ step1_data = None
|
|
|
|
|
+ step2_data = None
|
|
|
|
|
+
|
|
|
|
|
+ # 直接从当前子目录查找 step1 和 step2 文件
|
|
|
|
|
+ step1_files = list(subdir.glob("all_step1_*.json"))
|
|
|
|
|
+ step2_files = list(subdir.glob("all_step2_*.json"))
|
|
|
|
|
+
|
|
|
|
|
+ if step1_files:
|
|
|
|
|
+ try:
|
|
|
|
|
+ with open(step1_files[0], 'r', encoding='utf-8') as f:
|
|
|
|
|
+ step1_data = json.load(f)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"警告: 读取 {step1_files[0]} 失败: {e}")
|
|
|
|
|
+
|
|
|
|
|
+ if step2_files:
|
|
|
|
|
+ try:
|
|
|
|
|
+ with open(step2_files[0], 'r', encoding='utf-8') as f:
|
|
|
|
|
+ step2_data = json.load(f)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"警告: 读取 {step2_files[0]} 失败: {e}")
|
|
|
|
|
+
|
|
|
|
|
+ results.append({
|
|
|
|
|
+ "summary": data,
|
|
|
|
|
+ "step1": step1_data,
|
|
|
|
|
+ "step2": step2_data,
|
|
|
|
|
+ "inspiration_name": subdir.name
|
|
|
|
|
+ })
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"警告: 读取 {summary_file} 失败: {e}")
|
|
|
|
|
+
|
|
|
|
|
+ return results
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def load_posts_data(posts_dir: str) -> Dict[str, Dict[str, Any]]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 加载所有帖子详情数据
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ posts_dir: 帖子目录路径
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 帖子ID到帖子详情的映射
|
|
|
|
|
+ """
|
|
|
|
|
+ posts_path = Path(posts_dir)
|
|
|
|
|
+ posts_map = {}
|
|
|
|
|
+
|
|
|
|
|
+ for post_file in posts_path.glob("*.json"):
|
|
|
|
|
+ try:
|
|
|
|
|
+ with open(post_file, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ post_data = json.load(f)
|
|
|
|
|
+ post_id = post_data.get("channel_content_id")
|
|
|
|
|
+ if post_id:
|
|
|
|
|
+ posts_map[post_id] = post_data
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"警告: 读取 {post_file} 失败: {e}")
|
|
|
|
|
+
|
|
|
|
|
+ return posts_map
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def generate_inspiration_card_html(inspiration_data: Dict[str, Any]) -> str:
|
|
|
|
|
+ """
|
|
|
|
|
+ 生成单个灵感点的卡片HTML
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ inspiration_data: 灵感点数据
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ HTML字符串
|
|
|
|
|
+ """
|
|
|
|
|
+ summary = inspiration_data.get("summary", {})
|
|
|
|
|
+ step1 = inspiration_data.get("step1", {})
|
|
|
|
|
+ step2 = inspiration_data.get("step2", {})
|
|
|
|
|
+ inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
|
|
|
|
|
+
|
|
|
|
|
+ # 提取关键指标
|
|
|
|
|
+ metrics = summary.get("关键指标", {})
|
|
|
|
|
+ step1_score = metrics.get("step1_top1_score", 0)
|
|
|
|
|
+ step2_score = metrics.get("step2_score", 0)
|
|
|
|
|
+ step1_match_element = metrics.get("step1_top1_匹配要素", "")
|
|
|
|
|
+ step2_increment_count = metrics.get("step2_增量词数量", 0)
|
|
|
|
|
+
|
|
|
|
|
+ # 确定卡片颜色(基于Step1分数)
|
|
|
|
|
+ if step1_score >= 0.7:
|
|
|
|
|
+ border_color = "#10b981"
|
|
|
|
|
+ step1_color = "#10b981"
|
|
|
|
|
+ elif step1_score >= 0.5:
|
|
|
|
|
+ border_color = "#f59e0b"
|
|
|
|
|
+ step1_color = "#f59e0b"
|
|
|
|
|
+ elif step1_score >= 0.3:
|
|
|
|
|
+ border_color = "#3b82f6"
|
|
|
|
|
+ step1_color = "#3b82f6"
|
|
|
|
|
+ else:
|
|
|
|
|
+ border_color = "#ef4444"
|
|
|
|
|
+ step1_color = "#ef4444"
|
|
|
|
|
+
|
|
|
|
|
+ # Step2颜色
|
|
|
|
|
+ if step2_score >= 0.7:
|
|
|
|
|
+ step2_color = "#10b981"
|
|
|
|
|
+ elif step2_score >= 0.5:
|
|
|
|
|
+ step2_color = "#f59e0b"
|
|
|
|
|
+ elif step2_score >= 0.3:
|
|
|
|
|
+ step2_color = "#3b82f6"
|
|
|
|
|
+ else:
|
|
|
|
|
+ step2_color = "#ef4444"
|
|
|
|
|
+
|
|
|
|
|
+ # 转义HTML
|
|
|
|
|
+ inspiration_name_escaped = html_module.escape(inspiration_name)
|
|
|
|
|
+ step1_match_element_escaped = html_module.escape(step1_match_element)
|
|
|
|
|
+
|
|
|
|
|
+ # 获取Step1匹配结果(简要展示)
|
|
|
|
|
+ step1_matches = step1.get("匹配结果列表", []) if step1 else []
|
|
|
|
|
+ step1_match_preview = ""
|
|
|
|
|
+ if step1_matches:
|
|
|
|
|
+ top_match = step1_matches[0]
|
|
|
|
|
+ # 从新的数据结构中提取信息
|
|
|
|
|
+ input_info = top_match.get("输入信息", {})
|
|
|
|
|
+ match_result = top_match.get("匹配结果", {})
|
|
|
|
|
+ element_name = input_info.get("A", "")
|
|
|
|
|
+ match_score = match_result.get("score", 0)
|
|
|
|
|
+ same_parts = match_result.get("相同部分", {})
|
|
|
|
|
+ increment_parts = match_result.get("增量部分", {})
|
|
|
|
|
+
|
|
|
|
|
+ # 生成相同部分和增量部分的HTML
|
|
|
|
|
+ parts_html = ""
|
|
|
|
|
+ if same_parts:
|
|
|
|
|
+ same_items = [f"{html_module.escape(k)}" for k in same_parts.keys()]
|
|
|
|
|
+ parts_html += f'<div class="preview-parts same"><strong>相同:</strong> {", ".join(same_items)}</div>'
|
|
|
|
|
+
|
|
|
|
|
+ if increment_parts:
|
|
|
|
|
+ inc_items = [f"{html_module.escape(k)}" for k in increment_parts.keys()]
|
|
|
|
|
+ parts_html += f'<div class="preview-parts increment"><strong>增量:</strong> {", ".join(inc_items)}</div>'
|
|
|
|
|
+
|
|
|
|
|
+ step1_match_preview = f'''
|
|
|
|
|
+ <div class="match-preview">
|
|
|
|
|
+ <div class="match-preview-header">🎯 Step1 Top1匹配</div>
|
|
|
|
|
+ <div class="match-preview-content">
|
|
|
|
|
+ <span class="match-preview-name">{html_module.escape(element_name)}</span>
|
|
|
|
|
+ <span class="match-preview-score" style="color: {step1_color};">{match_score:.2f}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {parts_html}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ # 获取Step2匹配结果(简要展示)
|
|
|
|
|
+ step2_match_preview = ""
|
|
|
|
|
+ if step2:
|
|
|
|
|
+ input_info = step2.get("输入信息", {})
|
|
|
|
|
+ match_result = step2.get("匹配结果", {})
|
|
|
|
|
+ increment_word = input_info.get("B", "")
|
|
|
|
|
+ match_score = match_result.get("score", 0)
|
|
|
|
|
+ same_parts = match_result.get("相同部分", {})
|
|
|
|
|
+ increment_parts = match_result.get("增量部分", {})
|
|
|
|
|
+
|
|
|
|
|
+ # 只有当增量词不为空时才显示
|
|
|
|
|
+ if increment_word.strip():
|
|
|
|
|
+ # 生成相同部分和增量部分的HTML
|
|
|
|
|
+ parts_html = ""
|
|
|
|
|
+ if same_parts:
|
|
|
|
|
+ same_items = [f"{html_module.escape(k)}" for k in same_parts.keys()]
|
|
|
|
|
+ parts_html += f'<div class="preview-parts same"><strong>相同:</strong> {", ".join(same_items)}</div>'
|
|
|
|
|
+
|
|
|
|
|
+ if increment_parts:
|
|
|
|
|
+ inc_items = [f"{html_module.escape(k)}" for k in increment_parts.keys()]
|
|
|
|
|
+ parts_html += f'<div class="preview-parts increment"><strong>增量:</strong> {", ".join(inc_items)}</div>'
|
|
|
|
|
+
|
|
|
|
|
+ step2_match_preview = f'''
|
|
|
|
|
+ <div class="match-preview">
|
|
|
|
|
+ <div class="match-preview-header">➕ Step2 Top1增量词</div>
|
|
|
|
|
+ <div class="match-preview-content">
|
|
|
|
|
+ <span class="match-preview-name">{html_module.escape(increment_word)}</span>
|
|
|
|
|
+ <span class="match-preview-score" style="color: {step2_color};">{match_score:.2f}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {parts_html}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ # 准备详细数据用于弹窗
|
|
|
|
|
+ detail_data_json = json.dumps(inspiration_data, ensure_ascii=False)
|
|
|
|
|
+ detail_data_json_escaped = html_module.escape(detail_data_json)
|
|
|
|
|
+
|
|
|
|
|
+ # 生成详细HTML并进行HTML转义
|
|
|
|
|
+ detail_html = generate_detail_html(inspiration_data)
|
|
|
|
|
+ detail_html_escaped = html_module.escape(detail_html)
|
|
|
|
|
+
|
|
|
|
|
+ html = f'''
|
|
|
|
|
+ <div class="inspiration-card" style="border-left-color: {border_color};"
|
|
|
|
|
+ data-inspiration-name="{inspiration_name_escaped}"
|
|
|
|
|
+ data-detail="{detail_data_json_escaped}"
|
|
|
|
|
+ data-detail-html="{detail_html_escaped}"
|
|
|
|
|
+ data-step1-score="{step1_score}"
|
|
|
|
|
+ data-step2-score="{step2_score}"
|
|
|
|
|
+ onclick="showInspirationDetail(this)">
|
|
|
|
|
+ <div class="card-header">
|
|
|
|
|
+ <h3 class="inspiration-name">{inspiration_name_escaped}</h3>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="score-section">
|
|
|
|
|
+ <div class="score-item">
|
|
|
|
|
+ <div class="score-label">Step1分数</div>
|
|
|
|
|
+ <div class="score-value" style="color: {step1_color};">{step1_score:.3f}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="score-divider"></div>
|
|
|
|
|
+ <div class="score-item">
|
|
|
|
|
+ <div class="score-label">Step2分数</div>
|
|
|
|
|
+ <div class="score-value" style="color: {step2_color};">{step2_score:.3f}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {step1_match_preview}
|
|
|
|
|
+ {step2_match_preview}
|
|
|
|
|
+
|
|
|
|
|
+ <div class="metrics-section">
|
|
|
|
|
+ <div class="metric-item">
|
|
|
|
|
+ <span class="metric-icon">📊</span>
|
|
|
|
|
+ <span class="metric-label">增量词数:</span>
|
|
|
|
|
+ <span class="metric-value">{step2_increment_count}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="click-hint">点击查看详情 →</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ return html
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def generate_detail_html(inspiration_data: Dict[str, Any]) -> str:
|
|
|
|
|
+ """
|
|
|
|
|
+ 生成灵感点的详细信息HTML
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ inspiration_data: 灵感点数据
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 详细信息的HTML字符串
|
|
|
|
|
+ """
|
|
|
|
|
+ import html as html_module
|
|
|
|
|
+
|
|
|
|
|
+ summary = inspiration_data.get("summary", {})
|
|
|
|
|
+ step1 = inspiration_data.get("step1", {})
|
|
|
|
|
+ step2 = inspiration_data.get("step2", {})
|
|
|
|
|
+ inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
|
|
|
|
|
+
|
|
|
|
|
+ content = f'''
|
|
|
|
|
+ <div class="modal-header">
|
|
|
|
|
+ <h2 class="modal-title">{html_module.escape(inspiration_name)}</h2>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ # 获取元数据,用于后面的日志链接
|
|
|
|
|
+ metadata = summary.get("元数据", {})
|
|
|
|
|
+
|
|
|
|
|
+ # Step1 详细信息
|
|
|
|
|
+ if step1 and step1.get("灵感"):
|
|
|
|
|
+ inspiration = step1.get("灵感", "")
|
|
|
|
|
+ matches = step1.get("匹配结果列表", [])
|
|
|
|
|
+
|
|
|
|
|
+ content += f'''
|
|
|
|
|
+ <div class="modal-section">
|
|
|
|
|
+ <h3>🎯 Step1: 灵感人设匹配</h3>
|
|
|
|
|
+ <div class="step-content">
|
|
|
|
|
+ <div class="step-field">
|
|
|
|
|
+ <span class="step-field-label">灵感内容:</span>
|
|
|
|
|
+ <span class="step-field-value">{html_module.escape(inspiration)}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ # 显示匹配结果(只显示Top1)
|
|
|
|
|
+ if matches:
|
|
|
|
|
+ content += f'''
|
|
|
|
|
+ <div class="step-field">
|
|
|
|
|
+ <span class="step-field-label">Top1匹配结果:</span>
|
|
|
|
|
+ <div class="matches-list">
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ for index, match in enumerate(matches[:1]):
|
|
|
|
|
+ input_info = match.get("输入信息", {})
|
|
|
|
|
+ match_result = match.get("匹配结果", {})
|
|
|
|
|
+
|
|
|
|
|
+ element_a = input_info.get("A", "")
|
|
|
|
|
+ context_a = input_info.get("A_Context", "")
|
|
|
|
|
+ score = match_result.get("score", 0)
|
|
|
|
|
+ score_explain = match_result.get("score说明", "")
|
|
|
|
|
+ same_parts = match_result.get("相同部分", {})
|
|
|
|
|
+ increment_parts = match_result.get("增量部分", {})
|
|
|
|
|
+
|
|
|
|
|
+ content += f'''
|
|
|
|
|
+ <div class="match-item">
|
|
|
|
|
+ <div class="match-header">
|
|
|
|
|
+ <span class="match-element-name">{html_module.escape(element_a)}</span>
|
|
|
|
|
+ <span class="match-score">{score:.2f}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ if context_a:
|
|
|
|
|
+ content += f'<div class="match-context"><strong>📍 所属分类:</strong> {html_module.escape(context_a).replace(chr(10), "<br>")}</div>'
|
|
|
|
|
+
|
|
|
|
|
+ if score_explain:
|
|
|
|
|
+ content += f'<div class="match-explain"><strong>💡 分数说明:</strong> {html_module.escape(score_explain)}</div>'
|
|
|
|
|
+
|
|
|
|
|
+ # 相同部分
|
|
|
|
|
+ if same_parts:
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ <div class="match-parts same-parts">
|
|
|
|
|
+ <div class="parts-header">✅ 相同部分</div>
|
|
|
|
|
+ <div class="parts-content">
|
|
|
|
|
+ '''
|
|
|
|
|
+ for key, value in same_parts.items():
|
|
|
|
|
+ content += f'''
|
|
|
|
|
+ <div class="part-item">
|
|
|
|
|
+ <span class="part-key">{html_module.escape(key)}:</span>
|
|
|
|
|
+ <span class="part-value">{html_module.escape(value)}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ # 增量部分
|
|
|
|
|
+ if increment_parts:
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ <div class="match-parts increment-parts">
|
|
|
|
|
+ <div class="parts-header">➕ 增量部分</div>
|
|
|
|
|
+ <div class="parts-content">
|
|
|
|
|
+ '''
|
|
|
|
|
+ for key, value in increment_parts.items():
|
|
|
|
|
+ content += f'''
|
|
|
|
|
+ <div class="part-item">
|
|
|
|
|
+ <span class="part-key">{html_module.escape(key)}:</span>
|
|
|
|
|
+ <span class="part-value">{html_module.escape(value)}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ # Step2 详细信息
|
|
|
|
|
+ if step2 and step2.get("灵感"):
|
|
|
|
|
+ input_info = step2.get("输入信息", {})
|
|
|
|
|
+ match_result = step2.get("匹配结果", {})
|
|
|
|
|
+
|
|
|
|
|
+ increment_word = input_info.get("B", "")
|
|
|
|
|
+ b_context = input_info.get("B_Context", "")
|
|
|
|
|
+ score = match_result.get("score", 0)
|
|
|
|
|
+ score_explain = match_result.get("score说明", "")
|
|
|
|
|
+ same_parts = match_result.get("相同部分", {})
|
|
|
|
|
+ increment_parts = match_result.get("增量部分", {})
|
|
|
|
|
+
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ <div class="modal-section">
|
|
|
|
|
+ <h3>➕ Step2: 增量词匹配</h3>
|
|
|
|
|
+ <div class="step-content">
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ if increment_word.strip():
|
|
|
|
|
+ content += f'''
|
|
|
|
|
+ <div class="step-field">
|
|
|
|
|
+ <span class="step-field-label">增量词:</span>
|
|
|
|
|
+ <span class="step-field-value">{html_module.escape(increment_word)}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ if b_context:
|
|
|
|
|
+ content += f'''
|
|
|
|
|
+ <div class="increment-context">
|
|
|
|
|
+ <strong>📌 增量词来源:</strong> {html_module.escape(b_context)}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ content += f'''
|
|
|
|
|
+ <div class="increment-item">
|
|
|
|
|
+ <div class="increment-header">
|
|
|
|
|
+ <span class="increment-words">分数</span>
|
|
|
|
|
+ <span class="increment-score">{score:.2f}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ if score_explain:
|
|
|
|
|
+ content += f'''
|
|
|
|
|
+ <div class="match-explain">
|
|
|
|
|
+ <strong>💡 分数说明:</strong> {html_module.escape(score_explain)}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ # 相同部分
|
|
|
|
|
+ if same_parts:
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ <div class="match-parts same-parts">
|
|
|
|
|
+ <div class="parts-header">✅ 相同部分</div>
|
|
|
|
|
+ <div class="parts-content">
|
|
|
|
|
+ '''
|
|
|
|
|
+ for key, value in same_parts.items():
|
|
|
|
|
+ content += f'''
|
|
|
|
|
+ <div class="part-item">
|
|
|
|
|
+ <span class="part-key">{html_module.escape(key)}:</span>
|
|
|
|
|
+ <span class="part-value">{html_module.escape(value)}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ # 增量部分
|
|
|
|
|
+ if increment_parts:
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ <div class="match-parts increment-parts">
|
|
|
|
|
+ <div class="parts-header">➕ 增量部分</div>
|
|
|
|
|
+ <div class="parts-content">
|
|
|
|
|
+ '''
|
|
|
|
|
+ for key, value in increment_parts.items():
|
|
|
|
|
+ content += f'''
|
|
|
|
|
+ <div class="part-item">
|
|
|
|
|
+ <span class="part-key">{html_module.escape(key)}:</span>
|
|
|
|
|
+ <span class="part-value">{html_module.escape(value)}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+ else:
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ <div class="empty-state">暂无增量词匹配结果</div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ content += '''
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ # 日志链接
|
|
|
|
|
+ if metadata.get("log_url"):
|
|
|
|
|
+ content += f'''
|
|
|
|
|
+ <div class="modal-link">
|
|
|
|
|
+ <a href="{metadata["log_url"]}" target="_blank" class="modal-link-btn">
|
|
|
|
|
+ 🔗 查看详细日志
|
|
|
|
|
+ </a>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+ return content
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def generate_detail_modal_content_js() -> str:
|
|
|
|
|
+ """
|
|
|
|
|
+ 生成详情弹窗内容的JavaScript函数
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ JavaScript代码字符串
|
|
|
|
|
+ """
|
|
|
|
|
+ return '''
|
|
|
|
|
+ // Tab切换功能
|
|
|
|
|
+ function switchTab(event, tabId) {
|
|
|
|
|
+ // 移除所有tab的active状态
|
|
|
|
|
+ const tabButtons = document.querySelectorAll('.tab-button');
|
|
|
|
|
+ tabButtons.forEach(button => {
|
|
|
|
|
+ button.classList.remove('active');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏所有tab内容
|
|
|
|
|
+ const tabContents = document.querySelectorAll('.tab-content');
|
|
|
|
|
+ tabContents.forEach(content => {
|
|
|
|
|
+ content.classList.remove('active');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 激活当前tab
|
|
|
|
|
+ event.currentTarget.classList.add('active');
|
|
|
|
|
+ document.getElementById(tabId).classList.add('active');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function showInspirationDetail(element) {
|
|
|
|
|
+ const detailHtml = element.dataset.detailHtml;
|
|
|
|
|
+ const modal = document.getElementById('detailModal');
|
|
|
|
|
+ const modalBody = document.getElementById('modalBody');
|
|
|
|
|
+
|
|
|
|
|
+ modalBody.innerHTML = detailHtml;
|
|
|
|
|
+ modal.classList.add('active');
|
|
|
|
|
+ document.body.style.overflow = 'hidden';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function closeModal() {
|
|
|
|
|
+ const modal = document.getElementById('detailModal');
|
|
|
|
|
+ modal.classList.remove('active');
|
|
|
|
|
+ document.body.style.overflow = '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function closeModalOnOverlay(event) {
|
|
|
|
|
+ if (event.target.id === 'detailModal') {
|
|
|
|
|
+ closeModal();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ESC键关闭Modal
|
|
|
|
|
+ document.addEventListener('keydown', function(event) {
|
|
|
|
|
+ if (event.key === 'Escape') {
|
|
|
|
|
+ closeModal();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 搜索和过滤功能
|
|
|
|
|
+ function filterInspirations() {
|
|
|
|
|
+ const searchInput = document.getElementById('searchInput').value.toLowerCase();
|
|
|
|
|
+ const sortSelect = document.getElementById('sortSelect').value;
|
|
|
|
|
+ const cards = document.querySelectorAll('.inspiration-card');
|
|
|
|
|
+
|
|
|
|
|
+ let visibleCards = Array.from(cards);
|
|
|
|
|
+
|
|
|
|
|
+ // 搜索过滤
|
|
|
|
|
+ visibleCards.forEach(card => {
|
|
|
|
|
+ const name = card.dataset.inspirationName.toLowerCase();
|
|
|
|
|
+ if (name.includes(searchInput)) {
|
|
|
|
|
+ card.style.display = '';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ card.style.display = 'none';
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 获取可见的卡片
|
|
|
|
|
+ visibleCards = Array.from(cards).filter(card => card.style.display !== 'none');
|
|
|
|
|
+
|
|
|
|
|
+ // 排序
|
|
|
|
|
+ if (sortSelect === 'step1-desc' || sortSelect === 'step1-asc') {
|
|
|
|
|
+ visibleCards.sort((a, b) => {
|
|
|
|
|
+ const step1A = parseFloat(a.dataset.step1Score) || 0;
|
|
|
|
|
+ const step1B = parseFloat(b.dataset.step1Score) || 0;
|
|
|
|
|
+ const step2A = parseFloat(a.dataset.step2Score) || 0;
|
|
|
|
|
+ const step2B = parseFloat(b.dataset.step2Score) || 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (sortSelect === 'step1-desc') {
|
|
|
|
|
+ return step1B !== step1A ? step1B - step1A : step2B - step2A;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return step1A !== step1B ? step1A - step1B : step2A - step2B;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else if (sortSelect === 'step2-desc' || sortSelect === 'step2-asc') {
|
|
|
|
|
+ visibleCards.sort((a, b) => {
|
|
|
|
|
+ const step2A = parseFloat(a.dataset.step2Score) || 0;
|
|
|
|
|
+ const step2B = parseFloat(b.dataset.step2Score) || 0;
|
|
|
|
|
+ const step1A = parseFloat(a.dataset.step1Score) || 0;
|
|
|
|
|
+ const step1B = parseFloat(b.dataset.step1Score) || 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (sortSelect === 'step2-desc') {
|
|
|
|
|
+ return step2B !== step2A ? step2B - step2A : step1B - step1A;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return step2A !== step2B ? step2A - step2B : step1A - step1B;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else if (sortSelect === 'name-asc' || sortSelect === 'name-desc') {
|
|
|
|
|
+ visibleCards.sort((a, b) => {
|
|
|
|
|
+ const nameA = a.dataset.inspirationName;
|
|
|
|
|
+ const nameB = b.dataset.inspirationName;
|
|
|
|
|
+ return sortSelect === 'name-asc' ? nameA.localeCompare(nameB) : nameB.localeCompare(nameA);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重新排列卡片
|
|
|
|
|
+ const container = document.querySelector('.inspirations-grid');
|
|
|
|
|
+ visibleCards.forEach(card => {
|
|
|
|
|
+ container.appendChild(card);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 更新统计
|
|
|
|
|
+ updateStats();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function updateStats() {
|
|
|
|
|
+ const cards = document.querySelectorAll('.inspiration-card');
|
|
|
|
|
+ const visibleCards = Array.from(cards).filter(card => card.style.display !== 'none');
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('totalCount').textContent = visibleCards.length;
|
|
|
|
|
+
|
|
|
|
|
+ let step1ExcellentCount = 0;
|
|
|
|
|
+ let step1GoodCount = 0;
|
|
|
|
|
+ let step1NormalCount = 0;
|
|
|
|
|
+ let step1NeedOptCount = 0;
|
|
|
|
|
+ let step2ExcellentCount = 0;
|
|
|
|
|
+ let step2GoodCount = 0;
|
|
|
|
|
+ let step2NormalCount = 0;
|
|
|
|
|
+ let step2NeedOptCount = 0;
|
|
|
|
|
+ let totalStep1Score = 0;
|
|
|
|
|
+ let totalStep2Score = 0;
|
|
|
|
|
+
|
|
|
|
|
+ visibleCards.forEach(card => {
|
|
|
|
|
+ const step1Score = parseFloat(card.dataset.step1Score) || 0;
|
|
|
|
|
+ const step2Score = parseFloat(card.dataset.step2Score) || 0;
|
|
|
|
|
+
|
|
|
|
|
+ totalStep1Score += step1Score;
|
|
|
|
|
+ totalStep2Score += step2Score;
|
|
|
|
|
+
|
|
|
|
|
+ // Step1 统计
|
|
|
|
|
+ if (step1Score >= 0.7) step1ExcellentCount++;
|
|
|
|
|
+ else if (step1Score >= 0.5) step1GoodCount++;
|
|
|
|
|
+ else if (step1Score >= 0.3) step1NormalCount++;
|
|
|
|
|
+ else step1NeedOptCount++;
|
|
|
|
|
+
|
|
|
|
|
+ // Step2 统计
|
|
|
|
|
+ if (step2Score >= 0.7) step2ExcellentCount++;
|
|
|
|
|
+ else if (step2Score >= 0.5) step2GoodCount++;
|
|
|
|
|
+ else if (step2Score >= 0.3) step2NormalCount++;
|
|
|
|
|
+ else step2NeedOptCount++;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('step1ExcellentCount').textContent = step1ExcellentCount;
|
|
|
|
|
+ document.getElementById('step1GoodCount').textContent = step1GoodCount;
|
|
|
|
|
+ document.getElementById('step1NormalCount').textContent = step1NormalCount;
|
|
|
|
|
+ document.getElementById('step1NeedOptCount').textContent = step1NeedOptCount;
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('step2ExcellentCount').textContent = step2ExcellentCount;
|
|
|
|
|
+ document.getElementById('step2GoodCount').textContent = step2GoodCount;
|
|
|
|
|
+ document.getElementById('step2NormalCount').textContent = step2NormalCount;
|
|
|
|
|
+ document.getElementById('step2NeedOptCount').textContent = step2NeedOptCount;
|
|
|
|
|
+
|
|
|
|
|
+ const avgStep1Score = visibleCards.length > 0 ? (totalStep1Score / visibleCards.length).toFixed(3) : '0.000';
|
|
|
|
|
+ const avgStep2Score = visibleCards.length > 0 ? (totalStep2Score / visibleCards.length).toFixed(3) : '0.000';
|
|
|
|
|
+ document.getElementById('avgStep1Score').textContent = avgStep1Score;
|
|
|
|
|
+ document.getElementById('avgStep2Score').textContent = avgStep2Score;
|
|
|
|
|
+ }
|
|
|
|
|
+ '''
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def generate_persona_structure_html(persona_data: Dict[str, Any]) -> str:
|
|
|
|
|
+ """
|
|
|
|
|
+ 生成人设结构的树状HTML
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ persona_data: 人设数据
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 人设结构的HTML字符串
|
|
|
|
|
+ """
|
|
|
|
|
+ if not persona_data:
|
|
|
|
|
+ return '<div class="empty-state">暂无人设数据</div>'
|
|
|
|
|
+
|
|
|
|
|
+ inspiration_list = persona_data.get("灵感点列表", [])
|
|
|
|
|
+ if not inspiration_list:
|
|
|
|
|
+ return '<div class="empty-state">暂无灵感点列表数据</div>'
|
|
|
|
|
+
|
|
|
|
|
+ html_parts = ['<div class="tree">']
|
|
|
|
|
+
|
|
|
|
|
+ for perspective_idx, perspective in enumerate(inspiration_list):
|
|
|
|
|
+ perspective_name = perspective.get("视角名称", "未知视角")
|
|
|
|
|
+ perspective_desc = perspective.get("视角描述", "")
|
|
|
|
|
+ pattern_list = perspective.get("模式列表", [])
|
|
|
|
|
+
|
|
|
|
|
+ # 一级节点:视角
|
|
|
|
|
+ html_parts.append(f'''
|
|
|
|
|
+ <ul>
|
|
|
|
|
+ <li>
|
|
|
|
|
+ <div class="tree-node level-1">
|
|
|
|
|
+ <span class="node-icon">📁</span>
|
|
|
|
|
+ <span class="node-name">{html_module.escape(perspective_name)}</span>
|
|
|
|
|
+ <span class="node-count">{len(pattern_list)}个分类</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ''')
|
|
|
|
|
+
|
|
|
|
|
+ if perspective_desc:
|
|
|
|
|
+ html_parts.append(f'''
|
|
|
|
|
+ <div class="node-desc">{html_module.escape(perspective_desc)}</div>
|
|
|
|
|
+ ''')
|
|
|
|
|
+
|
|
|
|
|
+ # 二级节点:分类
|
|
|
|
|
+ if pattern_list:
|
|
|
|
|
+ html_parts.append('<ul>')
|
|
|
|
|
+ for pattern in pattern_list:
|
|
|
|
|
+ category_name = pattern.get("分类名称", "未知分类")
|
|
|
|
|
+ core_definition = pattern.get("核心定义", "")
|
|
|
|
|
+ subcategories = pattern.get("二级细分", [])
|
|
|
|
|
+ total_posts = sum(len(sub.get("帖子ID列表", [])) for sub in subcategories)
|
|
|
|
|
+
|
|
|
|
|
+ html_parts.append(f'''
|
|
|
|
|
+ <li>
|
|
|
|
|
+ <div class="tree-node level-2">
|
|
|
|
|
+ <span class="node-icon">📂</span>
|
|
|
|
|
+ <span class="node-name">{html_module.escape(category_name)}</span>
|
|
|
|
|
+ <span class="node-count">{total_posts}个帖子</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ''')
|
|
|
|
|
+
|
|
|
|
|
+ if core_definition:
|
|
|
|
|
+ html_parts.append(f'''
|
|
|
|
|
+ <div class="node-desc">{html_module.escape(core_definition)}</div>
|
|
|
|
|
+ ''')
|
|
|
|
|
+
|
|
|
|
|
+ # 三级节点:细分
|
|
|
|
|
+ if subcategories:
|
|
|
|
|
+ html_parts.append('<ul>')
|
|
|
|
|
+ for subcategory in subcategories:
|
|
|
|
|
+ sub_name = subcategory.get("分类名称", "未知细分")
|
|
|
|
|
+ sub_definition = subcategory.get("分类定义", "")
|
|
|
|
|
+ post_ids = subcategory.get("帖子ID列表", [])
|
|
|
|
|
+
|
|
|
|
|
+ html_parts.append(f'''
|
|
|
|
|
+ <li>
|
|
|
|
|
+ <div class="tree-node level-3">
|
|
|
|
|
+ <span class="node-icon">📄</span>
|
|
|
|
|
+ <span class="node-name">{html_module.escape(sub_name)}</span>
|
|
|
|
|
+ <span class="node-count">{len(post_ids)}个帖子</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ''')
|
|
|
|
|
+
|
|
|
|
|
+ if sub_definition:
|
|
|
|
|
+ html_parts.append(f'''
|
|
|
|
|
+ <div class="node-desc">{html_module.escape(sub_definition)}</div>
|
|
|
|
|
+ ''')
|
|
|
|
|
+
|
|
|
|
|
+ if post_ids:
|
|
|
|
|
+ html_parts.append(f'''
|
|
|
|
|
+ <div class="node-posts">
|
|
|
|
|
+ <span class="posts-label">📋 帖子ID:</span>
|
|
|
|
|
+ <span class="posts-ids">{", ".join([html_module.escape(str(pid)) for pid in post_ids[:5]])}</span>
|
|
|
|
|
+ {f'<span class="posts-more">... 等{len(post_ids)}个</span>' if len(post_ids) > 5 else ''}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ''')
|
|
|
|
|
+
|
|
|
|
|
+ html_parts.append('</li>')
|
|
|
|
|
+ html_parts.append('</ul>')
|
|
|
|
|
+
|
|
|
|
|
+ html_parts.append('</li>')
|
|
|
|
|
+ html_parts.append('</ul>')
|
|
|
|
|
+
|
|
|
|
|
+ html_parts.append('</li>')
|
|
|
|
|
+ html_parts.append('</ul>')
|
|
|
|
|
+
|
|
|
|
|
+ html_parts.append('</div>')
|
|
|
|
|
+ return ''.join(html_parts)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def generate_html(
|
|
|
|
|
+ inspirations_data: List[Dict[str, Any]],
|
|
|
|
|
+ posts_map: Dict[str, Dict[str, Any]],
|
|
|
|
|
+ persona_data: Dict[str, Any],
|
|
|
|
|
+ output_path: str
|
|
|
|
|
+) -> str:
|
|
|
|
|
+ """
|
|
|
|
|
+ 生成完整的可视化HTML
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ inspirations_data: 灵感点数据列表
|
|
|
|
|
+ posts_map: 帖子数据映射
|
|
|
|
|
+ persona_data: 人设数据
|
|
|
|
|
+ output_path: 输出文件路径
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 输出文件路径
|
|
|
|
|
+ """
|
|
|
|
|
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
+
|
|
|
|
|
+ # 统计信息
|
|
|
|
|
+ total_count = len(inspirations_data)
|
|
|
|
|
+
|
|
|
|
|
+ # Step1 统计
|
|
|
|
|
+ step1_excellent_count = sum(1 for d in inspirations_data
|
|
|
|
|
+ if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) >= 0.7)
|
|
|
|
|
+ step1_good_count = sum(1 for d in inspirations_data
|
|
|
|
|
+ if 0.5 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.7)
|
|
|
|
|
+ step1_normal_count = sum(1 for d in inspirations_data
|
|
|
|
|
+ if 0.3 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.5)
|
|
|
|
|
+ step1_need_opt_count = sum(1 for d in inspirations_data
|
|
|
|
|
+ if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.3)
|
|
|
|
|
+
|
|
|
|
|
+ # Step2 统计
|
|
|
|
|
+ step2_excellent_count = sum(1 for d in inspirations_data
|
|
|
|
|
+ if d["summary"].get("关键指标", {}).get("step2_score", 0) >= 0.7)
|
|
|
|
|
+ step2_good_count = sum(1 for d in inspirations_data
|
|
|
|
|
+ if 0.5 <= d["summary"].get("关键指标", {}).get("step2_score", 0) < 0.7)
|
|
|
|
|
+ step2_normal_count = sum(1 for d in inspirations_data
|
|
|
|
|
+ if 0.3 <= d["summary"].get("关键指标", {}).get("step2_score", 0) < 0.5)
|
|
|
|
|
+ step2_need_opt_count = sum(1 for d in inspirations_data
|
|
|
|
|
+ if d["summary"].get("关键指标", {}).get("step2_score", 0) < 0.3)
|
|
|
|
|
+
|
|
|
|
|
+ # 平均分数
|
|
|
|
|
+ total_step1_score = sum(d["summary"].get("关键指标", {}).get("step1_top1_score", 0)
|
|
|
|
|
+ for d in inspirations_data)
|
|
|
|
|
+ total_step2_score = sum(d["summary"].get("关键指标", {}).get("step2_score", 0)
|
|
|
|
|
+ for d in inspirations_data)
|
|
|
|
|
+ avg_step1_score = total_step1_score / total_count if total_count > 0 else 0
|
|
|
|
|
+ avg_step2_score = total_step2_score / total_count if total_count > 0 else 0
|
|
|
|
|
+
|
|
|
|
|
+ # 按Step1分数排序(Step2作为次要排序)
|
|
|
|
|
+ inspirations_data_sorted = sorted(
|
|
|
|
|
+ inspirations_data,
|
|
|
|
|
+ key=lambda x: (x["summary"].get("关键指标", {}).get("step1_top1_score", 0),
|
|
|
|
|
+ x["summary"].get("关键指标", {}).get("step2_score", 0)),
|
|
|
|
|
+ reverse=True
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 生成卡片HTML
|
|
|
|
|
+ cards_html = [generate_inspiration_card_html(data) for data in inspirations_data_sorted]
|
|
|
|
|
+ cards_html_str = '\n'.join(cards_html)
|
|
|
|
|
+
|
|
|
|
|
+ # 生成人设结构HTML
|
|
|
|
|
+ persona_structure_html = generate_persona_structure_html(persona_data)
|
|
|
|
|
+
|
|
|
|
|
+ # 生成JavaScript
|
|
|
|
|
+ detail_modal_js = generate_detail_modal_content_js()
|
|
|
|
|
+
|
|
|
|
|
+ # 完整HTML
|
|
|
|
|
+ html_content = f'''<!DOCTYPE html>
|
|
|
|
|
+<html lang="zh-CN">
|
|
|
|
|
+<head>
|
|
|
|
|
+ <meta charset="UTF-8">
|
|
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
+ <title>灵感点分析可视化</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: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .container {{
|
|
|
|
|
+ max-width: 1600px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .header {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ padding: 40px;
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
+ box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .header h1 {{
|
|
|
|
|
+ font-size: 42px;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
+ -webkit-background-clip: text;
|
|
|
|
|
+ -webkit-text-fill-color: transparent;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .header-subtitle {{
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stats-overview {{
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ margin-top: 25px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-box {{
|
|
|
|
|
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ transition: transform 0.3s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-box:hover {{
|
|
|
|
|
+ transform: translateY(-5px);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-label {{
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-value {{
|
|
|
|
|
+ font-size: 32px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #1a1a1a;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-box.excellent .stat-value {{
|
|
|
|
|
+ color: #10b981;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-box.good .stat-value {{
|
|
|
|
|
+ color: #f59e0b;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-box.normal .stat-value {{
|
|
|
|
|
+ color: #3b82f6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-box.need-opt .stat-value {{
|
|
|
|
|
+ color: #ef4444;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .controls-section {{
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ padding: 25px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-box {{
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 250px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-input {{
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ padding: 12px 20px;
|
|
|
|
|
+ border: 2px solid #e5e7eb;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-input:focus {{
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ border-color: #667eea;
|
|
|
|
|
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .sort-box {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .sort-label {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .sort-select {{
|
|
|
|
|
+ padding: 10px 16px;
|
|
|
|
|
+ border: 2px solid #e5e7eb;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .sort-select:focus {{
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ border-color: #667eea;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .inspirations-section {{
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .inspirations-grid {{
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
|
|
|
|
+ gap: 25px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .inspiration-card {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 14px;
|
|
|
|
|
+ padding: 25px;
|
|
|
|
|
+ border-left: 6px solid #10b981;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .inspiration-card:hover {{
|
|
|
|
|
+ transform: translateY(-8px);
|
|
|
|
|
+ box-shadow: 0 12px 30px rgba(102, 126, 234, 0.2);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .card-header {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .inspiration-name {{
|
|
|
|
|
+ font-size: 19px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #1a1a1a;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .grade-badge {{
|
|
|
|
|
+ background: #10b981;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ padding: 6px 14px;
|
|
|
|
|
+ border-radius: 20px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-section {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 25px;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-item {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .main-score {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-circle {{
|
|
|
|
|
+ width: 90px;
|
|
|
|
|
+ height: 90px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ border: 6px solid #10b981;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-value {{
|
|
|
|
|
+ font-size: 26px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ color: #10b981;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-label {{
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .sub-scores {{
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .sub-score-item {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 10px 15px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .sub-score-label {{
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .sub-score-value {{
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #2563eb;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .metrics-section {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ margin-bottom: 15px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .metric-item {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #4b5563;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .metric-icon {{
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .metric-label {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .metric-value {{
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-preview {{
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ border-left: 3px solid #8b5cf6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-preview-header {{
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-preview-content {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-preview-name {{
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-preview-score {{
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .preview-parts {{
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ padding: 8px 10px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .preview-parts.same {{
|
|
|
|
|
+ background: #f0fdf4;
|
|
|
|
|
+ color: #15803d;
|
|
|
|
|
+ border-left: 3px solid #10b981;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .preview-parts.increment {{
|
|
|
|
|
+ background: #fff7ed;
|
|
|
|
|
+ color: #92400e;
|
|
|
|
|
+ border-left: 3px solid #f59e0b;
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .preview-parts strong {{
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ margin-right: 6px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-divider {{
|
|
|
|
|
+ width: 1px;
|
|
|
|
|
+ height: 40px;
|
|
|
|
|
+ background: #e5e7eb;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .click-hint {{
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 15px;
|
|
|
|
|
+ right: 15px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #8b5cf6;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transition: opacity 0.3s ease;
|
|
|
|
|
+ background: rgba(139, 92, 246, 0.1);
|
|
|
|
|
+ padding: 6px 12px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .inspiration-card:hover .click-hint {{
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* Modal样式 */
|
|
|
|
|
+ .modal-overlay {{
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.8);
|
|
|
|
|
+ z-index: 1000;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-overlay.active {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-content {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ max-width: 1200px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ max-height: 90vh;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-close {{
|
|
|
|
|
+ position: sticky;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ font-size: 32px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ padding: 15px 20px;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+ text-align: right;
|
|
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-close:hover {{
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-body {{
|
|
|
|
|
+ padding: 30px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-header {{
|
|
|
|
|
+ margin-bottom: 25px;
|
|
|
|
|
+ padding-bottom: 20px;
|
|
|
|
|
+ border-bottom: 2px solid #e5e7eb;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-title {{
|
|
|
|
|
+ font-size: 28px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ color: #1a1a1a;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-section {{
|
|
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-section h3 {{
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ margin-bottom: 15px;
|
|
|
|
|
+ padding-bottom: 10px;
|
|
|
|
|
+ border-bottom: 2px solid #f3f4f6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .info-grid {{
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
|
|
|
+ gap: 15px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .info-item {{
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ padding: 12px 16px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ border-left: 3px solid #8b5cf6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .info-label {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .info-value {{
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .metrics-grid {{
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
+ gap: 15px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .metric-box {{
|
|
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .metric-box.wide {{
|
|
|
|
|
+ grid-column: span 2;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .metric-box-label {{
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ opacity: 0.9;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .metric-box-value {{
|
|
|
|
|
+ font-size: 28px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .metric-box-value.small {{
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .step-content {{
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .step-field {{
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .step-field-label {{
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .step-field-value {{
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ line-height: 1.7;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .matches-list {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 15px;
|
|
|
|
|
+ margin-top: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-item {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ padding: 18px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ border-left: 5px solid #3b82f6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-item.top1 {{
|
|
|
|
|
+ border-left-color: #fbbf24;
|
|
|
|
|
+ background: linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, white 100%);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-item.top2 {{
|
|
|
|
|
+ border-left-color: #c0c0c0;
|
|
|
|
|
+ background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 50%, white 100%);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-item.top3 {{
|
|
|
|
|
+ border-left-color: #cd7f32;
|
|
|
|
|
+ background: linear-gradient(135deg, #fef3c7 0%, #fed7aa 50%, white 100%);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-header {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-rank {{
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-element-name {{
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-score {{
|
|
|
|
|
+ font-size: 22px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ color: #2563eb;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ padding: 6px 14px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-detail {{
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.7);
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #4b5563;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-reason {{
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ line-height: 1.7;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .increment-matches {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-top: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .increment-item {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ border-left: 4px solid #10b981;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .increment-header {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .increment-words {{
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .increment-score {{
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ color: #10b981;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .increment-reason {{
|
|
|
|
|
+ color: #4b5563;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .empty-state {{
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 40px;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-link {{
|
|
|
|
|
+ margin-top: 25px;
|
|
|
|
|
+ padding-top: 20px;
|
|
|
|
|
+ border-top: 2px solid #e5e7eb;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-link-btn {{
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ padding: 12px 24px;
|
|
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ text-decoration: none;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-link-btn:hover {{
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+ box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .timestamp {{
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ margin-top: 30px;
|
|
|
|
|
+ opacity: 0.8;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-context {{
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ margin: 8px 0;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-explain {{
|
|
|
|
|
+ background: #fef3c7;
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ margin: 10px 0;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #92400e;
|
|
|
|
|
+ line-height: 1.7;
|
|
|
|
|
+ border-left: 3px solid #f59e0b;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-parts {{
|
|
|
|
|
+ margin: 12px 0;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-parts.same-parts {{
|
|
|
|
|
+ background: #f0fdf4;
|
|
|
|
|
+ border: 2px solid #10b981;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-parts.increment-parts {{
|
|
|
|
|
+ background: #fff7ed;
|
|
|
|
|
+ border: 2px solid #f59e0b;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .parts-header {{
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .same-parts .parts-header {{
|
|
|
|
|
+ background: #dcfce7;
|
|
|
|
|
+ color: #15803d;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .increment-parts .parts-header {{
|
|
|
|
|
+ background: #fed7aa;
|
|
|
|
|
+ color: #92400e;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .parts-content {{
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .part-item {{
|
|
|
|
|
+ padding: 6px 0;
|
|
|
|
|
+ border-bottom: 1px solid rgba(0,0,0,0.05);
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .part-item:last-child {{
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .part-key {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ margin-right: 6px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .part-value {{
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .increment-context {{
|
|
|
|
|
+ background: #fef3c7;
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ margin: 10px 0;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #92400e;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ border-left: 3px solid #f59e0b;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* Tab样式 */
|
|
|
|
|
+ .tabs-nav {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ padding: 0 30px;
|
|
|
|
|
+ border-radius: 16px 16px 0 0;
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+ box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tab-button {{
|
|
|
|
|
+ padding: 15px 30px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ border-bottom: 3px solid transparent;
|
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tab-button:hover {{
|
|
|
|
|
+ color: #667eea;
|
|
|
|
|
+ background: rgba(102, 126, 234, 0.05);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tab-button.active {{
|
|
|
|
|
+ color: #667eea;
|
|
|
|
|
+ border-bottom-color: #667eea;
|
|
|
|
|
+ background: rgba(102, 126, 234, 0.05);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tab-content {{
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ padding: 30px;
|
|
|
|
|
+ border-radius: 0 0 16px 16px;
|
|
|
|
|
+ box-shadow: 0 10px 40px rgba(0,0,0,0.15);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tab-content.active {{
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 人设结构样式 */
|
|
|
|
|
+ .persona-structure-section h2 {{
|
|
|
|
|
+ font-size: 28px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ margin-bottom: 25px;
|
|
|
|
|
+ color: #1a1a1a;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 树状图样式 */
|
|
|
|
|
+ .tree {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree ul {{
|
|
|
|
|
+ padding-left: 30px;
|
|
|
|
|
+ list-style: none;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree ul ul {{
|
|
|
|
|
+ padding-left: 40px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree li {{
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ padding: 8px 0;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree li::before {{
|
|
|
|
|
+ content: "";
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: -20px;
|
|
|
|
|
+ border-left: 2px solid #d1d5db;
|
|
|
|
|
+ border-bottom: 2px solid #d1d5db;
|
|
|
|
|
+ width: 20px;
|
|
|
|
|
+ height: 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree li::after {{
|
|
|
|
|
+ content: "";
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 20px;
|
|
|
|
|
+ left: -20px;
|
|
|
|
|
+ border-left: 2px solid #d1d5db;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree li:last-child::after {{
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree > ul > li::before,
|
|
|
|
|
+ .tree > ul > li::after {{
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node {{
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ padding: 12px 16px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node:hover {{
|
|
|
|
|
+ transform: translateX(4px);
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node.level-1 {{
|
|
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ padding: 16px 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node.level-2 {{
|
|
|
|
|
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
|
|
|
|
+ color: #1e40af;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ border: 2px solid #3b82f6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node.level-3 {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .node-icon {{
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node.level-1 .node-icon {{
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .node-name {{
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .node-count {{
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.3);
|
|
|
|
|
+ padding: 4px 12px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node.level-1 .node-count {{
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.4);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node.level-2 .node-count {{
|
|
|
|
|
+ background: #bfdbfe;
|
|
|
|
|
+ color: #1e3a8a;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tree-node.level-3 .node-count {{
|
|
|
|
|
+ background: #dcfce7;
|
|
|
|
|
+ color: #166534;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .node-desc {{
|
|
|
|
|
+ margin: 8px 0 8px 50px;
|
|
|
|
|
+ padding: 12px 16px;
|
|
|
|
|
+ background: #fffbeb;
|
|
|
|
|
+ border-left: 3px solid #f59e0b;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ color: #92400e;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ line-height: 1.7;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .node-posts {{
|
|
|
|
|
+ margin: 8px 0 8px 50px;
|
|
|
|
|
+ padding: 12px 16px;
|
|
|
|
|
+ background: #f0fdf4;
|
|
|
|
|
+ border-left: 3px solid #10b981;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ line-height: 1.8;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .posts-label {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #15803d;
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .posts-ids {{
|
|
|
|
|
+ color: #166534;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .posts-more {{
|
|
|
|
|
+ color: #059669;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin-left: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ @media (max-width: 768px) {{
|
|
|
|
|
+ .inspirations-grid {{
|
|
|
|
|
+ grid-template-columns: 1fr;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .header h1 {{
|
|
|
|
|
+ font-size: 32px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stats-overview {{
|
|
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+ </style>
|
|
|
|
|
+</head>
|
|
|
|
|
+<body>
|
|
|
|
|
+ <div class="container">
|
|
|
|
|
+ <div class="header">
|
|
|
|
|
+ <h1>💡 灵感点分析可视化</h1>
|
|
|
|
|
+ <div class="header-subtitle">基于HOW人设的灵感点匹配分析结果</div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="stats-overview">
|
|
|
|
|
+ <div class="stat-box">
|
|
|
|
|
+ <div class="stat-label">分析总数</div>
|
|
|
|
|
+ <div class="stat-value" id="totalCount">{total_count}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-box excellent">
|
|
|
|
|
+ <div class="stat-label">Step1优秀 (≥0.7)</div>
|
|
|
|
|
+ <div class="stat-value" id="step1ExcellentCount">{step1_excellent_count}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-box good">
|
|
|
|
|
+ <div class="stat-label">Step1良好 (0.5-0.7)</div>
|
|
|
|
|
+ <div class="stat-value" id="step1GoodCount">{step1_good_count}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-box normal">
|
|
|
|
|
+ <div class="stat-label">Step1一般 (0.3-0.5)</div>
|
|
|
|
|
+ <div class="stat-value" id="step1NormalCount">{step1_normal_count}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-box need-opt">
|
|
|
|
|
+ <div class="stat-label">Step1待优化 (<0.3)</div>
|
|
|
|
|
+ <div class="stat-value" id="step1NeedOptCount">{step1_need_opt_count}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-box excellent">
|
|
|
|
|
+ <div class="stat-label">Step2优秀 (≥0.7)</div>
|
|
|
|
|
+ <div class="stat-value" id="step2ExcellentCount">{step2_excellent_count}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-box good">
|
|
|
|
|
+ <div class="stat-label">Step2良好 (0.5-0.7)</div>
|
|
|
|
|
+ <div class="stat-value" id="step2GoodCount">{step2_good_count}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-box normal">
|
|
|
|
|
+ <div class="stat-label">Step2一般 (0.3-0.5)</div>
|
|
|
|
|
+ <div class="stat-value" id="step2NormalCount">{step2_normal_count}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-box need-opt">
|
|
|
|
|
+ <div class="stat-label">Step2待优化 (<0.3)</div>
|
|
|
|
|
+ <div class="stat-value" id="step2NeedOptCount">{step2_need_opt_count}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-box">
|
|
|
|
|
+ <div class="stat-label">Step1平均分</div>
|
|
|
|
|
+ <div class="stat-value" id="avgStep1Score">{avg_step1_score:.3f}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-box">
|
|
|
|
|
+ <div class="stat-label">Step2平均分</div>
|
|
|
|
|
+ <div class="stat-value" id="avgStep2Score">{avg_step2_score:.3f}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="tabs-nav">
|
|
|
|
|
+ <button class="tab-button active" onclick="switchTab(event, 'tab-inspirations')">
|
|
|
|
|
+ 灵感点分析
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button class="tab-button" onclick="switchTab(event, 'tab-persona')">
|
|
|
|
|
+ 人设结构
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div id="tab-inspirations" class="tab-content active">
|
|
|
|
|
+ <div class="controls-section">
|
|
|
|
|
+ <div class="search-box">
|
|
|
|
|
+ <input type="text"
|
|
|
|
|
+ id="searchInput"
|
|
|
|
|
+ class="search-input"
|
|
|
|
|
+ placeholder="🔍 搜索灵感点名称..."
|
|
|
|
|
+ oninput="filterInspirations()">
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="sort-box">
|
|
|
|
|
+ <span class="sort-label">排序方式:</span>
|
|
|
|
|
+ <select id="sortSelect" class="sort-select" onchange="filterInspirations()">
|
|
|
|
|
+ <option value="step1-desc">Step1分数从高到低</option>
|
|
|
|
|
+ <option value="step1-asc">Step1分数从低到高</option>
|
|
|
|
|
+ <option value="step2-desc">Step2分数从高到低</option>
|
|
|
|
|
+ <option value="step2-asc">Step2分数从低到高</option>
|
|
|
|
|
+ <option value="name-asc">名称A-Z</option>
|
|
|
|
|
+ <option value="name-desc">名称Z-A</option>
|
|
|
|
|
+ </select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="inspirations-section">
|
|
|
|
|
+ <div class="inspirations-grid">
|
|
|
|
|
+ {cards_html_str}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div id="tab-persona" class="tab-content">
|
|
|
|
|
+ <div class="persona-structure-section">
|
|
|
|
|
+ <h2>📚 人设结构</h2>
|
|
|
|
|
+ {persona_structure_html}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="timestamp">生成时间: {timestamp}</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Modal -->
|
|
|
|
|
+ <div id="detailModal" class="modal-overlay" onclick="closeModalOnOverlay(event)">
|
|
|
|
|
+ <div class="modal-content">
|
|
|
|
|
+ <button class="modal-close" onclick="closeModal()">×</button>
|
|
|
|
|
+ <div class="modal-body" id="modalBody">
|
|
|
|
|
+ <!-- Content will be inserted here -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <script>
|
|
|
|
|
+ {detail_modal_js}
|
|
|
|
|
+ </script>
|
|
|
|
|
+</body>
|
|
|
|
|
+</html>'''
|
|
|
|
|
+
|
|
|
|
|
+ # 写入文件
|
|
|
|
|
+ output_file = Path(output_path)
|
|
|
|
|
+ output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+
|
|
|
|
|
+ with open(output_file, 'w', encoding='utf-8') as f:
|
|
|
|
|
+ f.write(html_content)
|
|
|
|
|
+
|
|
|
|
|
+ return str(output_file.absolute())
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def load_persona_data(persona_path: str) -> Dict[str, Any]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 加载人设数据
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ persona_path: 人设JSON文件路径
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 人设数据字典
|
|
|
|
|
+ """
|
|
|
|
|
+ try:
|
|
|
|
|
+ with open(persona_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ return json.load(f)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"警告: 读取人设文件失败: {e}")
|
|
|
|
|
+ return {}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def main():
|
|
|
|
|
+ """主函数"""
|
|
|
|
|
+ import sys
|
|
|
|
|
+
|
|
|
|
|
+ # 配置路径
|
|
|
|
|
+ inspiration_dir = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/how/灵感点"
|
|
|
|
|
+ posts_dir = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/作者历史帖子"
|
|
|
|
|
+ persona_path = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/人设.json"
|
|
|
|
|
+ output_path = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/how/灵感点可视化.html"
|
|
|
|
|
+
|
|
|
|
|
+ print("=" * 60)
|
|
|
|
|
+ print("灵感点分析可视化脚本")
|
|
|
|
|
+ print("=" * 60)
|
|
|
|
|
+
|
|
|
|
|
+ # 加载数据
|
|
|
|
|
+ print("\n📂 正在加载灵感点数据...")
|
|
|
|
|
+ inspirations_data = load_inspiration_points_data(inspiration_dir)
|
|
|
|
|
+ print(f"✅ 成功加载 {len(inspirations_data)} 个灵感点")
|
|
|
|
|
+
|
|
|
|
|
+ print("\n📂 正在加载帖子数据...")
|
|
|
|
|
+ posts_map = load_posts_data(posts_dir)
|
|
|
|
|
+ print(f"✅ 成功加载 {len(posts_map)} 个帖子")
|
|
|
|
|
+
|
|
|
|
|
+ print("\n📂 正在加载人设数据...")
|
|
|
|
|
+ persona_data = load_persona_data(persona_path)
|
|
|
|
|
+ print(f"✅ 成功加载人设数据")
|
|
|
|
|
+
|
|
|
|
|
+ # 生成HTML
|
|
|
|
|
+ print("\n🎨 正在生成可视化HTML...")
|
|
|
|
|
+ result_path = generate_html(inspirations_data, posts_map, persona_data, output_path)
|
|
|
|
|
+
|
|
|
|
|
+ print(f"\n✅ 可视化文件已生成!")
|
|
|
|
|
+ print(f"📄 文件路径: {result_path}")
|
|
|
|
|
+ print(f"\n💡 在浏览器中打开该文件即可查看可视化结果")
|
|
|
|
|
+ print("=" * 60)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
|
+ main()
|