|
|
@@ -57,7 +57,7 @@ def load_inspiration_points_data(inspiration_dir: str) -> List[Dict[str, Any]]:
|
|
|
except Exception as e:
|
|
|
print(f"警告: 读取 {step2_files[0]} 失败: {e}")
|
|
|
|
|
|
- # 加载搜索结果
|
|
|
+ # 加载搜索结果和匹配分数
|
|
|
search_results = {}
|
|
|
search_dir = subdir / "search"
|
|
|
if search_dir.exists() and search_dir.is_dir():
|
|
|
@@ -69,11 +69,27 @@ def load_inspiration_points_data(inspiration_dir: str) -> List[Dict[str, Any]]:
|
|
|
# 从JSON内容中读取真实的keyword,而不是从文件名提取
|
|
|
keyword = search_data.get("search_params", {}).get("keyword", "")
|
|
|
if keyword:
|
|
|
- search_results[keyword] = search_data
|
|
|
+ # 尝试加载对应的匹配结果文件
|
|
|
+ match_file = search_dir / "all_step4_搜索结果匹配_gemini-2.5-pro.json"
|
|
|
+ match_data = None
|
|
|
+ if match_file.exists():
|
|
|
+ try:
|
|
|
+ with open(match_file, 'r', encoding='utf-8') as mf:
|
|
|
+ match_data = json.load(mf)
|
|
|
+ except Exception as e:
|
|
|
+ print(f"警告: 读取匹配文件 {match_file} 失败: {e}")
|
|
|
+
|
|
|
+ search_results[keyword] = {
|
|
|
+ "search_data": search_data,
|
|
|
+ "match_data": match_data
|
|
|
+ }
|
|
|
else:
|
|
|
# 如果JSON中没有keyword,则从文件名提取
|
|
|
keyword = search_file.stem.replace("all_search_", "")
|
|
|
- search_results[keyword] = search_data
|
|
|
+ search_results[keyword] = {
|
|
|
+ "search_data": search_data,
|
|
|
+ "match_data": None
|
|
|
+ }
|
|
|
except Exception as e:
|
|
|
print(f"警告: 读取 {search_file} 失败: {e}")
|
|
|
|
|
|
@@ -139,6 +155,7 @@ def generate_post_card_html(post: Dict[str, Any], note_id_prefix: str = "info-po
|
|
|
link = post.get("link", "")
|
|
|
author = post.get("channel_account_name", "")
|
|
|
post_id = post.get("channel_content_id", "")
|
|
|
+ publish_time = post.get("publish_time", "")
|
|
|
|
|
|
# 生成唯一的note_id
|
|
|
note_id = f"{note_id_prefix}-{random.randint(10000, 99999)}"
|
|
|
@@ -236,12 +253,18 @@ def generate_post_card_html(post: Dict[str, Any], note_id_prefix: str = "info-po
|
|
|
if points_sections:
|
|
|
points_html = f'<div class="note-points">{"".join(points_sections)}</div>'
|
|
|
|
|
|
+ # 生成发布日期HTML
|
|
|
+ publish_date_html = ""
|
|
|
+ if publish_time:
|
|
|
+ publish_date_html = f'<div class="note-publish-date">📅 {html_module.escape(publish_time)}</div>'
|
|
|
+
|
|
|
card_html = f'''
|
|
|
<div class="search-note-item" data-note-data='{note_data_json_escaped}' onclick="showNoteDetail(this)">
|
|
|
{images_html}
|
|
|
<div class="note-content">
|
|
|
<div class="note-title">{html_module.escape(title) if title else "无标题"}</div>
|
|
|
<div class="note-desc">{html_module.escape(desc) if desc else "暂无描述"}</div>
|
|
|
+ {publish_date_html}
|
|
|
<div class="note-footer">
|
|
|
<div class="note-author">@{html_module.escape(author) if author else "匿名"}</div>
|
|
|
<div class="note-stats">
|
|
|
@@ -312,8 +335,8 @@ def generate_inspiration_card_html(
|
|
|
match_result = top_match.get("匹配结果", {})
|
|
|
element_name = input_info.get("A", "")
|
|
|
match_score = match_result.get("score", 0)
|
|
|
- same_parts = match_result.get("相同部分", {})
|
|
|
- increment_parts = match_result.get("增量部分", {})
|
|
|
+ same_parts = match_result.get("相同部分", {}) or {}
|
|
|
+ increment_parts = match_result.get("增量部分", {}) or {}
|
|
|
|
|
|
# 生成相同部分和增量部分的HTML
|
|
|
parts_html = ""
|
|
|
@@ -389,9 +412,13 @@ def generate_inspiration_card_html(
|
|
|
element_name = input_info.get("A", "")
|
|
|
context = input_info.get("A_Context", "")
|
|
|
score = match_result.get("score", 0)
|
|
|
- score_explain = match_result.get("score说明", "")
|
|
|
- same_parts = match_result.get("相同部分", {})
|
|
|
- increment_parts = match_result.get("增量部分", {})
|
|
|
+ score_explain = match_result.get("score说明", "") or ""
|
|
|
+ same_parts = match_result.get("相同部分", {}) or {}
|
|
|
+ increment_parts = match_result.get("增量部分", {}) or {}
|
|
|
+
|
|
|
+ # 为搜索结果容器生成唯一ID
|
|
|
+ safe_insp_name = ''.join(c if c.isalnum() else '_' for c in inspiration_name)
|
|
|
+ unique_match_id = f"{safe_insp_name}-match-{idx}"
|
|
|
|
|
|
# 解析层级
|
|
|
hierarchy = []
|
|
|
@@ -434,11 +461,49 @@ def generate_inspiration_card_html(
|
|
|
# 生成搜索结果HTML(网格展示,图片轮播)
|
|
|
search_html = ""
|
|
|
if element_name in search_results:
|
|
|
- search_data = search_results[element_name]
|
|
|
+ result_obj = search_results[element_name]
|
|
|
+ search_data = result_obj.get("search_data", {})
|
|
|
+ match_data = result_obj.get("match_data", None)
|
|
|
+
|
|
|
search_params = search_data.get("search_params", {})
|
|
|
notes = search_data.get("notes", [])
|
|
|
notes_count = len(notes)
|
|
|
|
|
|
+ # 构建匹配分数字典 {channel_content_id: match_info}
|
|
|
+ match_scores = {}
|
|
|
+ if match_data and "匹配结果列表" in match_data:
|
|
|
+ for match_item in match_data["匹配结果列表"]:
|
|
|
+ business_info = match_item.get("业务信息", {})
|
|
|
+ input_info = match_item.get("输入信息", {})
|
|
|
+ content_id = business_info.get("channel_content_id", "")
|
|
|
+ if content_id:
|
|
|
+ match_scores[content_id] = {
|
|
|
+ "score": match_item.get("匹配结果", {}).get("score", 0),
|
|
|
+ "score说明": match_item.get("匹配结果", {}).get("score说明", "") or "",
|
|
|
+ "相同部分": match_item.get("匹配结果", {}).get("相同部分", {}) or {},
|
|
|
+ "增量部分": match_item.get("匹配结果", {}).get("增量部分", {}) or {},
|
|
|
+ "输入B": input_info.get("B", "") or "",
|
|
|
+ "输入A": input_info.get("A", "") or "",
|
|
|
+ "B_Context": input_info.get("B_Context", "") or "",
|
|
|
+ "A_Context": input_info.get("A_Context", "") or ""
|
|
|
+ }
|
|
|
+
|
|
|
+ # 为notes添加匹配分数、原始索引,并准备排序
|
|
|
+ notes_with_scores = []
|
|
|
+ for original_idx, note in enumerate(notes):
|
|
|
+ note_id = note.get("channel_content_id", "")
|
|
|
+ score_info = match_scores.get(note_id, None)
|
|
|
+ notes_with_scores.append({
|
|
|
+ "note": note,
|
|
|
+ "score_info": score_info,
|
|
|
+ "original_index": original_idx, # 原始搜索结果位置(0-based)
|
|
|
+ "page": (original_idx // 20) + 1, # 第几页(假设每页20条)
|
|
|
+ "position_in_page": (original_idx % 20) + 1 # 页内位置
|
|
|
+ })
|
|
|
+
|
|
|
+ # 默认按分数降序排序(没有分数的放到最后)
|
|
|
+ notes_with_scores.sort(key=lambda x: x["score_info"]["score"] if x["score_info"] else -1, reverse=True)
|
|
|
+
|
|
|
# 生成搜索参数HTML
|
|
|
search_params_html = ""
|
|
|
if search_params:
|
|
|
@@ -471,19 +536,35 @@ def generate_inspiration_card_html(
|
|
|
</div>
|
|
|
'''
|
|
|
|
|
|
- # 生成搜索结果统计HTML
|
|
|
+ # 生成搜索结果统计HTML(带排序按钮)
|
|
|
search_summary_html = f'''
|
|
|
<div class="search-summary-section">
|
|
|
- <div class="search-summary-title">📊 搜索结果</div>
|
|
|
- <div class="search-summary-content">
|
|
|
- 共找到 <span class="search-result-count">{notes_count}</span> 条相关内容
|
|
|
+ <div class="search-summary-header">
|
|
|
+ <div class="search-summary-left">
|
|
|
+ <div class="search-summary-title">📊 搜索结果</div>
|
|
|
+ <div class="search-summary-content">
|
|
|
+ 共找到 <span class="search-result-count">{notes_count}</span> 条相关内容
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="search-sort-buttons">
|
|
|
+ <span class="search-sort-label">排序:</span>
|
|
|
+ <button class="search-sort-btn active" data-sort="score" onclick="sortSearchResults(this, '{unique_match_id}-search-results')">匹配分数</button>
|
|
|
+ <button class="search-sort-btn" data-sort="original" onclick="sortSearchResults(this, '{unique_match_id}-search-results')">原始顺序</button>
|
|
|
+ <button class="search-sort-btn" data-sort="likes" onclick="sortSearchResults(this, '{unique_match_id}-search-results')">点赞数</button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
'''
|
|
|
|
|
|
if notes_count > 0:
|
|
|
notes_items = ""
|
|
|
- for note_idx, note in enumerate(notes): # 显示所有结果
|
|
|
+ for note_idx, item in enumerate(notes_with_scores): # 使用包含元数据的列表
|
|
|
+ note = item["note"]
|
|
|
+ score_info = item["score_info"]
|
|
|
+ original_index = item["original_index"]
|
|
|
+ page = item["page"]
|
|
|
+ position_in_page = item["position_in_page"]
|
|
|
+
|
|
|
title = note.get("title", "")
|
|
|
desc = note.get("desc", "")
|
|
|
link = note.get("link", "")
|
|
|
@@ -491,6 +572,8 @@ def generate_inspiration_card_html(
|
|
|
like_count = note.get("like_count", 0)
|
|
|
comment_count = note.get("comment_count", 0)
|
|
|
images = note.get("images", [])
|
|
|
+ content_id = note.get("channel_content_id", "")
|
|
|
+ publish_time = note.get("publish_time", "")
|
|
|
|
|
|
note_id = f"note-{idx}-{note_idx}"
|
|
|
|
|
|
@@ -544,14 +627,64 @@ def generate_inspiration_card_html(
|
|
|
note_data_json = json.dumps(note_data, ensure_ascii=False)
|
|
|
note_data_escaped = html_module.escape(note_data_json)
|
|
|
|
|
|
+ # 生成匹配分数HTML
|
|
|
+ score_badge_html = ""
|
|
|
+ score_detail_html = ""
|
|
|
+ if score_info:
|
|
|
+ note_score = score_info["score"]
|
|
|
+ note_score_explain = score_info.get("score说明", "") or ""
|
|
|
+ note_same_parts = score_info.get("相同部分", {}) or {}
|
|
|
+ note_increment_parts = score_info.get("增量部分", {}) or {}
|
|
|
+ input_b = score_info.get("输入B", "") or ""
|
|
|
+ input_a = score_info.get("输入A", "") or ""
|
|
|
+ b_context = score_info.get("B_Context", "") or ""
|
|
|
+ a_context = score_info.get("A_Context", "") or ""
|
|
|
+
|
|
|
+ # 分数详情JSON
|
|
|
+ score_detail_data = {
|
|
|
+ "score": note_score,
|
|
|
+ "score说明": note_score_explain,
|
|
|
+ "相同部分": note_same_parts,
|
|
|
+ "增量部分": note_increment_parts,
|
|
|
+ "输入B": input_b,
|
|
|
+ "输入A": input_a,
|
|
|
+ "B_Context": b_context,
|
|
|
+ "A_Context": a_context
|
|
|
+ }
|
|
|
+ score_detail_json = json.dumps(score_detail_data, ensure_ascii=False)
|
|
|
+ score_detail_escaped = html_module.escape(score_detail_json)
|
|
|
+
|
|
|
+ score_badge_html = f'''
|
|
|
+ <div class="note-score-badge" onclick="event.stopPropagation(); showScoreDetail(this)" data-score-detail='{score_detail_escaped}'>
|
|
|
+ <span class="score-label">匹配分数</span>
|
|
|
+ <span class="score-value">{note_score:.2f}</span>
|
|
|
+ </div>
|
|
|
+ '''
|
|
|
+
|
|
|
+ # 生成发布日期HTML
|
|
|
+ publish_date_html = ""
|
|
|
+ if publish_time:
|
|
|
+ publish_date_html = f'<div class="note-publish-date">📅 {html_module.escape(publish_time)}</div>'
|
|
|
+
|
|
|
+ # 计算匹配分数(用于排序)
|
|
|
+ sort_score = score_info["score"] if score_info else -1
|
|
|
+
|
|
|
notes_items += f'''
|
|
|
- <div class="search-note-item" data-note-data='{note_data_escaped}' onclick="showNoteDetail(this)">
|
|
|
+ <div class="search-note-item"
|
|
|
+ data-note-data='{note_data_escaped}'
|
|
|
+ data-original-index="{original_index}"
|
|
|
+ data-score="{sort_score}"
|
|
|
+ data-likes="{like_count}"
|
|
|
+ onclick="showNoteDetail(this)">
|
|
|
+ {score_badge_html}
|
|
|
{images_html}
|
|
|
<div class="note-content">
|
|
|
<div class="note-title">{html_module.escape(title) if title else "无标题"}</div>
|
|
|
<div class="note-desc">{html_module.escape(desc) if desc else "暂无描述"}</div>
|
|
|
+ {publish_date_html}
|
|
|
<div class="note-footer">
|
|
|
<div class="note-author">@{html_module.escape(author)}</div>
|
|
|
+ <div class="note-position">P{page}-{position_in_page}</div>
|
|
|
<div class="note-stats">
|
|
|
<span>👍 {like_count}</span>
|
|
|
<span>💬 {comment_count}</span>
|
|
|
@@ -565,7 +698,7 @@ def generate_inspiration_card_html(
|
|
|
<div class="search-results-section">
|
|
|
{search_params_html}
|
|
|
{search_summary_html}
|
|
|
- <div class="search-notes-list">
|
|
|
+ <div class="search-notes-list" id="{unique_match_id}-search-results">
|
|
|
{notes_items}
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -603,7 +736,7 @@ def generate_inspiration_card_html(
|
|
|
# 生成帖子卡片
|
|
|
insp_posts_html = ""
|
|
|
for post in insp_posts[:6]: # 最多显示6个
|
|
|
- insp_posts_html += generate_post_card_html(post, f"match-{idx}-insp-post", post_to_mapping_data)
|
|
|
+ insp_posts_html += generate_post_card_html(post, f"{unique_match_id}-insp-post", post_to_mapping_data)
|
|
|
|
|
|
insp_detail_html = f'''
|
|
|
<div class="info-detail-column">
|
|
|
@@ -629,7 +762,7 @@ def generate_inspiration_card_html(
|
|
|
# 生成帖子卡片
|
|
|
cat_posts_html = ""
|
|
|
for post in cat_posts[:6]: # 最多显示6个
|
|
|
- cat_posts_html += generate_post_card_html(post, f"match-{idx}-cat-post", post_to_mapping_data)
|
|
|
+ cat_posts_html += generate_post_card_html(post, f"{unique_match_id}-cat-post", post_to_mapping_data)
|
|
|
|
|
|
cat_detail_html = f'''
|
|
|
<div class="info-detail-column">
|
|
|
@@ -664,7 +797,7 @@ def generate_inspiration_card_html(
|
|
|
<div class="step-toggle">▼</div>
|
|
|
</div>
|
|
|
<div class="step-wrapper-content">
|
|
|
- <div class="match-analysis-section" id="match-{idx}-step1" data-step-name="灵感点匹配灵感分类">
|
|
|
+ <div class="match-analysis-section" id="{unique_match_id}-step1" data-step-name="灵感点匹配灵感分类">
|
|
|
<div class="match-parts-container">
|
|
|
<div class="match-parts-column">
|
|
|
{same_parts_html}
|
|
|
@@ -692,7 +825,7 @@ def generate_inspiration_card_html(
|
|
|
<div class="step-toggle">▼</div>
|
|
|
</div>
|
|
|
<div class="step-wrapper-content">
|
|
|
- <div class="step-section expanded" data-step="2" id="match-{idx}-step2" data-step-name="灵感分类搜索">
|
|
|
+ <div class="step-section expanded" data-step="2" id="{unique_match_id}-step2" data-step-name="灵感分类搜索">
|
|
|
<div class="step-section-header" onclick="toggleStep(this)">
|
|
|
<div class="step-section-title">
|
|
|
<span class="step-sub-number">2.1</span>
|
|
|
@@ -712,7 +845,7 @@ def generate_inspiration_card_html(
|
|
|
safe_element_id = ''.join(c if c.isalnum() or c in '_-' else '_' for c in element_name)
|
|
|
|
|
|
matches_html += f'''
|
|
|
- <div class="match-item{expanded_class}" data-index="{idx}" id="match-{idx}" data-match-name="{html_module.escape(element_name)}">
|
|
|
+ <div class="match-item{expanded_class}" data-index="{idx}" id="{unique_match_id}" data-match-name="{html_module.escape(element_name)}">
|
|
|
<div class="match-main-header" onclick="toggleMainMatch(this)">
|
|
|
<div class="match-header-row">
|
|
|
<div class="match-header-left">
|
|
|
@@ -826,9 +959,9 @@ def generate_detail_html(inspiration_data: Dict[str, Any]) -> str:
|
|
|
element_a = input_info.get("A", "")
|
|
|
context_a = input_info.get("A_Context", "")
|
|
|
score = match_result.get("score", 0)
|
|
|
- score_explain = match_result.get("score说明", "")
|
|
|
- same_parts = match_result.get("相同部分", {})
|
|
|
- increment_parts = match_result.get("增量部分", {})
|
|
|
+ score_explain = match_result.get("score说明", "") or ""
|
|
|
+ same_parts = match_result.get("相同部分", {}) or {}
|
|
|
+ increment_parts = match_result.get("增量部分", {}) or {}
|
|
|
|
|
|
content += f'''
|
|
|
<div class="match-item">
|
|
|
@@ -1087,10 +1220,169 @@ def generate_detail_modal_content_js() -> str:
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 显示分数详情
|
|
|
+ function showScoreDetail(element) {
|
|
|
+ const scoreDetailStr = element.dataset.scoreDetail;
|
|
|
+ if (!scoreDetailStr) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const scoreData = JSON.parse(scoreDetailStr);
|
|
|
+ const modal = document.getElementById('scoreDetailModal');
|
|
|
+ const modalBody = document.getElementById('scoreModalBody');
|
|
|
+
|
|
|
+ // 生成相同部分HTML
|
|
|
+ let samePartsHTML = '';
|
|
|
+ if (scoreData.相同部分 && Object.keys(scoreData.相同部分).length > 0) {
|
|
|
+ const sameItems = Object.entries(scoreData.相同部分).map(([key, value]) =>
|
|
|
+ `<div class="score-part-item"><span class="score-part-key">${key}:</span><span class="score-part-value">${value}</span></div>`
|
|
|
+ ).join('');
|
|
|
+ samePartsHTML = `
|
|
|
+ <div class="score-parts same-parts">
|
|
|
+ <div class="score-parts-title">✅ 相同部分</div>
|
|
|
+ ${sameItems}
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成增量部分HTML
|
|
|
+ let incrementPartsHTML = '';
|
|
|
+ if (scoreData.增量部分 && Object.keys(scoreData.增量部分).length > 0) {
|
|
|
+ const incItems = Object.entries(scoreData.增量部分).map(([key, value]) =>
|
|
|
+ `<div class="score-part-item"><span class="score-part-key">${key}:</span><span class="score-part-value">${value}</span></div>`
|
|
|
+ ).join('');
|
|
|
+ incrementPartsHTML = `
|
|
|
+ <div class="score-parts increment-parts">
|
|
|
+ <div class="score-parts-title">➕ 增量部分</div>
|
|
|
+ ${incItems}
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成分数说明HTML
|
|
|
+ let explainHTML = '';
|
|
|
+ if (scoreData.score说明) {
|
|
|
+ explainHTML = `
|
|
|
+ <div class="score-detail-explain">
|
|
|
+ <div class="score-explain-title">💡 分数说明</div>
|
|
|
+ <div class="score-explain-text">${scoreData.score说明}</div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成输入信息HTML
|
|
|
+ let inputInfoHTML = '';
|
|
|
+ if (scoreData.输入B || scoreData.输入A) {
|
|
|
+ inputInfoHTML = `
|
|
|
+ <div class="score-input-info">
|
|
|
+ <div class="score-input-title">📝 输入信息</div>
|
|
|
+ ${scoreData.输入B ? `
|
|
|
+ <div class="score-input-item">
|
|
|
+ <div class="score-input-label">输入B(灵感点):</div>
|
|
|
+ <div class="score-input-value">${scoreData.输入B}</div>
|
|
|
+ </div>
|
|
|
+ ` : ''}
|
|
|
+ ${scoreData.B_Context ? `
|
|
|
+ <div class="score-input-item">
|
|
|
+ <div class="score-input-label">B_Context:</div>
|
|
|
+ <div class="score-input-value">${scoreData.B_Context}</div>
|
|
|
+ </div>
|
|
|
+ ` : ''}
|
|
|
+ ${scoreData.输入A ? `
|
|
|
+ <div class="score-input-item">
|
|
|
+ <div class="score-input-label">输入A(帖子标题):</div>
|
|
|
+ <div class="score-input-value">${scoreData.输入A}</div>
|
|
|
+ </div>
|
|
|
+ ` : ''}
|
|
|
+ ${scoreData.A_Context ? `
|
|
|
+ <div class="score-input-item">
|
|
|
+ <div class="score-input-label">A_Context(帖子内容摘要):</div>
|
|
|
+ <div class="score-input-value">${scoreData.A_Context}</div>
|
|
|
+ </div>
|
|
|
+ ` : ''}
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ modalBody.innerHTML = `
|
|
|
+ <div class="score-detail-container">
|
|
|
+ <h2 class="score-detail-title">匹配分数详情</h2>
|
|
|
+ <div class="score-detail-score">
|
|
|
+ <span class="score-detail-label">匹配分数:</span>
|
|
|
+ <span class="score-detail-value">${scoreData.score.toFixed(2)}</span>
|
|
|
+ </div>
|
|
|
+ ${inputInfoHTML}
|
|
|
+ ${explainHTML}
|
|
|
+ <div class="score-parts-container">
|
|
|
+ ${samePartsHTML}
|
|
|
+ ${incrementPartsHTML}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ modal.classList.add('active');
|
|
|
+ document.body.style.overflow = 'hidden';
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Failed to parse score detail:', e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭分数详情
|
|
|
+ function closeScoreDetail() {
|
|
|
+ const modal = document.getElementById('scoreDetailModal');
|
|
|
+ if (modal) {
|
|
|
+ modal.classList.remove('active');
|
|
|
+ document.body.style.overflow = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 点击Modal背景关闭分数详情
|
|
|
+ function closeScoreDetailModal(event) {
|
|
|
+ if (event.target.id === 'scoreDetailModal') {
|
|
|
+ closeScoreDetail();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 搜索结果排序
|
|
|
+ function sortSearchResults(button, containerId) {
|
|
|
+ const sortType = button.dataset.sort;
|
|
|
+ const container = document.getElementById(containerId);
|
|
|
+ if (!container) return;
|
|
|
+
|
|
|
+ // 更新按钮状态
|
|
|
+ const allButtons = button.parentElement.querySelectorAll('.search-sort-btn');
|
|
|
+ allButtons.forEach(btn => btn.classList.remove('active'));
|
|
|
+ button.classList.add('active');
|
|
|
+
|
|
|
+ // 获取所有搜索结果卡片
|
|
|
+ const items = Array.from(container.querySelectorAll('.search-note-item'));
|
|
|
+
|
|
|
+ // 根据排序类型排序
|
|
|
+ items.sort((a, b) => {
|
|
|
+ if (sortType === 'score') {
|
|
|
+ const scoreA = parseFloat(a.dataset.score) || -1;
|
|
|
+ const scoreB = parseFloat(b.dataset.score) || -1;
|
|
|
+ return scoreB - scoreA; // 降序
|
|
|
+ } else if (sortType === 'original') {
|
|
|
+ const indexA = parseInt(a.dataset.originalIndex) || 0;
|
|
|
+ const indexB = parseInt(b.dataset.originalIndex) || 0;
|
|
|
+ return indexA - indexB; // 升序
|
|
|
+ } else if (sortType === 'likes') {
|
|
|
+ const likesA = parseInt(a.dataset.likes) || 0;
|
|
|
+ const likesB = parseInt(b.dataset.likes) || 0;
|
|
|
+ return likesB - likesA; // 降序
|
|
|
+ }
|
|
|
+ return 0;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 重新排列DOM
|
|
|
+ items.forEach(item => container.appendChild(item));
|
|
|
+ }
|
|
|
+
|
|
|
// ESC键关闭详情
|
|
|
document.addEventListener('keydown', function(event) {
|
|
|
if (event.key === 'Escape') {
|
|
|
closeNoteDetail();
|
|
|
+ closeScoreDetail();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
@@ -2129,8 +2421,8 @@ def generate_html(
|
|
|
}}
|
|
|
|
|
|
.info-posts-grid .search-note-item {{
|
|
|
- flex: 0 0 280px;
|
|
|
- min-width: 280px;
|
|
|
+ flex: 0 0 260px;
|
|
|
+ min-width: 260px;
|
|
|
}}
|
|
|
|
|
|
.matches-list {{
|
|
|
@@ -2309,7 +2601,7 @@ def generate_html(
|
|
|
}}
|
|
|
|
|
|
.step-section-wrapper.expanded .step-wrapper-content {{
|
|
|
- max-height: 10000px;
|
|
|
+ max-height: 30000px;
|
|
|
}}
|
|
|
|
|
|
.step-number-badge {{
|
|
|
@@ -2501,7 +2793,7 @@ def generate_html(
|
|
|
}}
|
|
|
|
|
|
.step-section.expanded .step-section-content {{
|
|
|
- max-height: 5000px;
|
|
|
+ max-height: 20000px;
|
|
|
}}
|
|
|
|
|
|
.match-content {{
|
|
|
@@ -2653,6 +2945,17 @@ def generate_html(
|
|
|
margin-bottom: 20px;
|
|
|
}}
|
|
|
|
|
|
+ .search-summary-header {{
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ gap: 20px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .search-summary-left {{
|
|
|
+ flex: 1;
|
|
|
+ }}
|
|
|
+
|
|
|
.search-summary-title {{
|
|
|
font-size: 15px;
|
|
|
font-weight: 700;
|
|
|
@@ -2672,10 +2975,51 @@ def generate_html(
|
|
|
margin: 0 4px;
|
|
|
}}
|
|
|
|
|
|
+ .search-sort-buttons {{
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .search-sort-label {{
|
|
|
+ font-size: 13px;
|
|
|
+ color: #047857;
|
|
|
+ font-weight: 600;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .search-sort-btn {{
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #a7f3d0;
|
|
|
+ color: #065f46;
|
|
|
+ padding: 6px 12px;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .search-sort-btn:hover {{
|
|
|
+ background: #d1fae5;
|
|
|
+ border-color: #6ee7b7;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .search-sort-btn.active {{
|
|
|
+ background: #059669;
|
|
|
+ color: white;
|
|
|
+ border-color: #059669;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .note-position {{
|
|
|
+ font-size: 11px;
|
|
|
+ color: #9ca3af;
|
|
|
+ font-weight: 600;
|
|
|
+ }}
|
|
|
+
|
|
|
.search-notes-list {{
|
|
|
display: grid;
|
|
|
- grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
|
- gap: 20px;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
|
+ gap: 16px;
|
|
|
}}
|
|
|
|
|
|
.search-note-item {{
|
|
|
@@ -2685,6 +3029,7 @@ def generate_html(
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
|
border: 1px solid #e5e7eb;
|
|
|
transition: all 0.3s;
|
|
|
+ position: relative;
|
|
|
}}
|
|
|
|
|
|
.search-note-item:hover {{
|
|
|
@@ -2692,6 +3037,41 @@ def generate_html(
|
|
|
border-color: #667eea;
|
|
|
}}
|
|
|
|
|
|
+ .note-score-badge {{
|
|
|
+ position: absolute;
|
|
|
+ top: 12px;
|
|
|
+ right: 12px;
|
|
|
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
|
+ padding: 8px 14px;
|
|
|
+ border-radius: 8px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 2px;
|
|
|
+ cursor: pointer;
|
|
|
+ z-index: 5;
|
|
|
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
|
|
|
+ transition: all 0.3s;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .note-score-badge:hover {{
|
|
|
+ transform: scale(1.05);
|
|
|
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.5);
|
|
|
+ }}
|
|
|
+
|
|
|
+ .note-score-badge .score-label {{
|
|
|
+ font-size: 10px;
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .note-score-badge .score-value {{
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: white;
|
|
|
+ }}
|
|
|
+
|
|
|
.note-image-carousel {{
|
|
|
position: relative;
|
|
|
width: 100%;
|
|
|
@@ -2813,6 +3193,15 @@ def generate_html(
|
|
|
-webkit-box-orient: vertical;
|
|
|
}}
|
|
|
|
|
|
+ .note-publish-date {{
|
|
|
+ font-size: 12px;
|
|
|
+ color: #9ca3af;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ }}
|
|
|
+
|
|
|
.note-footer {{
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
@@ -3159,6 +3548,152 @@ def generate_html(
|
|
|
padding: 30px;
|
|
|
}}
|
|
|
|
|
|
+ /* 分数详情Modal样式 */
|
|
|
+ .score-detail-content {{
|
|
|
+ max-width: 800px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-detail-container {{
|
|
|
+ padding: 10px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-detail-title {{
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #1f2937;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ padding-bottom: 15px;
|
|
|
+ border-bottom: 2px solid #e5e7eb;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-detail-score {{
|
|
|
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 12px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 10px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-detail-label {{
|
|
|
+ font-size: 16px;
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
+ font-weight: 600;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-detail-value {{
|
|
|
+ font-size: 32px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: white;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-detail-explain {{
|
|
|
+ background: #fffbeb;
|
|
|
+ padding: 16px 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ border-left: 3px solid #f59e0b;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-input-info {{
|
|
|
+ background: #f0f9ff;
|
|
|
+ padding: 16px 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ border-left: 3px solid #3b82f6;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-input-title {{
|
|
|
+ font-weight: 700;
|
|
|
+ font-size: 14px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ color: #1e40af;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-input-item {{
|
|
|
+ margin-bottom: 12px;
|
|
|
+ padding-bottom: 12px;
|
|
|
+ border-bottom: 1px solid #bfdbfe;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-input-item:last-child {{
|
|
|
+ margin-bottom: 0;
|
|
|
+ padding-bottom: 0;
|
|
|
+ border-bottom: none;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-input-label {{
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #1e40af;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-input-value {{
|
|
|
+ font-size: 14px;
|
|
|
+ color: #374151;
|
|
|
+ line-height: 1.6;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ word-break: break-word;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-parts-container {{
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 20px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-parts {{
|
|
|
+ background: #f9fafb;
|
|
|
+ padding: 16px;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-parts.same-parts {{
|
|
|
+ background: #ecfdf5;
|
|
|
+ border-color: #10b981;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-parts.increment-parts {{
|
|
|
+ background: #fef3c7;
|
|
|
+ border-color: #f59e0b;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-parts-title {{
|
|
|
+ font-weight: 700;
|
|
|
+ font-size: 14px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ color: #374151;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-part-item {{
|
|
|
+ padding: 10px 0;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-part-item:last-child {{
|
|
|
+ border-bottom: none;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-part-key {{
|
|
|
+ font-weight: 500;
|
|
|
+ color: #374151;
|
|
|
+ margin-right: 8px;
|
|
|
+ }}
|
|
|
+
|
|
|
+ .score-part-value {{
|
|
|
+ color: #6b7280;
|
|
|
+ }}
|
|
|
+
|
|
|
.modal-header {{
|
|
|
margin-bottom: 25px;
|
|
|
padding-bottom: 20px;
|
|
|
@@ -3562,6 +4097,16 @@ def generate_html(
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- Score Detail Modal -->
|
|
|
+ <div id="scoreDetailModal" class="modal-overlay" onclick="closeScoreDetailModal(event)">
|
|
|
+ <div class="modal-content score-detail-content">
|
|
|
+ <button class="modal-close" onclick="closeScoreDetail()">×</button>
|
|
|
+ <div class="modal-body" id="scoreModalBody">
|
|
|
+ <!-- Score detail content will be inserted here -->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<script>
|