Compare commits
No commits in common. "35d635f0ab116fd5d006cc2d2177d91318cc6496" and "0e702cce9acb77d65df6cc8e2bd4ad1919d78db1" have entirely different histories.
35d635f0ab
...
0e702cce9a
@ -149,13 +149,4 @@ export const reqModelInfoConfigList = (params = {}) => {
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
params
|
params
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// 运营报表
|
|
||||||
export const reqOperationReport = (params = {}) => {
|
|
||||||
return request({
|
|
||||||
url: '/cntoai/model_usage_admin_report.dspy',
|
|
||||||
method: 'post',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
@ -539,29 +539,29 @@ export const asyncRoutes = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
// Token用量 - 一级菜单(所有登录用户都能看到)
|
// Token用量 - 一级菜单(所有登录用户都能看到)
|
||||||
{
|
// {
|
||||||
path: "/tokenUsage",
|
// path: "/tokenUsage",
|
||||||
component: Layout,
|
// component: Layout,
|
||||||
meta: {
|
// meta: {
|
||||||
title: "Token用量",
|
// title: "Token用量",
|
||||||
fullPath: "/tokenUsage",
|
// fullPath: "/tokenUsage",
|
||||||
noCache: true,
|
// noCache: true,
|
||||||
icon: "el-icon-data-line"
|
// icon: "el-icon-data-line"
|
||||||
},
|
// },
|
||||||
children: [
|
// children: [
|
||||||
{
|
// {
|
||||||
path: "",
|
// path: "",
|
||||||
component: () => import('@/views/tokenUsage/index.vue'),
|
// component: () => import('@/views/tokenUsage/index.vue'),
|
||||||
name: 'TokenUsage',
|
// name: 'TokenUsage',
|
||||||
meta: {
|
// meta: {
|
||||||
title: "Token用量",
|
// title: "Token用量",
|
||||||
fullPath: "/tokenUsage",
|
// fullPath: "/tokenUsage",
|
||||||
noCache: true,
|
// noCache: true,
|
||||||
icon: "el-icon-data-line"
|
// icon: "el-icon-data-line"
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
]
|
// ]
|
||||||
},
|
// },
|
||||||
// 模型体验
|
// 模型体验
|
||||||
{
|
{
|
||||||
path: "/modelExperience",
|
path: "/modelExperience",
|
||||||
|
|||||||
@ -2,529 +2,196 @@
|
|||||||
<div class="operation-report-page">
|
<div class="operation-report-page">
|
||||||
<div class="report-header">
|
<div class="report-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="title-line">
|
<h2>运营报表</h2>
|
||||||
<span class="title-icon">
|
<p>模型使用与计费数据概览</p>
|
||||||
<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>
|
||||||
|
<el-button size="small" icon="el-icon-document" @click="exportReport">导出报表</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat-grid">
|
<div class="stat-grid">
|
||||||
<div v-for="item in statCards" :key="item.label" class="stat-card" :class="item.type">
|
<div class="stat-card purple">
|
||||||
<div class="stat-card-head">
|
<div class="stat-title">活跃用户</div>
|
||||||
<div class="stat-title">{{ item.label }}</div>
|
<div class="stat-value">{{ statCards.activeUsers }}</div>
|
||||||
<i :class="item.icon"></i>
|
</div>
|
||||||
</div>
|
<div class="stat-card green">
|
||||||
<div class="stat-value">{{ item.value }}</div>
|
<div class="stat-title">Token消耗</div>
|
||||||
<div class="stat-desc">{{ item.desc }}</div>
|
<div class="stat-value">{{ statCards.tokenUsage }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card orange">
|
||||||
|
<div class="stat-title">Tokens总费用</div>
|
||||||
|
<div class="stat-value">¥{{ statCards.totalFee }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-card shadow="never" class="filter-card">
|
<el-card shadow="never" class="filter-card">
|
||||||
<div class="filter-bar">
|
<el-form :inline="true" :model="filterForm">
|
||||||
<div>
|
<el-form-item label="用户">
|
||||||
<h3>筛选条件</h3>
|
<el-input v-model="filterForm.userName" placeholder="搜索用户名" clearable />
|
||||||
<p>按模型和时间范围查询运营用量。</p>
|
</el-form-item>
|
||||||
</div>
|
<el-form-item label="模型">
|
||||||
<el-form :inline="true" :model="query" class="filter-form">
|
<el-select v-model="filterForm.modelName" placeholder="全部模型" clearable>
|
||||||
<el-select v-model="query.range" size="small" placeholder="快捷范围" @change="handleRangeChange">
|
<el-option v-for="item in modelOptions" :key="item" :label="item" :value="item" />
|
||||||
<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-select>
|
||||||
<el-input
|
</el-form-item>
|
||||||
v-model="query.model"
|
<el-form-item label="支付方式">
|
||||||
size="small"
|
<el-select v-model="filterForm.paymentMethod" placeholder="全部" clearable>
|
||||||
clearable
|
<el-option label="支付宝" value="支付宝" />
|
||||||
placeholder="搜索模型"
|
<el-option label="微信支付" value="微信支付" />
|
||||||
@keyup.enter.native="handleSearch"
|
</el-select>
|
||||||
@clear="handleSearch"
|
</el-form-item>
|
||||||
></el-input>
|
<el-form-item label="使用时间">
|
||||||
<el-input
|
<el-date-picker v-model="filterForm.date" type="date" placeholder="年/月/日" value-format="yyyy-MM-dd" />
|
||||||
v-model="query.customerid"
|
</el-form-item>
|
||||||
size="small"
|
<el-form-item>
|
||||||
clearable
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
placeholder="搜索用户ID"
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
@keyup.enter.native="handleSearch"
|
</el-form-item>
|
||||||
@clear="handleSearch"
|
</el-form>
|
||||||
></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>
|
||||||
|
|
||||||
<el-card shadow="never" class="table-card">
|
<el-card shadow="never" class="table-card">
|
||||||
<div class="table-title">
|
<el-table :data="pagedList" style="width: 100%">
|
||||||
<div>
|
<el-table-column type="index" label="序号" width="70" />
|
||||||
<h3>用量明细</h3>
|
<el-table-column prop="userId" label="用户ID" min-width="110" />
|
||||||
<p>共 {{ total }} 条记录</p>
|
<el-table-column prop="userName" label="用户名" min-width="100" />
|
||||||
</div>
|
<el-table-column prop="modelName" label="使用模型" min-width="140">
|
||||||
<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">
|
<template slot-scope="scope">
|
||||||
<el-tag size="mini" type="info">{{ scope.row.model || '-' }}</el-tag>
|
<el-tag type="info" size="mini">{{ scope.row.modelName }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="request_count" label="请求次数" width="100"></el-table-column>
|
<el-table-column prop="useTime" label="使用时间" min-width="160" />
|
||||||
<el-table-column prop="prompt_tokens" label="输入Token" min-width="110">
|
<el-table-column prop="inputToken" label="输入TOKEN" min-width="110" />
|
||||||
<template slot-scope="scope">{{ formatNumber(scope.row.prompt_tokens) }}</template>
|
<el-table-column prop="outputToken" label="输出TOKEN" min-width="110" />
|
||||||
|
<el-table-column prop="tokenCost" label="TOKEN费用(元)" min-width="120" />
|
||||||
|
<el-table-column prop="paymentMethod" label="支付方式" min-width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag size="mini" :type="scope.row.paymentMethod === '支付宝' ? 'primary' : 'success'">
|
||||||
|
{{ scope.row.paymentMethod }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="completion_tokens" label="输出Token" min-width="110">
|
<el-table-column prop="balance" label="账户余额(元)" min-width="120" />
|
||||||
<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>
|
</el-table>
|
||||||
|
|
||||||
<div class="pager-wrap">
|
<div class="pager-wrap">
|
||||||
<el-pagination
|
<el-pagination
|
||||||
background
|
background
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="prev, pager, next, total"
|
||||||
:total="total"
|
:total="filteredList.length"
|
||||||
:page-size="query.page_size"
|
:page-size="pageSize"
|
||||||
:current-page="query.current_page"
|
:current-page.sync="currentPage"
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
|
||||||
@size-change="handleSizeChange"
|
|
||||||
@current-change="handlePageChange"
|
@current-change="handlePageChange"
|
||||||
></el-pagination>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { reqOperationReport } from '@/api/model/model'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'OperationReport',
|
name: "OperationReport",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
pageSize: 10,
|
||||||
dateRange: [],
|
currentPage: 1,
|
||||||
summary: {
|
filterForm: {
|
||||||
request_count: 0,
|
userName: "",
|
||||||
prompt_tokens: 0,
|
modelName: "",
|
||||||
completion_tokens: 0,
|
paymentMethod: "",
|
||||||
total_tokens: 0,
|
date: ""
|
||||||
amount: 0
|
|
||||||
},
|
},
|
||||||
reportList: [],
|
reportList: [
|
||||||
total: 0,
|
{ userId: "U100001", userName: "张明远", modelName: "MiniMax-M2.7", useTime: "2026-04-20 14:32:18", inputToken: "1,520", outputToken: "2,380", tokenCost: "¥0.047", paymentMethod: "支付宝", balance: "¥87.53" },
|
||||||
timeText: '',
|
{ userId: "U100002", userName: "李恩涵", modelName: "DeepSeek-V3.2", useTime: "2026-04-20 14:28:45", inputToken: "3,200", outputToken: "4,500", tokenCost: "¥0.012", paymentMethod: "微信支付", balance: "¥32.18" },
|
||||||
query: {
|
{ userId: "U100003", userName: "王建国", modelName: "GLM-5.1", useTime: "2026-04-20 14:15:33", inputToken: "890", outputToken: "1,200", tokenCost: "¥0.017", paymentMethod: "支付宝", balance: "¥185.42" },
|
||||||
range: 'week',
|
{ userId: "U100004", userName: "陈晓鸽", modelName: "Qwen3.5-72B", useTime: "2026-04-20 13:58:21", inputToken: "2,100", outputToken: "3,500", tokenCost: "¥0.063", paymentMethod: "支付宝", balance: "¥56.91" },
|
||||||
start_time: '',
|
{ userId: "U100005", userName: "赵伟", modelName: "GPT-4", useTime: "2026-04-20 13:45:10", inputToken: "4,500", outputToken: "6,800", tokenCost: "¥0.576", paymentMethod: "微信支付", balance: "¥312.45" },
|
||||||
end_time: '',
|
{ userId: "U100006", userName: "刘芳", modelName: "ERNIE-4.5-Turbo", useTime: "2026-04-20 13:30:05", inputToken: "1,800", outputToken: "2,200", tokenCost: "¥0.042", paymentMethod: "支付宝", balance: "¥68.77" }
|
||||||
username: '',
|
]
|
||||||
model: '',
|
};
|
||||||
customerid: '',
|
|
||||||
current_page: 1,
|
|
||||||
page_size: 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
statCards() {
|
statCards() {
|
||||||
return [
|
return {
|
||||||
{ label: '请求次数', value: this.formatNumber(this.summary.request_count), desc: '当前筛选范围', type: 'purple', icon: 'el-icon-s-promotion' },
|
activeUsers: "1,286",
|
||||||
{ 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' },
|
tokenUsage: "3.2M",
|
||||||
{ label: 'Token总费用', value: `¥${this.formatAmount(this.summary.amount)}`, desc: '按调用记录汇总', type: 'orange', icon: 'el-icon-wallet' }
|
totalFee: "38,642"
|
||||||
]
|
};
|
||||||
},
|
},
|
||||||
filterTimeText() {
|
modelOptions() {
|
||||||
if (this.query.start_time && this.query.end_time) {
|
return [...new Set(this.reportList.map(item => item.modelName))];
|
||||||
return `${this.query.start_time} 至 ${this.query.end_time}`
|
},
|
||||||
}
|
filteredList() {
|
||||||
const labelMap = {
|
return this.reportList.filter(item => {
|
||||||
hour: '最近1小时',
|
const matchUser = !this.filterForm.userName || item.userName.includes(this.filterForm.userName);
|
||||||
day: '今天',
|
const matchModel = !this.filterForm.modelName || item.modelName === this.filterForm.modelName;
|
||||||
week: '最近一周'
|
const matchPay = !this.filterForm.paymentMethod || item.paymentMethod === this.filterForm.paymentMethod;
|
||||||
}
|
const matchDate = !this.filterForm.date || item.useTime.startsWith(this.filterForm.date);
|
||||||
return labelMap[this.query.range] || '当前范围'
|
return matchUser && matchModel && matchPay && matchDate;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pagedList() {
|
||||||
|
const start = (this.currentPage - 1) * this.pageSize;
|
||||||
|
return this.filteredList.slice(start, start + this.pageSize);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
this.getReportList()
|
|
||||||
},
|
|
||||||
methods: {
|
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() {
|
handleSearch() {
|
||||||
this.query.current_page = 1
|
this.currentPage = 1;
|
||||||
this.getReportList()
|
|
||||||
},
|
},
|
||||||
handleReset() {
|
resetSearch() {
|
||||||
this.dateRange = []
|
this.filterForm = { userName: "", modelName: "", paymentMethod: "", date: "" };
|
||||||
this.query = {
|
this.currentPage = 1;
|
||||||
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) {
|
handlePageChange(page) {
|
||||||
this.query.current_page = page
|
this.currentPage = page;
|
||||||
this.getReportList()
|
|
||||||
},
|
},
|
||||||
formatNumber(value) {
|
exportReport() {
|
||||||
return Number(value || 0).toLocaleString()
|
this.$message.success("报表导出任务已提交");
|
||||||
},
|
|
||||||
formatAmount(value) {
|
|
||||||
return Number(value || 0).toFixed(6).replace(/0+$/, '').replace(/\.$/, '.00')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.operation-report-page {
|
.operation-report-page {
|
||||||
min-height: 100vh;
|
padding: 20px;
|
||||||
padding: 24px;
|
background: #f5f7fb;
|
||||||
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 {
|
.report-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 20px 22px;
|
align-items: flex-start;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 16px;
|
||||||
overflow: hidden;
|
h2 { margin: 0; font-size: 28px; }
|
||||||
color: #ffffff;
|
p { margin: 6px 0 0; color: #8b95a7; }
|
||||||
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 {
|
.stat-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 16px;
|
gap: 14px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
position: relative;
|
background: #fff;
|
||||||
overflow: hidden;
|
border: 1px solid #eceff5;
|
||||||
padding: 20px;
|
border-radius: 12px;
|
||||||
background: #ffffff;
|
padding: 18px 20px;
|
||||||
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-title { color: #5f6b7d; margin-bottom: 10px; }
|
||||||
.stat-card-head {
|
.stat-value { font-size: 38px; font-weight: 700; line-height: 1; }
|
||||||
position: relative;
|
.purple .stat-value { color: #7f56d9; }
|
||||||
z-index: 1;
|
.green .stat-value { color: #16a34a; }
|
||||||
display: flex;
|
.orange .stat-value { color: #ea580c; }
|
||||||
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,
|
.filter-card,
|
||||||
.table-card {
|
.table-card {
|
||||||
margin-bottom: 16px;
|
border: 1px solid #eceff5;
|
||||||
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;
|
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;
|
margin-bottom: 14px;
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0 0 6px;
|
|
||||||
color: #1f2d3d;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p,
|
|
||||||
span {
|
|
||||||
margin: 0;
|
|
||||||
color: #98a2b3;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pager-wrap {
|
.pager-wrap {
|
||||||
|
margin-top: 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
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>
|
</style>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="token-usage-page">
|
<div class="token-usage-page">
|
||||||
<div class="usage-shell">
|
<div class="usage-shell">
|
||||||
<!-- <div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="title-line">
|
<div class="title-line">
|
||||||
<span class="title-icon">
|
<span class="title-icon">
|
||||||
@ -11,395 +11,79 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>查看模型调用 Token 消耗、调用次数和费用趋势。</p>
|
<p>查看模型调用 Token 消耗、调用次数和费用趋势。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<el-button size="small" icon="el-icon-refresh" class="refresh-btn">刷新</el-button>
|
||||||
<span class="range-badge">{{ filterTimeText }}</span>
|
</div>
|
||||||
<el-button size="small" icon="el-icon-refresh" class="refresh-btn" :loading="loading" @click="getTokenList">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="stat-row">
|
<div class="stat-row">
|
||||||
<div v-for="item in statCards" :key="item.label" class="stat-card" :class="item.type">
|
<div v-for="item in statCards" :key="item.label" class="stat-card" :class="item.type">
|
||||||
<div class="stat-card-head">
|
<span>{{ item.label }}</span>
|
||||||
<span>{{ item.label }}</span>
|
|
||||||
<i :class="item.icon"></i>
|
|
||||||
</div>
|
|
||||||
<strong>{{ item.value }}</strong>
|
<strong>{{ item.value }}</strong>
|
||||||
<em>{{ item.desc }}</em>
|
<em>{{ item.desc }}</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="usage-dashboard">
|
|
||||||
<div class="dashboard-card token-ratio-card">
|
|
||||||
<div class="dashboard-title">
|
|
||||||
<h3>Token 使用概览</h3>
|
|
||||||
<span>{{ rangeLabel }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="token-total">{{ formatNumber(summary.total_tokens) }}</div>
|
|
||||||
<p>总 Token 消耗</p>
|
|
||||||
<div ref="tokenRatioChart" class="chart-box token-ratio-chart"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-card model-rank-card">
|
|
||||||
<div class="dashboard-title">
|
|
||||||
<h3>模型消耗排行</h3>
|
|
||||||
<span>当前页数据</span>
|
|
||||||
</div>
|
|
||||||
<div ref="modelRankChart" class="chart-box model-rank-chart"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-card">
|
<div class="content-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div>
|
<div>
|
||||||
<h3>用量明细</h3>
|
<h3>用量明细</h3>
|
||||||
<p>按模型维度统计 Token 输入、输出与费用。</p>
|
<p>按模型维度统计 Token 输入、输出与费用。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-actions">
|
<el-date-picker
|
||||||
<el-select v-model="query.range" size="small" placeholder="快捷范围" @change="handleRangeChange">
|
v-model="dateRange"
|
||||||
<el-option label="最近1小时" value="hour"></el-option>
|
type="daterange"
|
||||||
<el-option label="今天" value="day"></el-option>
|
size="small"
|
||||||
<el-option label="最近一周" value="week"></el-option>
|
range-separator="至"
|
||||||
</el-select>
|
start-placeholder="开始日期"
|
||||||
<el-input
|
end-placeholder="结束日期"
|
||||||
v-model="query.model"
|
></el-date-picker>
|
||||||
size="small"
|
|
||||||
clearable
|
|
||||||
placeholder="搜索模型"
|
|
||||||
@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>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="usageList" class="usage-table" style="width: 100%">
|
<el-table :data="usageList" class="usage-table" style="width: 100%">
|
||||||
<el-table-column prop="model" label="模型名称" min-width="180" show-overflow-tooltip></el-table-column>
|
<el-table-column prop="modelName" label="模型名称" min-width="180"></el-table-column>
|
||||||
<el-table-column prop="request_count" label="调用次数" width="120"></el-table-column>
|
<el-table-column prop="calls" label="调用次数" width="120"></el-table-column>
|
||||||
<el-table-column prop="prompt_tokens" label="输入Token" min-width="140"></el-table-column>
|
<el-table-column prop="inputTokens" label="输入Token" min-width="140"></el-table-column>
|
||||||
<el-table-column prop="completion_tokens" label="输出Token" min-width="140"></el-table-column>
|
<el-table-column prop="outputTokens" label="输出Token" min-width="140"></el-table-column>
|
||||||
<el-table-column prop="total_tokens" label="总Token" min-width="140"></el-table-column>
|
<el-table-column prop="totalTokens" label="总Token" min-width="140"></el-table-column>
|
||||||
<el-table-column prop="amount" label="预估费用" width="130">
|
<el-table-column prop="cost" label="预估费用" width="120"></el-table-column>
|
||||||
<template slot-scope="scope">¥ {{ formatAmount(scope.row.amount) }}</template>
|
<el-table-column prop="updatedAt" label="更新时间" min-width="170"></el-table-column>
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="usage_time" label="使用时间" min-width="170" show-overflow-tooltip></el-table-column>
|
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="table-pagination">
|
|
||||||
<el-pagination
|
|
||||||
background
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
:total="total"
|
|
||||||
:current-page="query.current_page"
|
|
||||||
:page-size="query.page_size"
|
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
|
||||||
@size-change="handleSizeChange"
|
|
||||||
@current-change="handleCurrentChange"
|
|
||||||
></el-pagination>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import echarts from 'echarts'
|
|
||||||
import { reqTokenUsage } from '@/api/model/model'
|
import { reqTokenUsage } from '@/api/model/model'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TokenUsage',
|
name: 'TokenUsage',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
|
||||||
dateRange: [],
|
dateRange: [],
|
||||||
summary: {
|
|
||||||
request_count: 0,
|
|
||||||
prompt_tokens: 0,
|
|
||||||
completion_tokens: 0,
|
|
||||||
total_tokens: 0,
|
|
||||||
amount: 0
|
|
||||||
},
|
|
||||||
statCards: [
|
statCards: [
|
||||||
{ label: '总消耗 Token', value: '0', desc: '当前筛选范围', type: 'primary', icon: 'el-icon-coin' },
|
{ label: '总消耗 Token', value: '1,286,400', desc: '较昨日 +12.6%', type: 'primary' },
|
||||||
{ label: '调用次数', value: '0', desc: '当前筛选范围', type: 'success', icon: 'el-icon-s-promotion' },
|
{ label: '调用次数', value: '3,482', desc: '今日累计调用', type: 'success' },
|
||||||
{ label: '预估费用', value: '¥ 0.00', desc: '按当前单价估算', type: 'warning', icon: 'el-icon-wallet' },
|
{ label: '预估费用', value: '¥ 128.64', desc: '按当前单价估算', type: 'warning' },
|
||||||
{ label: '输入/输出 Token', value: '0 / 0', desc: 'Prompt / Completion', type: 'purple', icon: 'el-icon-pie-chart' }
|
{ label: '活跃模型', value: '8', desc: '最近 7 天有调用', type: 'purple' }
|
||||||
],
|
],
|
||||||
usageList: [],
|
usageList: [
|
||||||
total: 0,
|
|
||||||
tokenRatioChart: null,
|
]
|
||||||
modelRankChart: null,
|
|
||||||
query:{
|
|
||||||
userid:this.getCurrentUserId(),
|
|
||||||
range:'week',
|
|
||||||
start_time:'',
|
|
||||||
end_time:'',
|
|
||||||
model: '',
|
|
||||||
current_page:1,
|
|
||||||
page_size:10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
rangeLabel() {
|
|
||||||
const labelMap = {
|
|
||||||
hour: '最近1小时',
|
|
||||||
day: '今天',
|
|
||||||
week: '最近一周'
|
|
||||||
}
|
|
||||||
return labelMap[this.query.range] || '当前范围'
|
|
||||||
},
|
|
||||||
filterTimeText() {
|
|
||||||
if (this.query.start_time && this.query.end_time) {
|
|
||||||
return `${this.query.start_time} 至 ${this.query.end_time}`
|
|
||||||
}
|
|
||||||
return this.rangeLabel
|
|
||||||
},
|
|
||||||
modelUsageRank() {
|
|
||||||
const maxTokens = Math.max(...this.usageList.map(item => Number(item.total_tokens || 0)), 0)
|
|
||||||
return this.usageList
|
|
||||||
.map(item => ({
|
|
||||||
model: item.model,
|
|
||||||
total_tokens: Number(item.total_tokens || 0),
|
|
||||||
percent: maxTokens ? Math.round((Number(item.total_tokens || 0) / maxTokens) * 100) : 0
|
|
||||||
}))
|
|
||||||
.sort((a, b) => b.total_tokens - a.total_tokens)
|
|
||||||
.slice(0, 5)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getTokenList()
|
this.getTokenList()
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
this.initCharts()
|
|
||||||
this.renderCharts()
|
|
||||||
window.addEventListener('resize', this.resizeCharts)
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
window.removeEventListener('resize', this.resizeCharts)
|
|
||||||
if (this.tokenRatioChart) {
|
|
||||||
this.tokenRatioChart.dispose()
|
|
||||||
this.tokenRatioChart = null
|
|
||||||
}
|
|
||||||
if (this.modelRankChart) {
|
|
||||||
this.modelRankChart.dispose()
|
|
||||||
this.modelRankChart = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
getCurrentUserId() {
|
getCurrentUserId() {
|
||||||
return sessionStorage.getItem('userId') || localStorage.getItem('userId') || ''
|
return sessionStorage.getItem('userId') || localStorage.getItem('userId') || ''
|
||||||
},
|
},
|
||||||
handleDateChange(value) {
|
async getTokenList() {
|
||||||
if (value && value.length === 2) {
|
const userid = this.getCurrentUserId()
|
||||||
this.query.start_time = value[0]
|
const res = await reqTokenUsage({ userid:userid })
|
||||||
this.query.end_time = value[1]
|
console.log('token用量',res);
|
||||||
} else {
|
|
||||||
this.query.start_time = ''
|
}
|
||||||
this.query.end_time = ''
|
|
||||||
}
|
|
||||||
this.query.current_page = 1
|
|
||||||
this.getTokenList()
|
|
||||||
},
|
|
||||||
handleRangeChange() {
|
|
||||||
this.query.current_page = 1
|
|
||||||
this.getTokenList()
|
|
||||||
},
|
|
||||||
handleSearch() {
|
|
||||||
this.query.current_page = 1
|
|
||||||
this.getTokenList()
|
|
||||||
},
|
|
||||||
async getTokenList() {
|
|
||||||
this.query.userid = this.getCurrentUserId()
|
|
||||||
this.loading = true
|
|
||||||
try {
|
|
||||||
const res = await reqTokenUsage(this.query)
|
|
||||||
if(res.status === true) {
|
|
||||||
const data = res.data || {}
|
|
||||||
this.summary = {
|
|
||||||
...this.summary,
|
|
||||||
...(data.summary || {})
|
|
||||||
}
|
|
||||||
this.updateStatCards()
|
|
||||||
this.usageList = 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.$nextTick(this.renderCharts)
|
|
||||||
}
|
|
||||||
console.log('token用量',res);
|
|
||||||
} finally {
|
|
||||||
this.loading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateStatCards() {
|
|
||||||
this.statCards = [
|
|
||||||
{ label: '总消耗 Token', value: this.formatNumber(this.summary.total_tokens), desc: '当前筛选范围', type: 'primary', icon: 'el-icon-coin' },
|
|
||||||
{ label: '调用次数', value: this.formatNumber(this.summary.request_count), desc: '当前筛选范围', type: 'success', icon: 'el-icon-s-promotion' },
|
|
||||||
{ label: '预估费用', value: `¥ ${this.formatAmount(this.summary.amount)}`, desc: '按当前单价估算', type: 'warning', icon: 'el-icon-wallet' },
|
|
||||||
{ label: '输入/输出 Token', value: `${this.formatNumber(this.summary.prompt_tokens)} / ${this.formatNumber(this.summary.completion_tokens)}`, desc: 'Prompt / Completion', type: 'purple', icon: 'el-icon-pie-chart' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
formatNumber(value) {
|
|
||||||
return Number(value || 0).toLocaleString()
|
|
||||||
},
|
|
||||||
formatAmount(value) {
|
|
||||||
return Number(value || 0).toFixed(6).replace(/0+$/, '').replace(/\.$/, '.00')
|
|
||||||
},
|
|
||||||
initCharts() {
|
|
||||||
if (this.$refs.tokenRatioChart && !this.tokenRatioChart) {
|
|
||||||
this.tokenRatioChart = echarts.init(this.$refs.tokenRatioChart)
|
|
||||||
}
|
|
||||||
if (this.$refs.modelRankChart && !this.modelRankChart) {
|
|
||||||
this.modelRankChart = echarts.init(this.$refs.modelRankChart)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderCharts() {
|
|
||||||
if (!this.$refs.tokenRatioChart || !this.$refs.modelRankChart) return
|
|
||||||
this.initCharts()
|
|
||||||
this.renderTokenRatioChart()
|
|
||||||
this.renderModelRankChart()
|
|
||||||
},
|
|
||||||
renderTokenRatioChart() {
|
|
||||||
if (!this.tokenRatioChart) return
|
|
||||||
const promptTokens = Number(this.summary.prompt_tokens || 0)
|
|
||||||
const completionTokens = Number(this.summary.completion_tokens || 0)
|
|
||||||
const hasData = promptTokens || completionTokens
|
|
||||||
this.tokenRatioChart.setOption({
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
formatter: '{b}: {c} Token ({d}%)'
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
bottom: 0,
|
|
||||||
icon: 'circle',
|
|
||||||
textStyle: {
|
|
||||||
color: '#606266'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
color: ['#409eff', '#67c23a'],
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'Token占比',
|
|
||||||
type: 'pie',
|
|
||||||
radius: ['52%', '72%'],
|
|
||||||
center: ['50%', '44%'],
|
|
||||||
avoidLabelOverlap: true,
|
|
||||||
label: {
|
|
||||||
formatter: '{b}\n{d}%'
|
|
||||||
},
|
|
||||||
data: hasData
|
|
||||||
? [
|
|
||||||
{ name: '输入Token', value: promptTokens },
|
|
||||||
{ name: '输出Token', value: completionTokens }
|
|
||||||
]
|
|
||||||
: [{ name: '暂无数据', value: 1 }],
|
|
||||||
itemStyle: {
|
|
||||||
borderColor: '#fff',
|
|
||||||
borderWidth: 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
renderModelRankChart() {
|
|
||||||
if (!this.modelRankChart) return
|
|
||||||
const rankList = this.modelUsageRank.slice().reverse()
|
|
||||||
this.modelRankChart.setOption({
|
|
||||||
grid: {
|
|
||||||
top: 12,
|
|
||||||
right: 24,
|
|
||||||
bottom: 20,
|
|
||||||
left: 92
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: {
|
|
||||||
type: 'shadow'
|
|
||||||
},
|
|
||||||
formatter: params => {
|
|
||||||
const item = params && params[0]
|
|
||||||
return item ? `${item.name}<br/>${this.formatNumber(item.value)} Token` : ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
type: 'value',
|
|
||||||
axisLine: { show: false },
|
|
||||||
axisTick: { show: false },
|
|
||||||
splitLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: '#edf1f7'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: rankList.map(item => item.model || '-'),
|
|
||||||
axisLine: { show: false },
|
|
||||||
axisTick: { show: false },
|
|
||||||
axisLabel: {
|
|
||||||
color: '#606266',
|
|
||||||
width: 82,
|
|
||||||
overflow: 'truncate'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'Token消耗',
|
|
||||||
type: 'bar',
|
|
||||||
barWidth: 12,
|
|
||||||
data: rankList.map(item => item.total_tokens),
|
|
||||||
itemStyle: {
|
|
||||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
|
||||||
{ offset: 0, color: '#a78bfa' },
|
|
||||||
{ offset: 1, color: '#7c3aed' }
|
|
||||||
]),
|
|
||||||
barBorderRadius: [0, 8, 8, 0]
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
position: 'right',
|
|
||||||
color: '#344054',
|
|
||||||
formatter: params => this.formatNumber(params.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
resizeCharts() {
|
|
||||||
if (this.tokenRatioChart) this.tokenRatioChart.resize()
|
|
||||||
if (this.modelRankChart) this.modelRankChart.resize()
|
|
||||||
},
|
|
||||||
handleSizeChange(size) {
|
|
||||||
this.query.page_size = size
|
|
||||||
this.query.current_page = 1
|
|
||||||
this.getTokenList()
|
|
||||||
},
|
|
||||||
handleCurrentChange(page) {
|
|
||||||
this.query.current_page = page
|
|
||||||
this.getTokenList()
|
|
||||||
},
|
|
||||||
handleReset() {
|
|
||||||
this.dateRange = []
|
|
||||||
this.query = {
|
|
||||||
userid: this.getCurrentUserId(),
|
|
||||||
range: 'week',
|
|
||||||
start_time: '',
|
|
||||||
end_time: '',
|
|
||||||
model: '',
|
|
||||||
current_page: 1,
|
|
||||||
page_size: 10
|
|
||||||
}
|
|
||||||
this.getTokenList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -408,15 +92,13 @@ export default {
|
|||||||
.token-usage-page {
|
.token-usage-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background:
|
background: linear-gradient(180deg, #f3f7ff 0%, #f7f9fc 48%, #ffffff 100%);
|
||||||
radial-gradient(circle at top left, rgba(64, 158, 255, 0.16), transparent 34%),
|
|
||||||
linear-gradient(180deg, #f3f7ff 0%, #f7f9fc 48%, #ffffff 100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-shell {
|
.usage-shell {
|
||||||
min-height: calc(100vh - 48px);
|
min-height: calc(100vh - 48px);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: rgba(255, 255, 255, 0.92);
|
background: #ffffff;
|
||||||
border: 1px solid #edf1f7;
|
border: 1px solid #edf1f7;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
box-shadow: 0 12px 30px rgba(31, 45, 61, 0.06);
|
box-shadow: 0 12px 30px rgba(31, 45, 61, 0.06);
|
||||||
@ -431,18 +113,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
align-items: center;
|
margin-bottom: 24px;
|
||||||
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);
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: rgba(255, 255, 255, 0.82);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-line {
|
.title-line {
|
||||||
@ -453,7 +124,7 @@ export default {
|
|||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #ffffff;
|
color: #1f2d3d;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -464,9 +135,9 @@ export default {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
color: #ffffff;
|
color: #409eff;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
background: rgba(255, 255, 255, 0.18);
|
background: #eef5ff;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -478,26 +149,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.refresh-btn {
|
.refresh-btn {
|
||||||
color: #1e6fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-row {
|
.stat-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
@ -506,19 +160,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid #edf1f7;
|
border: 1px solid #edf1f7;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
span,
|
span,
|
||||||
em {
|
em {
|
||||||
@ -535,130 +180,20 @@ export default {
|
|||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&: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;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
background: linear-gradient(135deg, #eef5ff 0%, #ffffff 100%);
|
background: #eef5ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.success {
|
&.success {
|
||||||
background: linear-gradient(135deg, #f0fdf4 0%, #ffffff 100%);
|
background: #f0fdf4;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
background: linear-gradient(135deg, #fff7ed 0%, #ffffff 100%);
|
background: #fff7ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.purple {
|
&.purple {
|
||||||
background: linear-gradient(135deg, #f5f3ff 0%, #ffffff 100%);
|
background: #f5f3ff;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.usage-dashboard {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-card {
|
|
||||||
padding: 22px;
|
|
||||||
border: 1px solid #edf1f7;
|
|
||||||
border-radius: 16px;
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 10px 26px rgba(31, 45, 61, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: #1f2d3d;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: #909399;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-total {
|
|
||||||
color: #1f2d3d;
|
|
||||||
font-size: 34px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-ratio-card {
|
|
||||||
p {
|
|
||||||
margin: 6px 0 18px;
|
|
||||||
color: #909399;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-box {
|
|
||||||
width: 100%;
|
|
||||||
height: 240px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-rank-chart {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ratio-list,
|
|
||||||
.rank-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ratio-label,
|
|
||||||
.rank-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #606266;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
strong {
|
|
||||||
color: #1f2d3d;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -666,8 +201,6 @@ export default {
|
|||||||
padding: 22px;
|
padding: 22px;
|
||||||
border: 1px solid #edf1f7;
|
border: 1px solid #edf1f7;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 10px 26px rgba(31, 45, 61, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
@ -679,57 +212,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 8px;
|
|
||||||
background: #f6f8fb;
|
|
||||||
border: 1px solid #edf1f7;
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
/deep/ .el-select {
|
|
||||||
width: 110px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/deep/ .el-input {
|
|
||||||
width: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/deep/ .el-input__inner {
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.usage-table {
|
.usage-table {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
/deep/ .el-table__header th {
|
|
||||||
color: #475467;
|
|
||||||
font-weight: 700;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/deep/ .el-table__row:hover > td {
|
|
||||||
background: #f6faff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-pagination {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.stat-row {
|
.stat-row {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-dashboard {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
@ -741,15 +232,5 @@ export default {
|
|||||||
.card-header {
|
.card-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-actions {
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
/deep/ .el-date-editor {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user