This commit is contained in:
张成
2025-12-27 20:14:40 +08:00
parent 43382668a3
commit 43f7884e52
14 changed files with 1818 additions and 21 deletions

View 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()

View File

@@ -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

View 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>

View File

@@ -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: '禁用' }