235 lines
5.9 KiB
Vue
235 lines
5.9 KiB
Vue
<template>
|
|
<div class="logs-page">
|
|
<div class="stats-bar">
|
|
<el-card>
|
|
<div class="stat-item">
|
|
<span class="label">消耗额度:</span>
|
|
<span class="value">${{ stats.total_cost?.toFixed(2) || '0.00' }}</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="label">RPM:</span>
|
|
<span class="value">{{ stats.rpm || '0' }}</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="label">TPM:</span>
|
|
<span class="value">{{ stats.tpm || '0' }}</span>
|
|
</div>
|
|
</el-card>
|
|
</div>
|
|
|
|
<div class="filter-bar">
|
|
<el-date-picker
|
|
v-model="dateRange"
|
|
type="datetimerange"
|
|
range-separator="至"
|
|
start-placeholder="开始日期"
|
|
end-placeholder="结束日期"
|
|
format="YYYY-MM-DD HH:mm:ss"
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
/>
|
|
<el-input
|
|
v-model="filters.token_name"
|
|
placeholder="搜索令牌名称"
|
|
style="width: 200px"
|
|
clearable
|
|
>
|
|
<template #prefix>
|
|
<el-icon><Search /></el-icon>
|
|
</template>
|
|
</el-input>
|
|
<el-input
|
|
v-model="filters.model_name"
|
|
placeholder="搜索模型名称"
|
|
style="width: 200px"
|
|
clearable
|
|
>
|
|
<template #prefix>
|
|
<el-icon><Search /></el-icon>
|
|
</template>
|
|
</el-input>
|
|
<el-select v-model="filters.group" placeholder="分组" style="width: 150px" clearable>
|
|
<el-option label="全部" value="" />
|
|
<el-option label="Claude Code 官方编程模型" value="Claude Code 官方编程模型" />
|
|
<el-option label="Claude Code 企业专线" value="Claude Code 企业专线" />
|
|
<el-option label="备份服务器" value="备份服务器" />
|
|
</el-select>
|
|
<el-button type="primary" @click="loadLogs">查询</el-button>
|
|
<el-button @click="resetFilters">重置</el-button>
|
|
<el-button>列设置</el-button>
|
|
<el-button>紧凑列表</el-button>
|
|
</div>
|
|
|
|
<el-table
|
|
v-loading="loading"
|
|
:data="logs"
|
|
style="width: 100%"
|
|
empty-text="搜索无结果"
|
|
>
|
|
<el-table-column prop="created_at" label="时间" width="180" />
|
|
<el-table-column prop="token_name" label="令牌" />
|
|
<el-table-column prop="group" label="分组" />
|
|
<el-table-column prop="log_type" label="类型" />
|
|
<el-table-column prop="model" label="模型" />
|
|
<el-table-column prop="time_display" label="用时/首字" />
|
|
<el-table-column prop="input_tokens" label="输入" />
|
|
<el-table-column prop="output_tokens" label="输出" />
|
|
<el-table-column prop="cost" label="花费" width="100">
|
|
<template #default="{ row }">
|
|
${{ row.cost?.toFixed(4) || '0.0000' }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="ip_address" label="IP" />
|
|
<el-table-column label="详情" width="100">
|
|
<template #default="{ row }">
|
|
<el-button size="small" link @click="viewDetails(row)">查看</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<el-pagination
|
|
v-model:current-page="pagination.page"
|
|
v-model:page-size="pagination.per_page"
|
|
:total="pagination.total"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
@size-change="loadLogs"
|
|
@current-change="loadLogs"
|
|
style="margin-top: 20px; justify-content: flex-end"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, onMounted } from 'vue'
|
|
import { logsApi } from '@/api/logs'
|
|
import { ElMessage } from 'element-plus'
|
|
import { Search } from '@element-plus/icons-vue'
|
|
import dayjs from 'dayjs'
|
|
|
|
const loading = ref(false)
|
|
const logs = ref([])
|
|
const stats = reactive({
|
|
total_cost: 0,
|
|
rpm: 0,
|
|
tpm: 0
|
|
})
|
|
|
|
const dateRange = ref([
|
|
dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
|
dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
])
|
|
|
|
const filters = reactive({
|
|
token_name: '',
|
|
model_name: '',
|
|
group: ''
|
|
})
|
|
|
|
const pagination = reactive({
|
|
page: 1,
|
|
per_page: 20,
|
|
total: 0
|
|
})
|
|
|
|
const loadLogs = async () => {
|
|
loading.value = true
|
|
try {
|
|
const params = {
|
|
page: pagination.page,
|
|
per_page: pagination.per_page,
|
|
start_date: dateRange.value?.[0],
|
|
end_date: dateRange.value?.[1],
|
|
...filters
|
|
}
|
|
|
|
const response = await logsApi.getUsageLogs(params)
|
|
logs.value = response.logs
|
|
pagination.total = response.pagination.total
|
|
|
|
if (response.stats) {
|
|
Object.assign(stats, response.stats)
|
|
}
|
|
} catch (error) {
|
|
ElMessage.error('加载日志失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const resetFilters = () => {
|
|
dateRange.value = [
|
|
dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
|
dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
]
|
|
filters.token_name = ''
|
|
filters.model_name = ''
|
|
filters.group = ''
|
|
pagination.page = 1
|
|
loadLogs()
|
|
}
|
|
|
|
const viewDetails = (row) => {
|
|
// 实现详情查看
|
|
ElMessage.info('查看详情功能待实现')
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadLogs()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.logs-page {
|
|
background: rgba(255, 255, 255, 0.96);
|
|
padding: 20px;
|
|
border-radius: 16px;
|
|
border: 1px solid var(--border-color);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.stats-bar {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.stat-item {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-right: 30px;
|
|
}
|
|
|
|
.label {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.value {
|
|
font-weight: bold;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.filter-bar {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
padding: 12px;
|
|
border-radius: 12px;
|
|
background: rgba(91, 140, 255, 0.06);
|
|
border: 1px dashed rgba(91, 140, 255, 0.25);
|
|
}
|
|
|
|
.logs-page :deep(.el-table) {
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.logs-page :deep(.el-table th.el-table__cell) {
|
|
background: rgba(91, 140, 255, 0.08);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.logs-page :deep(.el-table__cell) {
|
|
border-color: rgba(230, 235, 245, 0.8);
|
|
}
|
|
</style>
|