增加读取身份证

This commit is contained in:
lxd 2025-04-07 11:14:01 +08:00
parent d1d149c3a0
commit a1ced33850
3 changed files with 355 additions and 2 deletions

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

View File

@ -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() //

View File

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