添加读取本地文件功能,支持以 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", () => {
|
app.on("activate", () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|||||||
@@ -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),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
10
src/electron.d.ts
vendored
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,6 +95,23 @@ const U2: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 先判断是否需要更新客户信息
|
||||||
|
const needUpdate = patientInfo?.is_update_patient_info === 1;
|
||||||
|
|
||||||
|
// 如果需要更新且用户未选择婚姻状况(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 是)
|
// 判断是否为 VIP 客户(0 否,1 是)
|
||||||
if (isVip === 1) {
|
if (isVip === 1) {
|
||||||
navigate("/u3");
|
navigate("/u3");
|
||||||
@@ -124,6 +154,95 @@ const U2: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 如果后端要求更新且用户修改了婚姻状况,则调用更新接口
|
||||||
|
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("updatePatientInfo error", err);
|
||||||
|
alert("更新客户信息异常,请联系前台工作人员");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 不需要更新或用户未修改,直接进入下一步
|
||||||
|
proceedToNext();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 返回头像地址:使用 gender_name 字段("女" 显示女性头像),忽略 photo_path
|
// 返回头像地址:使用 gender_name 字段("女" 显示女性头像),忽略 photo_path
|
||||||
const getAvatarSrc = () => {
|
const getAvatarSrc = () => {
|
||||||
const genderName = (patientInfo?.gender_name || "").trim();
|
const genderName = (patientInfo?.gender_name || "").trim();
|
||||||
@@ -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
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