/**
* 运营看板组件
* 指标卡片 + 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
}
}
}