Some checks failed
continuous-integration/drone/push Build is failing
- Add Vue 3 frontend with Element Plus - Implement login, dashboard, tenant management - Add app configuration, logs viewer, stats pages - Add user management for admins - Update Drone CI to build and deploy frontend - Frontend ports: 3001 (test), 4001 (prod)
182 lines
5.5 KiB
Vue
182 lines
5.5 KiB
Vue
<script setup>
|
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
|
import * as echarts from 'echarts'
|
|
import api from '@/api'
|
|
import dayjs from 'dayjs'
|
|
|
|
const loading = ref(false)
|
|
const query = reactive({
|
|
tenant_id: '',
|
|
app_code: '',
|
|
start_date: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
|
|
end_date: dayjs().format('YYYY-MM-DD')
|
|
})
|
|
|
|
const stats = ref({
|
|
total_calls: 0,
|
|
total_tokens: 0,
|
|
total_cost: 0
|
|
})
|
|
|
|
const dailyData = ref([])
|
|
const chartRef = ref(null)
|
|
let chartInstance = null
|
|
|
|
async function fetchStats() {
|
|
loading.value = true
|
|
try {
|
|
const res = await api.get('/api/stats/daily', { params: query })
|
|
dailyData.value = res.data.items || []
|
|
|
|
// 计算汇总
|
|
let totalCalls = 0, totalTokens = 0, totalCost = 0
|
|
dailyData.value.forEach(item => {
|
|
totalCalls += item.ai_calls || 0
|
|
totalTokens += item.ai_tokens || 0
|
|
totalCost += parseFloat(item.ai_cost) || 0
|
|
})
|
|
stats.value = { total_calls: totalCalls, total_tokens: totalTokens, total_cost: totalCost }
|
|
|
|
updateChart()
|
|
} catch (e) {
|
|
console.error(e)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function updateChart() {
|
|
if (!chartInstance) return
|
|
|
|
const dates = dailyData.value.map(d => d.stat_date)
|
|
const calls = dailyData.value.map(d => d.ai_calls || 0)
|
|
const tokens = dailyData.value.map(d => d.ai_tokens || 0)
|
|
|
|
chartInstance.setOption({
|
|
title: { text: 'AI 调用趋势' },
|
|
tooltip: { trigger: 'axis' },
|
|
legend: { data: ['调用次数', 'Token 消耗'], top: 30 },
|
|
grid: { left: '3%', right: '4%', bottom: '3%', top: 80, containLabel: true },
|
|
xAxis: { type: 'category', data: dates },
|
|
yAxis: [
|
|
{ type: 'value', name: '调用次数' },
|
|
{ type: 'value', name: 'Token' }
|
|
],
|
|
series: [
|
|
{
|
|
name: '调用次数',
|
|
type: 'bar',
|
|
data: calls,
|
|
itemStyle: { color: '#409eff' }
|
|
},
|
|
{
|
|
name: 'Token 消耗',
|
|
type: 'line',
|
|
yAxisIndex: 1,
|
|
data: tokens,
|
|
smooth: true,
|
|
itemStyle: { color: '#67c23a' }
|
|
}
|
|
]
|
|
})
|
|
}
|
|
|
|
function initChart() {
|
|
if (!chartRef.value) return
|
|
chartInstance = echarts.init(chartRef.value)
|
|
}
|
|
|
|
function handleSearch() {
|
|
fetchStats()
|
|
}
|
|
|
|
function handleResize() {
|
|
chartInstance?.resize()
|
|
}
|
|
|
|
onMounted(() => {
|
|
initChart()
|
|
fetchStats()
|
|
window.addEventListener('resize', handleResize)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', handleResize)
|
|
chartInstance?.dispose()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="page-container">
|
|
<div class="page-header">
|
|
<div class="title">统计分析</div>
|
|
</div>
|
|
|
|
<!-- 搜索栏 -->
|
|
<div class="search-bar">
|
|
<el-input v-model="query.tenant_id" placeholder="租户ID" clearable style="width: 160px" />
|
|
<el-select v-model="query.app_code" placeholder="应用" clearable style="width: 120px">
|
|
<el-option label="全部" value="" />
|
|
<el-option label="tools" value="tools" />
|
|
<el-option label="interview" value="interview" />
|
|
</el-select>
|
|
<el-date-picker
|
|
v-model="query.start_date"
|
|
type="date"
|
|
placeholder="开始日期"
|
|
value-format="YYYY-MM-DD"
|
|
style="width: 150px"
|
|
/>
|
|
<span style="color: #909399">至</span>
|
|
<el-date-picker
|
|
v-model="query.end_date"
|
|
type="date"
|
|
placeholder="结束日期"
|
|
value-format="YYYY-MM-DD"
|
|
style="width: 150px"
|
|
/>
|
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
|
</div>
|
|
|
|
<!-- 统计卡片 -->
|
|
<div class="stat-cards">
|
|
<div class="stat-card">
|
|
<div class="stat-title">AI 调用总次数</div>
|
|
<div class="stat-value">{{ stats.total_calls.toLocaleString() }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-title">Token 消耗总量</div>
|
|
<div class="stat-value">{{ stats.total_tokens.toLocaleString() }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-title">累计费用</div>
|
|
<div class="stat-value">¥{{ stats.total_cost.toFixed(2) }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 图表 -->
|
|
<div style="background: #fff; border-radius: 8px; padding: 20px; margin-bottom: 20px">
|
|
<div ref="chartRef" style="height: 350px" v-loading="loading"></div>
|
|
</div>
|
|
|
|
<!-- 数据表格 -->
|
|
<div style="background: #fff; border-radius: 8px; padding: 20px">
|
|
<h4 style="margin: 0 0 16px">日统计明细</h4>
|
|
<el-table :data="dailyData" style="width: 100%" v-loading="loading">
|
|
<el-table-column prop="stat_date" label="日期" width="120" />
|
|
<el-table-column prop="tenant_id" label="租户ID" width="120" />
|
|
<el-table-column prop="app_code" label="应用" width="100" />
|
|
<el-table-column prop="ai_calls" label="调用次数" width="120">
|
|
<template #default="{ row }">{{ (row.ai_calls || 0).toLocaleString() }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="ai_tokens" label="Token 消耗" width="150">
|
|
<template #default="{ row }">{{ (row.ai_tokens || 0).toLocaleString() }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="ai_cost" label="费用" width="100">
|
|
<template #default="{ row }">¥{{ parseFloat(row.ai_cost || 0).toFixed(4) }}</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</div>
|
|
</div>
|
|
</template>
|