Merge branch 'main' of https://git.ambigrat.com/hom/yuanhe-checkin-electron
This commit is contained in:
@@ -28,6 +28,7 @@ export interface PatientInfo {
|
|||||||
phone: string;
|
phone: string;
|
||||||
marital: number;
|
marital: number;
|
||||||
marital_name: string;
|
marital_name: string;
|
||||||
|
is_valid_exam: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// VIP认证结果
|
// VIP认证结果
|
||||||
|
|||||||
@@ -13,40 +13,18 @@ const U1: React.FC = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [reading, setReading] = useState(false);
|
const [reading, setReading] = useState(false);
|
||||||
|
const [countdown, setCountdown] = useState(6);
|
||||||
const timerRef = useRef<number | null>(null);
|
const timerRef = useRef<number | null>(null);
|
||||||
|
const intervalRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const handleStart = () => {
|
// 实时监听身份证读取
|
||||||
// localStorage.setItem("lastIdCardNo", "31010919571209004X");
|
useEffect(() => {
|
||||||
// navigate("/u2");
|
// 启动后端监听
|
||||||
if (reading) return; // 避免重复点击
|
|
||||||
setReading(true);
|
|
||||||
// 启动后端监听;如果启动失败立即恢复 UI 状态
|
|
||||||
window.electronAPI.startIdCardListen().catch((e: any) => {
|
window.electronAPI.startIdCardListen().catch((e: any) => {
|
||||||
console.error("start_idcard_listen failed", e);
|
console.error("start_idcard_listen failed", e);
|
||||||
window.electronAPI.log("error", `start_idcard_listen failed: ${e}`);
|
window.electronAPI.log("error", `start_idcard_listen failed: ${e}`);
|
||||||
setReading(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 6 秒超时恢复
|
|
||||||
if (timerRef.current) {
|
|
||||||
clearTimeout(timerRef.current);
|
|
||||||
timerRef.current = null;
|
|
||||||
}
|
|
||||||
timerRef.current = window.setTimeout(() => {
|
|
||||||
console.warn("未在 6 秒内读取到身份证信息,恢复初始状态");
|
|
||||||
window.electronAPI.log(
|
|
||||||
"warn",
|
|
||||||
"未在 6 秒内读取到身份证信息,恢复初始状态"
|
|
||||||
);
|
|
||||||
setReading(false);
|
|
||||||
window.electronAPI.stopIdCardListen().catch(() => {});
|
|
||||||
timerRef.current = null;
|
|
||||||
}, 6000);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!reading) return;
|
|
||||||
|
|
||||||
// 监听数据
|
// 监听数据
|
||||||
window.electronAPI.onIdCardData((e: any) => {
|
window.electronAPI.onIdCardData((e: any) => {
|
||||||
const payload = e.payload;
|
const payload = e.payload;
|
||||||
@@ -64,11 +42,18 @@ const U1: React.FC = () => {
|
|||||||
"info",
|
"info",
|
||||||
`Read IDCard success: ${payload.name} ${payload.id_card_no}`
|
`Read IDCard success: ${payload.name} ${payload.id_card_no}`
|
||||||
);
|
);
|
||||||
// 成功:清理定时器,停止监听
|
|
||||||
|
// 无论是否点击了开始签到,只要读到卡就跳转
|
||||||
|
// 清理定时器
|
||||||
if (timerRef.current) {
|
if (timerRef.current) {
|
||||||
clearTimeout(timerRef.current);
|
clearTimeout(timerRef.current);
|
||||||
timerRef.current = null;
|
timerRef.current = null;
|
||||||
}
|
}
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
// 停止监听(跳转后不再需要监听)
|
||||||
window.electronAPI.stopIdCardListen().catch(() => {});
|
window.electronAPI.stopIdCardListen().catch(() => {});
|
||||||
setReading(false);
|
setReading(false);
|
||||||
navigate("/u2");
|
navigate("/u2");
|
||||||
@@ -78,20 +63,54 @@ const U1: React.FC = () => {
|
|||||||
window.electronAPI.onIdCardError((e: any) => {
|
window.electronAPI.onIdCardError((e: any) => {
|
||||||
console.error("[idcard-error]", e.payload);
|
console.error("[idcard-error]", e.payload);
|
||||||
window.electronAPI.log("error", `[idcard-error] ${e.payload}`);
|
window.electronAPI.log("error", `[idcard-error] ${e.payload}`);
|
||||||
// 发生错误时立即恢复 UI 状态
|
// 错误不停止监听,继续等待下一次读取,除非是严重错误?
|
||||||
if (timerRef.current) {
|
// 这里仅记录日志,或者在 reading 状态下提示用户
|
||||||
clearTimeout(timerRef.current);
|
if (reading) {
|
||||||
timerRef.current = null;
|
// 如果正在倒计时中出错,可以选择停止倒计时或者忽略
|
||||||
|
// 目前保持监听
|
||||||
}
|
}
|
||||||
setReading(false);
|
|
||||||
window.electronAPI.stopIdCardListen().catch(() => {});
|
|
||||||
alert(`读取身份证失败: ${e.payload}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
window.electronAPI.stopIdCardListen().catch(() => {});
|
||||||
window.electronAPI.removeIdCardListeners();
|
window.electronAPI.removeIdCardListeners();
|
||||||
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
|
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||||
};
|
};
|
||||||
}, [reading, navigate]);
|
}, [navigate, reading]);
|
||||||
|
|
||||||
|
const handleStart = () => {
|
||||||
|
if (reading) return; // 避免重复点击
|
||||||
|
setReading(true);
|
||||||
|
setCountdown(6);
|
||||||
|
|
||||||
|
// 6 秒倒计时逻辑
|
||||||
|
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = window.setInterval(() => {
|
||||||
|
setCountdown((prev) => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// 6 秒超时恢复
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
timerRef.current = window.setTimeout(() => {
|
||||||
|
console.warn("未在 6 秒内读取到身份证信息,恢复初始状态");
|
||||||
|
setReading(false);
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
timerRef.current = null;
|
||||||
|
}, 6000);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="u1-root">
|
<div className="u1-root">
|
||||||
@@ -105,7 +124,9 @@ const U1: React.FC = () => {
|
|||||||
<span className="u1-instruction">请放置身份证在读卡区域</span>
|
<span className="u1-instruction">请放置身份证在读卡区域</span>
|
||||||
<img className="u1-semicircle" alt="start button" src={semicircle} />
|
<img className="u1-semicircle" alt="start button" src={semicircle} />
|
||||||
{!reading && <LongButton text="开始签到" onClick={handleStart} />}
|
{!reading && <LongButton text="开始签到" onClick={handleStart} />}
|
||||||
{reading && <LongButtonLoading text="身份信息读取中..." />}
|
{reading && (
|
||||||
|
<LongButtonLoading text={`身份信息读取中... ${countdown}s`} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,10 +27,26 @@
|
|||||||
background: url(../../assets/u2-card.png) 100% no-repeat;
|
background: url(../../assets/u2-card.png) 100% no-repeat;
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 40px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u2-info-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 40px 48px;
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u2-error-msg {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: red;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.u2-avatar-container {
|
.u2-avatar-container {
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ const U2: React.FC = () => {
|
|||||||
localStorage.setItem("name", res.Data.name);
|
localStorage.setItem("name", res.Data.name);
|
||||||
localStorage.setItem("gender", res.Data.gender_name);
|
localStorage.setItem("gender", res.Data.gender_name);
|
||||||
} else {
|
} else {
|
||||||
alert(`${res.Message},请联系前台`);
|
alert(`未查询到您的档案信息,请联系前台工作人员`);
|
||||||
|
navigate("/");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -70,6 +71,11 @@ const U2: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
|
// 判断是否为有效签到
|
||||||
|
if (!patientInfo?.is_valid_exam || patientInfo.is_valid_exam !== 1) {
|
||||||
|
// alert("当前签到无效,请联系前台工作人员处理");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!idCardNo) {
|
if (!idCardNo) {
|
||||||
alert("未获取到身份证号,请重新刷卡");
|
alert("未获取到身份证号,请重新刷卡");
|
||||||
navigate("/");
|
navigate("/");
|
||||||
@@ -130,7 +136,7 @@ const U2: React.FC = () => {
|
|||||||
else if (genderName === "女") title = "女士";
|
else if (genderName === "女") title = "女士";
|
||||||
return `尊敬的${
|
return `尊敬的${
|
||||||
firstChar ? `${firstChar}${title}` : title
|
firstChar ? `${firstChar}${title}` : title
|
||||||
},欢迎您的到来:`;
|
},请确认您的个人信息:`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 返回头像地址:使用 gender_name 字段("女" 显示女性头像),忽略 photo_path
|
// 返回头像地址:使用 gender_name 字段("女" 显示女性头像),忽略 photo_path
|
||||||
@@ -147,6 +153,17 @@ const U2: React.FC = () => {
|
|||||||
return genderName;
|
return genderName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 脱敏证件号:保留前6后4,其余替换为*
|
||||||
|
const maskIdCard = (id?: string | null) => {
|
||||||
|
if (!id) return "---";
|
||||||
|
const s = String(id).trim();
|
||||||
|
if (s.length <= 10) return s.replace(/.(?!.{4})/g, "*");
|
||||||
|
const head = s.slice(0, 6);
|
||||||
|
const tail = s.slice(-4);
|
||||||
|
const middleLen = s.length - 10;
|
||||||
|
return head + "*".repeat(middleLen) + tail;
|
||||||
|
};
|
||||||
|
|
||||||
// 在数据加载时显示占位,加载完成后再渲染完整内容
|
// 在数据加载时显示占位,加载完成后再渲染完整内容
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <></>;
|
return <></>;
|
||||||
@@ -157,52 +174,62 @@ const U2: React.FC = () => {
|
|||||||
<span className="u2-title">{getGreeting()}</span>
|
<span className="u2-title">{getGreeting()}</span>
|
||||||
<DecorLine />
|
<DecorLine />
|
||||||
<div className="u2-info-card">
|
<div className="u2-info-card">
|
||||||
<div className="u2-avatar-container">
|
<div className="u2-info-top">
|
||||||
<img className="u2-avatar" src={getAvatarSrc()} alt="avatar" />
|
<div className="u2-avatar-container">
|
||||||
</div>
|
<img className="u2-avatar" src={getAvatarSrc()} alt="avatar" />
|
||||||
<div className="u2-details-list">
|
|
||||||
<div className="u2-detail-row">
|
|
||||||
<div className="u2-detail-bar" />
|
|
||||||
<div className="u2-detail-text">
|
|
||||||
姓名:{loading ? "" : patientInfo?.name || "---"}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="u2-details-list">
|
||||||
<div className="u2-detail-row">
|
<div className="u2-detail-row">
|
||||||
<div className="u2-detail-bar" />
|
<div className="u2-detail-bar" />
|
||||||
<div className="u2-detail-text">
|
<div className="u2-detail-text">
|
||||||
性别:{loading ? "" : getGenderLabel()}
|
姓名:{loading ? "" : patientInfo?.name || "---"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="u2-detail-row">
|
<div className="u2-detail-row">
|
||||||
<div className="u2-detail-bar" />
|
<div className="u2-detail-bar" />
|
||||||
<div className="u2-detail-text">
|
<div className="u2-detail-text">
|
||||||
年龄:{loading ? "" : patientInfo?.age || "---"}
|
性别:{loading ? "" : getGenderLabel()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="u2-detail-row">
|
<div className="u2-detail-row">
|
||||||
<div className="u2-detail-bar" />
|
<div className="u2-detail-bar" />
|
||||||
<div className="u2-detail-text">
|
<div className="u2-detail-text">
|
||||||
证件号:{idCardNo || patientInfo?.IdCard || "---"}
|
年龄:{loading ? "" : patientInfo?.age || "---"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="u2-detail-row">
|
<div className="u2-detail-row">
|
||||||
<div className="u2-detail-bar" />
|
<div className="u2-detail-bar" />
|
||||||
<div className="u2-detail-text">
|
<div className="u2-detail-text">
|
||||||
手机号:{loading ? "" : patientInfo?.phone || "---"}
|
证件号:
|
||||||
|
{loading ? "" : maskIdCard(idCardNo || patientInfo?.IdCard)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="u2-detail-row">
|
<div className="u2-detail-row">
|
||||||
<div className="u2-detail-bar" />
|
<div className="u2-detail-bar" />
|
||||||
<div className="u2-detail-text">
|
<div className="u2-detail-text">
|
||||||
婚姻状况:{loading ? "" : patientInfo?.marital_name || "---"}
|
手机号:{loading ? "" : patientInfo?.phone || "---"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="u2-detail-row">
|
||||||
|
<div className="u2-detail-bar" />
|
||||||
|
<div className="u2-detail-text">
|
||||||
|
婚姻状况:{loading ? "" : patientInfo?.marital_name || "---"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!patientInfo?.is_valid_exam || patientInfo.is_valid_exam !== 1 ? (
|
||||||
|
<div className="u2-error-msg">
|
||||||
|
未查询到您当日的体检预约信息,建议核对预约日期!
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="u2-instruction">
|
<span className="u2-instruction">
|
||||||
<span className="u2-asterisk">*</span> 如信息有误,请联系前台
|
<span className="u2-asterisk">*</span> 如信息有误,请联系前台
|
||||||
|
|||||||
Reference in New Issue
Block a user