添加读取本地文件功能,支持以 base64 格式返回身份证照片;更新客户信息接口,支持上传婚姻状况及相关信息
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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<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(
|
||||
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<ApiResponse<SignInResponse>> {
|
||||
const response = await axiosInstance.post<ApiResponse<SignInResponse>>(
|
||||
"sign-in",
|
||||
|
||||
10
src/electron.d.ts
vendored
10
src/electron.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<boolean>(() => !!idCardNo);
|
||||
const [isVip, setIsVip] = useState<number | null>(null);
|
||||
const vipCalledRef = React.useRef(false);
|
||||
// 婚姻状况:10-未婚 20-已婚,初始使用后端返回的值
|
||||
const [selectedMarital, setSelectedMarital] = useState<number | null>(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 = () => {
|
||||
<div className="u2-detail-row">
|
||||
<div className="u2-detail-bar" />
|
||||
<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>
|
||||
|
||||
14
src/utils/idCard.ts
Normal file
14
src/utils/idCard.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user