531 lines
13 KiB
Vue
531 lines
13 KiB
Vue
<template>
|
|
<div class="operation-report-page">
|
|
<div class="report-header">
|
|
<div>
|
|
<div class="title-line">
|
|
<span class="title-icon">
|
|
<i class="el-icon-s-data"></i>
|
|
</span>
|
|
<h2>运营报表</h2>
|
|
</div>
|
|
<p>查看机构下模型调用次数、Token 消耗和费用统计。</p>
|
|
</div>
|
|
<div class="header-actions">
|
|
<span class="range-badge">{{ filterTimeText }}</span>
|
|
<el-button size="small" icon="el-icon-refresh" class="refresh-btn" :loading="loading" @click="getReportList">
|
|
刷新
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-grid">
|
|
<div v-for="item in statCards" :key="item.label" class="stat-card" :class="item.type">
|
|
<div class="stat-card-head">
|
|
<div class="stat-title">{{ item.label }}</div>
|
|
<i :class="item.icon"></i>
|
|
</div>
|
|
<div class="stat-value">{{ item.value }}</div>
|
|
<div class="stat-desc">{{ item.desc }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<el-card shadow="never" class="filter-card">
|
|
<div class="filter-bar">
|
|
<div>
|
|
<h3>筛选条件</h3>
|
|
<p>按模型和时间范围查询运营用量。</p>
|
|
</div>
|
|
<el-form :inline="true" :model="query" class="filter-form">
|
|
<el-select v-model="query.range" size="small" placeholder="快捷范围" @change="handleRangeChange">
|
|
<el-option label="最近1小时" value="hour"></el-option>
|
|
<el-option label="今天" value="day"></el-option>
|
|
<el-option label="最近一周" value="week"></el-option>
|
|
</el-select>
|
|
<el-input
|
|
v-model="query.model"
|
|
size="small"
|
|
clearable
|
|
placeholder="搜索模型"
|
|
@keyup.enter.native="handleSearch"
|
|
@clear="handleSearch"
|
|
></el-input>
|
|
<el-input
|
|
v-model="query.customerid"
|
|
size="small"
|
|
clearable
|
|
placeholder="搜索用户ID"
|
|
@keyup.enter.native="handleSearch"
|
|
@clear="handleSearch"
|
|
></el-input>
|
|
<el-date-picker
|
|
v-model="dateRange"
|
|
type="daterange"
|
|
size="small"
|
|
value-format="yyyy-MM-dd"
|
|
range-separator="至"
|
|
start-placeholder="开始日期"
|
|
end-placeholder="结束日期"
|
|
@change="handleDateChange"
|
|
></el-date-picker>
|
|
<el-button size="small" type="primary" :loading="loading" @click="handleSearch">查询</el-button>
|
|
<el-button size="small" @click="handleReset">重置</el-button>
|
|
</el-form>
|
|
</div>
|
|
</el-card>
|
|
|
|
<el-card shadow="never" class="table-card">
|
|
<div class="table-title">
|
|
<div>
|
|
<h3>用量明细</h3>
|
|
<p>共 {{ total }} 条记录</p>
|
|
</div>
|
|
<span>{{ timeText }}</span>
|
|
</div>
|
|
|
|
<el-table v-loading="loading" :data="reportList" class="report-table" style="width: 100%">
|
|
<el-table-column prop="customer_name" label="客户名称" min-width="120" show-overflow-tooltip></el-table-column>
|
|
<el-table-column prop="customerid" label="客户ID" min-width="240" show-overflow-tooltip></el-table-column>
|
|
|
|
<el-table-column prop="model" label="模型" min-width="140" show-overflow-tooltip>
|
|
<template slot-scope="scope">
|
|
<el-tag size="mini" type="info">{{ scope.row.model || '-' }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="request_count" label="请求次数" width="100"></el-table-column>
|
|
<el-table-column prop="prompt_tokens" label="输入Token" min-width="110">
|
|
<template slot-scope="scope">{{ formatNumber(scope.row.prompt_tokens) }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="completion_tokens" label="输出Token" min-width="110">
|
|
<template slot-scope="scope">{{ formatNumber(scope.row.completion_tokens) }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="total_tokens" label="总Token" min-width="110">
|
|
<template slot-scope="scope">{{ formatNumber(scope.row.total_tokens) }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="amount" label="费用(元)" min-width="110">
|
|
<template slot-scope="scope">¥{{ formatAmount(scope.row.amount) }}</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column prop="last_usage_time" label="最近使用时间" min-width="160" show-overflow-tooltip></el-table-column>
|
|
</el-table>
|
|
|
|
<div class="pager-wrap">
|
|
<el-pagination
|
|
background
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
:total="total"
|
|
:page-size="query.page_size"
|
|
:current-page="query.current_page"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
@size-change="handleSizeChange"
|
|
@current-change="handlePageChange"
|
|
></el-pagination>
|
|
</div>
|
|
</el-card>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { reqOperationReport } from '@/api/model/model'
|
|
|
|
export default {
|
|
name: 'OperationReport',
|
|
data() {
|
|
return {
|
|
loading: false,
|
|
dateRange: [],
|
|
summary: {
|
|
request_count: 0,
|
|
prompt_tokens: 0,
|
|
completion_tokens: 0,
|
|
total_tokens: 0,
|
|
amount: 0
|
|
},
|
|
reportList: [],
|
|
total: 0,
|
|
timeText: '',
|
|
query: {
|
|
range: 'week',
|
|
start_time: '',
|
|
end_time: '',
|
|
username: '',
|
|
model: '',
|
|
customerid: '',
|
|
current_page: 1,
|
|
page_size: 20
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
statCards() {
|
|
return [
|
|
{ label: '请求次数', value: this.formatNumber(this.summary.request_count), desc: '当前筛选范围', type: 'purple', icon: 'el-icon-s-promotion' },
|
|
{ label: 'Token消耗', value: this.formatNumber(this.summary.total_tokens), desc: `输入 ${this.formatNumber(this.summary.prompt_tokens)} / 输出 ${this.formatNumber(this.summary.completion_tokens)}`, type: 'green', icon: 'el-icon-coin' },
|
|
{ label: 'Token总费用', value: `¥${this.formatAmount(this.summary.amount)}`, desc: '按调用记录汇总', type: 'orange', icon: 'el-icon-wallet' }
|
|
]
|
|
},
|
|
filterTimeText() {
|
|
if (this.query.start_time && this.query.end_time) {
|
|
return `${this.query.start_time} 至 ${this.query.end_time}`
|
|
}
|
|
const labelMap = {
|
|
hour: '最近1小时',
|
|
day: '今天',
|
|
week: '最近一周'
|
|
}
|
|
return labelMap[this.query.range] || '当前范围'
|
|
}
|
|
},
|
|
created() {
|
|
this.getReportList()
|
|
},
|
|
methods: {
|
|
async getReportList() {
|
|
this.loading = true
|
|
try {
|
|
const res = await reqOperationReport(this.query)
|
|
if (res && res.status === true) {
|
|
const data = res.data || {}
|
|
this.summary = {
|
|
...this.summary,
|
|
...(data.summary || {})
|
|
}
|
|
this.reportList = Array.isArray(data.items) ? data.items : []
|
|
this.total = Number(data.total_count || 0)
|
|
this.query.current_page = Number(data.current_page || this.query.current_page)
|
|
this.query.page_size = Number(data.page_size || this.query.page_size)
|
|
this.timeText = data.start_time && data.end_time ? `${data.start_time} 至 ${data.end_time}` : ''
|
|
}
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
handleDateChange(value) {
|
|
if (value && value.length === 2) {
|
|
this.query.start_time = value[0]
|
|
this.query.end_time = value[1]
|
|
} else {
|
|
this.query.start_time = ''
|
|
this.query.end_time = ''
|
|
}
|
|
this.query.current_page = 1
|
|
this.getReportList()
|
|
},
|
|
handleRangeChange() {
|
|
this.query.current_page = 1
|
|
this.getReportList()
|
|
},
|
|
handleSearch() {
|
|
this.query.current_page = 1
|
|
this.getReportList()
|
|
},
|
|
handleReset() {
|
|
this.dateRange = []
|
|
this.query = {
|
|
range: 'week',
|
|
start_time: '',
|
|
end_time: '',
|
|
username: '',
|
|
model: '',
|
|
customerid: '',
|
|
current_page: 1,
|
|
page_size: 20
|
|
}
|
|
this.getReportList()
|
|
},
|
|
handleSizeChange(size) {
|
|
this.query.page_size = size
|
|
this.query.current_page = 1
|
|
this.getReportList()
|
|
},
|
|
handlePageChange(page) {
|
|
this.query.current_page = page
|
|
this.getReportList()
|
|
},
|
|
formatNumber(value) {
|
|
return Number(value || 0).toLocaleString()
|
|
},
|
|
formatAmount(value) {
|
|
return Number(value || 0).toFixed(6).replace(/0+$/, '').replace(/\.$/, '.00')
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.operation-report-page {
|
|
min-height: 100vh;
|
|
padding: 24px;
|
|
background:
|
|
radial-gradient(circle at top left, rgba(64, 158, 255, 0.16), transparent 34%),
|
|
linear-gradient(180deg, #f3f7ff 0%, #f7f9fc 48%, #ffffff 100%);
|
|
}
|
|
|
|
.report-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 20px 22px;
|
|
margin-bottom: 20px;
|
|
overflow: hidden;
|
|
color: #ffffff;
|
|
background: linear-gradient(135deg, #1e6fff 0%, #409eff 48%, #7c3aed 100%);
|
|
border-radius: 18px;
|
|
box-shadow: 0 14px 34px rgba(64, 158, 255, 0.22);
|
|
|
|
h2 {
|
|
margin: 0;
|
|
color: #ffffff;
|
|
font-size: 24px;
|
|
}
|
|
|
|
p {
|
|
margin: 6px 0 0;
|
|
color: rgba(255, 255, 255, 0.82);
|
|
}
|
|
}
|
|
|
|
.title-line {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.title-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 36px;
|
|
height: 36px;
|
|
color: #ffffff;
|
|
font-size: 18px;
|
|
background: rgba(255, 255, 255, 0.18);
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.range-badge {
|
|
padding: 7px 12px;
|
|
color: #ffffff;
|
|
font-size: 12px;
|
|
background: rgba(255, 255, 255, 0.16);
|
|
border: 1px solid rgba(255, 255, 255, 0.24);
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.refresh-btn {
|
|
color: #1e6fff;
|
|
border: none;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.stat-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.stat-card {
|
|
position: relative;
|
|
overflow: hidden;
|
|
padding: 20px;
|
|
background: #ffffff;
|
|
border: 1px solid #edf1f7;
|
|
border-radius: 16px;
|
|
box-shadow: 0 8px 24px rgba(31, 45, 61, 0.04);
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
|
|
&:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 12px 28px rgba(31, 45, 61, 0.08);
|
|
}
|
|
|
|
&:before {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
width: 86px;
|
|
height: 86px;
|
|
content: '';
|
|
background: rgba(255, 255, 255, 0.52);
|
|
border-radius: 0 0 0 86px;
|
|
}
|
|
|
|
&.purple {
|
|
background: linear-gradient(135deg, #f5f3ff 0%, #ffffff 100%);
|
|
}
|
|
|
|
&.green {
|
|
background: linear-gradient(135deg, #f0fdf4 0%, #ffffff 100%);
|
|
}
|
|
|
|
&.orange {
|
|
background: linear-gradient(135deg, #fff7ed 0%, #ffffff 100%);
|
|
}
|
|
}
|
|
|
|
.stat-card-head {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
|
|
i {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 34px;
|
|
height: 34px;
|
|
color: #409eff;
|
|
font-size: 18px;
|
|
background: rgba(64, 158, 255, 0.12);
|
|
border-radius: 12px;
|
|
}
|
|
}
|
|
|
|
.stat-title {
|
|
color: #5f6b7d;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 34px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
}
|
|
|
|
.stat-desc {
|
|
margin-top: 10px;
|
|
color: #98a2b3;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.purple .stat-value {
|
|
color: #7f56d9;
|
|
}
|
|
|
|
.green .stat-value {
|
|
color: #16a34a;
|
|
}
|
|
|
|
.orange .stat-value {
|
|
color: #ea580c;
|
|
}
|
|
|
|
.filter-card,
|
|
.table-card {
|
|
margin-bottom: 16px;
|
|
background: #ffffff;
|
|
border: 1px solid #edf1f7;
|
|
border-radius: 16px;
|
|
box-shadow: 0 10px 26px rgba(31, 45, 61, 0.05);
|
|
}
|
|
|
|
.filter-bar {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 20px;
|
|
|
|
h3 {
|
|
margin: 0 0 8px;
|
|
color: #1f2d3d;
|
|
font-size: 16px;
|
|
}
|
|
|
|
p {
|
|
margin: 0;
|
|
color: #909399;
|
|
font-size: 13px;
|
|
}
|
|
}
|
|
|
|
.filter-form {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 8px;
|
|
background: #f6f8fb;
|
|
border: 1px solid #edf1f7;
|
|
border-radius: 12px;
|
|
|
|
.el-select {
|
|
width: 110px;
|
|
}
|
|
|
|
.el-input {
|
|
width: 160px;
|
|
}
|
|
}
|
|
|
|
.table-title {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
margin-bottom: 14px;
|
|
|
|
h3 {
|
|
margin: 0 0 6px;
|
|
color: #1f2d3d;
|
|
font-size: 16px;
|
|
}
|
|
|
|
p,
|
|
span {
|
|
margin: 0;
|
|
color: #98a2b3;
|
|
font-size: 12px;
|
|
}
|
|
}
|
|
|
|
.pager-wrap {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.report-table {
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
|
|
.el-table__header th {
|
|
color: #475467;
|
|
font-weight: 700;
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.el-table__row:hover > td {
|
|
background: #f6faff;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.stat-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.report-header,
|
|
.table-title,
|
|
.filter-bar {
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.filter-form {
|
|
align-items: flex-start;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
|
|
.el-date-editor {
|
|
width: 100%;
|
|
}
|
|
}
|
|
}
|
|
</style>
|