添加读取本地文件功能,支持以 base64 格式返回身份证照片;更新客户信息接口,支持上传婚姻状况及相关信息

This commit is contained in:
yuchenglong
2025-12-09 13:55:46 +08:00
parent 88f8896a68
commit 81724a778f
8 changed files with 313 additions and 42 deletions

View File

@@ -381,6 +381,26 @@ app.whenReady().then(() => {
} }
}); });
// 读取本地文件并以 base64 返回(用于渲染进程获取 id card 照片)
ipcMain.handle("read-local-file", async (event, filePath) => {
try {
if (!filePath || typeof filePath !== "string") {
return { success: false, error: "invalid filePath" };
}
if (!fs.existsSync(filePath)) {
return { success: false, error: `file not exists: ${filePath}` };
}
const buffer = fs.readFileSync(filePath);
const base64 = buffer.toString("base64");
const mime = "image/bmp";
return { success: true, data: base64, mime };
} catch (err) {
log.error("read-local-file error:", err);
return { success: false, error: err.message || String(err) };
}
});
app.on("activate", () => { app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) { if (BrowserWindow.getAllWindows().length === 0) {
createWindow(); createWindow();

View File

@@ -21,4 +21,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
}, },
restartApp: () => ipcRenderer.send("restart-app"), restartApp: () => ipcRenderer.send("restart-app"),
quitApp: () => ipcRenderer.send("quit-app"), quitApp: () => ipcRenderer.send("quit-app"),
// 读取本地文件并返回 base64 字符串
readLocalFile: (filePath) => ipcRenderer.invoke("read-local-file", filePath),
}); });

View File

@@ -29,6 +29,7 @@ export interface PatientInfo {
marital: number; marital: number;
marital_name: string; marital_name: string;
is_valid_exam: number; is_valid_exam: number;
is_update_patient_info: number; // 是否需要更新客户信息(1-需要 0-不需要)
} }
// VIP认证结果 // VIP认证结果
@@ -151,21 +152,11 @@ function createAxiosInstance(baseURL: string): AxiosInstance {
return instance; return instance;
} }
// 默认使用外网URL,可以通过环境变量或配置切换 // 默认使用外网URL
let currentBaseURL = API_CONFIG.EXTERNAL_URL; let currentBaseURL = API_CONFIG.EXTERNAL_URL;
// let currentBaseURL = API_CONFIG.INTERNAL_URL;
let axiosInstance = createAxiosInstance(currentBaseURL); let axiosInstance = createAxiosInstance(currentBaseURL);
/**
* 设置API基础URL
* @param useInternal 是否使用内网URL
*/
export function setApiBaseUrl(useInternal: boolean = true) {
currentBaseURL = useInternal
? API_CONFIG.INTERNAL_URL
: API_CONFIG.EXTERNAL_URL;
axiosInstance = createAxiosInstance(currentBaseURL);
}
/** /**
* 1. 通过身份证号查询基本信息 * 1. 通过身份证号查询基本信息
*/ */
@@ -182,7 +173,49 @@ export async function getPatientInfo(
} }
/** /**
* 2. 获取VIP客户认证结果 * 2. 通过身份证号更新客户信息
*/
export async function updatePatientInfo(
id_no: string,
marital_status: number,
province?: string,
city?: string,
county?: string,
address?: string,
id_no_pic?: File | Blob
): Promise<ApiResponse<null>> {
// 如果包含图片文件,使用 multipart/form-data 上传
if (id_no_pic) {
const formData = new FormData();
formData.append("id_no", id_no);
formData.append("marital_status", marital_status.toString());
formData.append("province", province || "");
formData.append("city", city || "");
formData.append("county", county || "");
formData.append("address", address || "");
formData.append("id_no_pic", id_no_pic);
const response = await axiosInstance.post<ApiResponse<null>>(
"patient-edit",
formData
);
return response.data;
}
// 否则以 JSON 方式提交
const response = await axiosInstance.post<ApiResponse<null>>("patient-edit", {
id_no,
marital_status,
province: province || "",
city: city || "",
county: county || "",
address: address || "",
});
return response.data;
}
/**
* 3. 获取VIP客户认证结果
*/ */
export async function getVipStatus( export async function getVipStatus(
id_no: string id_no: string
@@ -343,7 +376,7 @@ export function parsePdfUrlResponse(
* @param package_code 套餐代码 * @param package_code 套餐代码
*/ */
export async function signIn( export async function signIn(
physical_exam_id: number, physical_exam_id: number
): Promise<ApiResponse<SignInResponse>> { ): Promise<ApiResponse<SignInResponse>> {
const response = await axiosInstance.post<ApiResponse<SignInResponse>>( const response = await axiosInstance.post<ApiResponse<SignInResponse>>(
"sign-in", "sign-in",

10
src/electron.d.ts vendored
View File

@@ -29,6 +29,16 @@ interface ElectronAPI {
removeIdCardListeners: () => void; removeIdCardListeners: () => void;
restartApp: () => void; restartApp: () => void;
quitApp: () => void; quitApp: () => void;
// 读取本地文件并返回 base64如果失败返回 { success:false, error }
readLocalFile: (
filePath: string
) => Promise<{
success: boolean;
data?: string;
mime?: string;
error?: string;
}>;
// 成功时返回 { success:true, data: base64String, mime?: mimeType }
} }
interface Window { interface Window {

View File

@@ -23,6 +23,9 @@ const U1: React.FC = () => {
// 实时监听身份证读取 // 实时监听身份证读取
useEffect(() => { useEffect(() => {
// localStorage.setItem("lastIdCardNo", '421126199307103856');
// navigate("/u2");
// return;
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}`);
@@ -38,6 +41,8 @@ const U1: React.FC = () => {
try { try {
if (payload?.id_card_no) { if (payload?.id_card_no) {
localStorage.setItem("lastIdCardNo", payload.id_card_no); localStorage.setItem("lastIdCardNo", payload.id_card_no);
// 存储完整的身份证信息供后续页面使用
localStorage.setItem("idCardData", JSON.stringify(payload));
} }
} catch (err) { } catch (err) {
console.warn("localStorage.setItem failed", err); console.warn("localStorage.setItem failed", err);

View File

@@ -86,6 +86,38 @@
font-size: 30px; font-size: 30px;
font-family: NotoSansCJKsc-Regular; font-family: NotoSansCJKsc-Regular;
line-height: 42px; line-height: 42px;
display: flex;
align-items: center;
gap: 12px;
}
.u2-marital-selector {
display: inline-flex;
gap: 16px;
align-items: center;
}
.u2-marital-option {
padding: 6px 18px;
font-size: 26px;
border: 2px solid rgba(0, 45, 93, 0.3);
border-radius: 6px;
background: white;
color: rgba(0, 45, 93, 0.8);
cursor: pointer;
transition: all 0.2s ease;
}
.u2-marital-option:hover {
border-color: rgba(0, 45, 93, 0.6);
background: rgba(0, 45, 93, 0.05);
}
.u2-marital-option.selected {
border-color: rgba(214, 30, 54, 1);
background: rgba(214, 30, 54, 0.08);
color: rgba(214, 30, 54, 1);
font-weight: 600;
} }
.u2-user-details { .u2-user-details {

View File

@@ -11,8 +11,10 @@ import {
getPatientInfo, getPatientInfo,
getVipStatus, getVipStatus,
getOptionalItemList, getOptionalItemList,
updatePatientInfo,
PatientInfo, PatientInfo,
} from "../../api/hisApi"; } from "../../api/hisApi";
import { parseRegionCodesFromId } from "../../utils/idCard";
const U2: React.FC = () => { const U2: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -21,6 +23,8 @@ const U2: React.FC = () => {
const [loading, setLoading] = useState<boolean>(() => !!idCardNo); const [loading, setLoading] = useState<boolean>(() => !!idCardNo);
const [isVip, setIsVip] = useState<number | null>(null); const [isVip, setIsVip] = useState<number | null>(null);
const vipCalledRef = React.useRef(false); const vipCalledRef = React.useRef(false);
// 婚姻状况10-未婚 20-已婚,初始使用后端返回的值
const [selectedMarital, setSelectedMarital] = useState<number | null>(null);
useEffect(() => { useEffect(() => {
if (idCardNo) { if (idCardNo) {
@@ -31,6 +35,15 @@ const U2: React.FC = () => {
setPatientInfo(res.Data); setPatientInfo(res.Data);
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);
// 初始化婚姻状况选择器
// 如果需要更新且marital不是10或20则设为null未选中
const needUpdate = res.Data.is_update_patient_info === 1;
const marital = res.Data.marital;
if (needUpdate && marital !== 10 && marital !== 20) {
setSelectedMarital(null);
} else {
setSelectedMarital(marital);
}
} else { } else {
alert(`未查询到您的档案信息,请联系前台工作人员`); alert(`未查询到您的档案信息,请联系前台工作人员`);
navigate("/"); navigate("/");
@@ -82,45 +95,151 @@ const U2: React.FC = () => {
return; return;
} }
// 判断是否为 VIP 客户0 否1 是) // 判断是否需要更新客户信息
if (isVip === 1) { const needUpdate = patientInfo?.is_update_patient_info === 1;
navigate("/u3");
return;
} else {
// 调用接口判断是否有可选套餐
getOptionalItemList(idCardNo)
.then((res) => {
if (res.Status === 200) {
localStorage.setItem(
"selectedExamId",
res.Data.packageInfo.physical_exam_id.toString() || ""
);
const isPackageUndecided = // 如果需要更新且用户未选择婚姻状况selectedMarital为null或不是10/20则提示
res.Data?.packageInfo?.is_optional_package === 1 && if (
res.Data?.packageInfo.registration_time?.length > 0; needUpdate &&
if (isPackageUndecided) { (selectedMarital === null ||
navigate("/u4", { state: { optionalData: res.Data } }); (selectedMarital !== 10 && selectedMarital !== 20))
) {
alert("请确认婚姻状况");
return;
}
const maritalChanged =
selectedMarital !== null && selectedMarital !== patientInfo?.marital;
const proceedToNext = () => {
// 判断是否为 VIP 客户0 否1 是)
if (isVip === 1) {
navigate("/u3");
return;
} else {
// 调用接口判断是否有可选套餐
getOptionalItemList(idCardNo)
.then((res) => {
if (res.Status === 200) {
localStorage.setItem(
"selectedExamId",
res.Data.packageInfo.physical_exam_id.toString() || ""
);
const isPackageUndecided =
res.Data?.packageInfo?.is_optional_package === 1 &&
res.Data?.packageInfo.registration_time?.length > 0;
if (isPackageUndecided) {
navigate("/u4", { state: { optionalData: res.Data } });
} else {
// 如果没有可选套餐,检查是否有错误消息需要提示
if (!res.Data?.packageInfo && res.Message) {
alert(res.Message);
} else {
navigate("/UI6");
}
}
} else { } else {
// 如果没有可选套餐,检查是否有错误消息需要提示 if (res.Message) {
if (!res.Data?.packageInfo && res.Message) {
alert(res.Message); alert(res.Message);
} else { } else {
navigate("/UI6"); navigate("/UI6");
} }
} }
} else { })
if (res.Message) { .catch((err) => {
alert(res.Message); console.error("getOptionalItemList error", err);
} else { navigate("/UI6");
navigate("/UI6"); });
}
};
// 如果后端要求更新且用户修改了婚姻状况,则调用更新接口
if (needUpdate && maritalChanged && selectedMarital !== null) {
// 读取存储的完整身份证数据
let idCardData: any = {};
try {
const storedData = localStorage.getItem("idCardData");
if (storedData) {
idCardData = JSON.parse(storedData);
}
} catch (err) {
console.warn("Failed to parse idCardData from localStorage", err);
}
const address = idCardData.address || "";
// 根据身份证号推断省/市/县编码(优先使用 idCardNo
const regionFromId = parseRegionCodesFromId(idCardNo || undefined);
const provinceCode = regionFromId.province || "";
const cityCode = regionFromId.city || "";
const countyCode = regionFromId.county || "";
const tryUploadWithPhoto = async () => {
try {
const photoPath = idCardData.photo_path;
if (
photoPath &&
window.electronAPI &&
window.electronAPI.readLocalFile
) {
const res = await window.electronAPI.readLocalFile(photoPath);
if (res && res.success && res.data) {
const base64 = res.data as string;
// 仅支持 BMP优先使用主进程返回的 mime否则默认为 image/bmp
const mime = res.mime || "image/bmp";
// base64 -> Blob
const byteCharacters = atob(base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: mime });
// 调用带图片的更新接口(传入推断的省市县编码)
const apiRes = await updatePatientInfo(
idCardNo,
selectedMarital,
provinceCode,
cityCode,
countyCode,
address,
blob
);
return apiRes;
} }
} }
} catch (err) {
console.warn("upload photo failed", err);
}
// 回退:无图片或上传失败时,走普通接口(包含推断的省市县编码)
return updatePatientInfo(
idCardNo,
selectedMarital,
provinceCode,
cityCode,
countyCode,
address
);
};
tryUploadWithPhoto()
.then((res) => {
if (res.Status === 200) {
console.log("客户信息更新成功");
proceedToNext();
} else {
alert(res.Message || "更新客户信息失败");
}
}) })
.catch((err) => { .catch((err) => {
console.error("getOptionalItemList error", err); console.error("updatePatientInfo error", err);
navigate("/UI6"); alert("更新客户信息异常,请联系前台工作人员");
}); });
} else {
// 不需要更新或用户未修改,直接进入下一步
proceedToNext();
} }
}; };
@@ -203,7 +322,43 @@ const U2: React.FC = () => {
<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?.is_update_patient_info === 1 ? (
// 需要更新:显示两个可选按钮
<div className="u2-marital-selector">
<span
className={`u2-marital-option ${
selectedMarital === 10 ? "selected" : ""
}`}
onClick={() => setSelectedMarital(10)}
>
</span>
<span
className={`u2-marital-option ${
selectedMarital === 20 ? "selected" : ""
}`}
onClick={() => setSelectedMarital(20)}
>
</span>
</div>
) : (
// 不需要更新:只显示当前已选中的按钮(只读)
<div className="u2-marital-selector">
{selectedMarital === 10 && (
<span className="u2-marital-option selected"></span>
)}
{selectedMarital === 20 && (
<span className="u2-marital-option selected"></span>
)}
{selectedMarital !== 10 && selectedMarital !== 20 && (
<span>{patientInfo?.marital_name || "---"}</span>
)}
</div>
)}
</div> </div>
</div> </div>
</div> </div>

14
src/utils/idCard.ts Normal file
View File

@@ -0,0 +1,14 @@
export function parseRegionCodesFromId(idNo: string | null | undefined) {
const result = { province: "", city: "", county: "" };
if (!idNo) return result;
const s = String(idNo).trim();
// 身份证号码前6位为行政区划代码
if (!/^[0-9]{6,}/.test(s)) return result;
const first6 = s.slice(4, 6);
const first4 = s.slice(2, 4);
const first2 = s.slice(0, 2);
result.county = first6;
result.city = `${first4}`;
result.province = `${first2}`;
return result;
}