{% extends "base.html" %} {% block title %}采集结果 - AI答案采集平台{% endblock %} {% block styles %} .header { background: white; padding: 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 25px; } .stat-card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); border: 1px solid #f0f0f0; } .stat-label { font-size: 13px; color: #8c8c8c; margin-bottom: 8px; display: flex; align-items: center; gap: 5px; } .stat-value { font-size: 28px; font-weight: 600; color: #262626; } .stat-unit { font-size: 14px; color: #8c8c8c; margin-left: 4px; font-weight: normal; } .result-card { background: white; border-radius: 8px; padding: 24px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); border: 1px solid #f0f0f0; transition: box-shadow 0.3s; } .result-card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); } .result-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid #f0f0f0; } .result-question { font-size: 16px; font-weight: 600; color: #262626; line-height: 1.5; flex: 1; } .result-content { margin-bottom: 20px; } .result-content:last-child { margin-bottom: 0; } .content-label { font-size: 13px; color: #8c8c8c; margin-bottom: 10px; font-weight: 500; display: flex; align-items: center; gap: 5px; } .content-text { font-size: 14px; color: #262626; line-height: 1.8; white-space: pre-wrap; word-wrap: break-word; } .references-list { list-style: none; padding: 0; margin: 0; } .reference-item { padding: 8px 12px; background: #fafafa; border-radius: 6px; margin-bottom: 6px; border: 1px solid #f0f0f0; transition: background 0.2s; } /* 排名列表样式 */ .rankings-list { list-style: none; padding: 0; margin: 0; } .ranking-item { display: flex; align-items: center; padding: 10px 12px; background: #fafafa; border-radius: 6px; margin-bottom: 6px; border: 1px solid #f0f0f0; transition: background 0.2s; } .ranking-item:hover { background: #f5f5f5; } .rank-badge { width: 24px; height: 24px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-size: 12px; font-weight: 600; display: flex; align-items: center; justify-content: center; margin-right: 12px; flex-shrink: 0; } .rank-badge.top3 { background: linear-gradient(135deg, #ffd700 0%, #ff8c00 100%); } .ranking-info { flex: 1; min-width: 0; } .ranking-name { font-weight: 600; color: #262626; margin-bottom: 2px; } .ranking-hospital { font-size: 13px; color: #8c8c8c; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ranking-group { font-size: 12px; color: #10b981; background: rgba(16, 185, 129, 0.1); padding: 2px 6px; border-radius: 4px; margin-left: 8px; } .reference-item:hover { background: #f0f9ff; } .reference-item:last-child { margin-bottom: 0; } .reference-title { font-size: 13px; color: #262626; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 2px; } .reference-url { font-size: 12px; color: #1890ff; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; } /* 百度指数风格 - 核心词卡片 */ .index-cards-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(48%, 1fr)); gap: 20px; margin-top: 20px; } .index-card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); border: 1px solid #f0f0f0; transition: all 0.3s; cursor: pointer; position: relative; overflow: hidden; } .index-card:hover { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); transform: translateY(-2px); } .index-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 15px; } .index-keyword { font-size: 18px; font-weight: 600; color: #262626; display: flex; align-items: center; gap: 8px; } .index-rank { background: linear-gradient(135deg, #1890ff 0%, #69c0ff 100%); color: white; font-size: 12px; padding: 2px 8px; border-radius: 10px; font-weight: 600; } .index-rank.no1 { background: linear-gradient(135deg, #fa8c16 0%, #ffa940 100%); } .index-rank.no2 { background: linear-gradient(135deg, #8c8c8c 0%, #bfbfbf 100%); } .index-rank.no3 { background: linear-gradient(135deg, #d46b08 0%, #fa8c16 100%); } .index-values { display: flex; gap: 30px; margin-bottom: 15px; } .index-value-item { display: flex; flex-direction: column; } .index-value-label { font-size: 12px; color: #8c8c8c; margin-bottom: 4px; } .index-value-number { font-size: 28px; font-weight: 600; color: #1890ff; line-height: 1; } .index-value-number.success { color: #52c41a; } .index-value-number.info { color: #8c8c8c; } .index-value-diff { font-size: 12px; margin-left: 8px; display: inline-flex; align-items: center; } .index-value-diff.up { color: #ff4d4f; } .index-value-diff.down { color: #52c41a; } /* 舆情标签样式 */ .sentiment-item .index-value-label { margin-bottom: 8px; } .sentiment-content { display: flex; gap: 6px; font-size: 14px; font-weight: 500; } .sentiment-tag { display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; border-radius: 12px; background: rgba(0, 0, 0, 0.05); transition: all 0.2s; } .sentiment-tag:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .sentiment-dot { width: 6px; height: 6px; border-radius: 50%; } .sentiment-text { font-size: 12px; font-weight: 500; } .sentiment-num { font-size: 14px; font-weight: 600; } /* 正面 */ .sentiment-tag.positive { background: rgba(82, 196, 26, 0.1); } .sentiment-tag.positive .sentiment-dot { background: #52c41a; } .sentiment-tag.positive .sentiment-text, .sentiment-tag.positive .sentiment-num { color: #52c41a; } /* 中性 */ .sentiment-tag.neutral { background: rgba(140, 140, 140, 0.1); } .sentiment-tag.neutral .sentiment-dot { background: #8c8c8c; } .sentiment-tag.neutral .sentiment-text, .sentiment-tag.neutral .sentiment-num { color: #8c8c8c; } /* 负面 */ .sentiment-tag.negative { background: rgba(255, 77, 79, 0.1); } .sentiment-tag.negative .sentiment-dot { background: #ff4d4f; } .sentiment-tag.negative .sentiment-text, .sentiment-tag.negative .sentiment-num { color: #ff4d4f; } /* 智慧舆情样式 */ .ai-sentiment { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; color: #8c8c8c; } .ai-item { display: inline-flex; align-items: center; gap: 2px; } .ai-dot { width: 5px; height: 5px; border-radius: 50%; } .ai-num { font-size: 11px; font-weight: 500; } .ai-item.positive .ai-dot { background: #52c41a; } .ai-item.positive .ai-num { color: #52c41a; } .ai-item.neutral .ai-dot { background: #8c8c8c; } .ai-item.neutral .ai-num { color: #8c8c8c; } .ai-item.negative .ai-dot { background: #ff4d4f; } .ai-item.negative .ai-num { color: #ff4d4f; } /* 舆情分隔符 */ .sentiment-divider { color: #e8e8e8; margin: 0 8px; } .sentiment-content { display: flex; align-items: center; gap: 4px; } .index-chart-container { height: 80px; width: 100%; margin-top: 10px; position: relative; } .index-card-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; padding-top: 15px; border-top: 1px solid #f0f0f0; } .index-rate-bar { flex: 1; height: 6px; background: #f0f0f0; border-radius: 3px; overflow: hidden; margin-right: 15px; } .index-rate-fill { height: 100%; border-radius: 3px; transition: width 0.5s ease; } .index-rate-fill.high { background: linear-gradient(90deg, #52c41a 0%, #73d13d 100%); } .index-rate-fill.medium { background: linear-gradient(90deg, #fa8c16 0%, #ffa940 100%); } .index-rate-fill.low { background: linear-gradient(90deg, #8c8c8c 0%, #bfbfbf 100%); } .index-rate-text { font-size: 14px; font-weight: 600; min-width: 60px; text-align: right; } .index-detail-btn { color: #1890ff; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 4px; } .index-detail-btn:hover { text-decoration: underline; } } .reference-url:hover { text-decoration: underline; } .screenshot-preview { max-width: 100%; cursor: pointer; transition: transform 0.3s; display: block; } .screenshot-preview:hover { transform: scale(1.01); } .exposure-badge { display: inline-flex; align-items: center; gap: 4px; padding: 0 8px; height: 24px; border-radius: 4px; font-size: 12px; font-weight: 500; line-height: 1; white-space: nowrap; } .exposure-yes { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; } .exposure-no { background: #fafafa; color: #8c8c8c; border: 1px solid #d9d9d9; } .keyword-tag { display: inline-block; padding: 3px 10px; background: #1890ff; color: white; border-radius: 4px; font-size: 12px; margin-right: 6px; margin-bottom: 4px; font-weight: 500; } .platform-stats { margin-top: 16px; } .platform-stat-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: #fafafa; border-radius: 6px; margin-bottom: 10px; border: 1px solid #f0f0f0; } .platform-stat-item:last-child { margin-bottom: 0; } /* 平台筛选器 */ .platform-filter { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } .platform-btn { height: 32px; padding: 0 14px; border-radius: 6px; border: 1px solid #d9d9d9; background: white; color: #595959; font-size: 13px; cursor: pointer; transition: all 0.2s; line-height: 30px; white-space: nowrap; } .platform-btn:hover { border-color: #1890ff; color: #1890ff; } .platform-btn.active { background: #1890ff; border-color: #1890ff; color: white; font-weight: 500; } .task-info-card { background: white; padding: 24px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); border: 1px solid #f0f0f0; } .task-info-card h2 { margin: 0 0 16px 0; font-size: 20px; font-weight: 600; color: #262626; } .task-info-card .info-row { display: flex; align-items: center; margin-bottom: 8px; font-size: 14px; color: #595959; } .task-info-card .info-row:last-child { margin-bottom: 0; } .task-info-card .info-label { color: #8c8c8c; margin-right: 8px; } .task-info-card .info-divider { color: #d9d9d9; margin: 0 16px; } .section-card { background: white; padding: 24px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); border: 1px solid #f0f0f0; } .section-card h3 { margin: 0 0 16px 0; font-size: 16px; font-weight: 600; color: #262626; } {% endblock %} {% block content %}
返回 核心词列表 问题列表 问题列表 答案浏览
导出GEO效果 GEO长截图下载 竞品分析

[[ task.name ]]

品牌名称: [[ task.brand_name || '-' ]] | 监控平台: [[ task.platforms.length ]] 个 | 监控问题: [[ task.questions.length ]] 个 | 舆情配置: [[ task.sentiment_config_name || '已绑定' ]] 使用系统配置
品牌曝光词: [[ task.brand_keywords.join(', ') ]]
总采集数
[[ filteredStats.total_answers ]]
品牌曝光数
[[ filteredStats.exposure_count ]]
品牌曝光率
[[ filteredStats.exposure_rate ]] %

品牌曝光指数

共 [[ keywordTableData.length ]] 个核心词
[[ item.keyword ]]
TOP [[ index + 1 ]]
品牌提及数 [[ item.exposure_count ]] [[ item.exposure_count > 0 ? '↑' : '-' ]]
提问总数 [[ item.total_count ]]
平均排名 #[[ item.rank ]]
舆情 (智慧舆情 [[ item.ai_sentiment_positive ]] [[ item.ai_sentiment_neutral ]] [[ item.ai_sentiment_negative ]] [[ item.sentiment_positive ]] [[ item.sentiment_neutral ]] [[ item.sentiment_negative ]]
暂无品牌曝光词数据
请在任务设置中配置品牌曝光词

问题列表 - [[ selectedKeyword ]]

共 [[ questionTableData.length ]] 个问题

[[ selectedQuestion ]]

共 [[ filteredAnswerList.length ]] 个答案 已筛选:[[ getPlatformName(selectedPlatform) ]]
[[ getPlatformName(currentAnswer.platform) ]] 查看截图 ↗
[[ answerPage ]] / [[ filteredAnswerList.length ]]
[[ checkBrandExposure(currentAnswer) ? '有品牌曝光' : '无品牌曝光' ]]
重新采集 删除
曝光关键词: [[ keyword ]] 舆情: [[ kw ]] [[ analyzeSentiment(currentAnswer, kw).label ]]
分析理由:[[ currentAnswer.ai_sentiment.reason ]]
  • [[ refIndex + 1 ]]. [[ ref.title || '无标题' ]] 匹配GEO稿件: [[ getGeoMatchCount(ref) ]]
    [[ ref.url ]]
[[ refsExpanded ? '收起' : `展开全部 ${currentAnswer.references.length} 条` ]]
采集时间:[[ formatDate(currentAnswer.created_at) ]]
暂无答案数据
{% endblock %} {% block scripts %} const taskId = {{ task_id }}; const app = createApp({ delimiters: ['[[', ']]'], data() { return { task: null, results: [], exposureStats: { total_answers: 0, exposure_count: 0, exposure_rate: 0, platform_stats: {}, keyword_stats: {} }, expandedSentimentReasons: [], updatingSentiment: null, chartInstances: {}, // 视图控制 currentView: 'keywords', // keywords | questions | answer selectedKeyword: '', selectedQuestion: '', // 平台筛选 selectedPlatform: '', // 时间筛选 [startDate, endDate] 或 null dateRange: null, dateShortcuts: [ { text: '今天', value: () => { const d = new Date(); return [d, d]; } }, { text: '近7天', value: () => { const e = new Date(); const s = new Date(); s.setDate(s.getDate()-6); return [s, e]; } }, { text: '近30天', value: () => { const e = new Date(); const s = new Date(); s.setDate(s.getDate()-29); return [s, e]; } }, { text: '近90天', value: () => { const e = new Date(); const s = new Date(); s.setDate(s.getDate()-89); return [s, e]; } }, ], // 答案浏览 answerList: [], answerPage: 1, // 当前页(1-based) refsExpanded: false, // 引用参考是否展开 geoManuscripts: [], // GEO稿件列表(用于匹配引用参考) // 重新采集状态 { platform_id: true/false } recollecting: {}, exportingGeo: false, exportingScreenshots: false, // 平台列表(从 API 动态获取) availablePlatforms: [] }; }, computed: { // 实际有数据的平台列表(用于筛选按钮) activePlatforms() { const ids = [...new Set(this.results.map(r => r.platform))]; return this.availablePlatforms.filter(p => ids.includes(p.id)); }, // 按提及率降序排序的核心词数据 keywordTableDataSorted() { return [...this.keywordTableData].sort((a, b) => b.exposure_rate - a.exposure_rate); }, // 按平台+时间筛选后的结果集(所有视图共用) filteredResults() { let list = this.results; if (this.selectedPlatform) { list = list.filter(r => r.platform === this.selectedPlatform); } if (this.dateRange && this.dateRange[0] && this.dateRange[1]) { const start = new Date(this.dateRange[0]); start.setHours(0, 0, 0, 0); const end = new Date(this.dateRange[1]); end.setHours(23, 59, 59, 999); console.log('[日期筛选] 开始日期:', start.toISOString()); console.log('[日期筛选] 结束日期:', end.toISOString()); console.log('[日期筛选] 筛选前数量:', list.length); list = list.filter(r => { const t = new Date(r.created_at); const match = t >= start && t <= end; console.log('[日期筛选] 检查:', r.created_at, '->', t.toISOString(), '匹配:', match); return match; }); console.log('[日期筛选] 筛选后数量:', list.length); } return list; }, // 筛选后的统计数据 filteredStats() { const results = this.filteredResults; const keywords = this.task ? this.task.brand_keywords : []; const total = results.length; let exposed = 0; const keywordStats = {}; results.forEach(r => { const text = r.answer || ''; let hasExp = false; keywords.forEach(kw => { if (text.includes(kw)) { hasExp = true; keywordStats[kw] = (keywordStats[kw] || 0) + 1; } }); if (hasExp) exposed++; }); return { total_answers: total, exposure_count: exposed, exposure_rate: total > 0 ? Math.round(exposed / total * 100) : 0, keyword_stats: keywordStats }; }, // 核心词表格数据(基于筛选后结果) keywordTableData() { if (!this.task || !this.task.brand_keywords) return []; const keywordStats = this.filteredStats.keyword_stats || {}; const total_count = this.filteredResults.length; return this.task.brand_keywords.map(keyword => { const exposure_count = keywordStats[keyword] || 0; const exposure_rate = total_count > 0 ? Math.round((exposure_count / total_count) * 100) : 0; // 计算该关键词的平均排名 let avgRank = '-'; const ranks = []; this.filteredResults .filter(r => r.rankings && r.rankings.length > 0) .forEach(r => { r.rankings.forEach(rankInfo => { if (rankInfo.name && (keyword.includes(rankInfo.name) || rankInfo.name.includes(keyword))) { ranks.push(rankInfo.rank); } }); }); if (ranks.length > 0) { const sum = ranks.reduce((a, b) => a + b, 0); avgRank = Math.round(sum / ranks.length); } else if (exposure_count > 0) { // 如果没有解析排名但有曝光,根据提及位置估算排名 let estimatedRank = 1; this.filteredResults .filter(r => r.rankings && r.rankings.length > 0 && (r.answer || '').includes(keyword)) .forEach(r => { const allRanks = r.rankings.map(ri => ri.rank).filter(rk => rk <= 100); if (allRanks.length > 0) { const avgPosition = Math.round(allRanks.reduce((a, b) => a + b, 0) / allRanks.length); ranks.push(avgPosition); } }); if (ranks.length > 0) { const sum = ranks.reduce((a, b) => a + b, 0); avgRank = Math.round(sum / ranks.length); } else { avgRank = 1; } } // 统计舆情(基于关键词的本地分析) let sentiment_positive = 0; let sentiment_neutral = 0; let sentiment_negative = 0; // 统计智慧舆情(AI分析) let ai_sentiment_positive = 0; let ai_sentiment_neutral = 0; let ai_sentiment_negative = 0; this.filteredResults .filter(r => (r.answer || '').includes(keyword)) .forEach(r => { // 本地舆情分析 const sentiment = this.analyzeSentiment(r, keyword); if (sentiment.label === '正面') { sentiment_positive++; } else if (sentiment.label === '负面') { sentiment_negative++; } else { sentiment_neutral++; } // 智慧舆情分析 if (r.ai_sentiment && r.ai_sentiment.label) { const ai_label = r.ai_sentiment.label; if (ai_label === '正面') { ai_sentiment_positive++; } else if (ai_label === '负面') { ai_sentiment_negative++; } else { ai_sentiment_neutral++; } } }); return { keyword, exposure_count, total_count, exposure_rate, rank: avgRank, sentiment_positive, sentiment_neutral, sentiment_negative, ai_sentiment_positive, ai_sentiment_neutral, ai_sentiment_negative }; }).sort((a, b) => b.exposure_count - a.exposure_count); }, // 问题表格数据(基于筛选后结果) questionTableData() { if (!this.selectedKeyword || !this.task) return []; const questionMap = {}; this.filteredResults.forEach(result => { if (!questionMap[result.question]) { questionMap[result.question] = { question: result.question, exposure_count: 0, total_count: 0, screenshot_count: 0, // 舆情(基于关键词的本地分析) local_sentiment_stats: { positive: 0, negative: 0, neutral: 0, total: 0 }, // 智慧舆情(AI分析) ai_sentiment_stats: { positive: 0, negative: 0, neutral: 0, total: 0 } }; } questionMap[result.question].total_count++; // 统计截图数量 if (result.screenshot_path && result.screenshot_path.trim()) { questionMap[result.question].screenshot_count++; } if ((result.answer || '').includes(this.selectedKeyword)) { questionMap[result.question].exposure_count++; } // 统计舆情(基于关键词的本地分析) const localSentiment = this.analyzeSentiment(result, this.selectedKeyword); if (localSentiment.label === '正面') { questionMap[result.question].local_sentiment_stats.positive++; } else if (localSentiment.label === '负面') { questionMap[result.question].local_sentiment_stats.negative++; } else { questionMap[result.question].local_sentiment_stats.neutral++; } questionMap[result.question].local_sentiment_stats.total++; // 统计智慧舆情(AI分析) if (result.ai_sentiment && result.ai_sentiment.label) { const label = result.ai_sentiment.label; if (label === '正面') { questionMap[result.question].ai_sentiment_stats.positive++; } else if (label === '负面') { questionMap[result.question].ai_sentiment_stats.negative++; } else { questionMap[result.question].ai_sentiment_stats.neutral++; } questionMap[result.question].ai_sentiment_stats.total++; } }); return Object.values(questionMap).map(item => { const exposure_rate = item.total_count > 0 ? Math.round((item.exposure_count / item.total_count) * 100) : 0; // 计算平均排名(从解析的排名信息中提取) let avgRank = '-'; const ranks = []; // 遍历该问题的所有采集结果,收集排名信息 this.filteredResults .filter(r => r.question === item.question && r.rankings && r.rankings.length > 0) .forEach(r => { // 尝试从排名中找到品牌关键词 let foundRank = false; r.rankings.forEach(rankInfo => { if (rankInfo.name && this.task.brand_keywords.some(kw => kw.includes(rankInfo.name) || rankInfo.name.includes(kw))) { ranks.push(rankInfo.rank); foundRank = true; } }); // 如果没有找到品牌关键词的排名,但有曝光,根据提及位置估算排名 if (!foundRank && (r.answer || '').includes(this.selectedKeyword)) { // 策略 1:如果排名列表中有医院相关条目(如"长宁院区"、"浦东院区"等),取最小排名 const hospitalRanks = r.rankings .filter(ri => ri.name && ( ri.name.includes('院区') || ri.name.includes('医院') || ri.name.includes('总院') || ri.name.includes('分院') )) .map(ri => ri.rank); if (hospitalRanks.length > 0) { // 取医院排名的最小值 +1 作为估算排名 const minHospitalRank = Math.min(...hospitalRanks); ranks.push(minHospitalRank + 1); } else { // 策略 2:如果排名列表中有其他条目,取平均排名作为估算 const allRanks = r.rankings.map(ri => ri.rank).filter(rk => rk <= 100); if (allRanks.length > 0) { const avgPosition = Math.round(allRanks.reduce((a, b) => a + b, 0) / allRanks.length); ranks.push(avgPosition); } else { // 策略 3:默认显示为第 1 位(有曝光但未明确排名) ranks.push(1); } } } // 注意:移除了策略4,因为如果没有匹配到品牌关键词且没有曝光, // 这个结果与品牌排名无关,不应该纳入平均排名计算 }); // 计算平均排名 if (ranks.length > 0) { const sum = ranks.reduce((a, b) => a + b, 0); avgRank = Math.round(sum / ranks.length); } // 确定主导舆情(基于关键词的本地分析) let localDominant = null; if (item.local_sentiment_stats.total > 0) { const maxCount = Math.max(item.local_sentiment_stats.positive, item.local_sentiment_stats.negative, item.local_sentiment_stats.neutral); if (maxCount === item.local_sentiment_stats.positive) { localDominant = { label: '正面', color: '#52c41a' }; } else if (maxCount === item.local_sentiment_stats.negative) { localDominant = { label: '负面', color: '#f5222d' }; } else { localDominant = { label: '中性', color: '#fa8c16' }; } } // 确定主导智慧舆情(AI分析) let aiDominant = null; if (item.ai_sentiment_stats.total > 0) { const maxCount = Math.max(item.ai_sentiment_stats.positive, item.ai_sentiment_stats.negative, item.ai_sentiment_stats.neutral); if (maxCount === item.ai_sentiment_stats.positive) { aiDominant = { label: '正面', color: '#52c41a' }; } else if (maxCount === item.ai_sentiment_stats.negative) { aiDominant = { label: '负面', color: '#f5222d' }; } else { aiDominant = { label: '中性', color: '#fa8c16' }; } } return { ...item, exposure_rate, rank: avgRank, local_dominant_sentiment: localDominant, ai_dominant_sentiment: aiDominant }; }).sort((a, b) => b.exposure_count - a.exposure_count); }, // 答案浏览列表(基于筛选后结果) filteredAnswerList() { if (!this.selectedPlatform) return this.answerList; return this.answerList.filter(r => r.platform === this.selectedPlatform); }, // 当前翻页显示的答案 currentAnswer() { if (!this.filteredAnswerList.length) return null; const idx = Math.min(this.answerPage, this.filteredAnswerList.length) - 1; return this.filteredAnswerList[idx]; } }, watch: { // 翻页时重置引用折叠状态 answerPage() { this.refsExpanded = false; } }, created() { this.loadResults(); }, mounted() { this.fetchPlatforms(); }, methods: { async fetchPlatforms() { try { const response = await axios.get('/api/platforms'); if (response.data.success) { this.availablePlatforms = response.data.platforms; console.log('平台列表已加载:', this.availablePlatforms); } } catch (error) { console.error('获取平台列表失败:', error); // 失败时使用空数组,activePlatforms 会基于实际数据自动生成 } }, toggleSentimentReason(id) { const index = this.expandedSentimentReasons.indexOf(id); if (index > -1) { this.expandedSentimentReasons.splice(index, 1); } else { this.expandedSentimentReasons.push(id); } }, async updateSentiment(resultId) { this.updatingSentiment = resultId; try { const response = await axios.post(`/api/results/${resultId}/update-sentiment`); if (response.data.success) { // 更新本地数据 const result = this.results.find(r => r.id === resultId); if (result) { result.ai_sentiment = response.data.ai_sentiment; result.ai_sentiment_updated_at = response.data.updated_at; } ElMessage.success('舆情分析已更新'); } else { ElMessage.error(response.data.message || '更新失败'); } } catch (error) { console.error('更新舆情失败:', error); ElMessage.error('更新失败: ' + (error.response?.data?.message || error.message)); } finally { this.updatingSentiment = null; } }, async loadResults() { try { console.log('开始加载数据...'); const response = await axios.get(`/api/tasks/${taskId}/results`); console.log('API 响应:', response.data); this.task = response.data.task; this.results = response.data.results; this.exposureStats = response.data.exposure_stats; // 只有在首次加载且没有设置日期范围时,才设置默认的7天日期范围 if (!this.dateRange) { const endDate = new Date(); const startDate = new Date(); startDate.setDate(startDate.getDate() - 6); // 近7天(包含今天) this.dateRange = [startDate, endDate]; console.log('设置默认日期范围:', this.dateRange); } console.log('任务数据:', this.task); console.log('采集结果数:', this.results.length); console.log('品牌曝光词:', this.task ? this.task.brand_keywords : 'null'); console.log('当前日期范围:', this.dateRange); // 如果当前视图是answer但没有选择问题,自动选择第一个问题 if (this.currentView === 'answer' && !this.selectedQuestion && this.results.length > 0) { // 获取第一个不重复的问题 const questions = [...new Set(this.results.map(r => r.question))]; if (questions.length > 0) { this.selectedQuestion = questions[0]; this.answerList = this.filteredResults.filter(r => r.question === this.selectedQuestion); console.log('自动选择第一个问题:', this.selectedQuestion); } } // 加载GEO稿件数据用于匹配引用参考 await this.loadGeoManuscripts(); // 等待 Vue 更新 DOM await this.$nextTick(); console.log('核心词表格数据:', this.keywordTableData); // 初始化百度指数图表 if (this.currentView === 'keywords') { this.initIndexCharts(); } if (this.results.length === 0) { ElMessage.warning('暂无采集数据'); } else { ElMessage.success(`加载成功,共 ${this.results.length} 条数据`); } } catch (error) { console.error('加载结果失败:', error); ElMessage.error('加载结果失败: ' + (error.response?.data?.message || error.message)); } }, // 导航方法 goToKeywords() { this.currentView = 'keywords'; this.selectedKeyword = ''; this.selectedQuestion = ''; this.$nextTick(() => { this.initIndexCharts(); }); }, goToQuestions() { this.currentView = 'questions'; this.selectedQuestion = ''; }, viewQuestions(keyword) { this.selectedKeyword = keyword; this.currentView = 'questions'; }, viewAnswer(question) { this.selectedQuestion = question; this.currentView = 'answer'; this.answerPage = 1; this.refsExpanded = false; this.answerList = this.filteredResults.filter(r => r.question === question); }, setPlatform(platformId) { this.selectedPlatform = platformId; this.answerPage = 1; this.refsExpanded = false; if (this.currentView === 'keywords') { this.$nextTick(() => { this.initIndexCharts(); }); } if (this.currentView === 'answer' && this.selectedQuestion) { this.answerList = this.filteredResults.filter(r => r.question === this.selectedQuestion); } }, toggleRefs() { this.refsExpanded = !this.refsExpanded; }, async deleteAnswer(resultId) { try { await ElMessageBox.confirm( '确定要删除这条 AI 答案吗?删除后无法恢复。', '删除确认', { confirmButtonText: '确认删除', cancelButtonText: '取消', type: 'warning' } ); } catch { return; } try { await axios.delete(`/api/results/${resultId}`); this.answerList = this.answerList.filter(r => r.id !== resultId); const idx = this.results.findIndex(r => r.id === resultId); if (idx !== -1) this.results.splice(idx, 1); // 删除后页码不超出范围 if (this.answerPage > this.filteredAnswerList.length && this.answerPage > 1) { this.answerPage--; } ElMessage.success('已删除'); } catch (e) { ElMessage.error('删除失败:' + (e.response?.data?.message || e.message)); } }, // 判断是否为该平台在当前问题下的最新一条记录 isLatestAnswer(answer) { const samePlatform = this.answerList .filter(r => r.platform === answer.platform) .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); return samePlatform.length > 0 && samePlatform[0].id === answer.id; }, // 重新采集(覆盖最新一条) async recollect(platformId, resultId) { if (this.recollecting[platformId]) return; const platformName = this.getPlatformName(platformId); try { await ElMessageBox.confirm( `将对「${this.selectedQuestion}」在 ${platformName} 重新采集,新结果将替换当前这条答案。确认继续?`, '重新采集', { confirmButtonText: '确认替换', cancelButtonText: '取消', type: 'warning' } ); } catch { return; } this.recollecting = { ...this.recollecting, [platformId]: true }; try { const res = await axios.post(`/api/tasks/${taskId}/recollect`, { question: this.selectedQuestion, platform: platformId, result_id: resultId }); if (!res.data.success) { ElMessage.error(res.data.message || '重新采集失败'); return; } const jobId = res.data.job_id; ElMessage.info(`${platformName} 正在采集中...`); // 轮询状态,每 3 秒查一次,最多等 5 分钟 const maxWait = 300000; const interval = 3000; const startTime = Date.now(); const poll = async () => { if (Date.now() - startTime > maxWait) { ElMessage.warning('采集超时,请手动刷新页面'); this.recollecting = { ...this.recollecting, [platformId]: false }; return; } try { const statusRes = await axios.get(`/api/recollect-status/${jobId}`); const job = statusRes.data; if (job.status === 'done') { // 用返回的新数据直接更新本地 answerList,无需刷新页面 const newResult = job.result; const idx = this.answerList.findIndex(r => r.id === newResult.id); if (idx !== -1) { this.answerList.splice(idx, 1, newResult); } // 同步更新 results 全量数据 const ridx = this.results.findIndex(r => r.id === newResult.id); if (ridx !== -1) { this.results.splice(ridx, 1, newResult); } ElMessage.success(`${platformName} 重新采集完成`); this.recollecting = { ...this.recollecting, [platformId]: false }; } else if (job.status === 'error') { ElMessage.error(`采集失败:${job.error}`); this.recollecting = { ...this.recollecting, [platformId]: false }; } else { // 还在运行,继续轮询 setTimeout(poll, interval); } } catch (e) { setTimeout(poll, interval); } }; setTimeout(poll, interval); } catch (e) { ElMessage.error('请求失败:' + (e.response?.data?.message || e.message)); this.recollecting = { ...this.recollecting, [platformId]: false }; } }, getPlatformName(platformId) { const platform = this.availablePlatforms.find(p => p.id === platformId); return platform ? platform.name : platformId; }, getProgressColor(rate) { if (rate >= 70) return '#52c41a'; if (rate >= 40) return '#faad14'; return '#f5222d'; }, getRateClass(rate) { if (rate >= 70) return 'high'; if (rate >= 40) return 'medium'; return 'low'; }, // 初始化百度指数风格的图表 initIndexCharts() { if (typeof echarts === 'undefined') return; Object.values(this.chartInstances).forEach(chart => { chart && chart.dispose && chart.dispose(); }); this.chartInstances = {}; const dateStats = {}; const allDates = new Set(); this.filteredResults.forEach(r => { if (!r.created_at) return; const date = new Date(r.created_at); const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; allDates.add(dateStr); if (!dateStats[dateStr]) { dateStats[dateStr] = {}; } const answer = r.answer || ''; this.task.brand_keywords.forEach(kw => { if (answer.includes(kw)) { dateStats[dateStr][kw] = (dateStats[dateStr][kw] || 0) + 1; } }); }); const dateList = [...allDates].sort(); console.log('[图表] 日期列表:', dateList); this.$nextTick(() => { this.keywordTableDataSorted.forEach((item, idx) => { const chartId = 'chart-' + item.keyword.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, ''); const dom = document.getElementById(chartId); if (!dom) return; const chart = echarts.init(dom); this.chartInstances[chartId] = chart; const trendData = dateList.map(date => { return dateStats[date]?.[item.keyword] || 0; }); const option = { tooltip: { trigger: 'axis', backgroundColor: 'rgba(255, 255, 255, 0.95)', borderColor: '#1890ff', borderWidth: 1, textStyle: { color: '#333', fontSize: 12 }, formatter: function(params) { return `
${item.keyword}
${params[0].name}: ${params[0].value} 次提及
`; }, axisPointer: { type: 'line', lineStyle: { color: '#1890ff', type: 'dashed' } } }, grid: { top: 10, right: 0, bottom: 10, left: 0 }, xAxis: { type: 'category', data: dateList, show: false, axisPointer: { show: true } }, yAxis: { type: 'value', show: false }, series: [{ data: trendData, type: 'line', smooth: true, symbol: 'circle', symbolSize: 6, emphasis: { scale: true, itemStyle: { color: '#1890ff', borderColor: '#fff', borderWidth: 2 } }, lineStyle: { width: 2, color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [ { offset: 0, color: '#1890ff' }, { offset: 1, color: '#69c0ff' } ]) }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: 'rgba(24, 144, 255, 0.3)' }, { offset: 1, color: 'rgba(24, 144, 255, 0.02)' } ]) } }] }; chart.setOption(option); }); }); }, formatDate(dateStr) { if (!dateStr) return '-'; const date = new Date(dateStr); return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }); }, goBack() { window.location.href = '/dashboard'; }, exportData() { const platform = this.selectedPlatform || ''; const platformName = platform ? this.getPlatformName(platform) : '全部平台'; let url = `/api/tasks/${taskId}/export?platform=${platform}`; if (this.dateRange && this.dateRange[0] && this.dateRange[1]) { const fmt = d => d.toISOString().slice(0, 10); url += `&date_start=${fmt(new Date(this.dateRange[0]))}`; url += `&date_end=${fmt(new Date(this.dateRange[1]))}`; } ElMessage.info(`正在导出 ${platformName} 的数据...`); window.open(url, '_blank'); }, async downloadFile(url, fallbackFilename, successMessage = '导出成功!') { const response = await fetch(url, { method: 'GET', credentials: 'include' }); if (!response.ok) { let errorMsg = '导出失败'; try { const errorData = await response.json(); errorMsg = errorData.message || errorData.error || errorMsg; } catch (e) { // 响应不是 JSON } throw new Error(errorMsg); } const disposition = response.headers.get('Content-Disposition'); let filename = fallbackFilename; const utf8Match = disposition && /filename\*=UTF-8''([^;]+)/i.exec(disposition); const asciiMatch = disposition && /filename="?([^";]+)"?/i.exec(disposition); if (utf8Match && utf8Match[1]) { filename = decodeURIComponent(utf8Match[1]); } else if (asciiMatch && asciiMatch[1]) { filename = asciiMatch[1]; } const blob = await response.blob(); const downloadUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = downloadUrl; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(downloadUrl); ElMessage.success(successMessage); }, async exportGeoResults() { if (this.exportingGeo) return; this.exportingGeo = true; let url = `/api/tasks/${taskId}/export-geo`; const params = []; // 添加日期范围参数 if (this.dateRange && this.dateRange[0] && this.dateRange[1]) { // 使用本地日期格式,避免时区偏移问题 const fmt = d => { const date = new Date(d); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; params.push(`date_start=${fmt(this.dateRange[0])}`); params.push(`date_end=${fmt(this.dateRange[1])}`); } // 添加平台筛选参数 console.log('[导出GEO] selectedPlatform:', this.selectedPlatform); console.log('[导出GEO] selectedPlatform类型:', typeof this.selectedPlatform); if (this.selectedPlatform && this.selectedPlatform !== '') { params.push(`platform=${encodeURIComponent(this.selectedPlatform)}`); console.log('[导出GEO] 已添加平台参数:', this.selectedPlatform); } else { console.log('[导出GEO] 未添加平台参数(selectedPlatform为空或空字符串)'); } // 拼接参数 if (params.length > 0) { url += '?' + params.join('&'); } console.log('[导出GEO] 完整URL:', url); ElMessage.info('正在导出GEO效果...'); try { await this.downloadFile(url, 'GEO效果.xlsx'); } catch (error) { console.error('[导出GEO] 错误:', error); ElMessage.error('导出失败:' + (error.message || '网络错误')); } finally { this.exportingGeo = false; } }, async exportGeoScreenshotsZip() { if (this.exportingScreenshots) return; this.exportingScreenshots = true; try { const params = []; // 添加日期范围参数 if (this.dateRange && this.dateRange[0] && this.dateRange[1]) { // 使用本地日期格式,避免时区偏移问题 const fmt = d => { const date = new Date(d); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; params.push(`date_start=${fmt(this.dateRange[0])}`); params.push(`date_end=${fmt(this.dateRange[1])}`); } // 添加平台筛选参数 if (this.selectedPlatform && this.selectedPlatform !== '') { params.push(`platform=${encodeURIComponent(this.selectedPlatform)}`); } // 拼接URL let url = `/api/tasks/${taskId}/export-screenshots-zip`; if (params.length > 0) { url += '?' + params.join('&'); } console.log('[导出GEO长截图] URL:', url); ElMessage.info('正在导出GEO长截图,请稍候...'); await this.downloadFile(url, 'GEO长截图.zip'); } catch (error) { console.error('[导出GEO长截图] 错误:', error); ElMessage.error('导出失败:' + (error.message || '网络错误')); } finally { this.exportingScreenshots = false; } }, // 跳转到竞品分析页面 goToCompetitorAnalysis() { let url = `/task/${taskId}/competitor-analysis`; window.location.href = url; }, // 加载GEO稿件数据 async loadGeoManuscripts() { try { const response = await axios.get('/api/geo-manuscripts', { params: { task_id: taskId } }); if (response.data.success) { this.geoManuscripts = response.data.manuscripts; console.log('GEO稿件数据:', this.geoManuscripts); } } catch (error) { console.error('加载GEO稿件失败:', error); } }, // 检查引用参考是否匹配GEO稿件,返回匹配数量 getGeoMatchCount(ref) { if (!ref || !this.geoManuscripts.length) { return 0; } const refUrl = (ref.url || '').toLowerCase().trim(); let matchCount = 0; for (const manuscript of this.geoManuscripts) { const msUrl = (manuscript.url || '').toLowerCase().trim(); // URL匹配规则: // 1. 引用URL包含稿件URL(去掉扩展名后) // 2. 稿件URL包含引用URL的关键部分 // 3. 域名相同 // 4. 路径中的数字/关键部分匹配 const urlMatch = this.urlMatch(refUrl, msUrl); if (urlMatch) { matchCount++; } } return matchCount; }, // URL匹配核心逻辑:引用参考URL必须包含GEO稿件URL分割后的所有部分 urlMatch(refUrl, msUrl) { if (!refUrl || !msUrl) return false; // 规则:将GEO稿件URL用"/"分割,引用参考URL必须包含所有分割后的部分 // 例如:GEO稿件URL是"39.net/a/260511/d6wkqfi.html" // 分割后:['39.net', 'a', '260511', 'd6wkqfi.html'] // 引用参考URL必须同时包含所有这些部分才算匹配成功 // 分割GEO稿件URL const parts = msUrl.split('/').filter(part => part.trim() !== ''); // 如果分割后没有部分,返回false if (parts.length === 0) return false; // 检查引用参考URL是否包含所有分割后的部分 for (const part of parts) { if (!refUrl.includes(part)) { return false; } } return true; }, // 清理URL:去掉协议、扩展名、末尾斜杠 cleanUrl(url) { let clean = url; // 去掉协议 clean = clean.replace(/^https?:\/\//, ''); // 去掉www.和m.前缀 clean = clean.replace(/^(www|m)\./, ''); // 去掉文件扩展名 clean = clean.replace(/\.(html?|htm|php|aspx|jsp)$/, ''); // 去掉末尾斜杠 clean = clean.replace(/\/$/, ''); return clean; }, // 提取URL中的数字串 extractNumbers(url) { const matches = url.match(/\d{5,}/g); // 提取5位以上的数字串 return matches || []; }, // 提取URL的域名部分 extractDomain(url) { try { // 处理没有协议的URL if (!url.startsWith('http')) { url = 'http://' + url; } const urlObj = new URL(url); return urlObj.hostname.toLowerCase(); } catch { return ''; } }, onDateChange() { // 切换时间后,如果在答案浏览视图,重新过滤 answerList if (this.currentView === 'answer' && this.selectedQuestion) { this.answerList = this.filteredResults.filter(r => r.question === this.selectedQuestion); this.answerPage = 1; } // 问题列表视图:不需要额外处理,因为 questionTableData 是 computed 属性会自动更新 // 核心词视图:重新渲染趋势图表 if (this.currentView === 'keywords') { this.$nextTick(() => { this.initIndexCharts(); }); } // 重置翻页状态 if (this.currentView === 'questions') { // questionTableData 会自动更新,这里不需要额外操作 } }, // 检查答案中是否包含品牌曝光词 checkBrandExposure(answer) { if (!answer || !answer.answer || !this.task || !this.task.brand_keywords) { return false; } const answerText = answer.answer.toLowerCase(); return this.task.brand_keywords.some(keyword => answerText.includes(keyword.toLowerCase()) ); }, // 获取答案中曝光的品牌曝光词列表 getExposedKeywords(answer) { if (!answer || !answer.answer || !this.task || !this.task.brand_keywords) { return []; } const answerText = answer.answer.toLowerCase(); return this.task.brand_keywords.filter(keyword => answerText.includes(keyword.toLowerCase()) ); }, // 情感分析:判断舆情倾向 analyzeSentiment(answer, keyword) { if (!answer || !answer.answer) { return { sentiment: 'neutral', score: 0, label: '中性', color: '#8c8c8c' }; } const positiveWords = [ '好', '不错', '优秀', '棒', '赞', '推荐', '喜欢', '满意', '靠谱', '推荐', '好评', '出色', '卓越', '完美', '理想', '满意', '认可', '信赖', '放心', '值得', '划算', '实惠', '优质', '高端', '专业', '有效', '神奇', '见效', '吸收', '补水', '保湿', '美白', '抗衰', '修复', '温和', '清爽', '不油腻', '无刺激', '抗过敏', '口碑好', '效果好', '质量好', '性价比高', '回购', '正品', '放心', '安全', '健康', '自然', '纯正', '地道', '正宗' ]; const negativeWords = [ '差', '不好', '烂', '垃圾', '失望', '后悔', '差评', '糟糕', '骗人', '虚假', '没效果', '无用', '无效', '过敏', '刺激', '油腻', '干燥', '紧绷', '爆痘', '泛红', '瘙痒', '副作用', '假货', '劣质', '粗糙', '廉价', '智商税', '智商税', '坑人', '受骗', '上当', '不推荐', '避雷', '踩坑', '鸡肋', '失望', '不值', '浪费', '昂贵', '伤皮肤', '有问题', '不合格', '超标', '有害', '致癌', '激素', '荧光剂', '重金属' ]; let text = answer.answer; if (keyword) { const kwIndex = text.indexOf(keyword); if (kwIndex !== -1) { const start = Math.max(0, kwIndex - 50); const end = Math.min(text.length, kwIndex + keyword.length + 50); text = text.substring(start, end); } } let positiveScore = 0; let negativeScore = 0; positiveWords.forEach(word => { const regex = new RegExp(word, 'g'); const matches = text.match(regex); if (matches) positiveScore += matches.length; }); negativeWords.forEach(word => { const regex = new RegExp(word, 'g'); const matches = text.match(regex); if (matches) negativeScore += matches.length; }); const total = positiveScore + negativeScore; if (total === 0) { return { sentiment: 'neutral', score: 0, label: '中性', color: '#8c8c8c' }; } const ratio = (positiveScore - negativeScore) / total; if (ratio > 0.3) { return { sentiment: 'positive', score: ratio, label: '正面', color: '#52c41a' }; } else if (ratio < -0.3) { return { sentiment: 'negative', score: ratio, label: '负面', color: '#ff4d4f' }; } else { return { sentiment: 'neutral', score: ratio, label: '中性', color: '#fa8c16' }; } }, // 高亮显示品牌曝光词 highlightKeywords(text) { if (!text || !this.task || !this.task.brand_keywords) { return text; } let highlightedText = text; // 对每个品牌曝光词进行高亮处理 this.task.brand_keywords.forEach(keyword => { // 使用正则表达式进行全局替换,保持大小写 const regex = new RegExp(`(${keyword})`, 'gi'); highlightedText = highlightedText.replace(regex, '$1' ); }); return highlightedText; } } }); app.use(ElementPlus); // 全局注册所有 Element Plus 图标 for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component); } app.mount('#app'); {% endblock %}