1
This commit is contained in:
59
admin/src/api/system/ai_call_records_server.js
Normal file
59
admin/src/api/system/ai_call_records_server.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* AI调用记录 API 服务
|
||||
*/
|
||||
|
||||
class AiCallRecordsServer {
|
||||
/**
|
||||
* 分页查询AI调用记录
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('/ai_call_records/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取AI调用记录详情
|
||||
* @param {Number|String} id - 记录ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(id) {
|
||||
return window.framework.http.get('/ai_call_records/detail', { id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计数据
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Number} param.user_id - 用户ID
|
||||
* @param {String} param.sn_code - 设备序列号
|
||||
* @param {String} param.business_type - 业务类型
|
||||
* @param {String} param.start_date - 开始日期
|
||||
* @param {String} param.end_date - 结束日期
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getStats(param) {
|
||||
return window.framework.http.post('/ai_call_records/stats', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除AI调用记录
|
||||
* @param {Object} row - 记录数据(包含id)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('/ai_call_records/delete', { id: row.id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除AI调用记录
|
||||
* @param {Array} ids - 记录ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
batchDelete(ids) {
|
||||
return window.framework.http.post('/ai_call_records/batch_delete', { ids })
|
||||
}
|
||||
}
|
||||
|
||||
export default new AiCallRecordsServer()
|
||||
@@ -22,6 +22,7 @@ import SystemConfig from '@/views/system/system_config.vue'
|
||||
import Version from '@/views/system/version.vue'
|
||||
import JobTypes from '@/views/work/job_types.vue'
|
||||
import PricingPlans from '@/views/system/pricing_plans.vue'
|
||||
import AiCallRecords from '@/views/system/ai_call_records.vue'
|
||||
|
||||
// 首页模块
|
||||
import HomeIndex from '@/views/home/index.vue'
|
||||
@@ -55,10 +56,9 @@ const componentMap = {
|
||||
'system/version': Version,
|
||||
'work/job_types': JobTypes,
|
||||
'system/pricing_plans': PricingPlans,
|
||||
'system/pricing_plans.vue': PricingPlans,
|
||||
'system/ai_call_records': AiCallRecords,
|
||||
'home/index': HomeIndex,
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default componentMap
|
||||
|
||||
399
admin/src/views/system/ai_call_records.vue
Normal file
399
admin/src/views/system/ai_call_records.vue
Normal file
@@ -0,0 +1,399 @@
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<!-- 顶部工具栏 -->
|
||||
<div class="table-head-tool">
|
||||
<Button type="error" @click="showBatchDelete" :disabled="selectedIds.length === 0">
|
||||
批量删除 ({{ selectedIds.length }})
|
||||
</Button>
|
||||
<Button type="primary" @click="showStatsModal" class="ml10">查看统计</Button>
|
||||
|
||||
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80" class="mt10">
|
||||
<FormItem label="筛选条件" :label-width="80">
|
||||
<Select v-model="gridOption.param.seachOption.key" style="width: 120px">
|
||||
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||
</Select>
|
||||
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px"
|
||||
placeholder="请输入关键字" @on-search="query(1)" search />
|
||||
</FormItem>
|
||||
<FormItem label="状态">
|
||||
<Select v-model="gridOption.param.seachOption.status" style="width: 120px" clearable @on-change="query(1)">
|
||||
<Option value="success">成功</Option>
|
||||
<Option value="failed">失败</Option>
|
||||
<Option value="timeout">超时</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="服务类型">
|
||||
<Select v-model="gridOption.param.seachOption.service_type" style="width: 140px" clearable @on-change="query(1)">
|
||||
<Option value="chat">聊天</Option>
|
||||
<Option value="completion">文本生成</Option>
|
||||
<Option value="embedding">向量化</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="模型">
|
||||
<Select v-model="gridOption.param.seachOption.model_name" style="width: 150px" clearable @on-change="query(1)">
|
||||
<Option value="gpt-3.5-turbo">GPT-3.5 Turbo</Option>
|
||||
<Option value="gpt-4">GPT-4</Option>
|
||||
<Option value="gpt-4-turbo">GPT-4 Turbo</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="日期范围">
|
||||
<DatePicker type="daterange" v-model="dateRange" style="width: 220px"
|
||||
@on-change="handleDateChange" placeholder="选择日期范围" />
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="query(1)">查询</Button>
|
||||
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="table-body">
|
||||
<tables :columns="listColumns" :value="gridOption.data"
|
||||
:pageOption="gridOption.param.pageOption"
|
||||
@changePage="query"
|
||||
@on-selection-change="handleSelectionChange">
|
||||
</tables>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<Modal v-model="detailModal.show" title="AI调用详情" width="800" :footer-hide="true">
|
||||
<div v-if="detailModal.data" class="detail-content">
|
||||
<Row :gutter="16">
|
||||
<Col span="12">
|
||||
<p><strong>记录ID:</strong> {{ detailModal.data.id }}</p>
|
||||
<p><strong>用户ID:</strong> {{ detailModal.data.user_id || '-' }}</p>
|
||||
<p><strong>设备序列号:</strong> {{ detailModal.data.sn_code || '-' }}</p>
|
||||
<p><strong>服务类型:</strong> {{ detailModal.data.service_type }}</p>
|
||||
<p><strong>模型名称:</strong> {{ detailModal.data.model_name }}</p>
|
||||
<p><strong>API提供商:</strong> {{ detailModal.data.api_provider }}</p>
|
||||
</Col>
|
||||
<Col span="12">
|
||||
<p><strong>输入Token:</strong> {{ detailModal.data.prompt_tokens }}</p>
|
||||
<p><strong>输出Token:</strong> {{ detailModal.data.completion_tokens }}</p>
|
||||
<p><strong>总Token:</strong> {{ detailModal.data.total_tokens }}</p>
|
||||
<p><strong>费用:</strong> ¥{{ detailModal.data.cost_amount || 0 }}</p>
|
||||
<p><strong>响应时间:</strong> {{ detailModal.data.response_time || '-' }}ms</p>
|
||||
<p><strong>创建时间:</strong> {{ formatDate(detailModal.data.create_time) }}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider />
|
||||
<p><strong>请求内容:</strong></p>
|
||||
<pre class="content-box">{{ detailModal.data.request_content || '无' }}</pre>
|
||||
<p><strong>响应内容:</strong></p>
|
||||
<pre class="content-box">{{ detailModal.data.response_content || '无' }}</pre>
|
||||
<p v-if="detailModal.data.error_message"><strong>错误信息:</strong></p>
|
||||
<pre v-if="detailModal.data.error_message" class="content-box error">{{ detailModal.data.error_message }}</pre>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- 统计弹窗 -->
|
||||
<Modal v-model="statsModal.show" title="Token使用统计" width="700" :footer-hide="true">
|
||||
<div v-if="statsModal.data" class="stats-content">
|
||||
<Row :gutter="16">
|
||||
<Col span="8">
|
||||
<div class="stat-card">
|
||||
<p class="stat-label">总调用次数</p>
|
||||
<p class="stat-value">{{ statsModal.data.total_calls }}</p>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span="8">
|
||||
<div class="stat-card">
|
||||
<p class="stat-label">总Token数</p>
|
||||
<p class="stat-value">{{ statsModal.data.total_tokens.toLocaleString() }}</p>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span="8">
|
||||
<div class="stat-card">
|
||||
<p class="stat-label">总费用</p>
|
||||
<p class="stat-value">¥{{ statsModal.data.total_cost.toFixed(2) }}</p>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider />
|
||||
<p><strong>按模型统计:</strong></p>
|
||||
<ul class="stats-list">
|
||||
<li v-for="(value, key) in statsModal.data.by_model" :key="key">
|
||||
{{ key }}: {{ value.count }}次, {{ value.total_tokens.toLocaleString() }} tokens, ¥{{ value.total_cost.toFixed(2) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import aiCallRecordsServer from '@/api/system/ai_call_records_server.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
seachTypes: [
|
||||
{ key: 'user_id', value: '用户ID' },
|
||||
{ key: 'sn_code', value: '设备序列号' },
|
||||
{ key: 'reference_id', value: '业务ID' }
|
||||
],
|
||||
gridOption: {
|
||||
param: {
|
||||
seachOption: {
|
||||
key: 'user_id',
|
||||
value: '',
|
||||
status: null,
|
||||
service_type: null,
|
||||
model_name: null,
|
||||
start_date: null,
|
||||
end_date: null
|
||||
},
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
},
|
||||
data: []
|
||||
},
|
||||
selectedIds: [],
|
||||
dateRange: [],
|
||||
detailModal: {
|
||||
show: false,
|
||||
data: null
|
||||
},
|
||||
statsModal: {
|
||||
show: false,
|
||||
data: null
|
||||
},
|
||||
listColumns: [
|
||||
{ type: 'selection', width: 60, align: 'center' },
|
||||
{ title: 'ID', key: 'id', minWidth: 70 },
|
||||
{ title: '用户ID', key: 'user_id', minWidth: 80 },
|
||||
{ title: '设备序列号', key: 'sn_code', minWidth: 120 },
|
||||
{ title: '服务类型', key: 'service_type', minWidth: 100 },
|
||||
{ title: '模型', key: 'model_name', minWidth: 140 },
|
||||
{
|
||||
title: 'Token使用',
|
||||
key: 'total_tokens',
|
||||
minWidth: 150,
|
||||
render: (h, params) => {
|
||||
return h('span', `输入:${params.row.prompt_tokens} + 输出:${params.row.completion_tokens} = ${params.row.total_tokens}`)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '费用',
|
||||
key: 'cost_amount',
|
||||
minWidth: 80,
|
||||
render: (h, params) => {
|
||||
return h('span', `¥${(params.row.cost_amount || 0).toFixed(4)}`)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
minWidth: 80,
|
||||
render: (h, params) => {
|
||||
const colorMap = { success: 'success', failed: 'error', timeout: 'warning' }
|
||||
const textMap = { success: '成功', failed: '失败', timeout: '超时' }
|
||||
return h('Tag', {
|
||||
props: { color: colorMap[params.row.status] || 'default' }
|
||||
}, textMap[params.row.status] || params.row.status)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '响应时间',
|
||||
key: 'response_time',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
return h('span', params.row.response_time ? `${params.row.response_time}ms` : '-')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'create_time',
|
||||
minWidth: 150,
|
||||
render: (h, params) => {
|
||||
return h('span', this.formatDate(params.row.create_time))
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
render: (h, params) => {
|
||||
return h('div', [
|
||||
h('Button', {
|
||||
props: { type: 'primary', size: 'small' },
|
||||
style: { marginRight: '5px' },
|
||||
on: { click: () => this.showDetail(params.row) }
|
||||
}, '详情'),
|
||||
h('Button', {
|
||||
props: { type: 'error', size: 'small' },
|
||||
on: { click: () => this.del(params.row) }
|
||||
}, '删除')
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.query(1)
|
||||
},
|
||||
methods: {
|
||||
query(page) {
|
||||
if (page) {
|
||||
this.gridOption.param.pageOption.page = page
|
||||
}
|
||||
const param = {
|
||||
pageOption: this.gridOption.param.pageOption,
|
||||
seachOption: { ...this.gridOption.param.seachOption }
|
||||
}
|
||||
|
||||
aiCallRecordsServer.page(param).then(res => {
|
||||
if (res.code === 0) {
|
||||
const data = res.data
|
||||
this.gridOption.data = data.rows
|
||||
this.gridOption.param.pageOption.total = data.count || data.total || 0
|
||||
} else {
|
||||
this.$Message.error(res.message || '查询失败')
|
||||
}
|
||||
}).catch(err => {
|
||||
this.$Message.error('查询失败:' + (err.message || '未知错误'))
|
||||
})
|
||||
},
|
||||
resetQuery() {
|
||||
this.gridOption.param.seachOption = {
|
||||
key: 'user_id',
|
||||
value: '',
|
||||
status: null,
|
||||
service_type: null,
|
||||
model_name: null,
|
||||
start_date: null,
|
||||
end_date: null
|
||||
}
|
||||
this.dateRange = []
|
||||
this.query(1)
|
||||
},
|
||||
handleDateChange(dates) {
|
||||
if (dates && dates.length === 2) {
|
||||
this.gridOption.param.seachOption.start_date = dates[0]
|
||||
this.gridOption.param.seachOption.end_date = dates[1]
|
||||
} else {
|
||||
this.gridOption.param.seachOption.start_date = null
|
||||
this.gridOption.param.seachOption.end_date = null
|
||||
}
|
||||
},
|
||||
handleSelectionChange(selection) {
|
||||
this.selectedIds = selection.map(item => item.id)
|
||||
},
|
||||
showDetail(row) {
|
||||
aiCallRecordsServer.getById(row.id).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.detailModal.data = res.data
|
||||
this.detailModal.show = true
|
||||
} else {
|
||||
this.$Message.error(res.message || '获取详情失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
showStatsModal() {
|
||||
const param = { ...this.gridOption.param.seachOption }
|
||||
aiCallRecordsServer.getStats(param).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.statsModal.data = res.data
|
||||
this.statsModal.show = true
|
||||
} else {
|
||||
this.$Message.error(res.message || '获取统计失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
del(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除这条AI调用记录吗?`,
|
||||
onOk: () => {
|
||||
aiCallRecordsServer.del(row).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$Message.success('删除成功')
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} else {
|
||||
this.$Message.error(res.message || '删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
showBatchDelete() {
|
||||
if (this.selectedIds.length === 0) {
|
||||
this.$Message.warning('请先选择要删除的记录')
|
||||
return
|
||||
}
|
||||
this.$Modal.confirm({
|
||||
title: '确认批量删除',
|
||||
content: `确定要删除选中的 ${this.selectedIds.length} 条记录吗?`,
|
||||
onOk: () => {
|
||||
aiCallRecordsServer.batchDelete(this.selectedIds).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$Message.success('批量删除成功')
|
||||
this.selectedIds = []
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} else {
|
||||
this.$Message.error(res.message || '批量删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
formatDate(date) {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
return d.toLocaleString('zh-CN', { hour12: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-view {
|
||||
padding: 16px;
|
||||
}
|
||||
.detail-content p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.content-box {
|
||||
background: #f5f5f5;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.content-box.error {
|
||||
background: #fee;
|
||||
color: #c00;
|
||||
}
|
||||
.stats-content .stat-card {
|
||||
background: #f5f7fa;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-label {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.stat-value {
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stats-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.stats-list li {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
</style>
|
||||
@@ -203,8 +203,7 @@ export default {
|
||||
{
|
||||
title: '是否推荐',
|
||||
key: 'featured',
|
||||
type: 'radio',
|
||||
required: true,
|
||||
com: 'Radio',
|
||||
options: [
|
||||
{ value: 1, label: '推荐' },
|
||||
{ value: 0, label: '普通' }
|
||||
@@ -213,8 +212,7 @@ export default {
|
||||
{
|
||||
title: '是否启用',
|
||||
key: 'is_active',
|
||||
type: 'radio',
|
||||
required: true,
|
||||
com: 'Radio',
|
||||
options: [
|
||||
{ value: 1, label: '启用' },
|
||||
{ value: 0, label: '禁用' }
|
||||
|
||||
Reference in New Issue
Block a user