diff --git a/frontend/src/components/analytics/RankBarList.vue b/frontend/src/components/analytics/RankBarList.vue new file mode 100644 index 0000000..b15b4e4 --- /dev/null +++ b/frontend/src/components/analytics/RankBarList.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/frontend/src/components/analytics/StatCard.vue b/frontend/src/components/analytics/StatCard.vue new file mode 100644 index 0000000..6faf493 --- /dev/null +++ b/frontend/src/components/analytics/StatCard.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/frontend/src/components/analytics/TrendChart.vue b/frontend/src/components/analytics/TrendChart.vue new file mode 100644 index 0000000..04f3ce8 --- /dev/null +++ b/frontend/src/components/analytics/TrendChart.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/frontend/src/composables/useAnalytics.js b/frontend/src/composables/useAnalytics.js new file mode 100644 index 0000000..e2dd894 --- /dev/null +++ b/frontend/src/composables/useAnalytics.js @@ -0,0 +1,74 @@ +import { ref } from 'vue' +import { request } from '@/api/client' + +export const RANGE_OPTIONS = [ + { code: '1h', label: '近1小时' }, + { code: '12h', label: '近12小时' }, + { code: '24h', label: '近24小时' }, + { code: '7d', label: '近7天' }, +] + +function emptyBusiness() { + return { + range: null, + kpis: { + question_count: 0, + active_user_count: 0, + prev_active_user_count: 0, + user_delta_pct: null, + avg_questions_per_user: 0, + avg_latency_ms: 0, + success_count: 0, + success_ratio: 0, + fail_count: 0, + fail_ratio: 0, + }, + trend: [], + top_questions: [], + } +} + +function emptyResources() { + return { + range: null, + kpis: { + document_count: 0, + total_tokens: 0, + total_pu: 0, + api_call_count: 0, + api_success_ratio: 0, + avg_api_latency_ms: 0, + }, + token_trend: [], + top_documents: [], + } +} + +export function useAnalytics() { + const business = ref(emptyBusiness()) + const resources = ref(emptyResources()) + const loading = ref(false) + const error = ref('') + + async function loadBusiness(rangeCode) { + business.value = await request(`/api/stats/dashboard/business?range=${encodeURIComponent(rangeCode)}`) + } + + async function loadResources(rangeCode) { + resources.value = await request(`/api/stats/dashboard/resources?range=${encodeURIComponent(rangeCode)}`) + } + + async function loadAll(rangeCode) { + loading.value = true + error.value = '' + try { + await Promise.all([loadBusiness(rangeCode), loadResources(rangeCode)]) + } catch (err) { + error.value = err?.message || String(err) + } finally { + loading.value = false + } + } + + return { business, resources, loading, error, loadAll, loadBusiness, loadResources } +} diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index a1982c1..574b446 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -42,7 +42,7 @@ const routes = [ { path: '/analytics', name: 'analytics', - component: () => import('@/views/PlaceholderView.vue'), + component: () => import('@/views/AnalyticsView.vue'), meta: { title: '数据分析', subtitle: '使用量、热门问题、命中率等数据看板。', diff --git a/frontend/src/views/AnalyticsView.vue b/frontend/src/views/AnalyticsView.vue new file mode 100644 index 0000000..bd0842e --- /dev/null +++ b/frontend/src/views/AnalyticsView.vue @@ -0,0 +1,403 @@ + + + + +