From 81724a778f6699cc04391a7d9b15246793f08d89 Mon Sep 17 00:00:00 2001 From: yuchenglong Date: Tue, 9 Dec 2025 13:55:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AF=BB=E5=8F=96=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E6=96=87=E4=BB=B6=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=BB=A5=20base64=20=E6=A0=BC=E5=BC=8F=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E8=BA=AB=E4=BB=BD=E8=AF=81=E7=85=A7=E7=89=87=EF=BC=9B?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=AE=A2=E6=88=B7=E4=BF=A1=E6=81=AF=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=8C=E6=94=AF=E6=8C=81=E4=B8=8A=E4=BC=A0=E5=A9=9A?= =?UTF-8?q?=E5=A7=BB=E7=8A=B6=E5=86=B5=E5=8F=8A=E7=9B=B8=E5=85=B3=E4=BF=A1?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main.js | 20 +++++ electron/preload.js | 2 + src/api/hisApi.ts | 61 ++++++++++--- src/electron.d.ts | 10 +++ src/pages/U1/u1.tsx | 5 ++ src/pages/U2/u2.css | 32 +++++++ src/pages/U2/u2.tsx | 211 ++++++++++++++++++++++++++++++++++++++------ src/utils/idCard.ts | 14 +++ 8 files changed, 313 insertions(+), 42 deletions(-) create mode 100644 src/utils/idCard.ts diff --git a/electron/main.js b/electron/main.js index 17703ef..5334fee 100644 --- a/electron/main.js +++ b/electron/main.js @@ -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", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); diff --git a/electron/preload.js b/electron/preload.js index 40a3353..9dfcc4d 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -21,4 +21,6 @@ contextBridge.exposeInMainWorld("electronAPI", { }, restartApp: () => ipcRenderer.send("restart-app"), quitApp: () => ipcRenderer.send("quit-app"), + // 读取本地文件并返回 base64 字符串 + readLocalFile: (filePath) => ipcRenderer.invoke("read-local-file", filePath), }); diff --git a/src/api/hisApi.ts b/src/api/hisApi.ts index 43cc7ce..61c84c9 100644 --- a/src/api/hisApi.ts +++ b/src/api/hisApi.ts @@ -29,6 +29,7 @@ export interface PatientInfo { marital: number; marital_name: string; is_valid_exam: number; + is_update_patient_info: number; // 是否需要更新客户信息(1-需要 0-不需要) } // VIP认证结果 @@ -151,21 +152,11 @@ function createAxiosInstance(baseURL: string): AxiosInstance { return instance; } -// 默认使用外网URL,可以通过环境变量或配置切换 +// 默认使用外网URL let currentBaseURL = API_CONFIG.EXTERNAL_URL; +// let currentBaseURL = API_CONFIG.INTERNAL_URL; 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. 通过身份证号查询基本信息 */ @@ -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> { + // 如果包含图片文件,使用 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>( + "patient-edit", + formData + ); + return response.data; + } + + // 否则以 JSON 方式提交 + const response = await axiosInstance.post>("patient-edit", { + id_no, + marital_status, + province: province || "", + city: city || "", + county: county || "", + address: address || "", + }); + return response.data; +} + +/** + * 3. 获取VIP客户认证结果 */ export async function getVipStatus( id_no: string @@ -343,7 +376,7 @@ export function parsePdfUrlResponse( * @param package_code 套餐代码 */ export async function signIn( - physical_exam_id: number, + physical_exam_id: number ): Promise> { const response = await axiosInstance.post>( "sign-in", diff --git a/src/electron.d.ts b/src/electron.d.ts index ec3a779..c035364 100644 --- a/src/electron.d.ts +++ b/src/electron.d.ts @@ -29,6 +29,16 @@ interface ElectronAPI { removeIdCardListeners: () => void; restartApp: () => 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 { diff --git a/src/pages/U1/u1.tsx b/src/pages/U1/u1.tsx index 3d088fe..55e0233 100644 --- a/src/pages/U1/u1.tsx +++ b/src/pages/U1/u1.tsx @@ -23,6 +23,9 @@ const U1: React.FC = () => { // 实时监听身份证读取 useEffect(() => { + // localStorage.setItem("lastIdCardNo", '421126199307103856'); + // navigate("/u2"); + // return; window.electronAPI.startIdCardListen().catch((e: any) => { console.error("start_idcard_listen failed", e); window.electronAPI.log("error", `start_idcard_listen failed: ${e}`); @@ -38,6 +41,8 @@ const U1: React.FC = () => { try { if (payload?.id_card_no) { localStorage.setItem("lastIdCardNo", payload.id_card_no); + // 存储完整的身份证信息供后续页面使用 + localStorage.setItem("idCardData", JSON.stringify(payload)); } } catch (err) { console.warn("localStorage.setItem failed", err); diff --git a/src/pages/U2/u2.css b/src/pages/U2/u2.css index 4bcb4b6..d91bd51 100644 --- a/src/pages/U2/u2.css +++ b/src/pages/U2/u2.css @@ -86,6 +86,38 @@ font-size: 30px; font-family: NotoSansCJKsc-Regular; 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 { diff --git a/src/pages/U2/u2.tsx b/src/pages/U2/u2.tsx index 63d651c..9874e76 100644 --- a/src/pages/U2/u2.tsx +++ b/src/pages/U2/u2.tsx @@ -11,8 +11,10 @@ import { getPatientInfo, getVipStatus, getOptionalItemList, + updatePatientInfo, PatientInfo, } from "../../api/hisApi"; +import { parseRegionCodesFromId } from "../../utils/idCard"; const U2: React.FC = () => { const navigate = useNavigate(); @@ -21,6 +23,8 @@ const U2: React.FC = () => { const [loading, setLoading] = useState(() => !!idCardNo); const [isVip, setIsVip] = useState(null); const vipCalledRef = React.useRef(false); + // 婚姻状况:10-未婚 20-已婚,初始使用后端返回的值 + const [selectedMarital, setSelectedMarital] = useState(null); useEffect(() => { if (idCardNo) { @@ -31,6 +35,15 @@ const U2: React.FC = () => { setPatientInfo(res.Data); localStorage.setItem("name", res.Data.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 { alert(`未查询到您的档案信息,请联系前台工作人员`); navigate("/"); @@ -82,45 +95,151 @@ const U2: React.FC = () => { return; } - // 判断是否为 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 needUpdate = patientInfo?.is_update_patient_info === 1; - const isPackageUndecided = - res.Data?.packageInfo?.is_optional_package === 1 && - res.Data?.packageInfo.registration_time?.length > 0; - if (isPackageUndecided) { - navigate("/u4", { state: { optionalData: res.Data } }); + // 如果需要更新且用户未选择婚姻状况(selectedMarital为null或不是10/20),则提示 + if ( + needUpdate && + (selectedMarital === null || + (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 { - // 如果没有可选套餐,检查是否有错误消息需要提示 - if (!res.Data?.packageInfo && res.Message) { + if (res.Message) { alert(res.Message); } else { navigate("/UI6"); } } - } else { - if (res.Message) { - alert(res.Message); - } else { - navigate("/UI6"); + }) + .catch((err) => { + console.error("getOptionalItemList error", err); + 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) => { - console.error("getOptionalItemList error", err); - navigate("/UI6"); + console.error("updatePatientInfo error", err); + alert("更新客户信息异常,请联系前台工作人员"); }); + } else { + // 不需要更新或用户未修改,直接进入下一步 + proceedToNext(); } }; @@ -203,7 +322,43 @@ const U2: React.FC = () => {
- 婚姻状况:{loading ? "" : patientInfo?.marital_name || "---"} + 婚姻状况: + {loading ? ( + "" + ) : patientInfo?.is_update_patient_info === 1 ? ( + // 需要更新:显示两个可选按钮 +
+ setSelectedMarital(10)} + > + 未婚 + + setSelectedMarital(20)} + > + 已婚 + +
+ ) : ( + // 不需要更新:只显示当前已选中的按钮(只读) +
+ {selectedMarital === 10 && ( + 未婚 + )} + {selectedMarital === 20 && ( + 已婚 + )} + {selectedMarital !== 10 && selectedMarital !== 20 && ( + {patientInfo?.marital_name || "---"} + )} +
+ )}
diff --git a/src/utils/idCard.ts b/src/utils/idCard.ts new file mode 100644 index 0000000..ce3f486 --- /dev/null +++ b/src/utils/idCard.ts @@ -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; +}