|
@@ -0,0 +1,4911 @@
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
|
|
+"""
|
|
|
|
|
+搜索评估和解构整合可视化工具
|
|
|
|
|
+在评估结果基础上,为完全匹配帖子增加解构和相似度展示
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+import json
|
|
|
|
|
+import os
|
|
|
|
|
+from datetime import datetime
|
|
|
|
|
+from typing import List, Dict, Any
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def load_data(json_path: str) -> List[Dict[str, Any]]:
|
|
|
|
|
+ """加载JSON数据"""
|
|
|
|
|
+ with open(json_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ return json.load(f)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def load_deconstruction_data(json_path: str) -> Dict[str, Any]:
|
|
|
|
|
+ """加载解构数据"""
|
|
|
|
|
+ with open(json_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ data = json.load(f)
|
|
|
|
|
+
|
|
|
|
|
+ # 创建note_id到解构数据的映射
|
|
|
|
|
+ mapping = {}
|
|
|
|
|
+ for result in data.get('results', []):
|
|
|
|
|
+ note_id = result.get('note_id')
|
|
|
|
|
+ if note_id:
|
|
|
|
|
+ mapping[note_id] = result
|
|
|
|
|
+
|
|
|
|
|
+ return mapping
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def load_similarity_data(json_path: str) -> Dict[str, Any]:
|
|
|
|
|
+ """加载相似度分析数据"""
|
|
|
|
|
+ with open(json_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ data = json.load(f)
|
|
|
|
|
+
|
|
|
|
|
+ # 创建note_id到相似度数据的映射
|
|
|
|
|
+ mapping = {}
|
|
|
|
|
+ for result in data.get('results', []):
|
|
|
|
|
+ note_id = result.get('note_id')
|
|
|
|
|
+ if note_id:
|
|
|
|
|
+ mapping[note_id] = result
|
|
|
|
|
+
|
|
|
|
|
+ return mapping
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def load_persona_library(json_path: str) -> Dict[str, Any]:
|
|
|
|
|
+ """加载人设特征库"""
|
|
|
|
|
+ with open(json_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ return json.load(f)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def extract_all_features_from_how(how_json_path: str) -> Dict[str, Dict[str, Any]]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 从how解构结果中提取所有特征的完整信息(适配新数据结构)
|
|
|
|
|
+
|
|
|
|
|
+ 新结构说明:
|
|
|
|
|
+ - 使用 '解构结果' 而不是 'how解构结果'
|
|
|
|
|
+ - 点的名称本身就是特征名,不再有特征列表
|
|
|
|
|
+ - 相似度从点的 '匹配人设结果' 获取
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ Dict[特征名称, {
|
|
|
|
|
+ 'similarity': float, # 最高相似度
|
|
|
|
|
+ 'weight': float, # 权重(新结构默认1.0)
|
|
|
|
|
+ 'dimension': str, # 所属维度
|
|
|
|
|
+ 'category': str # 分类:'已搜索', '待搜索', '低相似度'
|
|
|
|
|
+ }]
|
|
|
|
|
+ """
|
|
|
|
|
+ with open(how_json_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ how_data = json.load(f)
|
|
|
|
|
+
|
|
|
|
|
+ features = {}
|
|
|
|
|
+
|
|
|
|
|
+ for dimension_key in ['灵感点列表', '目的点列表', '关键点列表']:
|
|
|
|
|
+ # 适配新结构:使用 '解构结果' 而不是 'how解构结果'
|
|
|
|
|
+ dimension_list = how_data.get('解构结果', {}).get(dimension_key, [])
|
|
|
|
|
+ dimension_name = dimension_key.replace('列表', '')
|
|
|
|
|
+
|
|
|
|
|
+ for point in dimension_list:
|
|
|
|
|
+ # 适配新结构:点的名称本身就是特征名
|
|
|
|
|
+ feature_name = point.get('名称', '')
|
|
|
|
|
+ if not feature_name:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # 新结构默认权重为1.0
|
|
|
|
|
+ weight = 1.0
|
|
|
|
|
+
|
|
|
|
|
+ # 适配新结构:从点的匹配人设结果获取最高相似度
|
|
|
|
|
+ max_similarity = 0
|
|
|
|
|
+ match_results = point.get('匹配人设结果', [])
|
|
|
|
|
+
|
|
|
|
|
+ for match in match_results:
|
|
|
|
|
+ similarity = match.get('相似度', 0)
|
|
|
|
|
+ max_similarity = max(max_similarity, similarity)
|
|
|
|
|
+
|
|
|
|
|
+ # 确定分类(使用0.5阈值,因为新流程筛选的是0.5-0.8)
|
|
|
|
|
+ if max_similarity < 0.5:
|
|
|
|
|
+ category = '低相似度'
|
|
|
|
|
+ elif 0.5 <= max_similarity < 0.8:
|
|
|
|
|
+ category = '待搜索' # 后续会根据评估数据更新为'已搜索'
|
|
|
|
|
+ else:
|
|
|
|
|
+ category = '高相似度' # >=0.8,孤立点
|
|
|
|
|
+
|
|
|
|
|
+ features[feature_name] = {
|
|
|
|
|
+ 'similarity': max_similarity,
|
|
|
|
|
+ 'weight': weight,
|
|
|
|
|
+ 'dimension': dimension_name,
|
|
|
|
|
+ 'category': category
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return features
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def is_category_or_feature(persona_name: str, persona_data: Dict[str, Any]) -> str:
|
|
|
|
|
+ """
|
|
|
|
|
+ 递归判断人设项是特征还是分类
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ persona_name: 人设项名称
|
|
|
|
|
+ persona_data: 人设库数据
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 'feature' 或 'category'
|
|
|
|
|
+ """
|
|
|
|
|
+ def search_in_dict(data, name, path=""):
|
|
|
|
|
+ """递归搜索字典"""
|
|
|
|
|
+ if isinstance(data, dict):
|
|
|
|
|
+ # 检查是否有特征列表
|
|
|
|
|
+ if '特征列表' in data:
|
|
|
|
|
+ for feature in data['特征列表']:
|
|
|
|
|
+ if feature.get('特征名称') == name:
|
|
|
|
|
+ return 'feature'
|
|
|
|
|
+
|
|
|
|
|
+ # 递归搜索子节点
|
|
|
|
|
+ for key, value in data.items():
|
|
|
|
|
+ if key == '_meta':
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # 如果键名匹配,且值是字典(说明是分类节点)
|
|
|
|
|
+ if key == name and isinstance(value, dict):
|
|
|
|
|
+ return 'category'
|
|
|
|
|
+
|
|
|
|
|
+ # 递归搜索
|
|
|
|
|
+ result = search_in_dict(value, name, f"{path}/{key}")
|
|
|
|
|
+ if result:
|
|
|
|
|
+ return result
|
|
|
|
|
+
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ # 在三个主要维度中搜索
|
|
|
|
|
+ for dimension in ['灵感点列表', '目的点列表', '关键点列表']:
|
|
|
|
|
+ if dimension in persona_data:
|
|
|
|
|
+ result = search_in_dict(persona_data[dimension], persona_name)
|
|
|
|
|
+ if result:
|
|
|
|
|
+ return result
|
|
|
|
|
+
|
|
|
|
|
+ # 默认返回特征
|
|
|
|
|
+ return 'feature'
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def extract_relationship_data(how_json_path: str, persona_data: Dict[str, Any]) -> tuple:
|
|
|
|
|
+ """
|
|
|
|
|
+ 从how解构结果中提取关系数据
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ Tuple of (relationships, all_high_matches):
|
|
|
|
|
+ - relationships: 最高相似度匹配的关系列表
|
|
|
|
|
+ - all_high_matches: 所有相似度>0.8的匹配列表
|
|
|
|
|
+
|
|
|
|
|
+ Data format:
|
|
|
|
|
+ {
|
|
|
|
|
+ 'post_feature': str, # 帖子特征名称
|
|
|
|
|
+ 'dimension': str, # 所属维度(灵感点/目的点/关键点)
|
|
|
|
|
+ 'weight': float, # 权重
|
|
|
|
|
+ 'persona_item': str, # 人设特征/分类名称
|
|
|
|
|
+ 'similarity': float, # 相似度
|
|
|
|
|
+ 'item_type': str, # 'feature' or 'category'
|
|
|
|
|
+ }
|
|
|
|
|
+ """
|
|
|
|
|
+ with open(how_json_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ how_data = json.load(f)
|
|
|
|
|
+
|
|
|
|
|
+ relationships = []
|
|
|
|
|
+ all_high_matches = [] # 所有相似度>0.8的匹配
|
|
|
|
|
+
|
|
|
|
|
+ # 遍历how解构结果
|
|
|
|
|
+ for dimension_key in ['灵感点列表', '目的点列表', '关键点列表']:
|
|
|
|
|
+ dimension_list = how_data.get('how解构结果', {}).get(dimension_key, [])
|
|
|
|
|
+ dimension_name = dimension_key.replace('列表', '')
|
|
|
|
|
+
|
|
|
|
|
+ for point in dimension_list:
|
|
|
|
|
+ features = point.get('特征列表', [])
|
|
|
|
|
+
|
|
|
|
|
+ for feature in features:
|
|
|
|
|
+ feature_name = feature.get('特征名称', '')
|
|
|
|
|
+ weight = feature.get('权重', 0)
|
|
|
|
|
+
|
|
|
|
|
+ # 查找匹配结果中相似度最高的
|
|
|
|
|
+ how_steps = point.get('how步骤列表', [])
|
|
|
|
|
+ max_match = None
|
|
|
|
|
+ max_similarity = -1
|
|
|
|
|
+
|
|
|
|
|
+ for step in how_steps:
|
|
|
|
|
+ step_features = step.get('特征列表', [])
|
|
|
|
|
+ for step_feature in step_features:
|
|
|
|
|
+ if step_feature.get('特征名称') == feature_name:
|
|
|
|
|
+ matches = step_feature.get('匹配结果', [])
|
|
|
|
|
+ for match in matches:
|
|
|
|
|
+ similarity = match.get('匹配结果', {}).get('相似度', 0)
|
|
|
|
|
+ persona_name = match.get('人设特征名称', '')
|
|
|
|
|
+
|
|
|
|
|
+ # 收集所有相似度>0.8的匹配
|
|
|
|
|
+ if similarity > 0.8:
|
|
|
|
|
+ item_type = is_category_or_feature(persona_name, persona_data)
|
|
|
|
|
+ all_high_matches.append({
|
|
|
|
|
+ 'post_feature': feature_name,
|
|
|
|
|
+ 'dimension': dimension_name,
|
|
|
|
|
+ 'weight': weight,
|
|
|
|
|
+ 'persona_item': persona_name,
|
|
|
|
|
+ 'similarity': similarity,
|
|
|
|
|
+ 'item_type': item_type
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ # 追踪最高相似度
|
|
|
|
|
+ if similarity > max_similarity:
|
|
|
|
|
+ max_similarity = similarity
|
|
|
|
|
+ max_match = match
|
|
|
|
|
+
|
|
|
|
|
+ # 如果找到匹配
|
|
|
|
|
+ if max_match:
|
|
|
|
|
+ persona_name = max_match.get('人设特征名称', '')
|
|
|
|
|
+ item_type = is_category_or_feature(persona_name, persona_data)
|
|
|
|
|
+
|
|
|
|
|
+ relationships.append({
|
|
|
|
|
+ 'post_feature': feature_name,
|
|
|
|
|
+ 'dimension': dimension_name,
|
|
|
|
|
+ 'weight': weight,
|
|
|
|
|
+ 'persona_item': persona_name,
|
|
|
|
|
+ 'similarity': max_similarity,
|
|
|
|
|
+ 'item_type': item_type
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return relationships, all_high_matches
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def generate_relationship_graph_html(relationships: List[Dict[str, Any]],
|
|
|
|
|
+ all_high_matches: List[Dict[str, Any]]) -> str:
|
|
|
|
|
+ """
|
|
|
|
|
+ 生成关系图的SVG HTML代码
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ relationships: 最高相似度匹配的关系数据列表
|
|
|
|
|
+ all_high_matches: 所有相似度>0.8的匹配列表
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ HTML字符串(包含SVG)
|
|
|
|
|
+ """
|
|
|
|
|
+ if not relationships:
|
|
|
|
|
+ return '<div style="padding: 40px; text-align: center; color: #6b7280;">暂无关系数据</div>'
|
|
|
|
|
+
|
|
|
|
|
+ # 合并所有需要显示的人设项(去重)
|
|
|
|
|
+ all_personas = set()
|
|
|
|
|
+
|
|
|
|
|
+ # 添加relationships中的人设项
|
|
|
|
|
+ for r in relationships:
|
|
|
|
|
+ all_personas.add((r['persona_item'], r['item_type'], r['similarity']))
|
|
|
|
|
+
|
|
|
|
|
+ # 添加所有>0.8的人设项
|
|
|
|
|
+ for match in all_high_matches:
|
|
|
|
|
+ all_personas.add((match['persona_item'], match['item_type'], match['similarity']))
|
|
|
|
|
+
|
|
|
|
|
+ persona_items = list(all_personas)
|
|
|
|
|
+
|
|
|
|
|
+ # 为每个帖子特征关联最高相似度
|
|
|
|
|
+ post_feature_map = {}
|
|
|
|
|
+ for r in relationships:
|
|
|
|
|
+ feature = r['post_feature']
|
|
|
|
|
+ if feature not in post_feature_map or r['similarity'] > post_feature_map[feature][3]:
|
|
|
|
|
+ post_feature_map[feature] = (feature, r['dimension'], r['weight'], r['similarity'])
|
|
|
|
|
+
|
|
|
|
|
+ post_features = list(post_feature_map.values())
|
|
|
|
|
+
|
|
|
|
|
+ # 排序:都按相似度降序
|
|
|
|
|
+ persona_items.sort(key=lambda x: x[2], reverse=True) # 按相似度降序
|
|
|
|
|
+ post_features.sort(key=lambda x: x[3], reverse=True) # 按相似度降序
|
|
|
|
|
+
|
|
|
|
|
+ # 布局参数
|
|
|
|
|
+ node_spacing = 60 # 节点间距
|
|
|
|
|
+ node_radius = 20 # 圆形半径
|
|
|
|
|
+ node_size = 40 # 方形大小
|
|
|
|
|
+ left_margin = 250 # 左边距
|
|
|
|
|
+ right_margin = 250 # 右边距
|
|
|
|
|
+ middle_space = 400 # 中间空间
|
|
|
|
|
+ top_margin = 50 # 顶部边距
|
|
|
|
|
+
|
|
|
|
|
+ # 计算SVG尺寸
|
|
|
|
|
+ max_nodes = max(len(persona_items), len(post_features))
|
|
|
|
|
+ svg_height = max_nodes * node_spacing + top_margin * 2
|
|
|
|
|
+ svg_width = left_margin + middle_space + right_margin
|
|
|
|
|
+
|
|
|
|
|
+ # 辅助函数:获取颜色
|
|
|
|
|
+ def get_color(similarity):
|
|
|
|
|
+ if similarity > 0.8:
|
|
|
|
|
+ return '#10b981' # 绿色
|
|
|
|
|
+ elif similarity > 0.4:
|
|
|
|
|
+ return '#f59e0b' # 黄色
|
|
|
|
|
+ else:
|
|
|
|
|
+ return '#ef4444' # 红色
|
|
|
|
|
+
|
|
|
|
|
+ # 辅助函数:获取线条样式
|
|
|
|
|
+ def get_stroke_dasharray(similarity):
|
|
|
|
|
+ if similarity > 0.8:
|
|
|
|
|
+ return 'none' # 实线
|
|
|
|
|
+ else:
|
|
|
|
|
+ return '5,5' # 虚线
|
|
|
|
|
+
|
|
|
|
|
+ # 创建节点位置映射
|
|
|
|
|
+ persona_positions = {}
|
|
|
|
|
+ post_positions = {}
|
|
|
|
|
+
|
|
|
|
|
+ for idx, (item, item_type, similarity) in enumerate(persona_items):
|
|
|
|
|
+ y = top_margin + idx * node_spacing
|
|
|
|
|
+ persona_positions[item] = {
|
|
|
|
|
+ 'x': left_margin,
|
|
|
|
|
+ 'y': y,
|
|
|
|
|
+ 'type': item_type,
|
|
|
|
|
+ 'similarity': similarity,
|
|
|
|
|
+ 'idx': idx # 添加索引用于生成ID
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for idx, (feature, dimension, weight, similarity) in enumerate(post_features):
|
|
|
|
|
+ y = top_margin + idx * node_spacing
|
|
|
|
|
+ post_positions[feature] = {
|
|
|
|
|
+ 'x': left_margin + middle_space,
|
|
|
|
|
+ 'y': y,
|
|
|
|
|
+ 'dimension': dimension,
|
|
|
|
|
+ 'weight': weight,
|
|
|
|
|
+ 'similarity': similarity,
|
|
|
|
|
+ 'idx': idx # 添加索引用于生成ID
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # 开始生成SVG
|
|
|
|
|
+ svg_parts = [f'''
|
|
|
|
|
+ <div class="relationship-graph-container">
|
|
|
|
|
+ <svg width="{svg_width}" height="{svg_height}" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
+ <!-- 定义箭头标记 -->
|
|
|
|
|
+ <defs>
|
|
|
|
|
+ <marker id="arrowhead-green" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
|
|
|
|
+ <polygon points="0 0, 10 3, 0 6" fill="#10b981" />
|
|
|
|
|
+ </marker>
|
|
|
|
|
+ <marker id="arrowhead-yellow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
|
|
|
|
+ <polygon points="0 0, 10 3, 0 6" fill="#f59e0b" />
|
|
|
|
|
+ </marker>
|
|
|
|
|
+ <marker id="arrowhead-red" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
|
|
|
|
+ <polygon points="0 0, 10 3, 0 6" fill="#ef4444" />
|
|
|
|
|
+ </marker>
|
|
|
|
|
+ </defs>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 左右标记 -->
|
|
|
|
|
+ <g class="labels">
|
|
|
|
|
+ <!-- 人设标记 -->
|
|
|
|
|
+ <text x="{left_margin}" y="30"
|
|
|
|
|
+ font-size="18" font-weight="600" fill="#6b7280"
|
|
|
|
|
+ text-anchor="middle">人设</text>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 帖子标记 -->
|
|
|
|
|
+ <text x="{left_margin + middle_space}" y="30"
|
|
|
|
|
+ font-size="18" font-weight="600" fill="#6b7280"
|
|
|
|
|
+ text-anchor="middle">帖子</text>
|
|
|
|
|
+ </g>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 连接线 -->
|
|
|
|
|
+ <g class="connections">
|
|
|
|
|
+ ''']
|
|
|
|
|
+
|
|
|
|
|
+ # 合并所有需要绘制的连接(去重)
|
|
|
|
|
+ all_connections = []
|
|
|
|
|
+ connection_keys = set()
|
|
|
|
|
+
|
|
|
|
|
+ # 添加主要关系(最高相似度匹配)
|
|
|
|
|
+ for rel in relationships:
|
|
|
|
|
+ key = (rel['persona_item'], rel['post_feature'])
|
|
|
|
|
+ if key not in connection_keys:
|
|
|
|
|
+ all_connections.append(rel)
|
|
|
|
|
+ connection_keys.add(key)
|
|
|
|
|
+
|
|
|
|
|
+ # 添加额外的>0.8关系
|
|
|
|
|
+ for match in all_high_matches:
|
|
|
|
|
+ key = (match['persona_item'], match['post_feature'])
|
|
|
|
|
+ if key not in connection_keys:
|
|
|
|
|
+ all_connections.append(match)
|
|
|
|
|
+ connection_keys.add(key)
|
|
|
|
|
+
|
|
|
|
|
+ # 绘制连接线
|
|
|
|
|
+ line_idx = 0 # 计数器用于标签错开
|
|
|
|
|
+ for rel in all_connections:
|
|
|
|
|
+ persona_pos = persona_positions.get(rel['persona_item'])
|
|
|
|
|
+ post_pos = post_positions.get(rel['post_feature'])
|
|
|
|
|
+
|
|
|
|
|
+ if persona_pos and post_pos:
|
|
|
|
|
+ x1 = persona_pos['x'] + (node_size if persona_pos['type'] == 'category' else node_radius)
|
|
|
|
|
+ y1 = persona_pos['y']
|
|
|
|
|
+ x2 = post_pos['x'] - node_radius
|
|
|
|
|
+ y2 = post_pos['y']
|
|
|
|
|
+
|
|
|
|
|
+ # 贝塞尔曲线控制点
|
|
|
|
|
+ cx1 = x1 + (x2 - x1) * 0.3
|
|
|
|
|
+ cx2 = x1 + (x2 - x1) * 0.7
|
|
|
|
|
+
|
|
|
|
|
+ color = get_color(rel['similarity'])
|
|
|
|
|
+ dasharray = get_stroke_dasharray(rel['similarity'])
|
|
|
|
|
+
|
|
|
|
|
+ # 生成节点ID
|
|
|
|
|
+ persona_id = f"persona-{persona_pos['idx']}"
|
|
|
|
|
+ post_id = f"post-{post_pos['idx']}"
|
|
|
|
|
+
|
|
|
|
|
+ # 标签位置:左右错开
|
|
|
|
|
+ if line_idx % 2 == 0:
|
|
|
|
|
+ label_x = x1 + (x2 - x1) * 0.35 # 左侧位置
|
|
|
|
|
+ else:
|
|
|
|
|
+ label_x = x1 + (x2 - x1) * 0.65 # 右侧位置
|
|
|
|
|
+ line_idx += 1
|
|
|
|
|
+
|
|
|
|
|
+ svg_parts.append(f'''
|
|
|
|
|
+ <path class="connection-line"
|
|
|
|
|
+ data-from="{persona_id}"
|
|
|
|
|
+ data-to="{post_id}"
|
|
|
|
|
+ d="M {x1},{y1} C {cx1},{y1} {cx2},{y2} {x2},{y2}"
|
|
|
|
|
+ stroke="{color}"
|
|
|
|
|
+ stroke-width="2"
|
|
|
|
|
+ stroke-dasharray="{dasharray}"
|
|
|
|
|
+ fill="none"
|
|
|
|
|
+ opacity="0.7" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 相似度标签 -->
|
|
|
|
|
+ <text x="{label_x}" y="{(y1 + y2) / 2 - 5}"
|
|
|
|
|
+ font-size="12"
|
|
|
|
|
+ fill="{color}"
|
|
|
|
|
+ text-anchor="middle"
|
|
|
|
|
+ font-weight="600"
|
|
|
|
|
+ pointer-events="none">
|
|
|
|
|
+ {rel['similarity']:.2f}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ ''')
|
|
|
|
|
+
|
|
|
|
|
+ svg_parts.append('</g>\n\n<!-- 左侧:人设特征/分类节点 -->\n<g class="persona-nodes">')
|
|
|
|
|
+
|
|
|
|
|
+ # 绘制左侧人设节点
|
|
|
|
|
+ for item, pos in persona_positions.items():
|
|
|
|
|
+ color = get_color(pos['similarity'])
|
|
|
|
|
+ node_id = f"persona-{pos['idx']}"
|
|
|
|
|
+
|
|
|
|
|
+ if pos['type'] == 'category':
|
|
|
|
|
+ # 方形
|
|
|
|
|
+ half_size = node_size / 2
|
|
|
|
|
+ svg_parts.append(f'''
|
|
|
|
|
+ <g class="clickable" data-node-id="{node_id}" onclick="handleNodeClick('{node_id}', 'persona')">
|
|
|
|
|
+ <rect x="{pos['x'] - half_size}" y="{pos['y'] - half_size}"
|
|
|
|
|
+ width="{node_size}" height="{node_size}"
|
|
|
|
|
+ fill="{color}"
|
|
|
|
|
+ stroke="white"
|
|
|
|
|
+ stroke-width="2"
|
|
|
|
|
+ rx="4" />
|
|
|
|
|
+ <text x="{pos['x'] - half_size - 10}" y="{pos['y'] + 5}"
|
|
|
|
|
+ font-size="14"
|
|
|
|
|
+ fill="#374151"
|
|
|
|
|
+ text-anchor="end"
|
|
|
|
|
+ font-weight="500"
|
|
|
|
|
+ pointer-events="none">⬜ {item}</text>
|
|
|
|
|
+ </g>
|
|
|
|
|
+ ''')
|
|
|
|
|
+ else:
|
|
|
|
|
+ # 圆形
|
|
|
|
|
+ svg_parts.append(f'''
|
|
|
|
|
+ <g class="clickable" data-node-id="{node_id}" onclick="handleNodeClick('{node_id}', 'persona')">
|
|
|
|
|
+ <circle cx="{pos['x']}" cy="{pos['y']}"
|
|
|
|
|
+ r="{node_radius}"
|
|
|
|
|
+ fill="{color}"
|
|
|
|
|
+ stroke="white"
|
|
|
|
|
+ stroke-width="2" />
|
|
|
|
|
+ <text x="{pos['x'] - node_radius - 10}" y="{pos['y'] + 5}"
|
|
|
|
|
+ font-size="14"
|
|
|
|
|
+ fill="#374151"
|
|
|
|
|
+ text-anchor="end"
|
|
|
|
|
+ font-weight="500"
|
|
|
|
|
+ pointer-events="none">⚪ {item}</text>
|
|
|
|
|
+ </g>
|
|
|
|
|
+ ''')
|
|
|
|
|
+
|
|
|
|
|
+ svg_parts.append('</g>\n\n<!-- 右侧:帖子特征节点 -->\n<g class="post-nodes">')
|
|
|
|
|
+
|
|
|
|
|
+ # 绘制右侧帖子节点
|
|
|
|
|
+ for feature, pos in post_positions.items():
|
|
|
|
|
+ # 帖子特征都是圆形,蓝色
|
|
|
|
|
+ node_id = f"post-{pos['idx']}"
|
|
|
|
|
+ svg_parts.append(f'''
|
|
|
|
|
+ <g class="clickable" data-node-id="{node_id}" onclick="handleNodeClick('{node_id}', 'post')">
|
|
|
|
|
+ <circle cx="{pos['x']}" cy="{pos['y']}"
|
|
|
|
|
+ r="{node_radius}"
|
|
|
|
|
+ fill="#3b82f6"
|
|
|
|
|
+ stroke="white"
|
|
|
|
|
+ stroke-width="2" />
|
|
|
|
|
+ <text x="{pos['x'] + node_radius + 10}" y="{pos['y'] + 5}"
|
|
|
|
|
+ font-size="14"
|
|
|
|
|
+ fill="#374151"
|
|
|
|
|
+ text-anchor="start"
|
|
|
|
|
+ font-weight="500"
|
|
|
|
|
+ pointer-events="none">📝 {feature}</text>
|
|
|
|
|
+ <text x="{pos['x'] + node_radius + 10}" y="{pos['y'] + 20}"
|
|
|
|
|
+ font-size="11"
|
|
|
|
|
+ fill="#6b7280"
|
|
|
|
|
+ text-anchor="start"
|
|
|
|
|
+ pointer-events="none">({pos['dimension']} · 权重{pos['weight']:.1f})</text>
|
|
|
|
|
+ </g>
|
|
|
|
|
+ ''')
|
|
|
|
|
+
|
|
|
|
|
+ svg_parts.append('''
|
|
|
|
|
+ </g>
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ''')
|
|
|
|
|
+
|
|
|
|
|
+ return ''.join(svg_parts)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def calculate_statistics(data: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
|
|
|
+ """计算统计数据(包括评估结果)"""
|
|
|
|
|
+ total_features = len(data)
|
|
|
|
|
+ total_search_words = 0
|
|
|
|
|
+ searched_count = 0
|
|
|
|
|
+ not_searched_count = 0
|
|
|
|
|
+ total_notes = 0
|
|
|
|
|
+ video_count = 0
|
|
|
|
|
+ normal_count = 0
|
|
|
|
|
+
|
|
|
|
|
+ # 评估统计
|
|
|
|
|
+ total_evaluated_notes = 0
|
|
|
|
|
+ total_filtered = 0
|
|
|
|
|
+ match_complete = 0
|
|
|
|
|
+ match_similar = 0
|
|
|
|
|
+ match_weak = 0
|
|
|
|
|
+ match_none = 0
|
|
|
|
|
+
|
|
|
|
|
+ for feature in data:
|
|
|
|
|
+ grouped_results = feature.get('组合评估结果_分组', [])
|
|
|
|
|
+
|
|
|
|
|
+ for group in grouped_results:
|
|
|
|
|
+ search_items = group.get('top10_searches', [])
|
|
|
|
|
+ total_search_words += len(search_items)
|
|
|
|
|
+
|
|
|
|
|
+ for search_item in search_items:
|
|
|
|
|
+ search_result = search_item.get('search_result', {})
|
|
|
|
|
+
|
|
|
|
|
+ if search_result:
|
|
|
|
|
+ searched_count += 1
|
|
|
|
|
+ notes = search_result.get('data', {}).get('data', [])
|
|
|
|
|
+ total_notes += len(notes)
|
|
|
|
|
+
|
|
|
|
|
+ for note in notes:
|
|
|
|
|
+ note_type = note.get('note_card', {}).get('type', '')
|
|
|
|
|
+ if note_type == 'video':
|
|
|
|
|
+ video_count += 1
|
|
|
|
|
+ else:
|
|
|
|
|
+ normal_count += 1
|
|
|
|
|
+
|
|
|
|
|
+ evaluation = search_item.get('evaluation_with_filter')
|
|
|
|
|
+ if evaluation:
|
|
|
|
|
+ total_evaluated_notes += evaluation.get('total_notes', 0)
|
|
|
|
|
+ total_filtered += evaluation.get('filtered_count', 0)
|
|
|
|
|
+
|
|
|
|
|
+ stats = evaluation.get('statistics', {})
|
|
|
|
|
+ match_complete += stats.get('完全匹配(0.8-1.0)', 0)
|
|
|
|
|
+ match_similar += stats.get('相似匹配(0.6-0.79)', 0)
|
|
|
|
|
+ match_weak += stats.get('弱相似(0.5-0.59)', 0)
|
|
|
|
|
+ match_none += stats.get('无匹配(≤0.4)', 0)
|
|
|
|
|
+ else:
|
|
|
|
|
+ not_searched_count += 1
|
|
|
|
|
+
|
|
|
|
|
+ total_remaining = total_evaluated_notes - total_filtered if total_evaluated_notes > 0 else 0
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'total_features': total_features,
|
|
|
|
|
+ 'total_search_words': total_search_words,
|
|
|
|
|
+ 'searched_count': searched_count,
|
|
|
|
|
+ 'not_searched_count': not_searched_count,
|
|
|
|
|
+ 'searched_percentage': round(searched_count / total_search_words * 100, 1) if total_search_words > 0 else 0,
|
|
|
|
|
+ 'total_notes': total_notes,
|
|
|
|
|
+ 'video_count': video_count,
|
|
|
|
|
+ 'normal_count': normal_count,
|
|
|
|
|
+ 'video_percentage': round(video_count / total_notes * 100, 1) if total_notes > 0 else 0,
|
|
|
|
|
+ 'normal_percentage': round(normal_count / total_notes * 100, 1) if total_notes > 0 else 0,
|
|
|
|
|
+ 'total_evaluated': total_evaluated_notes,
|
|
|
|
|
+ 'total_filtered': total_filtered,
|
|
|
|
|
+ 'total_remaining': total_remaining,
|
|
|
|
|
+ 'filter_rate': round(total_filtered / total_evaluated_notes * 100, 1) if total_evaluated_notes > 0 else 0,
|
|
|
|
|
+ 'match_complete': match_complete,
|
|
|
|
|
+ 'match_similar': match_similar,
|
|
|
|
|
+ 'match_weak': match_weak,
|
|
|
|
|
+ 'match_none': match_none,
|
|
|
|
|
+ 'complete_rate': round(match_complete / total_remaining * 100, 1) if total_remaining > 0 else 0,
|
|
|
|
|
+ 'similar_rate': round(match_similar / total_remaining * 100, 1) if total_remaining > 0 else 0,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
|
|
|
|
|
+ deconstruction_mapping: Dict[str, Any], similarity_mapping: Dict[str, Any],
|
|
|
|
|
+ relationship_graph_html: str,
|
|
|
|
|
+ all_features: Dict[str, Dict[str, Any]],
|
|
|
|
|
+ high_similarity_features: List[Dict[str, Any]],
|
|
|
|
|
+ low_similarity_features: List[Dict[str, Any]],
|
|
|
|
|
+ output_path: str):
|
|
|
|
|
+ """生成HTML可视化页面"""
|
|
|
|
|
+
|
|
|
|
|
+ # 准备数据JSON
|
|
|
|
|
+ data_json = json.dumps(data, ensure_ascii=False, indent=2)
|
|
|
|
|
+ deconstruction_json = json.dumps(deconstruction_mapping, ensure_ascii=False, indent=2)
|
|
|
|
|
+ similarity_json = json.dumps(similarity_mapping, ensure_ascii=False, indent=2)
|
|
|
|
|
+ all_features_json = json.dumps(all_features, ensure_ascii=False, indent=2)
|
|
|
|
|
+ high_similarity_json = json.dumps(high_similarity_features, ensure_ascii=False, indent=2)
|
|
|
|
|
+ low_similarity_json = json.dumps(low_similarity_features, ensure_ascii=False, indent=2)
|
|
|
|
|
+
|
|
|
|
|
+ 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: #f5f7fa;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ overflow-x: hidden;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 顶部统计面板 */
|
|
|
|
|
+ .stats-panel {{
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ z-index: 50;
|
|
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stats-container {{
|
|
|
|
|
+ max-width: 1400px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stats-row {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-around;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 15px;
|
|
|
|
|
+ margin-bottom: 15px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stats-row:last-child {{
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+ padding-top: 15px;
|
|
|
|
|
+ border-top: 1px solid rgba(255,255,255,0.2);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-item {{
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-value {{
|
|
|
|
|
+ font-size: 28px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ margin-bottom: 5px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-label {{
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ opacity: 0.9;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-item.small .stat-value {{
|
|
|
|
|
+ font-size: 22px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 过滤控制面板 */
|
|
|
|
|
+ .filter-panel {{
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ z-index: 40;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ max-width: 1400px;
|
|
|
|
|
+ margin: 20px auto;
|
|
|
|
|
+ padding: 15px 20px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .filter-label {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .filter-buttons {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .filter-btn {{
|
|
|
|
|
+ padding: 6px 12px;
|
|
|
|
|
+ border: 2px solid #e5e7eb;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .filter-btn:hover {{
|
|
|
|
|
+ border-color: #667eea;
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .filter-btn.active {{
|
|
|
|
|
+ border-color: #667eea;
|
|
|
|
|
+ background: #667eea;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .filter-btn.complete {{
|
|
|
|
|
+ border-color: #10b981;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .filter-btn.complete.active {{
|
|
|
|
|
+ background: #10b981;
|
|
|
|
|
+ border-color: #10b981;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .filter-btn.similar {{
|
|
|
|
|
+ border-color: #f59e0b;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .filter-btn.similar.active {{
|
|
|
|
|
+ background: #f59e0b;
|
|
|
|
|
+ border-color: #f59e0b;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .filter-btn.weak {{
|
|
|
|
|
+ border-color: #f97316;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .filter-btn.weak.active {{
|
|
|
|
|
+ background: #f97316;
|
|
|
|
|
+ border-color: #f97316;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .filter-btn.none {{
|
|
|
|
|
+ border-color: #ef4444;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .filter-btn.none.active {{
|
|
|
|
|
+ background: #ef4444;
|
|
|
|
|
+ border-color: #ef4444;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .filter-btn.filtered {{
|
|
|
|
|
+ border-color: #6b7280;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .filter-btn.filtered.active {{
|
|
|
|
|
+ background: #6b7280;
|
|
|
|
|
+ border-color: #6b7280;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 主容器 - 级联布局 */
|
|
|
|
|
+ .main-container {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ max-width: 1800px;
|
|
|
|
|
+ margin: 0 auto 20px;
|
|
|
|
|
+ gap: 25px;
|
|
|
|
|
+ padding: 0 20px;
|
|
|
|
|
+ height: calc(100vh - 260px);
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 左侧栏 - 原始特征列表 */
|
|
|
|
|
+ .left-sidebar {{
|
|
|
|
|
+ width: 20%;
|
|
|
|
|
+ min-width: 220px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ height: fit-content;
|
|
|
|
|
+ max-height: calc(100vh - 280px);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 中间栏 - base_word列表 */
|
|
|
|
|
+ .middle-sidebar {{
|
|
|
|
|
+ width: 18%;
|
|
|
|
|
+ min-width: 240px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ height: fit-content;
|
|
|
|
|
+ max-height: calc(100vh - 280px);
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .middle-sidebar.active {{
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 右侧栏 - 搜索词列表 */
|
|
|
|
|
+ .right-sidebar {{
|
|
|
|
|
+ width: 22%;
|
|
|
|
|
+ min-width: 280px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ height: fit-content;
|
|
|
|
|
+ max-height: calc(100vh - 280px);
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .right-sidebar.active {{
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 详情区域 */
|
|
|
|
|
+ .detail-area {{
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ max-height: calc(100vh - 280px);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 侧边栏标题 */
|
|
|
|
|
+ .sidebar-header {{
|
|
|
|
|
+ position: sticky;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+ padding: 15px 20px;
|
|
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ border-radius: 8px 8px 0 0;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .sidebar-content {{
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 详情区占位符 */
|
|
|
|
|
+ .detail-placeholder {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ height: 300px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .detail-content {{
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 候选词库固定区域 */
|
|
|
|
|
+ .candidate-library {{
|
|
|
|
|
+ position: sticky;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-bottom: 2px solid #e5e7eb;
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-header {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ background: linear-gradient(135deg, #f3e8ff 0%, #fae8ff 100%);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ border: 2px solid #d8b4fe;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-header:hover {{
|
|
|
|
|
+ background: linear-gradient(135deg, #e9d5ff 0%, #f3e8ff 100%);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-title {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #7c3aed;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-toggle {{
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #7c3aed;
|
|
|
|
|
+ transition: transform 0.3s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-toggle.rotated {{
|
|
|
|
|
+ transform: rotate(180deg);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-content {{
|
|
|
|
|
+ margin-top: 10px;
|
|
|
|
|
+ max-height: 0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ transition: max-height 0.3s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-content.expanded {{
|
|
|
|
|
+ max-height: 400px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-section {{
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-section-title {{
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ padding: 8px 10px;
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-list {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-item {{
|
|
|
|
|
+ padding: 4px 10px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ border: 1px solid transparent;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-item.persona {{
|
|
|
|
|
+ background: #d1fae5;
|
|
|
|
|
+ color: #065f46;
|
|
|
|
|
+ border-color: #a7f3d0;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-item.persona:hover {{
|
|
|
|
|
+ background: #a7f3d0;
|
|
|
|
|
+ border-color: #6ee7b7;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-item.post {{
|
|
|
|
|
+ background: #dbeafe;
|
|
|
|
|
+ color: #1e40af;
|
|
|
|
|
+ border-color: #bfdbfe;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-item.post:hover {{
|
|
|
|
|
+ background: #bfdbfe;
|
|
|
|
|
+ border-color: #93c5fd;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-item.highlighted {{
|
|
|
|
|
+ border-width: 2px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ transform: scale(1.05);
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-item.persona.highlighted {{
|
|
|
|
|
+ background: #6ee7b7;
|
|
|
|
|
+ border-color: #10b981;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .candidate-item.post.highlighted {{
|
|
|
|
|
+ background: #93c5fd;
|
|
|
|
|
+ border-color: #3b82f6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 来源分类路径 */
|
|
|
|
|
+ .source-path {{
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ padding: 6px 12px;
|
|
|
|
|
+ background: #fefce8;
|
|
|
|
|
+ border-left: 3px solid #fbbf24;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .source-path-label {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #92400e;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 搜索词推理区域 */
|
|
|
|
|
+ .search-word-reasoning {{
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ background: #f0f9ff;
|
|
|
|
|
+ border-left: 3px solid #3b82f6;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #1e3a8a;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .reasoning-label {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #2563eb;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .reasoning-text {{
|
|
|
|
|
+ display: -webkit-box;
|
|
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .reasoning-text.expanded {{
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ -webkit-line-clamp: unset;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .reasoning-toggle {{
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+ padding: 4px 10px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #3b82f6;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .reasoning-toggle:hover {{
|
|
|
|
|
+ color: #2563eb;
|
|
|
|
|
+ text-decoration: underline;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 搜索词组合词和匹配分 */
|
|
|
|
|
+ .search-word-combo {{
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ padding: 6px 10px;
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .combo-label {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .match-score {{
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
|
+ margin-left: 6px;
|
|
|
|
|
+ background: #dcfce7;
|
|
|
|
|
+ color: #166534;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-word-item.highlighted {{
|
|
|
|
|
+ background: #fef3c7 !important;
|
|
|
|
|
+ border-left-color: #f59e0b !important;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 级联列表项通用样式 */
|
|
|
|
|
+ .cascade-item {{
|
|
|
|
|
+ padding: 12px 15px;
|
|
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ border-left: 3px solid transparent;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .cascade-item:hover {{
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ border-left-color: #667eea;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .cascade-item.active {{
|
|
|
|
|
+ background: #ede9fe;
|
|
|
|
|
+ border-left-color: #7c3aed;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .cascade-item-title {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .cascade-item-meta {{
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 左侧栏特征项 */
|
|
|
|
|
+ .feature-item-left {{
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ border-left: 4px solid transparent;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-item-left:hover {{
|
|
|
|
|
+ background: #fef3c7;
|
|
|
|
|
+ border-left-color: #f59e0b;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-item-left.active {{
|
|
|
|
|
+ background: linear-gradient(90deg, #fef3c7 0%, #fefce8 100%);
|
|
|
|
|
+ border-left-color: #f59e0b;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-item-left.active::after {{
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: -25px;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ width: 25px;
|
|
|
|
|
+ height: 2px;
|
|
|
|
|
+ background: #f59e0b;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-item-left.active::before {{
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: -28px;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ transform: translateY(-50%);
|
|
|
|
|
+ width: 0;
|
|
|
|
|
+ height: 0;
|
|
|
|
|
+ border-left: 6px solid #f59e0b;
|
|
|
|
|
+ border-top: 4px solid transparent;
|
|
|
|
|
+ border-bottom: 4px solid transparent;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-name {{
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-meta-small {{
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-source {{
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ color: #92400e;
|
|
|
|
|
+ background: #fef3c7;
|
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-stats {{
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #7c3aed;
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 中间栏base_word项 */
|
|
|
|
|
+ .baseword-item {{
|
|
|
|
|
+ padding: 12px 15px;
|
|
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ border-left: 3px solid transparent;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .baseword-item:hover {{
|
|
|
|
|
+ background: #f0fdf4;
|
|
|
|
|
+ border-left-color: #22c55e;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .baseword-item.active {{
|
|
|
|
|
+ background: linear-gradient(90deg, #dcfce7 0%, #f0fdf4 100%);
|
|
|
|
|
+ border-left-color: #22c55e;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .baseword-item.active::after {{
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: -25px;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ width: 25px;
|
|
|
|
|
+ height: 2px;
|
|
|
|
|
+ background: #22c55e;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .baseword-item.active::before {{
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: -28px;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ transform: translateY(-50%);
|
|
|
|
|
+ width: 0;
|
|
|
|
|
+ height: 0;
|
|
|
|
|
+ border-left: 6px solid #22c55e;
|
|
|
|
|
+ border-top: 4px solid transparent;
|
|
|
|
|
+ border-bottom: 4px solid transparent;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 右侧栏搜索词项 */
|
|
|
|
|
+ .searchword-item {{
|
|
|
|
|
+ padding: 12px 15px;
|
|
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ border-left: 3px solid transparent;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .searchword-item:hover {{
|
|
|
|
|
+ background: #dbeafe;
|
|
|
|
|
+ border-left-color: #3b82f6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .searchword-item.active {{
|
|
|
|
|
+ background: linear-gradient(90deg, #dbeafe 0%, #eff6ff 100%);
|
|
|
|
|
+ border-left-color: #3b82f6;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .searchword-item.active::after {{
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: -25px;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ width: 25px;
|
|
|
|
|
+ height: 2px;
|
|
|
|
|
+ background: #3b82f6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .searchword-item.active::before {{
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: -28px;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ transform: translateY(-50%);
|
|
|
|
|
+ width: 0;
|
|
|
|
|
+ height: 0;
|
|
|
|
|
+ border-left: 6px solid #3b82f6;
|
|
|
|
|
+ border-top: 4px solid transparent;
|
|
|
|
|
+ border-bottom: 4px solid transparent;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-group {{
|
|
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-header {{
|
|
|
|
|
+ padding: 15px 20px;
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ transition: background 0.2s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-header:hover {{
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-header.active {{
|
|
|
|
|
+ background: #667eea;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-title {{
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin-bottom: 5px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .post-target-word {{
|
|
|
|
|
+ color: #2563eb;
|
|
|
|
|
+ background: linear-gradient(90deg, #dbeafe 0%, #f0f9ff 100%);
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ border-left: 4px solid #2563eb;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-meta {{
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-header.active .feature-meta {{
|
|
|
|
|
+ color: rgba(255,255,255,0.8);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-words-list {{
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-words-list.expanded {{
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .base-word-group {{
|
|
|
|
|
+ border-bottom: 1px solid #f3f4f6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .base-word-header {{
|
|
|
|
|
+ padding: 12px 20px 12px 30px;
|
|
|
|
|
+ background: #fafbfc;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ border-left: 3px solid transparent;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .base-word-header:hover {{
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ border-left-color: #a78bfa;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .base-word-header.active {{
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ border-left-color: #7c3aed;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .base-word-title {{
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #7c3aed;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .persona-feature {{
|
|
|
|
|
+ color: #059669;
|
|
|
|
|
+ background: linear-gradient(90deg, #d1fae5 0%, #ecfdf5 100%);
|
|
|
|
|
+ padding: 6px 10px;
|
|
|
|
|
+ border-radius: 5px;
|
|
|
|
|
+ border-left: 3px solid #059669;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .base-word-meta {{
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .base-word-desc {{
|
|
|
|
|
+ padding: 8px 20px 8px 30px;
|
|
|
|
|
+ background: #fefce8;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #854d0e;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ border-left: 3px solid #fbbf24;
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .base-word-desc.expanded {{
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-words-sublist {{
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-words-sublist.expanded {{
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-word-item {{
|
|
|
|
|
+ padding: 12px 20px 12px 50px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ border-left: 3px solid transparent;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-word-item:hover {{
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ border-left-color: #667eea;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-word-item.active {{
|
|
|
|
|
+ background: #ede9fe;
|
|
|
|
|
+ border-left-color: #7c3aed;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-word-text {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-word-score {{
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin-left: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-high {{
|
|
|
|
|
+ background: #d1fae5;
|
|
|
|
|
+ color: #065f46;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-medium {{
|
|
|
|
|
+ background: #fef3c7;
|
|
|
|
|
+ color: #92400e;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-low {{
|
|
|
|
|
+ background: #fee2e2;
|
|
|
|
|
+ color: #991b1b;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .eval-badge {{
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin-left: 6px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .eval-complete {{
|
|
|
|
|
+ background: #d1fae5;
|
|
|
|
|
+ color: #065f46;
|
|
|
|
|
+ border: 1px solid #10b981;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .eval-similar {{
|
|
|
|
|
+ background: #fef3c7;
|
|
|
|
|
+ color: #92400e;
|
|
|
|
|
+ border: 1px solid #f59e0b;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .eval-weak {{
|
|
|
|
|
+ background: #fed7aa;
|
|
|
|
|
+ color: #9a3412;
|
|
|
|
|
+ border: 1px solid #f97316;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .eval-none {{
|
|
|
|
|
+ background: #fee2e2;
|
|
|
|
|
+ color: #991b1b;
|
|
|
|
|
+ border: 1px solid #ef4444;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .eval-filtered {{
|
|
|
|
|
+ background: #e5e7eb;
|
|
|
|
|
+ color: #4b5563;
|
|
|
|
|
+ border: 1px solid #6b7280;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .search-word-eval {{
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* P值显示样式 */
|
|
|
|
|
+ .p-score-container {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+ padding: 6px 8px;
|
|
|
|
|
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ border-left: 3px solid #0ea5e9;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-score-label {{
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #0369a1;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-score-value {{
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #0284c7;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-score-info-icon {{
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #0ea5e9;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-score-info-icon:hover {{
|
|
|
|
|
+ background: #0ea5e9;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ transform: scale(1.1);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-score-arrow {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-score-arrow-symbol {{
|
|
|
|
|
+ color: #0ea5e9;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* P值详情模态窗口 */
|
|
|
|
|
+ .p-detail-modal {{
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
|
|
+ z-index: 10000;
|
|
|
|
|
+ animation: fadeIn 0.3s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-detail-modal.active {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-detail-window {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ width: 90%;
|
|
|
|
|
+ max-width: 800px;
|
|
|
|
|
+ max-height: 85vh;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
|
|
|
+ animation: slideUp 0.3s ease;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-detail-header {{
|
|
|
|
|
+ padding: 20px 25px;
|
|
|
|
|
+ background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-detail-title {{
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-detail-close-btn {{
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ width: 36px;
|
|
|
|
|
+ height: 36px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-detail-close-btn:hover {{
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.3);
|
|
|
|
|
+ transform: scale(1.1);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-detail-body {{
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 25px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-formula-section {{
|
|
|
|
|
+ background: #f8fafc;
|
|
|
|
|
+ border: 2px solid #e2e8f0;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-formula-title {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #0f172a;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-formula {{
|
|
|
|
|
+ font-family: 'Courier New', monospace;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ color: #0284c7;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-summary {{
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-summary-item {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border: 2px solid #e2e8f0;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-summary-label {{
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-summary-value {{
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #0f172a;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-summary-value.highlight {{
|
|
|
|
|
+ color: #0284c7;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-matches-section {{
|
|
|
|
|
+ margin-top: 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-matches-title {{
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #0f172a;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-match-item {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border: 2px solid #e2e8f0;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-match-item:hover {{
|
|
|
|
|
+ border-color: #0ea5e9;
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(14, 165, 233, 0.1);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-match-header {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-match-title {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #0f172a;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-match-contribution {{
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #0284c7;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-match-details {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-match-detail-item {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .p-match-detail-label {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* ========== 新增: 顶部统计面板折叠样式 ========== */
|
|
|
|
|
+ .stats-panel {{
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stats-panel.collapsed {{
|
|
|
|
|
+ max-height: 90px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stats-panel.collapsed .stats-row:not(:first-child) {{
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stats-toggle-icon {{
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: 30px;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ transform: translateY(-50%);
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
+ transition: transform 0.3s ease;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stats-panel.collapsed .stats-toggle-icon {{
|
|
|
|
|
+ transform: translateY(-50%) rotate(180deg);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* ========== 新增: 帖子浮层模态窗口样式 ========== */
|
|
|
|
|
+ .notes-modal {{
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.8);
|
|
|
|
|
+ z-index: 10001;
|
|
|
|
|
+ animation: fadeIn 0.3s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .notes-modal.active {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .notes-modal-window {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ width: 95%;
|
|
|
|
|
+ max-width: 1400px;
|
|
|
|
|
+ height: 90vh;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
|
|
|
+ animation: slideUp 0.3s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .notes-modal-header {{
|
|
|
|
|
+ padding: 20px 25px;
|
|
|
|
|
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .notes-modal-title {{
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .notes-modal-subtitle {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ opacity: 0.9;
|
|
|
|
|
+ margin-top: 5px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .notes-modal-close-btn {{
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ width: 36px;
|
|
|
|
|
+ height: 36px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .notes-modal-close-btn:hover {{
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.3);
|
|
|
|
|
+ transform: scale(1.1);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .notes-modal-body {{
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 25px;
|
|
|
|
|
+ background: #fafbfc;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .notes-grid-modal {{
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-card-modal {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-card-modal:hover {{
|
|
|
|
|
+ transform: translateY(-4px);
|
|
|
|
|
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-image-wrapper {{
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 200px;
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-image {{
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-image-placeholder {{
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 48px;
|
|
|
|
|
+ background: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-content {{
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-title {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-score {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-label {{
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-value {{
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-value.high {{
|
|
|
|
|
+ color: #10b981;
|
|
|
|
|
+ background: #d1fae5;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-value.medium {{
|
|
|
|
|
+ color: #f59e0b;
|
|
|
|
|
+ background: #fef3c7;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .score-value.low {{
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ background: #e5e7eb;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ @keyframes fadeIn {{
|
|
|
|
|
+ from {{ opacity: 0; }}
|
|
|
|
|
+ to {{ opacity: 1; }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ @keyframes slideUp {{
|
|
|
|
|
+ from {{
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transform: translateY(50px);
|
|
|
|
|
+ }}
|
|
|
|
|
+ to {{
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ transform: translateY(0);
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 右侧结果区 */
|
|
|
|
|
+ .right-content {{
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding-bottom: 40px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .result-block {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ scroll-margin-top: 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .result-header {{
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ padding-bottom: 15px;
|
|
|
|
|
+ border-bottom: 2px solid #e5e7eb;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .result-title {{
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #111827;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .result-stats {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-badge {{
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ padding: 4px 10px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-badge.eval {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-badge.eval.complete {{
|
|
|
|
|
+ background: #d1fae5;
|
|
|
|
|
+ color: #065f46;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-badge.eval.similar {{
|
|
|
|
|
+ background: #fef3c7;
|
|
|
|
|
+ color: #92400e;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-badge.eval.weak {{
|
|
|
|
|
+ background: #fed7aa;
|
|
|
|
|
+ color: #9a3412;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-badge.eval.none {{
|
|
|
|
|
+ background: #fee2e2;
|
|
|
|
|
+ color: #991b1b;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .stat-badge.eval.filtered {{
|
|
|
|
|
+ background: #e5e7eb;
|
|
|
|
|
+ color: #4b5563;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .notes-grid {{
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .empty-state {{
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 60px 40px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .empty-icon {{
|
|
|
|
|
+ font-size: 48px;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .empty-title {{
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .empty-desc {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ max-width: 400px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-card {{
|
|
|
|
|
+ border: 3px solid #e5e7eb;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-card:hover {{
|
|
|
|
|
+ transform: translateY(-4px);
|
|
|
|
|
+ box-shadow: 0 10px 25px rgba(0,0,0,0.15);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-card.eval-complete {{
|
|
|
|
|
+ border-color: #10b981;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-card.eval-similar {{
|
|
|
|
|
+ border-color: #f59e0b;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-card.eval-weak {{
|
|
|
|
|
+ border-color: #f97316;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-card.eval-none {{
|
|
|
|
|
+ border-color: #ef4444;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-card.eval-filtered {{
|
|
|
|
|
+ border-color: #6b7280;
|
|
|
|
|
+ opacity: 0.6;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 图片轮播 */
|
|
|
|
|
+ .image-carousel {{
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 280px;
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .carousel-images {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ transition: transform 0.3s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .carousel-image {{
|
|
|
|
|
+ min-width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .carousel-btn {{
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ transform: translateY(-50%);
|
|
|
|
|
+ background: rgba(0,0,0,0.5);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ width: 32px;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ transition: background 0.2s;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .carousel-btn:hover {{
|
|
|
|
|
+ background: rgba(0,0,0,0.7);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .carousel-btn.prev {{
|
|
|
|
|
+ left: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .carousel-btn.next {{
|
|
|
|
|
+ right: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-card:hover .carousel-btn {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .carousel-indicators {{
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 10px;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .dot {{
|
|
|
|
|
+ width: 8px;
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: rgba(255,255,255,0.5);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .dot.active {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ width: 24px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .image-counter {{
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 10px;
|
|
|
|
|
+ right: 10px;
|
|
|
|
|
+ background: rgba(0,0,0,0.6);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ padding: 4px 8px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-info {{
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-title {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #111827;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ display: -webkit-box;
|
|
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-meta {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-type {{
|
|
|
|
|
+ padding: 3px 8px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .type-video {{
|
|
|
|
|
+ background: #dbeafe;
|
|
|
|
|
+ color: #1e40af;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .type-normal {{
|
|
|
|
|
+ background: #d1fae5;
|
|
|
|
|
+ color: #065f46;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-author {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .author-avatar {{
|
|
|
|
|
+ width: 24px;
|
|
|
|
|
+ height: 24px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-eval {{
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ border-top: 1px solid #e5e7eb;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-eval-header {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-eval-score {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-eval-toggle {{
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-eval-details {{
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ padding-top: 8px;
|
|
|
|
|
+ border-top: 1px solid #e5e7eb;
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .note-eval-details.expanded {{
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .eval-detail-label {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+ margin-bottom: 2px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .eval-detail-label:first-child {{
|
|
|
|
|
+ margin-top: 0;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .eval-detail-text {{
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* ========== 新增: 解构面板样式 ========== */
|
|
|
|
|
+
|
|
|
|
|
+ .deconstruction-toggle-btn {{
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-top: 1px solid #e5e7eb;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ z-index: 1;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .deconstruction-toggle-btn:hover {{
|
|
|
|
|
+ background: linear-gradient(135deg, #5568d3 0%, #6a3f8f 100%);
|
|
|
|
|
+ transform: scale(1.02);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 浮层遮罩 */
|
|
|
|
|
+ .modal-overlay {{
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
|
|
+ z-index: 9998;
|
|
|
|
|
+ animation: fadeIn 0.3s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-overlay.active {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 浮层窗口 */
|
|
|
|
|
+ .modal-window {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ width: 90%;
|
|
|
|
|
+ max-width: 1200px;
|
|
|
|
|
+ max-height: 90vh;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
|
|
|
+ animation: slideUp 0.3s ease;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ @keyframes fadeIn {{
|
|
|
|
|
+ from {{ opacity: 0; }}
|
|
|
|
|
+ to {{ opacity: 1; }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ @keyframes slideUp {{
|
|
|
|
|
+ from {{
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transform: translateY(50px);
|
|
|
|
|
+ }}
|
|
|
|
|
+ to {{
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ transform: translateY(0);
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 浮层头部 */
|
|
|
|
|
+ .modal-header {{
|
|
|
|
|
+ padding: 20px 25px;
|
|
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-title {{
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-note-title {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ opacity: 0.9;
|
|
|
|
|
+ margin-top: 5px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-close-btn {{
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ width: 36px;
|
|
|
|
|
+ height: 36px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .modal-close-btn:hover {{
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.3);
|
|
|
|
|
+ transform: scale(1.1);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 浮层内容区 */
|
|
|
|
|
+ .modal-body {{
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 25px;
|
|
|
|
|
+ background: #fafbfc;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .deconstruction-content {{
|
|
|
|
|
+ max-width: 1000px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .deconstruction-header {{
|
|
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ padding: 12px 15px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ margin-bottom: 15px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .original-feature {{
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .dimension-card {{
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border: 2px solid #e5e7eb;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .dimension-header {{
|
|
|
|
|
+ padding: 10px 15px;
|
|
|
|
|
+ background: #667eea;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .dimension-header:hover {{
|
|
|
|
|
+ background: #5568d3;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .dimension-title {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .dimension-count {{
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ opacity: 0.9;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .dimension-toggle {{
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .dimension-body {{
|
|
|
|
|
+ max-height: 0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ transition: max-height 0.3s ease-in-out;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .dimension-body.expanded {{
|
|
|
|
|
+ max-height: 1000px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-list {{
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-item {{
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ border-left: 3px solid #e5e7eb;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-item:hover {{
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ border-left-color: #667eea;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-item.top-score {{
|
|
|
|
|
+ background: #fff9e6;
|
|
|
|
|
+ border-left: 3px solid #FFD700;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(255, 215, 0, 0.2);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-item.top-score .feature-name {{
|
|
|
|
|
+ color: #b8860b;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-name {{
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #111827;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .top-badge {{
|
|
|
|
|
+ background: #FFD700;
|
|
|
|
|
+ color: #000;
|
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-meta-row {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-dimension-detail {{
|
|
|
|
|
+ background: #e5e7eb;
|
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-weight {{
|
|
|
|
|
+ background: #dbeafe;
|
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .similarity-row {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .similarity-score {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ min-width: 50px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .similarity-score.high {{
|
|
|
|
|
+ color: #10b981;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .similarity-score.medium {{
|
|
|
|
|
+ color: #f59e0b;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .similarity-score.low {{
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .similarity-bar-container {{
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ background: #e5e7eb;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .similarity-bar {{
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ transition: width 0.3s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .similarity-bar.high {{
|
|
|
|
|
+ background: linear-gradient(90deg, #10b981 0%, #059669 100%);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .similarity-bar.medium {{
|
|
|
|
|
+ background: linear-gradient(90deg, #f59e0b 0%, #d97706 100%);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .similarity-bar.low {{
|
|
|
|
|
+ background: linear-gradient(90deg, #9ca3af 0%, #6b7280 100%);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .similarity-explanation {{
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ padding: 8px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-item:hover .similarity-explanation {{
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 滚动条样式 */
|
|
|
|
|
+ ::-webkit-scrollbar {{
|
|
|
|
|
+ width: 8px;
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ ::-webkit-scrollbar-track {{
|
|
|
|
|
+ background: #f1f1f1;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ ::-webkit-scrollbar-thumb {{
|
|
|
|
|
+ background: #888;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ ::-webkit-scrollbar-thumb:hover {{
|
|
|
|
|
+ background: #555;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .hidden {{
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 关系图样式 */
|
|
|
|
|
+ .relationship-section {{
|
|
|
|
|
+ margin: 20px;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .relationship-section .section-header {{
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ padding-bottom: 10px;
|
|
|
|
|
+ border-bottom: 2px solid #e5e7eb;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .relationship-graph-container {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow-x: auto;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .relationship-graph-container svg {{
|
|
|
|
|
+ max-width: 100%;
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* Tab导航样式 */
|
|
|
|
|
+ .tab-navigation {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ padding: 10px 20px 0;
|
|
|
|
|
+ margin: 0 20px;
|
|
|
|
|
+ border-bottom: 2px solid #e5e7eb;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tab-button {{
|
|
|
|
|
+ padding: 12px 24px;
|
|
|
|
|
+ background: none;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-bottom: 3px solid transparent;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ top: 2px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tab-button:hover {{
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ background: #f9fafb;
|
|
|
|
|
+ border-radius: 6px 6px 0 0;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tab-button.active {{
|
|
|
|
|
+ color: #2563eb;
|
|
|
|
|
+ border-bottom-color: #2563eb;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* Tab内容区域 */
|
|
|
|
|
+ .tab-content {{
|
|
|
|
|
+ min-height: 600px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tab-pane {{
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .tab-pane.active {{
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 交互高亮样式 */
|
|
|
|
|
+ .clickable {{
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: opacity 0.3s ease, filter 0.3s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .clickable:hover {{
|
|
|
|
|
+ filter: brightness(1.15);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .highlighted {{
|
|
|
|
|
+ filter: drop-shadow(0 0 8px currentColor);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .highlighted circle,
|
|
|
|
|
+ .highlighted rect {{
|
|
|
|
|
+ stroke-width: 3;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .dimmed {{
|
|
|
|
|
+ opacity: 0.15;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .connection-line {{
|
|
|
|
|
+ transition: opacity 0.3s ease, stroke-width 0.3s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .connection-line.highlighted {{
|
|
|
|
|
+ stroke-width: 4;
|
|
|
|
|
+ opacity: 1 !important;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .connection-line.dimmed {{
|
|
|
|
|
+ opacity: 0.1 !important;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* ========== 高相似度特征样式(绿色)========== */
|
|
|
|
|
+ .high-similarity-section {{
|
|
|
|
|
+ margin-top: 20px;
|
|
|
|
|
+ border-top: 2px solid #86efac;
|
|
|
|
|
+ padding-top: 15px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .high-similarity-header {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 12px 15px;
|
|
|
|
|
+ background: linear-gradient(135deg, #dcfce7 0%, #f0fdf4 100%);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ border: 2px solid #86efac;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .high-similarity-title {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #16a34a;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .high-similarity-count {{
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #15803d;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .high-similarity-list {{
|
|
|
|
|
+ margin-top: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .high-similarity-item {{
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ margin: 8px 0;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-left: 3px solid #22c55e;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .high-similarity-item:hover {{
|
|
|
|
|
+ background: #f0fdf4;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .high-feature-name {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #166534;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .high-feature-score {{
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #16a34a;
|
|
|
|
|
+ background: #dcfce7;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .high-feature-meta {{
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* ========== 部分匹配特征样式(橘黄色)========== */
|
|
|
|
|
+ .partial-similarity-section {{
|
|
|
|
|
+ margin-top: 20px;
|
|
|
|
|
+ border-top: 2px solid #fdba74;
|
|
|
|
|
+ padding-top: 15px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .partial-similarity-header {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 12px 15px;
|
|
|
|
|
+ background: linear-gradient(135deg, #fed7aa 0%, #fef3c7 100%);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ border: 2px solid #fdba74;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .partial-similarity-title {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #c2410c;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .partial-similarity-count {{
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #c2410c;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-header {{
|
|
|
|
|
+ border-left: 4px solid #f97316;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-title {{
|
|
|
|
|
+ color: #c2410c;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* ========== 右侧内容特征标题区域样式 ========== */
|
|
|
|
|
+ .feature-section {{
|
|
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-section-header {{
|
|
|
|
|
+ padding: 16px 20px;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-section-title {{
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-section-meta {{
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 高相似度特征 */
|
|
|
|
|
+ .feature-section-high .feature-section-header {{
|
|
|
|
|
+ border-left: 4px solid #22c55e;
|
|
|
|
|
+ background: linear-gradient(135deg, #dcfce7 0%, #f0fdf4 100%);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-section-high .feature-section-title {{
|
|
|
|
|
+ color: #22c55e;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 部分匹配特征 */
|
|
|
|
|
+ .feature-section-partial .feature-section-header {{
|
|
|
|
|
+ border-left: 4px solid #f97316;
|
|
|
|
|
+ background: linear-gradient(135deg, #fed7aa 0%, #fef3c7 100%);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-section-partial .feature-section-title {{
|
|
|
|
|
+ color: #f97316;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* 低相似度特征 */
|
|
|
|
|
+ .feature-section-low .feature-section-header {{
|
|
|
|
|
+ border-left: 4px solid #dc2626;
|
|
|
|
|
+ background: linear-gradient(135deg, #fee2e2 0%, #fef2f2 100%);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .feature-section-low .feature-section-title {{
|
|
|
|
|
+ color: #dc2626;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /* ========== 低相似度特征样式(红色)========== */
|
|
|
|
|
+ .low-similarity-section {{
|
|
|
|
|
+ margin-top: 20px;
|
|
|
|
|
+ border-top: 2px solid #fca5a5;
|
|
|
|
|
+ padding-top: 15px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .low-similarity-header {{
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 12px 15px;
|
|
|
|
|
+ background: linear-gradient(135deg, #fee2e2 0%, #fef2f2 100%);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ border: 2px solid #fca5a5;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .low-similarity-header:hover {{
|
|
|
|
|
+ background: linear-gradient(135deg, #fecaca 0%, #fee2e2 100%);
|
|
|
|
|
+ border-color: #f87171;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .low-similarity-title {{
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #dc2626;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .low-similarity-count {{
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #991b1b;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .toggle-icon {{
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #dc2626;
|
|
|
|
|
+ transition: transform 0.3s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .toggle-icon.rotated {{
|
|
|
|
|
+ transform: rotate(180deg);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .low-similarity-list {{
|
|
|
|
|
+ margin-top: 10px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .low-similarity-item {{
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ margin: 8px 0;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-left: 3px solid #dc2626;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .low-similarity-item:hover {{
|
|
|
|
|
+ background: #fef2f2;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .low-feature-name {{
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #991b1b;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .low-feature-score {{
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #dc2626;
|
|
|
|
|
+ background: #fee2e2;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ .low-feature-meta {{
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ }}
|
|
|
|
|
+ </style>
|
|
|
|
|
+</head>
|
|
|
|
|
+<body>
|
|
|
|
|
+ <!-- 统计面板 -->
|
|
|
|
|
+ <div class="stats-panel collapsed">
|
|
|
|
|
+ <div class="stats-toggle-icon">▼</div>
|
|
|
|
|
+ <div class="stats-container">
|
|
|
|
|
+ <div class="stats-row">
|
|
|
|
|
+ <div class="stat-item">
|
|
|
|
|
+ <div class="stat-value">📊 {stats['total_features']}</div>
|
|
|
|
|
+ <div class="stat-label">原始特征数</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item">
|
|
|
|
|
+ <div class="stat-value">🔍 {stats['total_search_words']}</div>
|
|
|
|
|
+ <div class="stat-label">搜索词总数</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item">
|
|
|
|
|
+ <div class="stat-value">✅ {stats['searched_count']}</div>
|
|
|
|
|
+ <div class="stat-label">已搜索 ({stats['searched_percentage']}%)</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item">
|
|
|
|
|
+ <div class="stat-value">⏸️ {stats['not_searched_count']}</div>
|
|
|
|
|
+ <div class="stat-label">未搜索</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item">
|
|
|
|
|
+ <div class="stat-value">📝 {stats['total_notes']}</div>
|
|
|
|
|
+ <div class="stat-label">帖子总数</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item">
|
|
|
|
|
+ <div class="stat-value">🎬 {stats['video_count']}</div>
|
|
|
|
|
+ <div class="stat-label">视频 ({stats['video_percentage']}%)</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item">
|
|
|
|
|
+ <div class="stat-value">📷 {stats['normal_count']}</div>
|
|
|
|
|
+ <div class="stat-label">图文 ({stats['normal_percentage']}%)</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stats-row">
|
|
|
|
|
+ <div class="stat-item small">
|
|
|
|
|
+ <div class="stat-value">⚡ {stats['total_evaluated']}</div>
|
|
|
|
|
+ <div class="stat-label">已评估</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item small">
|
|
|
|
|
+ <div class="stat-value">⚫ {stats['total_filtered']}</div>
|
|
|
|
|
+ <div class="stat-label">已过滤 ({stats['filter_rate']}%)</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item small">
|
|
|
|
|
+ <div class="stat-value">🟢 {stats['match_complete']}</div>
|
|
|
|
|
+ <div class="stat-label">完全匹配 ({stats['complete_rate']}%)</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item small">
|
|
|
|
|
+ <div class="stat-value">🟡 {stats['match_similar']}</div>
|
|
|
|
|
+ <div class="stat-label">相似匹配 ({stats['similar_rate']}%)</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item small">
|
|
|
|
|
+ <div class="stat-value">🟠 {stats['match_weak']}</div>
|
|
|
|
|
+ <div class="stat-label">弱相似</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-item small">
|
|
|
|
|
+ <div class="stat-value">🔴 {stats['match_none']}</div>
|
|
|
|
|
+ <div class="stat-label">无匹配</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 过滤控制面板 -->
|
|
|
|
|
+ <div class="filter-panel">
|
|
|
|
|
+ <span class="filter-label">🔍 筛选显示:</span>
|
|
|
|
|
+ <div class="filter-buttons">
|
|
|
|
|
+ <button class="filter-btn active" onclick="filterNotes('all')">全部</button>
|
|
|
|
|
+ <button class="filter-btn complete" onclick="filterNotes('complete')">🟢 完全匹配</button>
|
|
|
|
|
+ <button class="filter-btn similar" onclick="filterNotes('similar')">🟡 相似匹配</button>
|
|
|
|
|
+ <button class="filter-btn weak" onclick="filterNotes('weak')">🟠 弱相似</button>
|
|
|
|
|
+ <button class="filter-btn none" onclick="filterNotes('none')">🔴 无匹配</button>
|
|
|
|
|
+ <button class="filter-btn filtered" onclick="filterNotes('filtered')">⚫ 已过滤</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 主容器 - 级联布局 -->
|
|
|
|
|
+ <div class="main-container">
|
|
|
|
|
+ <!-- 左侧栏:原始特征列表 -->
|
|
|
|
|
+ <div class="left-sidebar" id="leftSidebar"></div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 中间栏:base_word列表 -->
|
|
|
|
|
+ <div class="middle-sidebar" id="middleSidebar">
|
|
|
|
|
+ <div class="sidebar-header">匹配的人设特征</div>
|
|
|
|
|
+ <div class="sidebar-content" id="middleContent"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 右侧栏:搜索词列表 -->
|
|
|
|
|
+ <div class="right-sidebar" id="rightSidebar">
|
|
|
|
|
+ <div class="sidebar-header">相关搜索词</div>
|
|
|
|
|
+ <div class="sidebar-content" id="rightSidebarContent"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 详情区域:搜索结果 -->
|
|
|
|
|
+ <div class="detail-area" id="detailArea">
|
|
|
|
|
+ <div class="detail-placeholder">👈 请从左侧选择特征查看详情</div>
|
|
|
|
|
+ <div class="detail-content" id="detailContent"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 解构结果模态窗口 -->
|
|
|
|
|
+ <div class="modal-overlay" id="deconstructionModal">
|
|
|
|
|
+ <div class="modal-window">
|
|
|
|
|
+ <div class="modal-header">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div class="modal-title">🎯 解构特征相似度分析</div>
|
|
|
|
|
+ <div class="modal-note-title" id="modalNoteTitle"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button class="modal-close-btn" onclick="closeModal()">×</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="modal-body">
|
|
|
|
|
+ <div class="deconstruction-content" id="modalContent"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- P值详情模态窗口 -->
|
|
|
|
|
+ <div class="p-detail-modal" id="pDetailModal">
|
|
|
|
|
+ <div class="p-detail-window">
|
|
|
|
|
+ <div class="p-detail-header">
|
|
|
|
|
+ <div class="p-detail-title">📊 综合得分P 计算详情</div>
|
|
|
|
|
+ <button class="p-detail-close-btn" onclick="closePDetailModal()">×</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="p-detail-body" id="pDetailContent"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 帖子浮层模态窗口 -->
|
|
|
|
|
+ <div class="notes-modal" id="notesModal">
|
|
|
|
|
+ <div class="notes-modal-window">
|
|
|
|
|
+ <div class="notes-modal-header">
|
|
|
|
|
+ <div class="notes-modal-title">📝 搜索词帖子列表</div>
|
|
|
|
|
+ <button class="notes-modal-close-btn" onclick="closeNotesModal()">×</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="notes-modal-body" id="notesModalContent"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <script>
|
|
|
|
|
+ // 数据
|
|
|
|
|
+ const data = {data_json};
|
|
|
|
|
+ const deconstructionData = {deconstruction_json};
|
|
|
|
|
+ const similarityData = {similarity_json};
|
|
|
|
|
+ const allFeatures = {all_features_json};
|
|
|
|
|
+ const highSimilarityFeatures = {high_similarity_json};
|
|
|
|
|
+ const lowSimilarityFeatures = {low_similarity_json};
|
|
|
|
|
+ let currentFilter = 'all';
|
|
|
|
|
+
|
|
|
|
|
+ // Tab切换功能
|
|
|
|
|
+ function switchTab(tabName) {{
|
|
|
|
|
+ // 更新Tab按钮状态
|
|
|
|
|
+ document.querySelectorAll('.tab-button').forEach(btn => {{
|
|
|
|
|
+ btn.classList.remove('active');
|
|
|
|
|
+ }});
|
|
|
|
|
+ event.target.classList.add('active');
|
|
|
|
|
+
|
|
|
|
|
+ // 更新Tab内容
|
|
|
|
|
+ document.querySelectorAll('.tab-pane').forEach(pane => {{
|
|
|
|
|
+ pane.classList.remove('active');
|
|
|
|
|
+ }});
|
|
|
|
|
+ document.getElementById('tab-' + tabName).classList.add('active');
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 关系图节点点击高亮功能
|
|
|
|
|
+ let selectedNodeId = null;
|
|
|
|
|
+
|
|
|
|
|
+ function handleNodeClick(nodeId, nodeType) {{
|
|
|
|
|
+ const svg = document.querySelector('.relationship-graph-container svg');
|
|
|
|
|
+ if (!svg) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果点击的是已选中节点,取消高亮
|
|
|
|
|
+ if (selectedNodeId === nodeId) {{
|
|
|
|
|
+ clearHighlight();
|
|
|
|
|
+ selectedNodeId = null;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ selectedNodeId = nodeId;
|
|
|
|
|
+
|
|
|
|
|
+ // 找到所有相关的连接线
|
|
|
|
|
+ const connections = svg.querySelectorAll('.connection-line');
|
|
|
|
|
+ const relatedNodes = new Set([nodeId]);
|
|
|
|
|
+
|
|
|
|
|
+ connections.forEach(conn => {{
|
|
|
|
|
+ const from = conn.getAttribute('data-from');
|
|
|
|
|
+ const to = conn.getAttribute('data-to');
|
|
|
|
|
+
|
|
|
|
|
+ if (from === nodeId || to === nodeId) {{
|
|
|
|
|
+ // 这是相关的连接线
|
|
|
|
|
+ conn.classList.add('highlighted');
|
|
|
|
|
+ conn.classList.remove('dimmed');
|
|
|
|
|
+
|
|
|
|
|
+ // 收集相关节点
|
|
|
|
|
+ relatedNodes.add(from);
|
|
|
|
|
+ relatedNodes.add(to);
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ // 非相关连接线变暗
|
|
|
|
|
+ conn.classList.add('dimmed');
|
|
|
|
|
+ conn.classList.remove('highlighted');
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 处理所有节点
|
|
|
|
|
+ const allNodes = svg.querySelectorAll('.clickable');
|
|
|
|
|
+ allNodes.forEach(node => {{
|
|
|
|
|
+ const nId = node.getAttribute('data-node-id');
|
|
|
|
|
+ if (relatedNodes.has(nId)) {{
|
|
|
|
|
+ node.classList.add('highlighted');
|
|
|
|
|
+ node.classList.remove('dimmed');
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ node.classList.add('dimmed');
|
|
|
|
|
+ node.classList.remove('highlighted');
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ function clearHighlight() {{
|
|
|
|
|
+ const svg = document.querySelector('.relationship-graph-container svg');
|
|
|
|
|
+ if (!svg) return;
|
|
|
|
|
+
|
|
|
|
|
+ svg.querySelectorAll('.highlighted, .dimmed').forEach(el => {{
|
|
|
|
|
+ el.classList.remove('highlighted', 'dimmed');
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 创建评估映射
|
|
|
|
|
+ const noteEvaluations = {{}};
|
|
|
|
|
+ data.forEach((feature, fIdx) => {{
|
|
|
|
|
+ const groups = feature['组合评估结果_分组'] || [];
|
|
|
|
|
+ groups.forEach((group, gIdx) => {{
|
|
|
|
|
+ const searches = group['top10_searches'] || [];
|
|
|
|
|
+ searches.forEach((search, sIdx) => {{
|
|
|
|
|
+ const evaluation = search['evaluation_with_filter'];
|
|
|
|
|
+ if (evaluation && evaluation.notes_evaluation) {{
|
|
|
|
|
+ evaluation.notes_evaluation.forEach(noteEval => {{
|
|
|
|
|
+ const key = `${{fIdx}}-${{gIdx}}-${{sIdx}}-${{noteEval.note_index}}`;
|
|
|
|
|
+ noteEvaluations[key] = noteEval;
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+ }});
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 获取评估类别
|
|
|
|
|
+ function getEvalCategory(noteEval) {{
|
|
|
|
|
+ if (!noteEval || noteEval['Query相关性'] !== '相关') {{
|
|
|
|
|
+ return 'filtered';
|
|
|
|
|
+ }}
|
|
|
|
|
+ const score = noteEval['综合得分'];
|
|
|
|
|
+ if (score >= 0.8) return 'complete';
|
|
|
|
|
+ if (score >= 0.6) return 'similar';
|
|
|
|
|
+ if (score >= 0.5) return 'weak';
|
|
|
|
|
+ return 'none';
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 提取所有候选词
|
|
|
|
|
+ function extractAllCandidates() {{
|
|
|
|
|
+ const personaCandidates = [];
|
|
|
|
|
+ const postCandidates = [];
|
|
|
|
|
+ const candidateMap = {{}};
|
|
|
|
|
+
|
|
|
|
|
+ data.forEach(feature => {{
|
|
|
|
|
+ const groups = feature['高相似度候选_按base_word'] || {{}};
|
|
|
|
|
+
|
|
|
|
|
+ Object.values(groups).forEach(candidateList => {{
|
|
|
|
|
+ if (!Array.isArray(candidateList)) return;
|
|
|
|
|
+
|
|
|
|
|
+ candidateList.forEach(candidate => {{
|
|
|
|
|
+ const name = candidate['候选词'] || '';
|
|
|
|
|
+ const type = candidate['候选词类型'] || 'unknown';
|
|
|
|
|
+ const similarity = candidate['相似度'] || 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (!name || candidateMap[name]) return;
|
|
|
|
|
+
|
|
|
|
|
+ candidateMap[name] = true;
|
|
|
|
|
+
|
|
|
|
|
+ const candidateObj = {{
|
|
|
|
|
+ name: name,
|
|
|
|
|
+ type: type,
|
|
|
|
|
+ similarity: similarity
|
|
|
|
|
+ }};
|
|
|
|
|
+
|
|
|
|
|
+ if (type === 'persona') {{
|
|
|
|
|
+ personaCandidates.push(candidateObj);
|
|
|
|
|
+ }} else if (type === 'post') {{
|
|
|
|
|
+ postCandidates.push(candidateObj);
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+ }});
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 按相似度降序排序
|
|
|
|
|
+ personaCandidates.sort((a, b) => b.similarity - a.similarity);
|
|
|
|
|
+ postCandidates.sort((a, b) => b.similarity - a.similarity);
|
|
|
|
|
+
|
|
|
|
|
+ return {{
|
|
|
|
|
+ persona: personaCandidates,
|
|
|
|
|
+ post: postCandidates
|
|
|
|
|
+ }};
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染候选词库
|
|
|
|
|
+ function renderCandidateLibrary(candidates) {{
|
|
|
|
|
+ const personaCount = candidates.persona.length;
|
|
|
|
|
+ const postCount = candidates.post.length;
|
|
|
|
|
+
|
|
|
|
|
+ let html = `
|
|
|
|
|
+ <div class="candidate-library">
|
|
|
|
|
+ <div class="candidate-header" onclick="toggleCandidateLibrary()">
|
|
|
|
|
+ <div class="candidate-title">📋 候选词库(区分人设/帖子来源)</div>
|
|
|
|
|
+ <div class="candidate-toggle" id="candidate-toggle">▼</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="candidate-content" id="candidate-content">
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ // 人设候选词
|
|
|
|
|
+ if (personaCount > 0) {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="candidate-section">
|
|
|
|
|
+ <div class="candidate-section-title">【人设候选词】(${{personaCount}}个)</div>
|
|
|
|
|
+ <div class="candidate-list">
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ candidates.persona.forEach(candidate => {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="candidate-item persona"
|
|
|
|
|
+ data-candidate="${{candidate.name}}"
|
|
|
|
|
+ onclick="highlightCandidateWord('${{candidate.name.replace(/'/g, "\\'")}}')"
|
|
|
|
|
+ title="相似度: ${{candidate.similarity.toFixed(2)}}">
|
|
|
|
|
+ ${{candidate.name}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 帖子候选词
|
|
|
|
|
+ if (postCount > 0) {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="candidate-section">
|
|
|
|
|
+ <div class="candidate-section-title">【帖子候选词】(${{postCount}}个)</div>
|
|
|
|
|
+ <div class="candidate-list">
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ candidates.post.forEach(candidate => {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="candidate-item post"
|
|
|
|
|
+ data-candidate="${{candidate.name}}"
|
|
|
|
|
+ onclick="highlightCandidateWord('${{candidate.name.replace(/'/g, "\\'")}}')"
|
|
|
|
|
+ title="相似度: ${{candidate.similarity.toFixed(2)}}">
|
|
|
|
|
+ ${{candidate.name}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ return html;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染左侧导航
|
|
|
|
|
+ // 全局选中状态
|
|
|
|
|
+ let selectedFeatureIdx = null;
|
|
|
|
|
+ let selectedBaseWordIdx = null;
|
|
|
|
|
+ let selectedSearchWordIdx = null;
|
|
|
|
|
+
|
|
|
|
|
+ function renderLeftSidebar() {{
|
|
|
|
|
+ const sidebar = document.getElementById('leftSidebar');
|
|
|
|
|
+ let html = '';
|
|
|
|
|
+
|
|
|
|
|
+ // 添加候选词库
|
|
|
|
|
+ const allCandidates = extractAllCandidates();
|
|
|
|
|
+ html += renderCandidateLibrary(allCandidates);
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 高相似度特征区域(≥0.8)
|
|
|
|
|
+ if (highSimilarityFeatures && highSimilarityFeatures.length > 0) {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="high-similarity-section">
|
|
|
|
|
+ <div class="high-similarity-header">
|
|
|
|
|
+ <div class="high-similarity-title">📋 当前帖子-已匹配(≥0.8)</div>
|
|
|
|
|
+ <div class="high-similarity-count">${{highSimilarityFeatures.length}}个</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="high-similarity-list">
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ highSimilarityFeatures.forEach((feature, idx) => {{
|
|
|
|
|
+ const name = feature.name || '';
|
|
|
|
|
+ const similarity = feature.similarity || 0;
|
|
|
|
|
+ const dimension = feature.dimension || '';
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="high-similarity-item">
|
|
|
|
|
+ <div class="high-feature-name">✓ ${{name}}</div>
|
|
|
|
|
+ <div class="high-feature-score">${{similarity.toFixed(2)}}</div>
|
|
|
|
|
+ <div class="high-feature-meta">${{dimension}}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 部分匹配特征区域(0.5-0.8)
|
|
|
|
|
+ const partialMatchCount = data.length;
|
|
|
|
|
+ if (partialMatchCount > 0) {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="partial-similarity-section">
|
|
|
|
|
+ <div class="partial-similarity-header">
|
|
|
|
|
+ <div class="partial-similarity-title">📋 当前帖子-部分匹配(0.5-0.8)</div>
|
|
|
|
|
+ <div class="partial-similarity-count">${{partialMatchCount}}个</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ data.forEach((feature, featureIdx) => {{
|
|
|
|
|
+ const featureName = feature['原始特征名称'];
|
|
|
|
|
+ const featureInfo = allFeatures[featureName] || {{}};
|
|
|
|
|
+ const similarity = featureInfo.similarity || 0;
|
|
|
|
|
+ const dimension = featureInfo.dimension || '';
|
|
|
|
|
+ const groups = feature['组合评估结果_分组'] || [];
|
|
|
|
|
+
|
|
|
|
|
+ // 获取来源人设分类路径
|
|
|
|
|
+ const top3Matches = feature['top3匹配信息'] || [];
|
|
|
|
|
+ let sourcePath = '';
|
|
|
|
|
+ if (top3Matches.length > 0) {{
|
|
|
|
|
+ sourcePath = top3Matches[0]['所属分类路径'] || '';
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ const isActive = selectedFeatureIdx === featureIdx;
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="feature-item-left ${{isActive ? 'active' : ''}}"
|
|
|
|
|
+ onclick="selectFeature(${{featureIdx}})"
|
|
|
|
|
+ id="feature-left-${{featureIdx}}">
|
|
|
|
|
+ <div class="feature-name">🎯 ${{featureName}}</div>
|
|
|
|
|
+ <div class="feature-meta-small">
|
|
|
|
|
+ 相似度: ${{similarity.toFixed(2)}} · ${{dimension}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ${{sourcePath ? `<div class="feature-source">${{sourcePath}}</div>` : ''}}
|
|
|
|
|
+ <div class="feature-stats">${{groups.length}}个base_word</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 低相似度特征区域(<0.5)
|
|
|
|
|
+ if (lowSimilarityFeatures && lowSimilarityFeatures.length > 0) {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="low-similarity-section">
|
|
|
|
|
+ <div class="low-similarity-header">
|
|
|
|
|
+ <div class="low-similarity-title">📋 当前帖子-未匹配(<0.5)</div>
|
|
|
|
|
+ <div class="low-similarity-count">${{lowSimilarityFeatures.length}}个</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="low-similarity-list">
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ lowSimilarityFeatures.forEach((feature, idx) => {{
|
|
|
|
|
+ const name = feature.name || '';
|
|
|
|
|
+ const similarity = feature.similarity || 0;
|
|
|
|
|
+ const dimension = feature.dimension || '';
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="low-similarity-item">
|
|
|
|
|
+ <div class="low-feature-name">✗ ${{name}}</div>
|
|
|
|
|
+ <div class="low-feature-score">${{similarity.toFixed(2)}}</div>
|
|
|
|
|
+ <div class="low-feature-meta">${{dimension}}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ sidebar.innerHTML = html;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 级联交互函数 ==========
|
|
|
|
|
+
|
|
|
|
|
+ // 选择特征 - 显示middle sidebar
|
|
|
|
|
+ function selectFeature(featureIdx) {{
|
|
|
|
|
+ selectedFeatureIdx = featureIdx;
|
|
|
|
|
+ selectedBaseWordIdx = null;
|
|
|
|
|
+ selectedSearchWordIdx = null;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新左侧active状态
|
|
|
|
|
+ document.querySelectorAll('.feature-item-left').forEach((el, idx) => {{
|
|
|
|
|
+ if (idx === featureIdx) {{
|
|
|
|
|
+ el.classList.add('active');
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ el.classList.remove('active');
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染middle sidebar
|
|
|
|
|
+ renderMiddleSidebar(featureIdx);
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏right sidebar和detail area
|
|
|
|
|
+ document.getElementById('rightSidebar').classList.remove('active');
|
|
|
|
|
+ document.getElementById('detailArea').querySelector('.detail-placeholder').style.display = 'flex';
|
|
|
|
|
+ document.getElementById('detailContent').innerHTML = '';
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染middle sidebar - 显示base_words
|
|
|
|
|
+ function renderMiddleSidebar(featureIdx) {{
|
|
|
|
|
+ const feature = data[featureIdx];
|
|
|
|
|
+ const groups = feature['组合评估结果_分组'] || [];
|
|
|
|
|
+
|
|
|
|
|
+ let html = '';
|
|
|
|
|
+ groups.forEach((group, groupIdx) => {{
|
|
|
|
|
+ const baseWord = group['base_word'] || '';
|
|
|
|
|
+ const baseSimilarity = group['base_word_similarity'] || 0;
|
|
|
|
|
+ const searches = group['top10_searches'] || [];
|
|
|
|
|
+
|
|
|
|
|
+ const relatedWords = feature['高相似度候选_按base_word']?.[baseWord] || [];
|
|
|
|
|
+ const relatedWordNames = relatedWords
|
|
|
|
|
+ .map(w => w['人设特征名称'] || '')
|
|
|
|
|
+ .filter(name => name.trim() !== '')
|
|
|
|
|
+ .slice(0, 5)
|
|
|
|
|
+ .join('、') || '暂无关联特征';
|
|
|
|
|
+
|
|
|
|
|
+ const isActive = selectedBaseWordIdx === groupIdx;
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="baseword-item ${{isActive ? 'active' : ''}}"
|
|
|
|
|
+ onclick="selectBaseWord(${{featureIdx}}, ${{groupIdx}})"
|
|
|
|
|
+ id="baseword-${{featureIdx}}-${{groupIdx}}">
|
|
|
|
|
+ <div class="cascade-item-title" style="color:#059669;">👤 ${{baseWord}}</div>
|
|
|
|
|
+ <div class="cascade-item-meta">
|
|
|
|
|
+ 相似度: ${{baseSimilarity.toFixed(2)}} · ${{searches.length}}个搜索词
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ${{relatedWordNames ? `<div style="font-size:10px;color:#92400e;margin-top:4px;">相关: ${{relatedWordNames}}</div>` : ''}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('middleContent').innerHTML = html;
|
|
|
|
|
+ document.getElementById('middleSidebar').classList.add('active');
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 选择base_word - 显示right sidebar
|
|
|
|
|
+ function selectBaseWord(featureIdx, baseWordIdx) {{
|
|
|
|
|
+ selectedBaseWordIdx = baseWordIdx;
|
|
|
|
|
+ selectedSearchWordIdx = null;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新middle sidebar active状态
|
|
|
|
|
+ document.querySelectorAll('.baseword-item').forEach((el, idx) => {{
|
|
|
|
|
+ if (idx === baseWordIdx) {{
|
|
|
|
|
+ el.classList.add('active');
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ el.classList.remove('active');
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染right sidebar
|
|
|
|
|
+ renderRightSidebar(featureIdx, baseWordIdx);
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏detail content
|
|
|
|
|
+ document.getElementById('detailArea').querySelector('.detail-placeholder').style.display = 'flex';
|
|
|
|
|
+ document.getElementById('detailContent').innerHTML = '';
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染right sidebar - 显示search words
|
|
|
|
|
+ function renderRightSidebar(featureIdx, baseWordIdx) {{
|
|
|
|
|
+ const feature = data[featureIdx];
|
|
|
|
|
+ const groups = feature['组合评估结果_分组'] || [];
|
|
|
|
|
+ const group = groups[baseWordIdx];
|
|
|
|
|
+ const searches = group['top10_searches'] || [];
|
|
|
|
|
+
|
|
|
|
|
+ let html = '';
|
|
|
|
|
+ searches.forEach((sw, swIdx) => {{
|
|
|
|
|
+ const searchWord = sw.search_word || '';
|
|
|
|
|
+ const sourceWord = sw.source_word || '';
|
|
|
|
|
+ const score = sw.score || 0;
|
|
|
|
|
+
|
|
|
|
|
+ const evaluation = sw['evaluation_with_filter'];
|
|
|
|
|
+ let evalBadges = '';
|
|
|
|
|
+ if (evaluation) {{
|
|
|
|
|
+ const stats = evaluation.statistics || {{}};
|
|
|
|
|
+ const complete = stats['完全匹配(0.8-1.0)'] || 0;
|
|
|
|
|
+ const similar = stats['相似匹配(0.6-0.79)'] || 0;
|
|
|
|
|
+ const weak = stats['弱相似(0.5-0.59)'] || 0;
|
|
|
|
|
+ const none = stats['无匹配(≤0.4)'] || 0;
|
|
|
|
|
+ const filtered = evaluation.filtered_count || 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (complete > 0) evalBadges += `<span class="eval-badge eval-complete">🟢${{complete}}</span>`;
|
|
|
|
|
+ if (similar > 0) evalBadges += `<span class="eval-badge eval-similar">🟡${{similar}}</span>`;
|
|
|
|
|
+ if (weak > 0) evalBadges += `<span class="eval-badge eval-weak">🟠${{weak}}</span>`;
|
|
|
|
|
+ if (none > 0) evalBadges += `<span class="eval-badge eval-none">🔴${{none}}</span>`;
|
|
|
|
|
+ if (filtered > 0) evalBadges += `<span class="eval-badge eval-filtered">⚫${{filtered}}</span>`;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ const isActive = selectedSearchWordIdx === swIdx;
|
|
|
|
|
+ const reasoning = sw.reasoning || '';
|
|
|
|
|
+ const reasoningId = `reasoning-${{featureIdx}}-${{baseWordIdx}}-${{swIdx}}`;
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="searchword-item ${{isActive ? 'active' : ''}}"
|
|
|
|
|
+ onclick="selectSearchWord(${{featureIdx}}, ${{baseWordIdx}}, ${{swIdx}})"
|
|
|
|
|
+ id="searchword-${{featureIdx}}-${{baseWordIdx}}-${{swIdx}}">
|
|
|
|
|
+ <div class="cascade-item-title">📝 ${{searchWord}}</div>
|
|
|
|
|
+ <div style="font-size:11px;color:#6b7280;margin-top:4px;">
|
|
|
|
|
+ 组合词: ${{sourceWord}} → [${{score.toFixed(2)}}]
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ${{evalBadges ? `<div style="margin-top:4px;">${{evalBadges}}</div>` : ''}}
|
|
|
|
|
+ ${{reasoning ? `
|
|
|
|
|
+ <div class="search-word-reasoning">
|
|
|
|
|
+ <span class="reasoning-label">💭 推理理由:</span>
|
|
|
|
|
+ <div class="reasoning-text" id="${{reasoningId}}-text">
|
|
|
|
|
+ ${{reasoning}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="reasoning-toggle"
|
|
|
|
|
+ onclick="event.stopPropagation(); toggleSearchWordReasoning('${{reasoningId}}')">
|
|
|
|
|
+ 展开 ▼
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ` : ''}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('rightSidebarContent').innerHTML = html;
|
|
|
|
|
+ document.getElementById('rightSidebar').classList.add('active');
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 折叠/展开推理理由
|
|
|
|
|
+ function toggleSearchWordReasoning(reasoningId) {{
|
|
|
|
|
+ const textEl = document.getElementById(reasoningId + '-text');
|
|
|
|
|
+ const toggleBtn = event.currentTarget;
|
|
|
|
|
+
|
|
|
|
|
+ if (textEl.classList.contains('expanded')) {{
|
|
|
|
|
+ // 当前是展开状态,收起
|
|
|
|
|
+ textEl.classList.remove('expanded');
|
|
|
|
|
+ toggleBtn.innerHTML = '展开 ▼';
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ // 当前是收起状态,展开
|
|
|
|
|
+ textEl.classList.add('expanded');
|
|
|
|
|
+ toggleBtn.innerHTML = '收起 ▲';
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 选择search word - 显示detail area
|
|
|
|
|
+ function selectSearchWord(featureIdx, baseWordIdx, swIdx) {{
|
|
|
|
|
+ selectedSearchWordIdx = swIdx;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新right sidebar active状态
|
|
|
|
|
+ document.querySelectorAll('.searchword-item').forEach((el, idx) => {{
|
|
|
|
|
+ if (idx === swIdx) {{
|
|
|
|
|
+ el.classList.add('active');
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ el.classList.remove('active');
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染detail area
|
|
|
|
|
+ renderDetailArea(featureIdx, baseWordIdx, swIdx);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染detail area - 显示搜索结果
|
|
|
|
|
+ function renderDetailArea(featureIdx, baseWordIdx, swIdx) {{
|
|
|
|
|
+ const feature = data[featureIdx];
|
|
|
|
|
+ const groups = feature['组合评估结果_分组'] || [];
|
|
|
|
|
+ const group = groups[baseWordIdx];
|
|
|
|
|
+ const searches = group['top10_searches'] || [];
|
|
|
|
|
+ const sw = searches[swIdx];
|
|
|
|
|
+
|
|
|
|
|
+ const searchWord = sw.search_word || '';
|
|
|
|
|
+ const sourceWord = sw.source_word || '';
|
|
|
|
|
+ const searchResult = sw.search_result || {{}};
|
|
|
|
|
+ const notes = searchResult.data?.data || [];
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏占位符
|
|
|
|
|
+ document.getElementById('detailArea').querySelector('.detail-placeholder').style.display = 'none';
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染搜索结果
|
|
|
|
|
+ let html = `
|
|
|
|
|
+ <div class="search-result-header" style="padding:20px;background:#f9fafb;border-bottom:2px solid #e5e7eb;">
|
|
|
|
|
+ <h3 style="margin:0 0 10px 0;">📝 ${{searchWord}}</h3>
|
|
|
|
|
+ <div style="font-size:12px;color:#6b7280;">
|
|
|
|
|
+ 组合词: ${{sourceWord}} · ${{notes.length}}个搜索结果
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="notes-grid" style="padding:20px;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:15px;">
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ notes.forEach((note, noteIdx) => {{
|
|
|
|
|
+ const noteCard = note.note_card || {{}};
|
|
|
|
|
+ const title = noteCard.display_title || '无标题';
|
|
|
|
|
+ const cover = (noteCard.image_list && noteCard.image_list[0]) || noteCard.cover?.url_default || '';
|
|
|
|
|
+ const noteId = note.id || note.note_id || '';
|
|
|
|
|
+ const type = noteCard.type || 'normal';
|
|
|
|
|
+ const typeIcon = type === 'video' ? '🎬' : '📷';
|
|
|
|
|
+
|
|
|
|
|
+ // 获取评估信息
|
|
|
|
|
+ const noteEval = sw.evaluation_with_filter?.notes_with_scores?.find(n => n.note_id === noteId || n.id === noteId);
|
|
|
|
|
+ const evalScore = noteEval?.evaluation_score || 0;
|
|
|
|
|
+ const matchLevel = noteEval?.match_level || '未评估';
|
|
|
|
|
+
|
|
|
|
|
+ let matchClass = '';
|
|
|
|
|
+ let matchColor = '';
|
|
|
|
|
+ if (matchLevel.includes('完全匹配')) {{
|
|
|
|
|
+ matchClass = 'match-complete';
|
|
|
|
|
+ matchColor = '#22c55e';
|
|
|
|
|
+ }} else if (matchLevel.includes('相似匹配')) {{
|
|
|
|
|
+ matchClass = 'match-similar';
|
|
|
|
|
+ matchColor = '#f59e0b';
|
|
|
|
|
+ }} else if (matchLevel.includes('弱相似')) {{
|
|
|
|
|
+ matchClass = 'match-weak';
|
|
|
|
|
+ matchColor = '#f97316';
|
|
|
|
|
+ }} else if (matchLevel.includes('无匹配')) {{
|
|
|
|
|
+ matchClass = 'match-none';
|
|
|
|
|
+ matchColor = '#dc2626';
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ matchClass = 'match-filtered';
|
|
|
|
|
+ matchColor = '#6b7280';
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="note-card ${{matchClass}}" style="border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;background:white;cursor:pointer;transition:all 0.2s;border-left:4px solid ${{matchColor}};"
|
|
|
|
|
+ onclick="openDeconstructionModal('${{noteId}}')">
|
|
|
|
|
+ ${{cover ? `<img src="${{cover}}" style="width:100%;height:180px;object-fit:cover;">` : `<div style="width:100%;height:180px;background:#f3f4f6;display:flex;align-items:center;justify-content:center;color:#9ca3af;">${{typeIcon}}</div>`}}
|
|
|
|
|
+ <div style="padding:12px;">
|
|
|
|
|
+ <div style="font-size:14px;font-weight:600;margin-bottom:8px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">
|
|
|
|
|
+ ${{title}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="font-size:11px;color:#6b7280;display:flex;justify-content:space-between;align-items:center;">
|
|
|
|
|
+ <span>${{typeIcon}} ${{type}}</span>
|
|
|
|
|
+ <span style="color:${{matchColor}};font-weight:600;">${{evalScore.toFixed(2)}}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('detailContent').innerHTML = html;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染右侧结果区
|
|
|
|
|
+ function renderRightContent() {{
|
|
|
|
|
+ const content = document.getElementById('rightContent');
|
|
|
|
|
+ let html = '';
|
|
|
|
|
+
|
|
|
|
|
+ data.forEach((feature, featureIdx) => {{
|
|
|
|
|
+ const groups = feature['组合评估结果_分组'] || [];
|
|
|
|
|
+
|
|
|
|
|
+ // 获取特征信息
|
|
|
|
|
+ const featureName = feature['原始特征名称'];
|
|
|
|
|
+ const featureInfo = allFeatures[featureName] || {{}};
|
|
|
|
|
+ const similarity = featureInfo.similarity || 0;
|
|
|
|
|
+ const weight = featureInfo.weight || 0;
|
|
|
|
|
+ const dimension = featureInfo.dimension || '';
|
|
|
|
|
+ const category = featureInfo.category || '';
|
|
|
|
|
+
|
|
|
|
|
+ // 根据相似度确定样式
|
|
|
|
|
+ let featureTitleClass = '';
|
|
|
|
|
+ let featureIcon = '';
|
|
|
|
|
+ let matchLabel = '';
|
|
|
|
|
+ let featureColor = '';
|
|
|
|
|
+
|
|
|
|
|
+ if (similarity >= 0.8) {{
|
|
|
|
|
+ featureTitleClass = 'feature-section-high';
|
|
|
|
|
+ featureIcon = '🟢';
|
|
|
|
|
+ matchLabel = '已匹配';
|
|
|
|
|
+ featureColor = '#22c55e';
|
|
|
|
|
+ }} else if (similarity >= 0.5) {{
|
|
|
|
|
+ featureTitleClass = 'feature-section-partial';
|
|
|
|
|
+ featureIcon = '🟡';
|
|
|
|
|
+ matchLabel = '部分匹配';
|
|
|
|
|
+ featureColor = '#f97316';
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ featureTitleClass = 'feature-section-low';
|
|
|
|
|
+ featureIcon = '🔴';
|
|
|
|
|
+ matchLabel = '未匹配';
|
|
|
|
|
+ featureColor = '#dc2626';
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 添加特征标题区域
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="feature-section ${{featureTitleClass}}" id="feature-section-${{featureIdx}}">
|
|
|
|
|
+ <div class="feature-section-header" style="border-left: 4px solid ${{featureColor}};">
|
|
|
|
|
+ <div class="feature-section-title" style="color: ${{featureColor}};">
|
|
|
|
|
+ ${{featureIcon}} ${{featureName}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="feature-section-meta">
|
|
|
|
|
+ ${{matchLabel}} · 相似度: ${{similarity.toFixed(2)}} · ${{dimension}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ groups.forEach((group, groupIdx) => {{
|
|
|
|
|
+ const searches = group['top10_searches'] || [];
|
|
|
|
|
+
|
|
|
|
|
+ searches.forEach((sw, swIdx) => {{
|
|
|
|
|
+ const blockId = `block-${{featureIdx}}-${{groupIdx}}-${{swIdx}}`;
|
|
|
|
|
+ const hasSearchResult = sw.search_result != null;
|
|
|
|
|
+ const searchResult = sw.search_result || {{}};
|
|
|
|
|
+ const notes = searchResult.data?.data || [];
|
|
|
|
|
+
|
|
|
|
|
+ const videoCount = notes.filter(n => n.note_card?.type === 'video').length;
|
|
|
|
|
+ const normalCount = notes.length - videoCount;
|
|
|
|
|
+
|
|
|
|
|
+ const evaluation = sw['evaluation_with_filter'];
|
|
|
|
|
+ let evalStats = '';
|
|
|
|
|
+ if (evaluation) {{
|
|
|
|
|
+ const stats = evaluation.statistics || {{}};
|
|
|
|
|
+ const complete = stats['完全匹配(0.8-1.0)'] || 0;
|
|
|
|
|
+ const similar = stats['相似匹配(0.6-0.79)'] || 0;
|
|
|
|
|
+ const weak = stats['弱相似(0.5-0.59)'] || 0;
|
|
|
|
|
+ const none = stats['无匹配(≤0.4)'] || 0;
|
|
|
|
|
+ const filtered = evaluation.filtered_count || 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (complete > 0) evalStats += `<span class="stat-badge eval complete">🟢 完全:${{complete}}</span>`;
|
|
|
|
|
+ if (similar > 0) evalStats += `<span class="stat-badge eval similar">🟡 相似:${{similar}}</span>`;
|
|
|
|
|
+ if (weak > 0) evalStats += `<span class="stat-badge eval weak">🟠 弱:${{weak}}</span>`;
|
|
|
|
|
+ if (none > 0) evalStats += `<span class="stat-badge eval none">🔴 无:${{none}}</span>`;
|
|
|
|
|
+ if (filtered > 0) evalStats += `<span class="stat-badge eval filtered">⚫ 过滤:${{filtered}}</span>`;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="result-block" id="${{blockId}}">
|
|
|
|
|
+ <div class="result-header">
|
|
|
|
|
+ <div class="result-title">${{sw.search_word}}</div>
|
|
|
|
|
+ <div class="result-stats">
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ if (!hasSearchResult) {{
|
|
|
|
|
+ html += `<span class="stat-badge" style="background:#fef3c7;color:#92400e;font-weight:600">⏸️ 未执行搜索</span>`;
|
|
|
|
|
+ }} else if (notes.length === 0) {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <span class="stat-badge">📝 0 条帖子</span>
|
|
|
|
|
+ <span class="stat-badge" style="background:#fee2e2;color:#991b1b;font-weight:600">❌ 未找到匹配</span>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <span class="stat-badge">📝 ${{notes.length}} 条帖子</span>
|
|
|
|
|
+ <span class="stat-badge">🎬 ${{videoCount}} 视频</span>
|
|
|
|
|
+ <span class="stat-badge">📷 ${{normalCount}} 图文</span>
|
|
|
|
|
+ ${{evalStats}}
|
|
|
|
|
+ `;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ html += `</div></div>`;
|
|
|
|
|
+
|
|
|
|
|
+ if (!hasSearchResult) {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="empty-state">
|
|
|
|
|
+ <div class="empty-icon">⏸️</div>
|
|
|
|
|
+ <div class="empty-title">该搜索词未执行搜索</div>
|
|
|
|
|
+ <div class="empty-desc">由于搜索次数限制,该搜索词未被执行</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }} else if (notes.length === 0) {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="empty-state">
|
|
|
|
|
+ <div class="empty-icon">❌</div>
|
|
|
|
|
+ <div class="empty-title">搜索完成,但未找到匹配的帖子</div>
|
|
|
|
|
+ <div class="empty-desc">该搜索词已执行,但小红书返回了 0 条结果</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="notes-grid">
|
|
|
|
|
+ ${{notes.map((note, noteIdx) => renderNoteCard(note, featureIdx, groupIdx, swIdx, noteIdx)).join('')}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ html += `</div>`;
|
|
|
|
|
+ }});
|
|
|
|
|
+ }});
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ content.innerHTML = html;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染单个帖子卡片
|
|
|
|
|
+ function renderNoteCard(note, featureIdx, groupIdx, swIdx, noteIdx) {{
|
|
|
|
|
+ const card = note.note_card || {{}};
|
|
|
|
|
+ const images = card.image_list || [];
|
|
|
|
|
+ const title = card.display_title || '无标题';
|
|
|
|
|
+ const noteType = card.type || 'normal';
|
|
|
|
|
+ const noteId = note.id || '';
|
|
|
|
|
+ const user = card.user || {{}};
|
|
|
|
|
+ const userName = user.nick_name || '未知用户';
|
|
|
|
|
+ const userAvatar = user.avatar || '';
|
|
|
|
|
+
|
|
|
|
|
+ const carouselId = `carousel-${{featureIdx}}-${{groupIdx}}-${{swIdx}}-${{noteIdx}}`;
|
|
|
|
|
+
|
|
|
|
|
+ const evalKey = `${{featureIdx}}-${{groupIdx}}-${{swIdx}}-${{noteIdx}}`;
|
|
|
|
|
+ const noteEval = noteEvaluations[evalKey];
|
|
|
|
|
+ const evalCategory = getEvalCategory(noteEval);
|
|
|
|
|
+ const evalClass = `eval-${{evalCategory}}`;
|
|
|
|
|
+
|
|
|
|
|
+ let evalSection = '';
|
|
|
|
|
+ if (noteEval) {{
|
|
|
|
|
+ const score = noteEval['综合得分'];
|
|
|
|
|
+ const scoreEmoji = score >= 0.8 ? '🟢' : score >= 0.6 ? '🟡' : score >= 0.5 ? '🟠' : '🔴';
|
|
|
|
|
+ const scoreText = score >= 0.8 ? '完全匹配' : score >= 0.6 ? '相似匹配' : score >= 0.5 ? '弱相似' : '无匹配';
|
|
|
|
|
+ const reasoning = noteEval['评分说明'] || '无';
|
|
|
|
|
+ const matchingPoints = (noteEval['关键匹配点'] || []).join('、') || '无';
|
|
|
|
|
+
|
|
|
|
|
+ evalSection = `
|
|
|
|
|
+ <div class="note-eval">
|
|
|
|
|
+ <div class="note-eval-header" onclick="event.stopPropagation(); toggleEvalDetails('${{carouselId}}')">
|
|
|
|
|
+ <span class="note-eval-score">${{scoreEmoji}} ${{scoreText}} (${{score}}分)</span>
|
|
|
|
|
+ <span class="note-eval-toggle" id="${{carouselId}}-toggle">▼ 详情</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="note-eval-details" id="${{carouselId}}-details">
|
|
|
|
|
+ <div class="eval-detail-label">评估理由:</div>
|
|
|
|
|
+ <div class="eval-detail-text">${{reasoning}}</div>
|
|
|
|
|
+ <div class="eval-detail-label">匹配要点:</div>
|
|
|
|
|
+ <div class="eval-detail-text">${{matchingPoints}}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }} else if (evalCategory === 'filtered') {{
|
|
|
|
|
+ evalSection = `
|
|
|
|
|
+ <div class="note-eval">
|
|
|
|
|
+ <div class="note-eval-score">⚫ 已过滤(与搜索无关)</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否有解构数据(仅完全匹配)
|
|
|
|
|
+ const hasSimilarity = !!similarityData[noteId];
|
|
|
|
|
+ const similarityFeatures = hasSimilarity ? (similarityData[noteId].deconstructed_features || []) : [];
|
|
|
|
|
+ const hasValidFeatures = similarityFeatures.length > 0;
|
|
|
|
|
+ const hasDeconstruction = evalCategory === 'complete' && hasValidFeatures;
|
|
|
|
|
+
|
|
|
|
|
+ // 调试日志: 记录解构按钮判断过程
|
|
|
|
|
+ if (noteId === '67e554a300000000090148a7' || hasDeconstruction) {{
|
|
|
|
|
+ console.log(`[解构按钮] noteId=${{noteId}}, evalCategory=${{evalCategory}}, hasSimilarity=${{hasSimilarity}}, 特征数=${{similarityFeatures.length}}, 显示按钮=${{hasDeconstruction}}`);
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ let deconstructionSection = '';
|
|
|
|
|
+
|
|
|
|
|
+ if (hasDeconstruction) {{
|
|
|
|
|
+ deconstructionSection = `
|
|
|
|
|
+ <button class="deconstruction-toggle-btn" data-note-id="${{noteId}}" data-note-title="${{title.replace(/"/g, '"')}}">
|
|
|
|
|
+ <span>📊</span>
|
|
|
|
|
+ <span>查看解构结果</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ return `
|
|
|
|
|
+ <div class="note-card ${{evalClass}}" data-eval-category="${{evalCategory}}" onclick="openNoteImagesModal(${{featureIdx}}, ${{groupIdx}}, ${{swIdx}}, ${{noteIdx}})">
|
|
|
|
|
+ <div class="image-carousel" id="${{carouselId}}">
|
|
|
|
|
+ <div class="carousel-images">
|
|
|
|
|
+ ${{images.map(img => `<img class="carousel-image" src="${{img}}" alt="帖子图片" loading="lazy">`).join('')}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ${{images.length > 1 ? `
|
|
|
|
|
+ <button class="carousel-btn prev" onclick="event.stopPropagation(); changeImage('${{carouselId}}', -1)">←</button>
|
|
|
|
|
+ <button class="carousel-btn next" onclick="event.stopPropagation(); changeImage('${{carouselId}}', 1)">→</button>
|
|
|
|
|
+ <div class="carousel-indicators">
|
|
|
|
|
+ ${{images.map((_, i) => `<span class="dot ${{i === 0 ? 'active' : ''}}" onclick="event.stopPropagation(); goToImage('${{carouselId}}', ${{i}})"></span>`).join('')}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span class="image-counter">1/${{images.length}}</span>
|
|
|
|
|
+ ` : ''}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="note-info">
|
|
|
|
|
+ <div class="note-title">${{title}}</div>
|
|
|
|
|
+ <div class="note-meta">
|
|
|
|
|
+ <span class="note-type type-${{noteType}}">
|
|
|
|
|
+ ${{noteType === 'video' ? '🎬 视频' : '📷 图文'}}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <div class="note-author">
|
|
|
|
|
+ ${{userAvatar ? `<img class="author-avatar" src="${{userAvatar}}" alt="${{userName}}">` : ''}}
|
|
|
|
|
+ <span>${{userName}}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ${{evalSection}}
|
|
|
|
|
+ ${{deconstructionSection}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 打开解构模态窗口
|
|
|
|
|
+ function openDeconstructionModal(noteId, noteTitle) {{
|
|
|
|
|
+ console.log('🔧 [调试] openDeconstructionModal被调用, noteId:', noteId);
|
|
|
|
|
+
|
|
|
|
|
+ const modal = document.getElementById('deconstructionModal');
|
|
|
|
|
+ const modalContent = document.getElementById('modalContent');
|
|
|
|
|
+ const modalNoteTitle = document.getElementById('modalNoteTitle');
|
|
|
|
|
+
|
|
|
|
|
+ if (!modal || !modalContent || !modalNoteTitle) {{
|
|
|
|
|
+ console.error('❌ [错误] 无法找到模态窗口元素');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 设置标题
|
|
|
|
|
+ modalNoteTitle.textContent = noteTitle || '解构分析';
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否有数据
|
|
|
|
|
+ const hasSimilarityData = !!similarityData[noteId];
|
|
|
|
|
+ console.log('📊 [调试] 相似度数据存在:', hasSimilarityData);
|
|
|
|
|
+
|
|
|
|
|
+ if (!hasSimilarityData) {{
|
|
|
|
|
+ console.warn('⚠️ [警告] 未找到相似度数据, noteId:', noteId);
|
|
|
|
|
+ console.log('📋 [调试] 可用的noteId列表:', Object.keys(similarityData));
|
|
|
|
|
+ modalContent.innerHTML = '<div style="padding: 30px; text-align: center; color: #6b7280;">暂无解构数据</div>';
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ try {{
|
|
|
|
|
+ modalContent.innerHTML = renderDeconstructionContent(noteId);
|
|
|
|
|
+ console.log('✅ [调试] 解构内容渲染成功');
|
|
|
|
|
+ }} catch (error) {{
|
|
|
|
|
+ console.error('❌ [错误] 渲染解构内容失败:', error);
|
|
|
|
|
+ modalContent.innerHTML = `<div style="padding: 30px; text-align: center; color: red;">渲染错误: ${{error.message}}</div>`;
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 显示模态窗口
|
|
|
|
|
+ modal.classList.add('active');
|
|
|
|
|
+ document.body.style.overflow = 'hidden'; // 禁止背景滚动
|
|
|
|
|
+ console.log('✅ [调试] 模态窗口已显示');
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 关闭模态窗口
|
|
|
|
|
+ function closeModal() {{
|
|
|
|
|
+ console.log('🔧 [调试] closeModal被调用');
|
|
|
|
|
+ const modal = document.getElementById('deconstructionModal');
|
|
|
|
|
+ if (modal) {{
|
|
|
|
|
+ modal.classList.remove('active');
|
|
|
|
|
+ document.body.style.overflow = ''; // 恢复滚动
|
|
|
|
|
+ console.log('✅ [调试] 模态窗口已关闭');
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // ESC键关闭模态窗口
|
|
|
|
|
+ document.addEventListener('keydown', function(e) {{
|
|
|
|
|
+ if (e.key === 'Escape') {{
|
|
|
|
|
+ const modal = document.getElementById('deconstructionModal');
|
|
|
|
|
+ if (modal && modal.classList.contains('active')) {{
|
|
|
|
|
+ closeModal();
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 点击遮罩层关闭模态窗口
|
|
|
|
|
+ document.addEventListener('click', function(e) {{
|
|
|
|
|
+ const modal = document.getElementById('deconstructionModal');
|
|
|
|
|
+ if (e.target === modal) {{
|
|
|
|
|
+ closeModal();
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ const pModal = document.getElementById('pDetailModal');
|
|
|
|
|
+ if (e.target === pModal) {{
|
|
|
|
|
+ closePDetailModal();
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 打开P值详情模态窗口
|
|
|
|
|
+ function openPDetailModal(searchWord, featureIdx, groupIdx, swIdx) {{
|
|
|
|
|
+ console.log('🔧 [调试] openPDetailModal被调用, searchWord:', searchWord);
|
|
|
|
|
+
|
|
|
|
|
+ const modal = document.getElementById('pDetailModal');
|
|
|
|
|
+ const modalContent = document.getElementById('pDetailContent');
|
|
|
|
|
+
|
|
|
|
|
+ if (!modal || !modalContent) {{
|
|
|
|
|
+ console.error('❌ [错误] 无法找到P值详情模态窗口元素');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 获取对应的搜索词数据
|
|
|
|
|
+ const feature = data[featureIdx];
|
|
|
|
|
+ const group = feature['组合评估结果_分组'][groupIdx];
|
|
|
|
|
+ const sw = group['top10_searches'][swIdx];
|
|
|
|
|
+
|
|
|
|
|
+ const pScore = sw.comprehensive_score;
|
|
|
|
|
+ const pDetail = sw.comprehensive_score_detail;
|
|
|
|
|
+
|
|
|
|
|
+ if (!pDetail) {{
|
|
|
|
|
+ console.warn('⚠️ [警告] 未找到P值详情数据');
|
|
|
|
|
+ modalContent.innerHTML = '<div style="padding: 30px; text-align: center; color: #6b7280;">暂无P值详情数据</div>';
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ try {{
|
|
|
|
|
+ modalContent.innerHTML = renderPDetailContent(searchWord, feature['原始特征名称'], sw.source_word, pScore, pDetail);
|
|
|
|
|
+ console.log('✅ [调试] P值详情内容渲染成功');
|
|
|
|
|
+ }} catch (error) {{
|
|
|
|
|
+ console.error('❌ [错误] 渲染P值详情内容失败:', error);
|
|
|
|
|
+ modalContent.innerHTML = `<div style="padding: 30px; text-align: center; color: red;">渲染错误: ${{error.message}}</div>`;
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 显示模态窗口
|
|
|
|
|
+ modal.classList.add('active');
|
|
|
|
|
+ document.body.style.overflow = 'hidden';
|
|
|
|
|
+ console.log('✅ [调试] P值详情模态窗口已显示');
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 关闭P值详情模态窗口
|
|
|
|
|
+ function closePDetailModal() {{
|
|
|
|
|
+ console.log('🔧 [调试] closePDetailModal被调用');
|
|
|
|
|
+ const modal = document.getElementById('pDetailModal');
|
|
|
|
|
+ if (modal) {{
|
|
|
|
|
+ modal.classList.remove('active');
|
|
|
|
|
+ document.body.style.overflow = '';
|
|
|
|
|
+ console.log('✅ [调试] P值详情模态窗口已关闭');
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染P值详情内容
|
|
|
|
|
+ function renderPDetailContent(searchWord, targetFeature, sourceWord, pScore, pDetail) {{
|
|
|
|
|
+ const N = pDetail.N || 0;
|
|
|
|
|
+ const M = pDetail.M || 0;
|
|
|
|
|
+ const totalContribution = pDetail.total_contribution || 0;
|
|
|
|
|
+ const matches = pDetail.complete_matches || [];
|
|
|
|
|
+
|
|
|
|
|
+ let html = `
|
|
|
|
|
+ <div class="p-formula-section">
|
|
|
|
|
+ <div class="p-formula-title">📐 计算公式</div>
|
|
|
|
|
+ <div class="p-formula">P = Σ(a × b) / N = ${{totalContribution.toFixed(3)}} / ${{N}} = ${{pScore.toFixed(3)}}</div>
|
|
|
|
|
+ <div style="font-size: 12px; color: #64748b; margin-top: 8px;">
|
|
|
|
|
+ <div>• a: 评估得分(综合得分)</div>
|
|
|
|
|
+ <div>• b: 最高相似度(解构特征与目标特征的最高相似度)</div>
|
|
|
|
|
+ <div>• N: 总帖子数</div>
|
|
|
|
|
+ <div>• M: 完全匹配数(得分 ≥ 0.8)</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="p-summary">
|
|
|
|
|
+ <div class="p-summary-item">
|
|
|
|
|
+ <div class="p-summary-label">搜索词</div>
|
|
|
|
|
+ <div class="p-summary-value" style="font-size: 16px;">${{searchWord}}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="p-summary-item">
|
|
|
|
|
+ <div class="p-summary-label">来源</div>
|
|
|
|
|
+ <div class="p-summary-value" style="font-size: 16px;">${{sourceWord}}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="p-summary-item">
|
|
|
|
|
+ <div class="p-summary-label">目标特征</div>
|
|
|
|
|
+ <div class="p-summary-value" style="font-size: 16px;">${{targetFeature}}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="p-summary">
|
|
|
|
|
+ <div class="p-summary-item">
|
|
|
|
|
+ <div class="p-summary-label">总帖子数 (N)</div>
|
|
|
|
|
+ <div class="p-summary-value">${{N}}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="p-summary-item">
|
|
|
|
|
+ <div class="p-summary-label">完全匹配数 (M)</div>
|
|
|
|
|
+ <div class="p-summary-value highlight">${{M}}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="p-summary-item">
|
|
|
|
|
+ <div class="p-summary-label">总贡献值</div>
|
|
|
|
|
+ <div class="p-summary-value highlight">${{totalContribution.toFixed(3)}}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="p-summary-item">
|
|
|
|
|
+ <div class="p-summary-label">综合得分 (P)</div>
|
|
|
|
|
+ <div class="p-summary-value highlight">${{pScore.toFixed(3)}}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ if (matches.length > 0) {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="p-matches-section">
|
|
|
|
|
+ <div class="p-matches-title">🎯 完全匹配帖子详情 (${{M}}个)</div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ matches.forEach((match, idx) => {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="p-match-item">
|
|
|
|
|
+ <div class="p-match-header">
|
|
|
|
|
+ <div class="p-match-title">${{idx + 1}}. ${{match.note_title || '未知标题'}}</div>
|
|
|
|
|
+ <div class="p-match-contribution">+${{match.contribution.toFixed(3)}}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="p-match-details">
|
|
|
|
|
+ <div class="p-match-detail-item">
|
|
|
|
|
+ <span class="p-match-detail-label">评估得分 (a):</span>
|
|
|
|
|
+ <span>${{match.evaluation_score.toFixed(3)}}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="p-match-detail-item">
|
|
|
|
|
+ <span class="p-match-detail-label">最高相似度 (b):</span>
|
|
|
|
|
+ <span>${{match.max_similarity.toFixed(3)}}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="p-match-detail-item">
|
|
|
|
|
+ <span class="p-match-detail-label">贡献值 (a×b):</span>
|
|
|
|
|
+ <span>${{match.contribution.toFixed(3)}}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ html += `</div>`;
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="p-matches-section">
|
|
|
|
|
+ <div style="text-align: center; padding: 30px; color: #6b7280;">
|
|
|
|
|
+ 没有完全匹配的帖子(得分 ≥ 0.8)
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ return html;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染解构内容
|
|
|
|
|
+ function renderDeconstructionContent(noteId) {{
|
|
|
|
|
+ const similarityInfo = similarityData[noteId];
|
|
|
|
|
+ if (!similarityInfo) {{
|
|
|
|
|
+ return `<div style="padding: 30px; text-align: center; color: #6b7280;">
|
|
|
|
|
+ <div style="font-size: 48px; margin-bottom: 10px;">📭</div>
|
|
|
|
|
+ <div style="font-size: 16px; margin-bottom: 8px;">暂无解构数据</div>
|
|
|
|
|
+ <div style="font-size: 14px; opacity: 0.7;">该帖子未进行特征解构分析</div>
|
|
|
|
|
+ </div>`;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ const originalFeature = similarityInfo.original_feature || '未知特征';
|
|
|
|
|
+ const features = similarityInfo.deconstructed_features || [];
|
|
|
|
|
+
|
|
|
|
|
+ if (features.length === 0) {{
|
|
|
|
|
+ return `<div style="padding: 30px; text-align: center; color: #6b7280;">
|
|
|
|
|
+ <div style="font-size: 48px; margin-bottom: 10px;">🔍</div>
|
|
|
|
|
+ <div style="font-size: 16px; margin-bottom: 8px;">未提取到解构特征</div>
|
|
|
|
|
+ <div style="font-size: 14px; opacity: 0.7;">原始特征:"${{originalFeature}}"</div>
|
|
|
|
|
+ <div style="font-size: 13px; opacity: 0.6; margin-top: 10px;">该帖子虽然评分较高,但AI未能从中提取到有效的解构特征</div>
|
|
|
|
|
+ </div>`;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 按维度分组
|
|
|
|
|
+ const dimensionGroups = {{}};
|
|
|
|
|
+ features.forEach(feat => {{
|
|
|
|
|
+ const dim = feat.dimension || '未分类';
|
|
|
|
|
+ if (!dimensionGroups[dim]) {{
|
|
|
|
|
+ dimensionGroups[dim] = [];
|
|
|
|
|
+ }}
|
|
|
|
|
+ dimensionGroups[dim].push(feat);
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 为每个维度找出最高分
|
|
|
|
|
+ Object.keys(dimensionGroups).forEach(dim => {{
|
|
|
|
|
+ const feats = dimensionGroups[dim];
|
|
|
|
|
+ if (feats.length > 0) {{
|
|
|
|
|
+ const maxScore = Math.max(...feats.map(f => f.similarity_score || 0));
|
|
|
|
|
+ feats.forEach(f => {{
|
|
|
|
|
+ f.isTopInDimension = (f.similarity_score === maxScore);
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ let html = `
|
|
|
|
|
+ <div class="deconstruction-header">
|
|
|
|
|
+ <div>🎯 解构特征相似度分析</div>
|
|
|
|
|
+ <div class="original-feature">目标特征: "${{originalFeature}}"</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ // 按维度排序: 灵感点 -> 目的点 -> 关键点
|
|
|
|
|
+ const dimensionOrder = ['灵感点-全新内容', '灵感点-共性差异', '灵感点-共性内容', '目的点', '关键点'];
|
|
|
|
|
+ const sortedDimensions = Object.keys(dimensionGroups).sort((a, b) => {{
|
|
|
|
|
+ const aIndex = dimensionOrder.findIndex(d => a.startsWith(d));
|
|
|
|
|
+ const bIndex = dimensionOrder.findIndex(d => b.startsWith(d));
|
|
|
|
|
+ if (aIndex === -1 && bIndex === -1) return a.localeCompare(b);
|
|
|
|
|
+ if (aIndex === -1) return 1;
|
|
|
|
|
+ if (bIndex === -1) return -1;
|
|
|
|
|
+ return aIndex - bIndex;
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ sortedDimensions.forEach((dimension, dimIdx) => {{
|
|
|
|
|
+ const feats = dimensionGroups[dimension];
|
|
|
|
|
+ const dimId = `dim-${{noteId}}-${{dimIdx}}`;
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="dimension-card">
|
|
|
|
|
+ <div class="dimension-header" onclick="event.stopPropagation(); toggleDimension('${{dimId}}')">
|
|
|
|
|
+ <div class="dimension-title">
|
|
|
|
|
+ <span>${{getDimensionIcon(dimension)}} ${{dimension}}</span>
|
|
|
|
|
+ <span class="dimension-count">(${{feats.length}}个特征)</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span class="dimension-toggle" id="${{dimId}}-toggle">▼</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="dimension-body expanded" id="${{dimId}}">
|
|
|
|
|
+ <div class="feature-list">
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ // 按分数降序排列
|
|
|
|
|
+ feats.sort((a, b) => (b.similarity_score || 0) - (a.similarity_score || 0));
|
|
|
|
|
+
|
|
|
|
|
+ feats.forEach(feat => {{
|
|
|
|
|
+ const score = feat.similarity_score || 0;
|
|
|
|
|
+ const scoreClass = score >= 0.7 ? 'high' : score >= 0.5 ? 'medium' : 'low';
|
|
|
|
|
+ const barWidth = Math.min(score * 100, 100);
|
|
|
|
|
+ const isTop = feat.isTopInDimension;
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ <div class="feature-item ${{isTop ? 'top-score' : ''}}">
|
|
|
|
|
+ <div class="feature-name">
|
|
|
|
|
+ ${{isTop ? '<span class="top-badge">🏆 最高分</span>' : ''}}
|
|
|
|
|
+ ${{feat.feature_name || '未命名特征'}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="feature-meta-row">
|
|
|
|
|
+ <span class="feature-dimension-detail">${{feat.dimension_detail || '无分类'}}</span>
|
|
|
|
|
+ <span class="feature-weight">权重: ${{(feat.weight || 0).toFixed(1)}}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="similarity-row">
|
|
|
|
|
+ <span class="similarity-score ${{scoreClass}}">${{score.toFixed(3)}}</span>
|
|
|
|
|
+ <div class="similarity-bar-container">
|
|
|
|
|
+ <div class="similarity-bar ${{scoreClass}}" style="width: ${{barWidth}}%"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="similarity-explanation">
|
|
|
|
|
+ ${{feat.similarity_explanation || '无说明'}}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ html += `
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ return html;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 获取维度图标
|
|
|
|
|
+ function getDimensionIcon(dimension) {{
|
|
|
|
|
+ if (dimension.includes('灵感点')) return '💡';
|
|
|
|
|
+ if (dimension.includes('目的点')) return '🎯';
|
|
|
|
|
+ if (dimension.includes('关键点')) return '🔑';
|
|
|
|
|
+ return '📋';
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 切换维度卡片
|
|
|
|
|
+ function toggleDimension(dimId) {{
|
|
|
|
|
+ const body = document.getElementById(dimId);
|
|
|
|
|
+ const toggle = document.getElementById(`${{dimId}}-toggle`);
|
|
|
|
|
+
|
|
|
|
|
+ if (body.classList.contains('expanded')) {{
|
|
|
|
|
+ body.classList.remove('expanded');
|
|
|
|
|
+ toggle.textContent = '▶';
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ body.classList.add('expanded');
|
|
|
|
|
+ toggle.textContent = '▼';
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 切换候选词库展开/收起
|
|
|
|
|
+ function toggleCandidateLibrary() {{
|
|
|
|
|
+ const content = document.getElementById('candidate-content');
|
|
|
|
|
+ const toggle = document.getElementById('candidate-toggle');
|
|
|
|
|
+
|
|
|
|
|
+ if (content.classList.contains('expanded')) {{
|
|
|
|
|
+ content.classList.remove('expanded');
|
|
|
|
|
+ toggle.classList.remove('rotated');
|
|
|
|
|
+ toggle.textContent = '▼';
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ content.classList.add('expanded');
|
|
|
|
|
+ toggle.classList.add('rotated');
|
|
|
|
|
+ toggle.textContent = '▲';
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 切换推理展开/收起
|
|
|
|
|
+ function toggleReasoning(reasoningId) {{
|
|
|
|
|
+ const reasoningDiv = document.getElementById(reasoningId);
|
|
|
|
|
+ const toggle = reasoningDiv.previousElementSibling;
|
|
|
|
|
+
|
|
|
|
|
+ if (reasoningDiv.classList.contains('expanded')) {{
|
|
|
|
|
+ reasoningDiv.classList.remove('expanded');
|
|
|
|
|
+ toggle.innerHTML = '点击查看推理 ▼';
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ reasoningDiv.classList.add('expanded');
|
|
|
|
|
+ toggle.innerHTML = '收起推理 ▲';
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 高亮候选词及其相关搜索词
|
|
|
|
|
+ let currentHighlightedCandidate = null;
|
|
|
|
|
+
|
|
|
|
|
+ function highlightCandidateWord(candidateWord) {{
|
|
|
|
|
+ // 如果点击的是已高亮的候选词,取消高亮
|
|
|
|
|
+ if (currentHighlightedCandidate === candidateWord) {{
|
|
|
|
|
+ clearCandidateHighlight();
|
|
|
|
|
+ currentHighlightedCandidate = null;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 清除之前的高亮
|
|
|
|
|
+ clearCandidateHighlight();
|
|
|
|
|
+
|
|
|
|
|
+ // 高亮候选词
|
|
|
|
|
+ const candidateItems = document.querySelectorAll('.candidate-item');
|
|
|
|
|
+ candidateItems.forEach(item => {{
|
|
|
|
|
+ if (item.getAttribute('data-candidate') === candidateWord) {{
|
|
|
|
|
+ item.classList.add('highlighted');
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 高亮包含该候选词的搜索词
|
|
|
|
|
+ const searchWordItems = document.querySelectorAll('.search-word-item');
|
|
|
|
|
+ searchWordItems.forEach(item => {{
|
|
|
|
|
+ const sourceWords = item.getAttribute('data-source-words') || '';
|
|
|
|
|
+ // 检查source_words中是否包含该候选词
|
|
|
|
|
+ if (sourceWords.includes(candidateWord)) {{
|
|
|
|
|
+ item.classList.add('highlighted');
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ currentHighlightedCandidate = candidateWord;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 清除候选词高亮
|
|
|
|
|
+ function clearCandidateHighlight() {{
|
|
|
|
|
+ // 清除候选词高亮
|
|
|
|
|
+ const candidateItems = document.querySelectorAll('.candidate-item.highlighted');
|
|
|
|
|
+ candidateItems.forEach(item => {{
|
|
|
|
|
+ item.classList.remove('highlighted');
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 清除搜索词高亮
|
|
|
|
|
+ const searchWordItems = document.querySelectorAll('.search-word-item.highlighted');
|
|
|
|
|
+ searchWordItems.forEach(item => {{
|
|
|
|
|
+ item.classList.remove('highlighted');
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 图片轮播逻辑
|
|
|
|
|
+ const carouselStates = {{}};
|
|
|
|
|
+
|
|
|
|
|
+ function changeImage(carouselId, direction) {{
|
|
|
|
|
+ if (!carouselStates[carouselId]) {{
|
|
|
|
|
+ carouselStates[carouselId] = {{ currentIndex: 0 }};
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ const carousel = document.getElementById(carouselId);
|
|
|
|
|
+ const imagesContainer = carousel.querySelector('.carousel-images');
|
|
|
|
|
+ const images = carousel.querySelectorAll('.carousel-image');
|
|
|
|
|
+ const dots = carousel.querySelectorAll('.dot');
|
|
|
|
|
+ const counter = carousel.querySelector('.image-counter');
|
|
|
|
|
+
|
|
|
|
|
+ let newIndex = carouselStates[carouselId].currentIndex + direction;
|
|
|
|
|
+ if (newIndex < 0) newIndex = images.length - 1;
|
|
|
|
|
+ if (newIndex >= images.length) newIndex = 0;
|
|
|
|
|
+
|
|
|
|
|
+ carouselStates[carouselId].currentIndex = newIndex;
|
|
|
|
|
+ imagesContainer.style.transform = `translateX(-${{newIndex * 100}}%)`;
|
|
|
|
|
+
|
|
|
|
|
+ dots.forEach((dot, i) => {{
|
|
|
|
|
+ dot.classList.toggle('active', i === newIndex);
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ if (counter) {{
|
|
|
|
|
+ counter.textContent = `${{newIndex + 1}}/${{images.length}}`;
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ function goToImage(carouselId, index) {{
|
|
|
|
|
+ if (!carouselStates[carouselId]) {{
|
|
|
|
|
+ carouselStates[carouselId] = {{ currentIndex: 0 }};
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ const carousel = document.getElementById(carouselId);
|
|
|
|
|
+ const imagesContainer = carousel.querySelector('.carousel-images');
|
|
|
|
|
+ const dots = carousel.querySelectorAll('.dot');
|
|
|
|
|
+ const counter = carousel.querySelector('.image-counter');
|
|
|
|
|
+
|
|
|
|
|
+ carouselStates[carouselId].currentIndex = index;
|
|
|
|
|
+ imagesContainer.style.transform = `translateX(-${{index * 100}}%)`;
|
|
|
|
|
+
|
|
|
|
|
+ dots.forEach((dot, i) => {{
|
|
|
|
|
+ dot.classList.toggle('active', i === index);
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ if (counter) {{
|
|
|
|
|
+ counter.textContent = `${{index + 1}}/${{dots.length}}`;
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ function toggleFeature(featureIdx) {{
|
|
|
|
|
+ const searchWordsList = document.getElementById(`search-words-${{featureIdx}}`);
|
|
|
|
|
+ const featureHeader = document.getElementById(`feature-header-${{featureIdx}}`);
|
|
|
|
|
+
|
|
|
|
|
+ searchWordsList.classList.toggle('expanded');
|
|
|
|
|
+ featureHeader.classList.toggle('active');
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ function toggleBaseWord(featureIdx, groupIdx) {{
|
|
|
|
|
+ const baseWordHeader = document.getElementById(`base-word-header-${{featureIdx}}-${{groupIdx}}`);
|
|
|
|
|
+ const baseWordDesc = document.getElementById(`base-word-desc-${{featureIdx}}-${{groupIdx}}`);
|
|
|
|
|
+ const searchWordsSublist = document.getElementById(`search-words-sublist-${{featureIdx}}-${{groupIdx}}`);
|
|
|
|
|
+
|
|
|
|
|
+ baseWordHeader.classList.toggle('active');
|
|
|
|
|
+ baseWordDesc.classList.toggle('expanded');
|
|
|
|
|
+ searchWordsSublist.classList.toggle('expanded');
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ function scrollToBlock(blockId) {{
|
|
|
|
|
+ const block = document.getElementById(blockId);
|
|
|
|
|
+ if (block) {{
|
|
|
|
|
+ block.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
|
|
|
|
|
+
|
|
|
|
|
+ document.querySelectorAll('.search-word-item').forEach(item => {{
|
|
|
|
|
+ item.classList.remove('active');
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ document.querySelectorAll(`[data-block-id="${{blockId}}"]`).forEach(item => {{
|
|
|
|
|
+ item.classList.add('active');
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ function toggleEvalDetails(carouselId) {{
|
|
|
|
|
+ const details = document.getElementById(`${{carouselId}}-details`);
|
|
|
|
|
+ const toggle = document.getElementById(`${{carouselId}}-toggle`);
|
|
|
|
|
+
|
|
|
|
|
+ if (details && toggle) {{
|
|
|
|
|
+ details.classList.toggle('expanded');
|
|
|
|
|
+ toggle.textContent = details.classList.contains('expanded') ? '▲ 收起' : '▼ 详情';
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ function filterNotes(category) {{
|
|
|
|
|
+ currentFilter = category;
|
|
|
|
|
+
|
|
|
|
|
+ document.querySelectorAll('.filter-btn').forEach(btn => {{
|
|
|
|
|
+ btn.classList.remove('active');
|
|
|
|
|
+ }});
|
|
|
|
|
+ event.target.classList.add('active');
|
|
|
|
|
+
|
|
|
|
|
+ document.querySelectorAll('.note-card').forEach(card => {{
|
|
|
|
|
+ const evalCategory = card.getAttribute('data-eval-category');
|
|
|
|
|
+ if (category === 'all' || evalCategory === category) {{
|
|
|
|
|
+ card.classList.remove('hidden');
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ card.classList.add('hidden');
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ document.querySelectorAll('.result-block').forEach(block => {{
|
|
|
|
|
+ const visibleCards = block.querySelectorAll('.note-card:not(.hidden)');
|
|
|
|
|
+ if (visibleCards.length === 0) {{
|
|
|
|
|
+ block.classList.add('hidden');
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ block.classList.remove('hidden');
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ function openNote(noteId) {{
|
|
|
|
|
+ if (noteId) {{
|
|
|
|
|
+ window.open(`https://www.xiaohongshu.com/explore/${{noteId}}`, '_blank');
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 页面加载时输出调试信息
|
|
|
|
|
+ console.log('='.repeat(60));
|
|
|
|
|
+ console.log('🚀 [系统] 页面脚本加载完成');
|
|
|
|
|
+ console.log('📊 [数据] 评估特征数:', data.length);
|
|
|
|
|
+ console.log('📊 [数据] 解构结果数:', Object.keys(deconstructionData).length);
|
|
|
|
|
+ console.log('📊 [数据] 相似度分析数:', Object.keys(similarityData).length);
|
|
|
|
|
+ console.log('📋 [数据] 相似度分析可用noteId:', Object.keys(similarityData));
|
|
|
|
|
+ console.log('='.repeat(60));
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', () => {{
|
|
|
|
|
+ console.log('✅ [系统] DOM加载完成,开始初始化...');
|
|
|
|
|
+
|
|
|
|
|
+ try {{
|
|
|
|
|
+ renderLeftSidebar();
|
|
|
|
|
+ console.log('✅ [系统] 左侧导航(级联模式)渲染完成');
|
|
|
|
|
+
|
|
|
|
|
+ // 级联模式:自动选择第一个特征
|
|
|
|
|
+ if (data.length > 0) {{
|
|
|
|
|
+ selectFeature(0);
|
|
|
|
|
+ console.log('✅ [系统] 自动选择第一个特征');
|
|
|
|
|
+
|
|
|
|
|
+ const firstGroups = data[0]['组合评估结果_分组'];
|
|
|
|
|
+ if (firstGroups && firstGroups.length > 0) {{
|
|
|
|
|
+ selectBaseWord(0, 0);
|
|
|
|
|
+ console.log('✅ [系统] 自动选择第一个base_word');
|
|
|
|
|
+
|
|
|
|
|
+ const firstSearches = firstGroups[0]['top10_searches'];
|
|
|
|
|
+ if (firstSearches && firstSearches.length > 0) {{
|
|
|
|
|
+ selectSearchWord(0, 0, 0);
|
|
|
|
|
+ console.log('✅ [系统] 自动选择第一个搜索词');
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ console.log('✅ [系统] 页面初始化完成');
|
|
|
|
|
+
|
|
|
|
|
+ // 为所有解构按钮添加事件监听器
|
|
|
|
|
+ setTimeout(() => {{
|
|
|
|
|
+ const buttons = document.querySelectorAll('.deconstruction-toggle-btn');
|
|
|
|
|
+ console.log('🔍 [系统] 找到解构按钮数量:', buttons.length);
|
|
|
|
|
+
|
|
|
|
|
+ buttons.forEach((btn, index) => {{
|
|
|
|
|
+ const noteId = btn.getAttribute('data-note-id');
|
|
|
|
|
+ const noteTitle = btn.getAttribute('data-note-title');
|
|
|
|
|
+ console.log(` 按钮[${{index}}] noteId:`, noteId, ', title:', noteTitle);
|
|
|
|
|
+
|
|
|
|
|
+ // 添加事件监听器打开模态窗口
|
|
|
|
|
+ btn.addEventListener('click', function(e) {{
|
|
|
|
|
+ console.log('🖱️ [事件] 按钮点击, noteId:', noteId);
|
|
|
|
|
+ e.stopPropagation();
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ openDeconstructionModal(noteId, noteTitle);
|
|
|
|
|
+ }});
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}, 500);
|
|
|
|
|
+
|
|
|
|
|
+ }} catch (error) {{
|
|
|
|
|
+ console.error('❌ [错误] 初始化失败:', error);
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 新增功能1: 统计面板折叠/展开 ==========
|
|
|
|
|
+ const statsPanel = document.querySelector('.stats-panel');
|
|
|
|
|
+ if (statsPanel) {{
|
|
|
|
|
+ statsPanel.addEventListener('click', function() {{
|
|
|
|
|
+ this.classList.toggle('collapsed');
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 新增功能2: 帖子图片浮层模态窗口 ==========
|
|
|
|
|
+ function openNoteImagesModal(featureIdx, groupIdx, searchIdx, noteIdx) {{
|
|
|
|
|
+ const feature = data[featureIdx];
|
|
|
|
|
+ const group = feature['组合评估结果_分组'][groupIdx];
|
|
|
|
|
+ const search = group['top10_searches'][searchIdx];
|
|
|
|
|
+ const searchResult = search.search_result || {{}};
|
|
|
|
|
+ const notes = searchResult.data?.data || [];
|
|
|
|
|
+ const note = notes[noteIdx];
|
|
|
|
|
+
|
|
|
|
|
+ if (!note) {{
|
|
|
|
|
+ console.log('❌ [浮层] 找不到帖子数据');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ const card = note.note_card || {{}};
|
|
|
|
|
+ const images = card.image_list || [];
|
|
|
|
|
+ const title = card.display_title || '无标题';
|
|
|
|
|
+ const noteId = note.id || '';
|
|
|
|
|
+
|
|
|
|
|
+ console.log('🔍 [帖子图片浮层] 帖子ID:', noteId);
|
|
|
|
|
+ console.log('🔍 [帖子图片浮层] 标题:', title);
|
|
|
|
|
+ console.log('🔍 [帖子图片浮层] 图片数量:', images.length);
|
|
|
|
|
+
|
|
|
|
|
+ if (images.length === 0) {{
|
|
|
|
|
+ console.log('❌ [浮层] 该帖子没有图片');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ const modal = document.getElementById('notesModal');
|
|
|
|
|
+ const modalContent = document.getElementById('notesModalContent');
|
|
|
|
|
+
|
|
|
|
|
+ // 更新模态窗口标题
|
|
|
|
|
+ const modalTitle = document.querySelector('.notes-modal-title');
|
|
|
|
|
+ if (modalTitle) {{
|
|
|
|
|
+ modalTitle.innerHTML = `
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div style="font-size: 18px; font-weight: 600;">📷 ${{title}}</div>
|
|
|
|
|
+ <div style="font-size: 14px; opacity: 0.9; margin-top: 5px;">共 ${{images.length}} 张图片</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 构建图片网格HTML
|
|
|
|
|
+ let imagesHtml = '<div class="notes-grid-modal">';
|
|
|
|
|
+
|
|
|
|
|
+ images.forEach((imageUrl, idx) => {{
|
|
|
|
|
+ imagesHtml += `
|
|
|
|
|
+ <div class="note-card-modal" onclick="window.open('https://www.xiaohongshu.com/explore/${{noteId}}', '_blank'); event.stopPropagation();">
|
|
|
|
|
+ <div class="note-image-wrapper" style="height: 300px;">
|
|
|
|
|
+ <img src="${{imageUrl}}" alt="图片 ${{idx + 1}}" class="note-image" style="object-fit: contain; background: #000;"/>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="note-content">
|
|
|
|
|
+ <div style="text-align: center; color: #6b7280; font-size: 13px;">
|
|
|
|
|
+ 第 ${{idx + 1}} / ${{images.length}} 张
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ imagesHtml += '</div>';
|
|
|
|
|
+
|
|
|
|
|
+ modalContent.innerHTML = imagesHtml;
|
|
|
|
|
+ modal.classList.add('active');
|
|
|
|
|
+
|
|
|
|
|
+ // 点击背景关闭
|
|
|
|
|
+ modal.onclick = function(e) {{
|
|
|
|
|
+ if (e.target === modal) {{
|
|
|
|
|
+ closeNotesModal();
|
|
|
|
|
+ }}
|
|
|
|
|
+ }};
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ function closeNotesModal() {{
|
|
|
|
|
+ const modal = document.getElementById('notesModal');
|
|
|
|
|
+ modal.classList.remove('active');
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 新增功能3: 右侧滚动与左侧联动 ==========
|
|
|
|
|
+ // 使用Intersection Observer监听右侧内容块的可见性
|
|
|
|
|
+ const observerOptions = {{
|
|
|
|
|
+ root: null,
|
|
|
|
|
+ rootMargin: '-20% 0px -70% 0px', // 当元素在视口上20%位置时触发
|
|
|
|
|
+ threshold: 0
|
|
|
|
|
+ }};
|
|
|
|
|
+
|
|
|
|
|
+ const observer = new IntersectionObserver((entries) => {{
|
|
|
|
|
+ entries.forEach(entry => {{
|
|
|
|
|
+ if (entry.isIntersecting) {{
|
|
|
|
|
+ const blockId = entry.target.id;
|
|
|
|
|
+ const leftItem = document.getElementById('search-' + blockId.replace('block-', ''));
|
|
|
|
|
+
|
|
|
|
|
+ if (leftItem) {{
|
|
|
|
|
+ // 移除所有active状态
|
|
|
|
|
+ document.querySelectorAll('.search-word-item').forEach(item => {{
|
|
|
|
|
+ item.classList.remove('active');
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 添加active到当前项
|
|
|
|
|
+ leftItem.classList.add('active');
|
|
|
|
|
+
|
|
|
|
|
+ // 滚动到可见区域
|
|
|
|
|
+ leftItem.scrollIntoView({{
|
|
|
|
|
+ behavior: 'smooth',
|
|
|
|
|
+ block: 'nearest'
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}, observerOptions);
|
|
|
|
|
+
|
|
|
|
|
+ // 观察所有result-block
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', () => {{
|
|
|
|
|
+ setTimeout(() => {{
|
|
|
|
|
+ const blocks = document.querySelectorAll('.result-block');
|
|
|
|
|
+ blocks.forEach(block => {{
|
|
|
|
|
+ observer.observe(block);
|
|
|
|
|
+ }});
|
|
|
|
|
+ console.log('✅ [系统] 已设置', blocks.length, '个内容块的滚动监听');
|
|
|
|
|
+ }}, 1000);
|
|
|
|
|
+ }});
|
|
|
|
|
+ </script>
|
|
|
|
|
+</body>
|
|
|
|
|
+</html>
|
|
|
|
|
+'''
|
|
|
|
|
+
|
|
|
|
|
+ with open(output_path, 'w', encoding='utf-8') as f:
|
|
|
|
|
+ f.write(html_content)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def main():
|
|
|
|
|
+ """主函数"""
|
|
|
|
|
+ script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
+ # 修复路径:从src/visualizers/回到项目根目录
|
|
|
|
|
+ project_root = os.path.dirname(os.path.dirname(script_dir))
|
|
|
|
|
+
|
|
|
|
|
+ # 加载数据(使用项目根目录)
|
|
|
|
|
+ evaluation_path = os.path.join(project_root, 'output_v2', 'evaluated_results.json')
|
|
|
|
|
+ deconstruction_path = os.path.join(project_root, 'output_v2', 'deep_analysis_results.json')
|
|
|
|
|
+ similarity_path = os.path.join(project_root, 'output_v2', 'similarity_analysis_results.json')
|
|
|
|
|
+ persona_library_path = os.path.join(project_root, 'optimized_clustered_data_gemini-3-pro-preview.json')
|
|
|
|
|
+ how_json_path = os.path.join(project_root, 'input/posts/690d977d0000000007036331_how.json')
|
|
|
|
|
+
|
|
|
|
|
+ output_dir = os.path.join(project_root, 'visualization')
|
|
|
|
|
+ os.makedirs(output_dir, exist_ok=True)
|
|
|
|
|
+
|
|
|
|
|
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
|
|
|
+ output_path = os.path.join(output_dir, f'integrated_results_{timestamp}.html')
|
|
|
|
|
+
|
|
|
|
|
+ print(f"📖 加载评估数据: {evaluation_path}")
|
|
|
|
|
+ data = load_data(evaluation_path)
|
|
|
|
|
+ print(f"✓ 加载了 {len(data)} 个原始特征")
|
|
|
|
|
+
|
|
|
|
|
+ print(f"📖 加载解构数据: {deconstruction_path}")
|
|
|
|
|
+ deconstruction_mapping = load_deconstruction_data(deconstruction_path)
|
|
|
|
|
+ print(f"✓ 加载了 {len(deconstruction_mapping)} 个解构结果")
|
|
|
|
|
+
|
|
|
|
|
+ print(f"📖 加载Stage8数据: {similarity_path}")
|
|
|
|
|
+ similarity_mapping = load_similarity_data(similarity_path)
|
|
|
|
|
+ print(f"✓ 加载了 {len(similarity_mapping)} 个相似度评分")
|
|
|
|
|
+
|
|
|
|
|
+ print("📊 计算统计数据...")
|
|
|
|
|
+ stats = calculate_statistics(data)
|
|
|
|
|
+ print(f"✓ 统计完成:")
|
|
|
|
|
+ print(f" - 原始特征: {stats['total_features']}")
|
|
|
|
|
+ print(f" - 搜索词总数: {stats['total_search_words']}")
|
|
|
|
|
+ print(f" - 帖子总数: {stats['total_notes']}")
|
|
|
|
|
+ print(f" - 完全匹配: {stats['match_complete']} ({stats['complete_rate']}%)")
|
|
|
|
|
+
|
|
|
|
|
+ # 关系图已移除
|
|
|
|
|
+ relationship_graph_html = ""
|
|
|
|
|
+
|
|
|
|
|
+ # 提取所有特征信息(包括低相似度和高相似度特征)
|
|
|
|
|
+ print(f"\n📊 提取所有特征信息...")
|
|
|
|
|
+ all_features = {}
|
|
|
|
|
+ high_similarity_features = []
|
|
|
|
|
+ low_similarity_features = []
|
|
|
|
|
+ try:
|
|
|
|
|
+ all_features = extract_all_features_from_how(how_json_path)
|
|
|
|
|
+
|
|
|
|
|
+ # 标记评估结果中已搜索的特征
|
|
|
|
|
+ evaluated_feature_names = set([item.get('原始特征名称') for item in data])
|
|
|
|
|
+ for feature_name in evaluated_feature_names:
|
|
|
|
|
+ if feature_name in all_features and all_features[feature_name]['category'] == '待搜索':
|
|
|
|
|
+ all_features[feature_name]['category'] = '已搜索'
|
|
|
|
|
+
|
|
|
|
|
+ # 提取高相似度特征(≥0.8)
|
|
|
|
|
+ high_similarity_features = [
|
|
|
|
|
+ {'name': name, **info}
|
|
|
|
|
+ for name, info in all_features.items()
|
|
|
|
|
+ if info['category'] == '高相似度'
|
|
|
|
|
+ ]
|
|
|
|
|
+ # 按相似度降序排序
|
|
|
|
|
+ high_similarity_features.sort(key=lambda x: x['similarity'], reverse=True)
|
|
|
|
|
+
|
|
|
|
|
+ # 提取低相似度特征(<0.5)
|
|
|
|
|
+ low_similarity_features = [
|
|
|
|
|
+ {'name': name, **info}
|
|
|
|
|
+ for name, info in all_features.items()
|
|
|
|
|
+ if info['category'] == '低相似度'
|
|
|
|
|
+ ]
|
|
|
|
|
+ # 按相似度降序排序
|
|
|
|
|
+ low_similarity_features.sort(key=lambda x: x['similarity'], reverse=True)
|
|
|
|
|
+
|
|
|
|
|
+ print(f"✓ 提取了 {len(all_features)} 个特征")
|
|
|
|
|
+ print(f" - 高相似度特征(≥0.8): {len(high_similarity_features)} 个")
|
|
|
|
|
+ print(f" - 部分匹配特征(0.5-0.8): {len([f for f in all_features.values() if f['category'] == '已搜索'])} 个")
|
|
|
|
|
+ print(f" - 低相似度特征(<0.5): {len(low_similarity_features)} 个")
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"⚠️ 警告: 提取特征信息时出错 - {e}")
|
|
|
|
|
+ high_similarity_features = []
|
|
|
|
|
+ low_similarity_features = []
|
|
|
|
|
+
|
|
|
|
|
+ print(f"\n🎨 生成可视化页面...")
|
|
|
|
|
+ generate_html(data, stats, deconstruction_mapping, similarity_mapping, relationship_graph_html, all_features, high_similarity_features, low_similarity_features, output_path)
|
|
|
|
|
+ print(f"✓ 生成完成: {output_path}")
|
|
|
|
|
+
|
|
|
|
|
+ print(f"\n🌐 在浏览器中打开查看:")
|
|
|
|
|
+ print(f" file://{output_path}")
|
|
|
|
|
+
|
|
|
|
|
+ return output_path
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == '__main__':
|
|
|
|
|
+ main()
|