/** * 运营看板组件 * 指标卡片 + Chart.js 趋势图表 + 知识库文档命中排行 + 未命中问题收集 */ import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue' import { getDashboardOverview, getDashboardTrend, getDashboardKnowledge, getDashboardCustom } from '../js/api.js' export default { template: `

📈 运营数据看板

|
{{ overview.conversationCount ?? 0 }}
今日对话数
{{ formatPercent(overview.satisfactionRate) }}%
满意率
{{ formatPercent(overview.ragHitRate) }}%
知识库命中率
{{ overview.avgResponseTime ?? 0 }}ms
平均响应时间

📊 对话量趋势

📊 满意度趋势

📚 文档命中排行 TOP-10

暂无数据
排名 文档标题 命中次数 平均得分
{{ idx + 1 }} {{ doc.document_title || '-' }} {{ doc.hit_count ?? doc.cnt ?? 0 }} {{ doc.avg_score != null ? Number(doc.avg_score).toFixed(3) : '-' }}

❓ 未命中问题收集

暂无数据
{{ idx + 1 }}. {{ item.user_query }} {{ formatDateShort(item.last_time) }}
加载中...
`, setup() { const loading = ref(false) const rangeDays = ref(7) const customStart = ref('') const customEnd = ref('') const overview = ref({}) const trendData = ref([]) const topHitDocuments = ref([]) const missQuestions = ref([]) // Chart.js 实例引用(用于销毁重建) let conversationChart = null let satisfactionChart = null const conversationChartRef = ref(null) const satisfactionChartRef = ref(null) // 满意率颜色:>=80% 绿色,>=60% 橙色,<60% 红色 const satisfactionColor = computed(() => { const rate = overview.value.satisfactionRate || 0 if (rate >= 80) return 'var(--success)' if (rate >= 60) return 'var(--warn)' return 'var(--danger)' }) // 格式化百分比(保留一位小数) function formatPercent(val) { if (val == null) return '0.0' return Number(val).toFixed(1) } // 格式化日期简写(仅月-日) function formatDateShort(dateVal) { if (!dateVal) return '' const d = new Date(dateVal) return `${d.getMonth() + 1}/${d.getDate()}` } // 设置时间范围并加载趋势 async function setRange(days) { rangeDays.value = days await loadTrend(days) } // 加载今日概览 async function loadOverview() { try { const res = await getDashboardOverview() if (res.success) overview.value = res.data } catch (e) { console.error('加载概览数据失败', e) } } // 加载趋势数据 async function loadTrend(days) { try { const res = await getDashboardTrend(days) if (res.success) { trendData.value = res.data || [] await nextTick() renderCharts() } } catch (e) { console.error('加载趋势数据失败', e) } } // 加载自定义范围数据 async function loadCustomRange() { if (!customStart.value || !customEnd.value) return loading.value = true try { const res = await getDashboardCustom(customStart.value, customEnd.value) if (res.success) { trendData.value = res.data || [] rangeDays.value = 0 await nextTick() renderCharts() } } catch (e) { console.error('加载自定义范围数据失败', e) } finally { loading.value = false } } // 加载知识库分析 async function loadKnowledgeAnalysis() { try { const res = await getDashboardKnowledge() if (res.success) { topHitDocuments.value = res.data?.topHitDocuments || [] missQuestions.value = res.data?.missQuestions || [] } } catch (e) { console.error('加载知识库分析数据失败', e) } } // 渲染 Chart.js 图表 async function renderCharts() { const { Chart, LineController, LineElement, PointElement, LinearScale, CategoryScale, Tooltip, Legend, Filler } = await import('chart.js') // 注册所需组件 Chart.register(LineController, LineElement, PointElement, LinearScale, CategoryScale, Tooltip, Legend, Filler) const labels = trendData.value.map(s => { const d = new Date(s.snapshotDate || s.snapshot_date) return `${d.getMonth() + 1}/${d.getDate()}` }) // === 对话量趋势图 === if (conversationChart) conversationChart.destroy() if (conversationChartRef.value) { const convData = trendData.value.map(s => s.conversationCount ?? s.conversation_count ?? 0) const msgData = trendData.value.map(s => s.messageCount ?? s.message_count ?? 0) conversationChart = new Chart(conversationChartRef.value, { type: 'line', data: { labels, datasets: [ { label: '对话数', data: convData, borderColor: '#3b82f6', backgroundColor: 'rgba(59,130,246,0.1)', fill: true, tension: 0.3, pointRadius: 4, pointHoverRadius: 6 }, { label: '消息数', data: msgData, borderColor: '#8b5cf6', backgroundColor: 'rgba(139,92,246,0.1)', fill: true, tension: 0.3, pointRadius: 4, pointHoverRadius: 6 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, padding: 16, font: { size: 12 } } } }, scales: { y: { beginAtZero: true, ticks: { font: { size: 11 } }, grid: { color: 'rgba(0,0,0,0.05)' } }, x: { ticks: { font: { size: 11 } }, grid: { display: false } } } } }) } // === 满意度趋势图(👍/👎 双线) === if (satisfactionChart) satisfactionChart.destroy() if (satisfactionChartRef.value) { const upData = trendData.value.map(s => s.thumbsUpCount ?? s.thumbs_up_count ?? 0) const downData = trendData.value.map(s => s.thumbsDownCount ?? s.thumbs_down_count ?? 0) satisfactionChart = new Chart(satisfactionChartRef.value, { type: 'line', data: { labels, datasets: [ { label: '👍 有帮助', data: upData, borderColor: '#10b981', backgroundColor: 'rgba(16,185,129,0.1)', fill: true, tension: 0.3, pointRadius: 4, pointHoverRadius: 6 }, { label: '👎 没帮助', data: downData, borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.1)', fill: true, tension: 0.3, pointRadius: 4, pointHoverRadius: 6 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, padding: 16, font: { size: 12 } } } }, scales: { y: { beginAtZero: true, ticks: { font: { size: 11 } }, grid: { color: 'rgba(0,0,0,0.05)' } }, x: { ticks: { font: { size: 11 } }, grid: { display: false } } } } }) } } // 销毁图表 function destroyCharts() { if (conversationChart) { conversationChart.destroy(); conversationChart = null } if (satisfactionChart) { satisfactionChart.destroy(); satisfactionChart = null } } onMounted(async () => { loading.value = true await Promise.all([ loadOverview(), loadTrend(7), loadKnowledgeAnalysis() ]) loading.value = false }) onBeforeUnmount(() => { destroyCharts() }) return { loading, rangeDays, customStart, customEnd, overview, trendData, topHitDocuments, missQuestions, conversationChartRef, satisfactionChartRef, satisfactionColor, formatPercent, formatDateShort, setRange, loadCustomRange } } }