新增证件识别接口

This commit is contained in:
BBIT-Kai
2025-10-29 13:53:55 +08:00
parent a1f0d0ad55
commit aff1b85ab0
14 changed files with 720 additions and 74 deletions
+1
View File
@@ -1,3 +1,4 @@
export * from './iva';
export * from './license';
export * from './sca';
export * from './ticket';
+21
View File
@@ -0,0 +1,21 @@
import { pyRequestClient } from '#/api/request';
/**
* 获取已分析的图片列表
*/
export async function refreshLicenseImageList(page = 1, pageSize = 10) {
return pyRequestClient.get('/llm/getLicenseImageList', {
params: { page, page_size: pageSize },
});
}
/**
* 上传图片分析任务
*/
export async function createLicenseImageTask(formData: FormData) {
return pyRequestClient.post('/llm/createLicenseImageTask', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
@@ -7,7 +7,7 @@ const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'ic:round-remove-red-eye',
authority: ['iva', 'sca', 'ysa', 'ticket'],
authority: ['iva', 'sca', 'ysa', 'ticket', 'license'],
keepAlive: true,
order: 2,
title: $t('计算机视觉'),
@@ -59,6 +59,17 @@ const routes: RouteRecordRaw[] = [
},
component: () => import('#/views/cv/ticket/index.vue'),
},
{
name: 'LICENSE',
path: '/cv/license',
meta: {
authority: ['license'],
icon: 'mdi:certificate',
title: $t('证件照片分析'),
keepAlive: true,
},
component: () => import('#/views/cv/license/index.vue'),
},
{
name: 'CVAT',
path: '/cv/cvat',
@@ -38,7 +38,7 @@ const routes: RouteRecordRaw[] = [
component: IFrameView,
meta: {
icon: 'mdi:wall-fire',
iframeSrc: 'http://ai.ronsunny.cn:13010/',
iframeSrc: 'http://s1.ronsunny.cn:13010/',
keepAlive: false,
title: 'RAG Flow',
},
@@ -0,0 +1,312 @@
<script setup lang="ts">
import { computed, onDeactivated, onMounted, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Form, Input, message } from 'ant-design-vue';
import * as api from '#/api';
const list = ref<any[]>([]);
const error = ref<null | string>(null);
const selectedItem = ref<any>(null);
async function loadList() {
error.value = null;
const res =
(await api.refreshLicenseImageList(page.value, pageSize.value)) || [];
list.value = res.items;
total.value = res.total;
}
function createTask() {
modalApi.open();
}
// 分页参数
const page = ref(1);
const pageSize = ref(9);
const total = ref(0); // 总条数
// 计算总页数
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
function changePage(newPage) {
page.value = newPage;
loadList();
}
async function selectItem(item: any) {
selectedItem.value = item;
refreshLineChart();
}
onMounted(() => {
loadList();
});
const showInfoStr = ref<Record<string, number | string>>({});
const showInfoStr2 = ref<Record<string, number | string>>({});
// key 映射表
const keyMap: Record<string, string> = {
// 身份证
side: '证件面',
name: '姓名',
gender: '性别',
ethnicity: '民族',
id_number: '身份证号',
birth_date: '出生日期',
address: '住址',
issuing_authority: '签发机关',
valid_period_start: '开始日期',
valid_period_end: '结束日期',
notes: '备注',
// 银行卡
bank_name: '发卡行',
card_number: '卡号',
card_holder: '持卡人',
expiry_date: '有效期',
card_type: '卡种',
result: '识别结果',
issuer_country: '发卡国家',
};
// 递归替换函数
function translateKeys(obj: any, map: Record<string, string>): any {
if (Array.isArray(obj)) {
return obj.map((item) => translateKeys(item, map));
} else if (obj !== null && typeof obj === 'object') {
const newObj: Record<string, any> = {};
for (const key in obj) {
const newKey = map[key] || key; // 没映射就用原来的 key
newObj[newKey] = translateKeys(obj[key], map);
}
return newObj;
}
return obj; // 基本类型直接返回
}
function refreshLineChart() {
const data = selectedItem.value;
showInfoStr.value = {
项目名: data.name,
上传时间: data.created_at,
文件名: data.file_name,
文件大小: `${data.size} MB`,
分辨率: data.resolution,
证件类型:
data.type === -1 ? '未知证件' : data.type === 0 ? '身份证' : '银行卡',
};
showInfoStr2.value = translateKeys(data.content, keyMap);
}
onDeactivated(() => {
// 离开路由时清理状态
selectedItem.value = null;
showInfoStr.value = {};
});
const projectName = ref('');
const fileName = ref('');
const selectedFile = ref<File | null>(null);
const fileInputRef = ref<HTMLInputElement | null>(null);
const [Modal, modalApi] = useVbenModal({
title: '新建证件照片分析任务',
class: 'w-[600px]',
onCancel() {
modalApi.close();
},
onConfirm() {
if (!selectedFile.value) {
message.warning('请选择证件照片');
return;
}
uploadFile();
},
});
async function uploadFile() {
if (!selectedFile.value) {
message.warning('请选择证件照');
return;
}
// 先关闭弹窗
modalApi.close();
try {
const formData = new FormData();
formData.append('file', selectedFile.value);
formData.append('projectName', projectName.value);
await api.createLicenseImageTask(formData);
// 接口完成后再触发事件
message.success('分析任务完成');
loadList();
// 清空表单
selectedFile.value = null;
projectName.value = '';
fileName.value = '';
} catch {
message.error('上传失败');
}
}
function selectFile() {
fileInputRef.value?.click();
}
function handleFileChange(event: Event) {
const files = (event.target as HTMLInputElement).files;
if (files && files.length > 0) {
selectedFile.value = files[0];
fileName.value = files[0].name;
}
}
</script>
<template>
<div class="flex h-[90dvh] w-full flex-col">
<Modal>
<Form layout="vertical">
<Form.Item label="任务名称">
<Input v-model:value="projectName" placeholder="可为空,将取随机值" />
</Form.Item>
<Form.Item label="证件照片" required>
<div
@click="selectFile"
style="
padding: 16px;
text-align: center;
cursor: pointer;
border: 1px dashed #d9d9d9;
"
>
{{ fileName || '点击选择文件' }}
<input
type="file"
accept="image/*"
ref="fileInputRef"
@change="handleFileChange"
style="display: none"
/>
</div>
</Form.Item>
</Form>
</Modal>
<BaseModal />
<div class="flex h-full w-full bg-gray-50">
<!-- 左侧筛选 + 列表 -->
<div class="flex w-64 flex-col border-r bg-white p-4">
<!-- 按钮组 -->
<div class="mb-4 flex justify-between space-x-2">
<Button type="primary" @click="createTask" class="flex-1">
新建任务
</Button>
</div>
<!-- 列表 -->
<div class="flex-1 space-y-2 overflow-auto">
<div
v-for="item in list"
:key="item.v_id"
@click="selectItem(item)"
class="cursor-pointer rounded border p-3 hover:bg-gray-100"
:class="{ 'bg-gray-100': item.id === selectedItem?.id }"
>
<div class="text-base font-medium">{{ item.name }}</div>
<div class="text-sm text-gray-400">{{ item.created_at }}</div>
</div>
</div>
<!-- 分页 -->
<div class="mt-2 flex justify-center space-x-2">
<button
:disabled="page === 1"
@click="changePage(page - 1)"
class="rounded border px-1"
>
上一页
</button>
<span> {{ page }} / {{ totalPages }} </span>
<button
:disabled="page === totalPages"
@click="changePage(page + 1)"
class="rounded border px-1"
>
下一页
</button>
</div>
</div>
<!-- 右侧Tab 内容区 -->
<div class="flex flex-1 flex-col overflow-hidden p-6">
<div
v-if="!selectedItem"
class="flex h-full items-center justify-center text-gray-400"
>
请先选择左侧列表中的分析任务
</div>
<template v-else>
<div class="flex h-full flex-col gap-4">
<!-- 主内容区域左右结构 -->
<div class="flex flex-1 gap-4">
<!-- 左侧 -->
<div class="flex w-72 flex-col gap-4">
<!-- 视频基础信息展示 -->
<div
class="w-full rounded border bg-white p-4"
id="video_base_info"
>
<div
v-for="(value, key) in showInfoStr"
:key="key"
class="mb-2 flex text-sm text-gray-700"
>
<div class="w-28 font-medium text-gray-900">
{{ key }}
</div>
<div class="flex-1 break-all text-gray-600">
{{ value || '—' }}
</div>
</div>
</div>
<!-- 空白卡片 -->
<div class="flex-1 rounded border bg-white p-4">
<div
v-for="(value, key) in showInfoStr2"
:key="key"
class="mb-2 flex text-sm text-gray-700"
>
<div class="w-32 font-medium text-gray-900">
{{ key }}
</div>
<div class="flex-1 break-all text-gray-600">
{{ value || '—' }}
</div>
</div>
</div>
</div>
<!-- 右侧 -->
<div class="flex flex-1 flex-col gap-4">
<!-- 左右两个图片显示 -->
<!-- 左图 -->
<div
class="flex h-full w-full items-center justify-center rounded border bg-white p-4"
>
<img
:src="selectedItem?.oss_url"
alt="左图"
class="h-[80dvh] w-full object-contain"
/>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
@@ -94,15 +94,14 @@ async function uploadFile() {
formData.append('file', selectedFile.value);
formData.append('projectName', projectName.value);
await api.createTicketImageTask(formData);
// 接口完成后再触发事件
message.success('分析任务完成');
loadList();
// 清空表单
selectedFile.value = null;
projectName.value = '';
fileName.value = '';
// 接口完成后再触发事件
message.success('分析任务完成');
loadList();
} catch {
message.error('上传失败');
}
@@ -43,6 +43,13 @@ const cv: WorkbenchQuickNavItem[] = [
title: '仪评指标联分析',
url: '/cv/ticket',
},
{
color: '#3fb27f',
authority: ['license'],
icon: 'mdi:certificate',
title: '证件照片分析',
url: '/cv/license',
},
{
color: '#3fb27f',
icon: 'ion:bar-chart-outline',