增加读取身份证
This commit is contained in:
parent
d1d149c3a0
commit
a1ced33850
347
src/components/IDCardReader.vue
Normal file
347
src/components/IDCardReader.vue
Normal file
@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<div class="id-card-reader">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="connecting"
|
||||
@click="showReader"
|
||||
>
|
||||
读取身份证
|
||||
</el-button>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="身份证信息"
|
||||
width="700px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="reader-controls" v-if="!device">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="connecting"
|
||||
@click="connectDevice"
|
||||
>
|
||||
连接读卡器
|
||||
</el-button>
|
||||
<span v-if="connecting">正在连接读卡器...</span>
|
||||
</div>
|
||||
|
||||
<div v-if="cardInfo" class="card-info">
|
||||
<div class="info-container">
|
||||
<div class="info-left">
|
||||
<p>姓名:{{ cardInfo.name }}</p>
|
||||
<p>性别:{{ cardInfo.gender }}</p>
|
||||
<p>民族:{{ cardInfo.nation }}</p>
|
||||
<p>出生日期:{{ cardInfo.birthday }}</p>
|
||||
<p>地址:{{ cardInfo.address }}</p>
|
||||
</div>
|
||||
<div class="info-right">
|
||||
<div class="id-card-back">
|
||||
<div class="id-number">
|
||||
<p class="id-label">公民身份号码</p>
|
||||
<p class="id-value">{{ cardInfo.idNumber }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="cardInfo.photo" class="photo">
|
||||
<img :src="'data:image/jpeg;base64,' + cardInfo.photo" alt="身份证照片" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">关 闭</el-button>
|
||||
<el-button v-if="device" type="primary" @click="readCard">
|
||||
重新读取
|
||||
</el-button>
|
||||
<el-button v-if="device" type="warning" @click="disconnect">
|
||||
断开连接
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onUnmounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
interface IDCardInfo {
|
||||
name: string;
|
||||
gender: string;
|
||||
nation: string;
|
||||
birthday: string;
|
||||
address: string;
|
||||
idNumber: string;
|
||||
photo?: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'IDCardReader',
|
||||
props: {
|
||||
cardId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['update:cardId', 'success'],
|
||||
setup(props, { emit }) {
|
||||
const device = ref<USBDevice | null>(null);
|
||||
const connecting = ref(false);
|
||||
const cardInfo = ref<IDCardInfo | null>(null);
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
// 华视 CVR-100UC 的 USB 设备过滤器
|
||||
const USB_FILTERS = [
|
||||
{
|
||||
vendorId: 0x0483,
|
||||
productId: 0x5750
|
||||
}
|
||||
];
|
||||
|
||||
// 华视读卡器命令
|
||||
const COMMANDS = {
|
||||
FIND_CARD: new Uint8Array([0x02, 0x01, 0x01, 0x03]), // 寻卡命令
|
||||
SELECT_CARD: new Uint8Array([0x02, 0x02, 0x01, 0x03]), // 选卡命令
|
||||
READ_BASEINFO: new Uint8Array([0x02, 0x03, 0x01, 0x03]), // 读基本信息
|
||||
READ_PHOTO: new Uint8Array([0x02, 0x04, 0x01, 0x03]) // 读照片
|
||||
};
|
||||
|
||||
const showReader = () => {
|
||||
dialogVisible.value = true;
|
||||
if (!device.value) {
|
||||
connectDevice();
|
||||
}
|
||||
};
|
||||
|
||||
const connectDevice = async () => {
|
||||
try {
|
||||
connecting.value = true;
|
||||
|
||||
const selectedDevice = await navigator.usb.requestDevice({
|
||||
filters: USB_FILTERS
|
||||
});
|
||||
|
||||
await selectedDevice.open();
|
||||
await selectedDevice.selectConfiguration(1);
|
||||
await selectedDevice.claimInterface(0);
|
||||
|
||||
await selectedDevice.controlTransferOut({
|
||||
requestType: 'vendor',
|
||||
recipient: 'device',
|
||||
request: 0x01,
|
||||
value: 0x01,
|
||||
index: 0x00
|
||||
});
|
||||
|
||||
device.value = selectedDevice;
|
||||
ElMessage.success('读卡器连接成功');
|
||||
// 连接成功后自动读卡
|
||||
readCard();
|
||||
} catch (error) {
|
||||
console.error('连接设备失败:', error);
|
||||
ElMessage.error('连接读卡器失败');
|
||||
} finally {
|
||||
connecting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const sendCommand = async (command: Uint8Array): Promise<Uint8Array | null> => {
|
||||
if (!device.value) return null;
|
||||
|
||||
try {
|
||||
await device.value.transferOut(1, command);
|
||||
const result = await device.value.transferIn(1, 1024); // 增加缓冲区大小
|
||||
|
||||
if (result.data) {
|
||||
return new Uint8Array(result.data.buffer);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送命令失败:', error);
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const parseCardInfo = (data: Uint8Array): IDCardInfo => {
|
||||
// 解析返回的数据,华视读卡器返回的是 GB2312 编码的数据
|
||||
const decoder = new TextDecoder('gb2312');
|
||||
const text = decoder.decode(data.slice(5, -1)); // 去掉头尾标识符
|
||||
|
||||
// 按照华视读卡器的数据格式解析
|
||||
const fields = text.split('\x1f');
|
||||
return {
|
||||
name: fields[0] || '',
|
||||
gender: fields[1] || '',
|
||||
nation: fields[2] || '',
|
||||
birthday: fields[3]?.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3') || '',
|
||||
address: fields[4] || '',
|
||||
idNumber: fields[5] || ''
|
||||
};
|
||||
};
|
||||
|
||||
const readCard = async () => {
|
||||
if (!device.value) {
|
||||
ElMessage.error('请先连接读卡器');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 寻卡
|
||||
const findResult = await sendCommand(COMMANDS.FIND_CARD);
|
||||
if (!findResult || findResult[0] !== 0x02) {
|
||||
throw new Error('未找到身份证');
|
||||
}
|
||||
|
||||
// 2. 选卡
|
||||
const selectResult = await sendCommand(COMMANDS.SELECT_CARD);
|
||||
if (!selectResult || selectResult[0] !== 0x02) {
|
||||
throw new Error('选卡失败');
|
||||
}
|
||||
|
||||
// 3. 读取基本信息
|
||||
const infoResult = await sendCommand(COMMANDS.READ_BASEINFO);
|
||||
if (!infoResult) {
|
||||
throw new Error('读取信息失败');
|
||||
}
|
||||
|
||||
// 4. 解析基本信息
|
||||
const info = parseCardInfo(infoResult);
|
||||
|
||||
// 5. 读取照片(可选)
|
||||
try {
|
||||
const photoResult = await sendCommand(COMMANDS.READ_PHOTO);
|
||||
if (photoResult) {
|
||||
info.photo = btoa(String.fromCharCode.apply(null, Array.from(photoResult.slice(5, -1))));
|
||||
}
|
||||
} catch (photoError) {
|
||||
console.warn('读取照片失败:', photoError);
|
||||
}
|
||||
|
||||
cardInfo.value = info;
|
||||
// 更新身份证号
|
||||
emit('update:cardId', info.idNumber);
|
||||
// 发送成功事件
|
||||
emit('success', info);
|
||||
// 关闭弹窗
|
||||
dialogVisible.value = false;
|
||||
ElMessage.success('读取身份证成功');
|
||||
} catch (error) {
|
||||
console.error('读卡失败:', error);
|
||||
ElMessage.error(error instanceof Error ? error.message : '读取身份证失败');
|
||||
}
|
||||
};
|
||||
|
||||
const disconnect = async () => {
|
||||
if (device.value) {
|
||||
try {
|
||||
await device.value.close();
|
||||
device.value = null;
|
||||
cardInfo.value = null;
|
||||
ElMessage.success('已断开连接');
|
||||
} catch (error) {
|
||||
console.error('断开连接失败:', error);
|
||||
ElMessage.error('断开连接失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnect();
|
||||
});
|
||||
|
||||
return {
|
||||
device,
|
||||
connecting,
|
||||
cardInfo,
|
||||
dialogVisible,
|
||||
showReader,
|
||||
connectDevice,
|
||||
readCard,
|
||||
disconnect
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.id-card-reader {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.reader-controls {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-info {
|
||||
border: 1px solid #e8e8e8;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.id-card-back {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 200px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.id-number {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.id-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.id-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.photo {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.photo img {
|
||||
max-width: 200px;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
@ -42,6 +42,12 @@ v-loading.fullscreen.lock="fullscreenLoading"
|
||||
class="!w-200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="cardId">
|
||||
<IDCardReader
|
||||
v-model:cardId="queryParams.cardId"
|
||||
@success="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="打印日期" prop="printTimeRange">
|
||||
<el-date-picker
|
||||
v-model="queryParams.printTimeRange"
|
||||
@ -179,7 +185,7 @@ import { PatientApi, type PatientVO } from '@/api/inspect/inspectpatient'
|
||||
import * as SummaryApi from "@/api/summary";
|
||||
import { newHiprintPrintTemplate } from "@/views/summary/utils/template-helper";
|
||||
import template from "@/views/summary/print/template";
|
||||
|
||||
import IDCardReader from '@/components/IDCardReader.vue';
|
||||
defineOptions({ name: 'Department' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
@ -12,7 +12,7 @@
|
||||
<div class="iframe-container">
|
||||
<iframe
|
||||
v-if="dialogVisible"
|
||||
src="/templates/report-template.html"
|
||||
src="/inspect/templates/report-template.html"
|
||||
frameborder="0"
|
||||
style="width: 100%; height: 100%; border: none"
|
||||
@load="handleIframeLoad"
|
||||
|
Loading…
Reference in New Issue
Block a user