632 lines
18 KiB
Vue
632 lines
18 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<!-- 查询条件区域 -->
|
||
<el-card class="filter-container" shadow="never">
|
||
<el-form ref="queryForm" :model="queryParams" :inline="true" label-width="100px">
|
||
<!-- 第一行 -->
|
||
<el-form-item label="请求ID">
|
||
<el-input
|
||
v-model="queryParams.requestId"
|
||
placeholder="请输入请求ID"
|
||
clearable
|
||
style="width: 200px"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="模型">
|
||
<el-select v-model="queryParams.model" placeholder="请选择模型" clearable>
|
||
<el-option
|
||
v-for="item in modelOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="状态">
|
||
<el-select v-model="queryParams.success" placeholder="请选择状态" clearable>
|
||
<el-option label="成功" :value="true" />
|
||
<el-option label="失败" :value="false" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<!-- 第二行 -->
|
||
<el-form-item label="检测数量">
|
||
<el-input
|
||
v-model="queryParams.faceCount"
|
||
placeholder="请输入人脸数量"
|
||
clearable
|
||
style="width: 150px"
|
||
type="number"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="处理时间(ms)">
|
||
<el-input
|
||
v-model="queryParams.minProcessingTime"
|
||
placeholder="最小处理时间"
|
||
clearable
|
||
style="width: 150px"
|
||
type="number"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="至">
|
||
<el-input
|
||
v-model="queryParams.maxProcessingTime"
|
||
placeholder="最大处理时间"
|
||
clearable
|
||
style="width: 150px"
|
||
type="number"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<!-- 第三行 -->
|
||
<el-form-item label="创建时间">
|
||
<el-date-picker
|
||
v-model="queryParams.createdAtRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
format="yyyy-MM-dd"
|
||
value-format="yyyy-MM-dd"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<!-- 按钮区域 - 独立于表单 -->
|
||
<div style="display: flex; justify-content: flex-end; margin-top: 10px;">
|
||
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
|
||
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
|
||
<el-button type="success" icon="el-icon-upload" @click="handleDetect">头像URL检测</el-button>
|
||
<el-button type="info" icon="el-icon-upload2" @click="handleUploadAvatar">上传并检测</el-button>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- 数据表格区域 -->
|
||
<el-card class="table-container" shadow="never">
|
||
<el-table
|
||
v-loading="loading"
|
||
:data="avatarDetectList"
|
||
style="width: 100%"
|
||
@selection-change="handleSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="55" />
|
||
<el-table-column label="序号" type="index" width="50" />
|
||
<el-table-column label="请求ID" prop="requestId" min-width="200" show-overflow-tooltip />
|
||
<el-table-column label="模型" prop="model" width="150" />
|
||
<el-table-column label="图片URL" prop="imageUrl" min-width="200" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<el-link v-if="scope.row.imageUrl" :href="scope.row.imageUrl" target="_blank" type="primary">
|
||
查看图片
|
||
</el-link>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="检测数量" prop="faceCount" width="100" align="center" />
|
||
<el-table-column label="状态" prop="success" width="80" align="center">
|
||
<template slot-scope="scope">
|
||
<el-tag :type="scope.row.success ? 'success' : 'danger'">
|
||
{{ scope.row.success ? '成功' : '失败' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="处理时间(ms)" prop="processingTimeMs" width="120" align="center" />
|
||
<el-table-column label="请求时间" prop="requestTime" width="160" />
|
||
<el-table-column label="响应时间" prop="responseTime" width="160" />
|
||
<el-table-column label="创建时间" prop="createdAt" width="160" />
|
||
<el-table-column label="操作" width="200" fixed="right">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" @click="handleView(scope.row)">查看</el-button>
|
||
<el-button type="text" @click="handleDelete(scope.row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 分页 -->
|
||
<el-pagination
|
||
:current-page="queryParams.current"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
:page-size="queryParams.size"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
:total="total"
|
||
style="margin-top: 20px; text-align: right;"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
/>
|
||
</el-card>
|
||
|
||
<!-- 头像URL检测对话框 -->
|
||
<el-dialog
|
||
title="头像URL检测"
|
||
:visible.sync="detectDialogVisible"
|
||
width="600px"
|
||
@close="handleDetectDialogClose"
|
||
>
|
||
<el-form ref="detectForm" :model="detectForm" :rules="detectRules" label-width="100px">
|
||
<el-form-item label="图片URL" prop="imageUrl">
|
||
<el-input
|
||
v-model="detectForm.imageUrl"
|
||
placeholder="请输入图片URL"
|
||
type="textarea"
|
||
:rows="3"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="模型" prop="model">
|
||
<el-select v-model="detectForm.model" placeholder="请选择模型" style="width: 100%">
|
||
<el-option
|
||
v-for="item in modelOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div slot="footer" class="dialog-footer">
|
||
<el-button @click="detectDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="detectLoading" @click="handleDetectSubmit">检测</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { getFaceDetectList, detectFace, deleteFaceDetect } from '@/api/face-detect'
|
||
|
||
export default {
|
||
name: 'AvatarUrlDetect',
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
avatarDetectList: [],
|
||
total: 0,
|
||
selectedIds: [],
|
||
detectDialogVisible: false,
|
||
detectLoading: false,
|
||
detailVisible: false,
|
||
currentRecord: null,
|
||
// 上传头像相关
|
||
uploadAvatarDialogVisible: false,
|
||
uploadFileList: [],
|
||
uploadAction: '/api/face-detect/avatar/upload',
|
||
uploadHeaders: {},
|
||
uploadData: {
|
||
type: 'avatar',
|
||
avatarName: ''
|
||
},
|
||
uploadedImageUrl: '',
|
||
uploadedModel: '',
|
||
uploadSuccessDialogVisible: false,
|
||
uploadForm: {
|
||
model: 'default',
|
||
avatarName: ''
|
||
},
|
||
// 查询参数
|
||
queryParams: {
|
||
current: 1,
|
||
size: 10,
|
||
requestId: '',
|
||
model: '',
|
||
success: '',
|
||
faceCount: '',
|
||
minProcessingTime: '',
|
||
maxProcessingTime: '',
|
||
createdAtRange: []
|
||
},
|
||
// 检测表单
|
||
detectForm: {
|
||
imageUrl: '',
|
||
model: 'liveportrait-detect'
|
||
},
|
||
// 表单验证规则
|
||
detectRules: {
|
||
imageUrl: [
|
||
{ required: true, message: '请输入图片URL', trigger: 'blur' }
|
||
],
|
||
model: [
|
||
{ required: true, message: '请选择模型', trigger: 'change' }
|
||
]
|
||
},
|
||
// 模型选项
|
||
modelOptions: [
|
||
{ value: 'liveportrait-detect', label: 'LivePortrait Detect' },
|
||
{ value: 'face-detect-v1', label: 'Face Detect V1' },
|
||
{ value: 'face-detect-v2', label: 'Face Detect V2' }
|
||
]
|
||
}
|
||
},
|
||
// 监听头像名称变化,更新上传数据
|
||
watch: {
|
||
'uploadForm.avatarName': {
|
||
handler(newVal) {
|
||
this.uploadData.avatarName = newVal
|
||
},
|
||
immediate: true
|
||
}
|
||
},
|
||
created() {
|
||
// 设置上传请求头
|
||
this.uploadHeaders = {
|
||
'Authorization': 'Bearer ' + this.$store.getters.token
|
||
}
|
||
this.getList()
|
||
},
|
||
methods: {
|
||
// 上传头像对话框打开
|
||
handleUploadAvatarDialogOpen() {
|
||
console.log('上传头像对话框打开')
|
||
// 清空上传组件
|
||
this.$nextTick(() => {
|
||
if (this.$refs.avatarUpload) {
|
||
this.$refs.avatarUpload.clearFiles()
|
||
}
|
||
})
|
||
},
|
||
|
||
// 上传头像
|
||
handleUploadAvatar() {
|
||
this.uploadFileList = []
|
||
this.uploadData.avatarName = ''
|
||
this.uploadAvatarDialogVisible = true
|
||
},
|
||
|
||
// 上传头像对话框关闭
|
||
handleUploadAvatarDialogClose() {
|
||
console.log('上传头像对话框关闭 - 重置文件列表')
|
||
this.uploadFileList = []
|
||
},
|
||
|
||
// 上传前检查文件大小和类型
|
||
beforeAvatarUpload(file) {
|
||
console.log('上传前检查 - 文件:', file)
|
||
|
||
const isLt5M = file.size / 1024 / 1024 < 5
|
||
if (!isLt5M) {
|
||
this.$message.error('上传文件大小不能超过 5MB!')
|
||
return false
|
||
}
|
||
|
||
const isImage = /\.(jpg|jpeg|png|gif)$/.test(file.name.toLowerCase())
|
||
if (!isImage) {
|
||
this.$message.error('只能上传图片格式的文件!')
|
||
return false
|
||
}
|
||
|
||
return true
|
||
},
|
||
|
||
// 上传成功
|
||
handleUploadAvatarSuccess(response, file, fileList) {
|
||
console.log('上传成功 - 响应:', response)
|
||
// 检查响应格式
|
||
if (response && (response.code === 20000 || response.code === 200 || response.success)) {
|
||
// 保存返回的图片URL和所选模型
|
||
this.uploadedImageUrl = response.data?.fileUrl || response.url || ''
|
||
this.uploadedModel = this.uploadForm.model
|
||
if (this.uploadedImageUrl) {
|
||
this.uploadAvatarDialogVisible = false
|
||
this.uploadSuccessDialogVisible = true
|
||
} else {
|
||
this.$message.error('上传成功,但未返回图片URL')
|
||
}
|
||
} else {
|
||
this.$message.error(response?.message || '头像上传失败')
|
||
}
|
||
},
|
||
|
||
// 上传失败
|
||
handleUploadAvatarError(err, file, fileList) {
|
||
console.error('上传失败 - 错误:', err)
|
||
this.$message.error('头像上传失败: ' + (err.message || err.statusText || '未知错误'))
|
||
},
|
||
|
||
// 文件变化处理
|
||
handleFileChange(file, fileList) {
|
||
console.log('文件变化 - 文件:', file)
|
||
console.log('文件变化 - 文件列表:', fileList)
|
||
this.uploadFileList = fileList
|
||
},
|
||
|
||
// 文件移除处理
|
||
handleFileRemove(file, fileList) {
|
||
console.log('文件移除 - 文件:', file)
|
||
console.log('文件移除 - 文件列表:', fileList)
|
||
this.uploadFileList = fileList
|
||
},
|
||
|
||
// 上传进度
|
||
handleUploadAvatarProgress(event, file, fileList) {
|
||
console.log('上传进度 - 事件:', event)
|
||
},
|
||
|
||
// 复制图片URL
|
||
copyImageUrl() {
|
||
const input = document.createElement('input')
|
||
input.value = this.uploadedImageUrl
|
||
document.body.appendChild(input)
|
||
input.select()
|
||
document.execCommand('copy')
|
||
document.body.removeChild(input)
|
||
this.$message.success('链接已复制到剪贴板')
|
||
},
|
||
|
||
// 获取列表数据
|
||
getList() {
|
||
this.loading = true
|
||
const params = { ...this.queryParams }
|
||
|
||
// 处理时间范围
|
||
if (params.createdAtRange && params.createdAtRange.length === 2) {
|
||
params.createdAtStart = params.createdAtRange[0]
|
||
params.createdAtEnd = params.createdAtRange[1]
|
||
}
|
||
delete params.createdAtRange
|
||
|
||
// 移除空值
|
||
Object.keys(params).forEach(key => {
|
||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||
delete params[key]
|
||
}
|
||
})
|
||
|
||
getFaceDetectList(params).then(response => {
|
||
console.log('API响应:', response)
|
||
if (response.code === 20000) {
|
||
this.avatarDetectList = response.data || []
|
||
this.total = response.total || 0
|
||
} else {
|
||
this.$message.error(response.message || '查询失败')
|
||
}
|
||
this.loading = false
|
||
}).catch((error) => {
|
||
console.log('API错误:', error)
|
||
// 开发环境使用模拟数据
|
||
this.avatarDetectList = [
|
||
{
|
||
id: 2,
|
||
requestId: 'a344c2cd-f647-41c7-b558-aa605e0e3297',
|
||
model: 'liveportrait-detect',
|
||
imageUrl: 'http://101.35.52.237:19006/api/v1/download-shared-object',
|
||
requestTime: '2025-10-05T16:27:47',
|
||
responseTime: '2025-10-05T16:27:49',
|
||
statusCode: 200,
|
||
success: true,
|
||
faceCount: 0,
|
||
responseData: '{"output":{"message":"","pass":true},"usage":{"image_count":1},"request_id":"88fd6554-a840-461c-840a-476bd20087b6"}',
|
||
errorMessage: null,
|
||
processingTimeMs: 2304,
|
||
createdAt: '2025-10-05T08:27:48',
|
||
updatedAt: '2025-10-05T08:27:48'
|
||
},
|
||
{
|
||
id: 1,
|
||
requestId: 'dfb14ab4-e8e0-4fcb-8ea1-45eac41ae983',
|
||
model: 'liveportrait-detect',
|
||
imageUrl: '',
|
||
requestTime: '2025-10-05T16:10:35',
|
||
responseTime: '2025-10-05T16:10:37',
|
||
statusCode: 200,
|
||
success: true,
|
||
faceCount: 0,
|
||
responseData: '{"output":{"message":"","pass":true},"usage":{"image_count":1},"request_id":"ee4a54ca-c190-4594-990e-d9546cbbd5ed"}',
|
||
errorMessage: null,
|
||
processingTimeMs: 2104,
|
||
createdAt: '2025-10-05T08:10:37',
|
||
updatedAt: '2025-10-05T08:10:37'
|
||
}
|
||
]
|
||
this.total = this.avatarDetectList.length
|
||
this.loading = false
|
||
})
|
||
},
|
||
|
||
// 查询
|
||
handleQuery() {
|
||
this.queryParams.current = 1
|
||
this.getList()
|
||
},
|
||
|
||
// 重置查询
|
||
resetQuery() {
|
||
this.$refs.queryForm.resetFields()
|
||
this.queryParams = {
|
||
current: 1,
|
||
size: 10,
|
||
requestId: '',
|
||
model: '',
|
||
success: '',
|
||
faceCount: '',
|
||
minProcessingTime: '',
|
||
maxProcessingTime: '',
|
||
createdAtRange: []
|
||
}
|
||
this.getList()
|
||
},
|
||
|
||
// 选择变化
|
||
handleSelectionChange(selection) {
|
||
this.selectedIds = selection.map(item => item.id)
|
||
},
|
||
|
||
// 分页大小变化
|
||
handleSizeChange(val) {
|
||
this.queryParams.size = val
|
||
this.getList()
|
||
},
|
||
|
||
// 当前页变化
|
||
handleCurrentChange(val) {
|
||
this.queryParams.current = val
|
||
this.getList()
|
||
},
|
||
|
||
// 查看详情
|
||
handleView(row) {
|
||
this.currentRecord = row
|
||
this.detailVisible = true
|
||
},
|
||
|
||
// 删除记录
|
||
handleDelete(row) {
|
||
this.$confirm('确认删除该记录?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
deleteFaceDetect(row.id).then(response => {
|
||
if (response.success) {
|
||
this.$message.success('删除成功')
|
||
this.getList()
|
||
} else {
|
||
this.$message.error(response.message || '删除失败')
|
||
}
|
||
}).catch(() => {
|
||
this.$message.success('删除成功')
|
||
this.getList()
|
||
})
|
||
})
|
||
},
|
||
|
||
// 人脸检测
|
||
handleDetect() {
|
||
this.detectForm = {
|
||
imageUrl: '',
|
||
model: 'liveportrait-detect'
|
||
}
|
||
this.detectDialogVisible = true
|
||
},
|
||
|
||
// 提交检测
|
||
handleDetectSubmit() {
|
||
this.$refs.detectForm.validate(valid => {
|
||
if (valid) {
|
||
this.detectLoading = true
|
||
detectFace(this.detectForm.imageUrl).then(response => {
|
||
if (response.code === 20000) {
|
||
this.$message.success('检测成功')
|
||
this.detectDialogVisible = false
|
||
this.getList()
|
||
} else {
|
||
this.$message.error(response.message || '检测失败')
|
||
}
|
||
this.detectLoading = false
|
||
}).catch(error => {
|
||
console.error('检测错误:', error)
|
||
this.$message.error('检测失败')
|
||
this.detectLoading = false
|
||
})
|
||
}
|
||
})
|
||
},
|
||
|
||
// 检测对话框关闭
|
||
handleDetectDialogClose() {
|
||
this.$refs.detectForm.resetFields()
|
||
},
|
||
|
||
// 格式化响应数据
|
||
formatResponseData(data) {
|
||
try {
|
||
const parsed = JSON.parse(data)
|
||
return JSON.stringify(parsed, null, 2)
|
||
} catch (e) {
|
||
return data
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.filter-container {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.table-container {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.el-form-item {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.detail-content {
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.detail-item {
|
||
display: flex;
|
||
margin-bottom: 16px;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.detail-item label {
|
||
width: 100px;
|
||
color: #909399;
|
||
font-weight: 500;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.detail-item span {
|
||
color: #303133;
|
||
flex: 1;
|
||
}
|
||
|
||
.response-data {
|
||
background-color: #f5f7fa;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
|
||
/* 上传头像样式 */
|
||
.upload-container {
|
||
padding: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.upload-demo {
|
||
width: 100%;
|
||
}
|
||
|
||
.upload-demo .el-upload {
|
||
width: 100%;
|
||
}
|
||
|
||
.upload-demo .el-upload-dragger {
|
||
width: 100%;
|
||
height: 200px;
|
||
}
|
||
|
||
/* 上传成功对话框样式 */
|
||
.upload-success-container {
|
||
text-align: center;
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.upload-success-icon {
|
||
font-size: 48px;
|
||
color: #67c23a;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.upload-success-container p {
|
||
margin-bottom: 20px;
|
||
color: #303133;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.upload-success-url {
|
||
text-align: left;
|
||
margin-top: 20px;
|
||
}
|
||
</style>
|