增加读卡器功能

This commit is contained in:
Euni4U 2025-04-09 15:04:42 +08:00
parent 9cf90e5423
commit a46a73e8b2
5 changed files with 433 additions and 270 deletions

View File

@ -56,7 +56,7 @@ export const PatientApi = {
},
// 修改患者信息
updatePatient: async (data: PatientVO) => {
updatePatient: async (data: Object) => {
return await request.put({ url: `/inspect/patient/update`, data })
},
@ -141,5 +141,9 @@ export const PatientApi = {
//生成体检报告
generateReport: async (medicalSn: string) => {
return await request.get({ url: `/inspect/patient/generateReport?medicalSn=` + medicalSn })
},
//更新体检日期
updateMedicalDateTime: async (medicalSn: string, medicalDateTime: Date) => {
return await request.put({ url: `/inspect/patient/updateMedicalDateTime?medicalSn=${medicalSn}&medicalDateTime=${medicalDateTime.toISOString()}` })
}
}

View File

@ -3,70 +3,17 @@
<el-button
type="primary"
:loading="connecting"
@click="showReader"
@click="readIdCard"
>
读取身份证
</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';
import sdk from './IDCardReader/sdk';
interface IDCardInfo {
name: string;
@ -75,6 +22,9 @@ interface IDCardInfo {
birthday: string;
address: string;
idNumber: string;
certOrg: string;
effDate: string;
expDate: string;
photo?: string;
}
@ -88,162 +38,96 @@ export default defineComponent({
},
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);
const deviceConnected = 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();
// SDK
const initSdk = () => {
try {
sdk.init_sdk();
console.log('SDK初始化成功');
} catch (error) {
console.error('SDK初始化失败:', error);
ElMessage.error('SDK初始化失败');
}
};
//
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) {
//
await sdk.open_device();
deviceConnected.value = true;
console.log('读卡器连接成功');
return true;
} catch (error: any) {
console.error('连接设备失败:', error);
ElMessage.error('连接读卡器失败');
//
ElMessage.error(error instanceof Error ? error.message : '连接读卡器失败');
return false;
} 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('请先连接读卡器');
//
const readIdCard = async () => {
if (connecting.value) {
ElMessage.warning('正在连接读卡器,请稍候...');
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);
// SDK
initSdk();
// 5.
try {
const photoResult = await sendCommand(COMMANDS.READ_PHOTO);
if (photoResult) {
info.photo = btoa(String.fromCharCode.apply(null, Array.from(photoResult.slice(5, -1))));
//
if (!deviceConnected.value) {
console.log('设备未连接,尝试连接...');
const connected = await connectDevice();
if (!connected) {
return;
}
} catch (photoError) {
console.warn('读取照片失败:', photoError);
ElMessage.success('读卡器连接成功');
}
cardInfo.value = info;
//
emit('update:cardId', info.idNumber);
//
emit('success', info);
//
dialogVisible.value = false;
ElMessage.success('读取身份证成功');
} catch (error) {
//
//
const result = await sdk.read_card();
if (result) {
//
console.log('组件接收到的身份证信息:', result);
//
emit('success', result);
//
await disconnect();
ElMessage.success('读取身份证成功,正在查询患者信息...');
} else {
throw new Error('读取身份证失败');
}
} catch (error: any) {
console.error('读卡失败:', error);
ElMessage.error(error instanceof Error ? error.message : '读取身份证失败');
}
};
//
const disconnect = async () => {
if (device.value) {
if (deviceConnected.value) {
try {
await device.value.close();
device.value = null;
cardInfo.value = null;
ElMessage.success('已断开连接');
await sdk.close_device();
deviceConnected.value = false;
console.log('已断开读卡器连接');
} catch (error) {
console.error('断开连接失败:', error);
ElMessage.error('断开连接失败');
}
}
};
@ -253,14 +137,8 @@ export default defineComponent({
});
return {
device,
connecting,
cardInfo,
dialogVisible,
showReader,
connectDevice,
readCard,
disconnect
readIdCard
};
}
});
@ -270,78 +148,4 @@ export default defineComponent({
.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

@ -0,0 +1,161 @@
/*
* @Author: Hikari
* @Date: 2021-04-26 18:52:16
* @LastEditTime: 2021-04-26 20:16:36
* @LastEditors: Please set LastEditors
* @Description: 读取身份证信息
* @FilePath: /cloud-desk-top/components/common/idcard/sdk.js
*/
import axios from "axios";
const sdk = new (function () {
var type = null;
// 初始化SDK 默认使用华视
this.init_sdk = function (qudao = "hs") {
this.type = qudao;
};
// 读取身份证信息
this.read_card = function () {
return new Promise((resolve, reject) => {
axios({
method: "get",
url: "http://localhost:19196/readcard",
})
.then((res) => {
let data = res.data;
let result = data;
if (result.resultFlag == 0) {
// 解析身份证信息
const idCardInfo = {
name: result.partyName || '',
gender: this.parseGender(result.gender),
nation: result.nation || '',
birthday: this.formatDate(result.bornDay) || '',
address: result.certAddress || '',
idNumber: result.certNumber || '',
certOrg: result.certOrg || '',
effDate: this.formatDate(result.effDate) || '',
expDate: result.expDate === '长期' ? '长期' : this.formatDate(result.expDate) || '',
photo: result.photo || ''
};
resolve(idCardInfo);
} else {
console.error('读取身份证失败:', result.errorMsg);
reject(new Error(result.errorMsg || '读取身份证失败'));
}
})
.catch((err) => {
console.error('读取身份证请求失败:', err);
reject(err);
});
});
};
// 格式化日期 YYYYMMDD -> YYYY-MM-DD
this.formatDate = function(dateStr) {
if (!dateStr || dateStr.length !== 8) return '';
const year = dateStr.substring(0, 4);
const month = dateStr.substring(4, 6);
const day = dateStr.substring(6, 8);
return `${year}-${month}-${day}`;
};
// 解析性别信息
this.parseGender = function(gender) {
if (!gender) return '';
// 处理字符串类型的性别
if (typeof gender === 'string') {
// 如果已经是"男"或"女",直接返回
if (gender === '男' || gender === '女') return gender;
// 处理数字字符串
if (gender === '1' || gender === 'M' || gender.toUpperCase() === 'MALE') return '男';
if (gender === '2' || gender === 'F' || gender.toUpperCase() === 'FEMALE') return '女';
}
// 处理数字类型的性别
if (typeof gender === 'number') {
return gender === 1 ? '男' : '女';
}
// 如果无法解析,返回空字符串
return '';
};
// 链接
this.open_device = function () {
return new Promise((resolve, reject) => {
axios({
method: "get",
url: "http://localhost:19196/OpenDevice",
})
.then((res) => {
let data = res.data;
let result = data;
if (result.resultFlag == 0) {
resolve(result);
} else {
console.error('连接设备失败:', result.errorMsg);
reject(new Error(result.errorMsg || '连接设备失败'));
}
})
.catch((err) => {
console.error('连接设备请求失败:', err);
reject(err);
});
});
};
// 断开
this.close_device = function () {
return new Promise((resolve, reject) => {
axios({
method: "get",
url: "http://localhost:19196/CloseDevice",
})
.then((res) => {
let data = res.data;
let result = data;
if (result.resultFlag == 0) {
resolve(result);
} else {
console.error('断开设备失败:', result.errorMsg);
reject(new Error(result.errorMsg || '断开设备失败'));
}
})
.catch((err) => {
console.error('断开设备请求失败:', err);
reject(err);
});
});
};
// 获取安全模块号
this.get_samid = function () {
return new Promise((resolve, reject) => {
axios({
method: "get",
url: "http://localhost:19196/getsamid",
})
.then((res) => {
let data = res.data;
let result = data;
if (result.resultFlag == 0) {
resolve(result.samid);
} else {
console.error('获取安全模块号失败:', result.errorMsg);
reject(new Error(result.errorMsg || '获取安全模块号失败'));
}
})
.catch((err) => {
console.error('获取安全模块号请求失败:', err);
reject(err);
});
});
};
})();
export default sdk;

View File

@ -963,6 +963,57 @@ const processItemData = (item) => {
itemStatus: item.itemStatus || '0',
sectionID: item.sectionID
}
// (BMI)
if (item.itemName && item.itemName.includes('体质指数') && item.itemName.includes('BMI') && item.itemResult) {
// BMI
const bmiValue = parseFloat(item.itemResult)
if (!isNaN(bmiValue)) {
// BMI<18.518.5BMI<2424BMI<2828BMI
if (bmiValue < 18.5) {
processedItem.note = '↓'
processedItem.risk = '体重过低'
processedItem.status = 'danger'
} else if (bmiValue >= 18.5 && bmiValue < 24) {
processedItem.note = '-'
processedItem.risk = '正常'
processedItem.status = ''
} else if (bmiValue >= 24 && bmiValue < 28) {
processedItem.note = '↑'
processedItem.risk = '超重'
processedItem.status = 'danger'
} else if (bmiValue >= 28) {
processedItem.note = '↑↑'
processedItem.risk = '肥胖'
processedItem.status = 'danger'
}
}
}
//
if (item.itemName && item.itemName.includes('血压') && item.itemResult) {
// systolic/diastolic 125/90
const bpMatch = item.itemResult.match(/(\d+)\/(\d+)/)
if (bpMatch) {
const systolic = parseInt(bpMatch[1]) //
const diastolic = parseInt(bpMatch[2]) //
if (!isNaN(systolic) && !isNaN(diastolic)) {
// : <130 <85
if (systolic >= 130 || diastolic >= 85) {
processedItem.note = '↑'
processedItem.risk = '血压偏高'
processedItem.status = 'danger'
} else {
processedItem.note = '-'
processedItem.risk = '正常'
processedItem.status = ''
}
}
}
}
return processedItem
}
//
@ -1052,6 +1103,116 @@ const getRowStatus = (item) => {
return ''
}
// BMI
const handleBmiResult = (item) => {
if (!item.value) return
const bmiValue = parseFloat(item.value)
if (isNaN(bmiValue)) return
let bmiStatus = ''
// BMI<18.518.5BMI<2424BMI<2828BMI
if (bmiValue < 18.5) {
item.note = '↓'
item.risk = '体重过低'
item.status = 'danger'
bmiStatus = `【体质指数(BMI)】${bmiValue},体重过低`
} else if (bmiValue >= 18.5 && bmiValue < 24) {
item.note = '-'
item.risk = '正常'
item.status = ''
//
} else if (bmiValue >= 24 && bmiValue < 28) {
item.note = '↑'
item.risk = '超重'
item.status = 'danger'
bmiStatus = `【体质指数(BMI)】${bmiValue},超重`
} else if (bmiValue >= 28) {
item.note = '↑↑'
item.risk = '肥胖'
item.status = 'danger'
bmiStatus = `【体质指数(BMI)】${bmiValue},肥胖`
}
//
updateGeneralSummary(bmiStatus)
}
//
const handleBloodPressureResult = (item) => {
if (!item.value) return
// systolic/diastolic 125/90
const bpMatch = item.value.match(/(\d+)\/(\d+)/)
if (!bpMatch) return
const systolic = parseInt(bpMatch[1]) //
const diastolic = parseInt(bpMatch[2]) //
let bpStatus = ''
if (isNaN(systolic) || isNaN(diastolic)) return
// : <130 <85
if (systolic >= 130 || diastolic >= 85) {
item.note = '↑'
item.risk = '血压偏高'
item.status = 'danger'
bpStatus = `【血压】${item.value},偏高`
} else {
item.note = '-'
item.risk = '正常'
item.status = ''
//
}
//
updateGeneralSummary(bpStatus)
}
//
const updateGeneralSummary = (newStatus) => {
if (!newStatus) return //
//
let currentSummary = conclusionData.value.general.summary || '未见异常'
//
const keywordMatch = newStatus.match(/【(.+?)】/)
if (!keywordMatch) return
const keyword = keywordMatch[1]
// ""
if (currentSummary === '未见异常') {
conclusionData.value.general.summary = newStatus
return
}
//
if (currentSummary.includes(`${keyword}`)) {
//
const regex = new RegExp(`${keyword}】[^;。]+[;。]?`, 'g')
currentSummary = currentSummary.replace(regex, '')
//
currentSummary = currentSummary.replace(/+/g, '').trim()
//
if (!currentSummary || currentSummary === '') {
conclusionData.value.general.summary = newStatus
} else {
//
conclusionData.value.general.summary = currentSummary + (currentSummary.endsWith('') ? '' : '') + newStatus
}
} else {
//
conclusionData.value.general.summary = currentSummary + (currentSummary.endsWith('') ? '' : '') + newStatus
}
//
conclusionData.value.general.summary = conclusionData.value.general.summary.replace(/{2,}/g, '')
}
//
const getSectionName = (sectionId) => {
//
@ -1526,6 +1687,18 @@ const handleResultChange = (item) => {
item.originalValue = item.value
// (BMI)
if (item.name && item.name.includes('体质指数') && item.name.includes('BMI')) {
handleBmiResult(item)
return
}
//
if (item.name && item.name.includes('血压')) {
handleBloodPressureResult(item)
return
}
if (item.reference && item.reference !== 'null-null') {
const value = parseFloat(item.value)
const [low, high] = item.reference
@ -1811,7 +1984,7 @@ const handleSaveAllResults = async () => {
ElMessage.warning('请选择总检医生')
return
}
try {
const userProfile = await getUserProfile()
user.value = userProfile
@ -1930,7 +2103,7 @@ const handleSaveAllResults = async () => {
await PatientApi.updatePatient({
id: selectedPatient.value.id,
status: 1, //
medicalDateTime: currentTimestamp //
})
//
@ -2191,7 +2364,19 @@ const debouncedStatusChange = debounce(async (value) => {
//
const handleStatusFilterChange = (value) => {
statusFilter.value = value
debouncedStatusChange(value)
// ""
if (value === '2') {
const today = new Date().toISOString().split('T')[0] // YYYY-MM-DD
dateRange.value = [today, today] //
// 使dateRangedebouncedStatusChange
handleDateRangeChange(dateRange.value)
} else {
//
dateRange.value = []
//
debouncedStatusChange(value)
}
}
//
@ -2501,7 +2686,7 @@ const handleDateRangeChange = async (val) => {
const params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
isprint: 1 //
isprint: 1, //
}
//
@ -2543,7 +2728,6 @@ const resetPatientStatus = async (patient) => {
await PatientApi.updatePatient({
id: patient.id,
status: 0, //
medicalDateTime: Date.now() //
})
//

View File

@ -42,12 +42,12 @@ v-loading.fullscreen.lock="fullscreenLoading"
class="!w-200px"
/>
</el-form-item>
<!-- <el-form-item prop="cardId">
<el-form-item prop="cardId">
<IDCardReader
v-model:cardId="queryParams.cardId"
@success="handleQuery"
@success="handleIdCardSuccess"
/>
</el-form-item> -->
</el-form-item>
<el-form-item label="打印日期" prop="printTimeRange">
<el-date-picker
v-model="queryParams.printTimeRange"
@ -260,6 +260,15 @@ const handleQuery = () => {
getList()
}
/** 处理身份证读卡成功事件 */
const handleIdCardSuccess = (cardInfo) => {
// 使
const tempCardId = queryParams.cardId;
queryParams.cardId = cardInfo.idNumber;
getList();
//
queryParams.cardId = tempCardId;
}
/** 添加/修改操作 */
const formRef = ref()
@ -276,6 +285,7 @@ const handlePrint = async (row: PatientVO) => {
await createPrint(row.medicalSn)
//
await PatientApi.updatePrintStatus(row.medicalSn, new Date())
await PatientApi.updateMedicalDateTime(row.medicalSn, new Date())
//
await getList()
message.success('打印成功')