2026-06-22 13:48:12 +08:00
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据身份证号计算年龄
|
|
|
|
|
|
* @param {string} idCard - 18位身份证号码
|
|
|
|
|
|
* @returns {number | null} 年龄(整数),无效身份证返回 null
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getAgeByIdCard(idCard) {
|
|
|
|
|
|
// 非空 & 类型校验
|
|
|
|
|
|
if (!idCard || typeof idCard !== "string") {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 只处理18位身份证
|
|
|
|
|
|
const id = idCard.trim();
|
|
|
|
|
|
if (id.length !== 18) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 提取出生年月日:第7~14位(索引6~13)
|
|
|
|
|
|
const birthStr = id.substring(6, 14); // 如 "19900101"
|
|
|
|
|
|
const year = Number.parseInt(birthStr.substring(0, 4), 10);
|
|
|
|
|
|
const month = Number.parseInt(birthStr.substring(4, 6), 10);
|
|
|
|
|
|
const day = Number.parseInt(birthStr.substring(6, 8), 10);
|
|
|
|
|
|
|
|
|
|
|
|
// 简单校验日期合法性(可选增强)
|
|
|
|
|
|
if (
|
|
|
|
|
|
year < 1900
|
|
|
|
|
|
|| year > new Date().getFullYear()
|
|
|
|
|
|
|| month < 1
|
|
|
|
|
|
|| month > 12
|
|
|
|
|
|
|| day < 1
|
|
|
|
|
|
|| day > 31
|
|
|
|
|
|
) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构造出生日期
|
|
|
|
|
|
const birthDate = new Date(year, month - 1, day); // 月份从0开始
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
|
|
|
|
|
|
// 计算年龄
|
|
|
|
|
|
let age = today.getFullYear() - birthDate.getFullYear();
|
|
|
|
|
|
const monthDiff = today.getMonth() - birthDate.getMonth();
|
|
|
|
|
|
const dayDiff = today.getDate() - birthDate.getDate();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果还没过生日,减1岁
|
|
|
|
|
|
if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
|
|
|
|
|
|
age -= 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
return age >= 0 ? age : null; // 年龄不能为负
|
|
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从18位身份证号中提取出生年月日
|
|
|
|
|
|
* @param {string} idCard - 身份证号码
|
|
|
|
|
|
* @returns {{ year: number; month: number; day: number } | null} 出生年月日对象,无效则返回 null
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getBirthDateFromIdCard(idCard) {
|
|
|
|
|
|
if (!idCard || typeof idCard !== "string") {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
const id = idCard.trim();
|
|
|
|
|
|
if (id.length !== 18) {
|
|
|
|
|
|
return null; // 仅支持18位身份证
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 提取第7~14位:YYYYMMDD
|
|
|
|
|
|
const birthStr = id.substring(6, 14); // 如 "19900307"
|
|
|
|
|
|
|
|
|
|
|
|
const year = Number.parseInt(birthStr.substring(0, 4), 10);
|
|
|
|
|
|
const month = Number.parseInt(birthStr.substring(4, 6), 10);
|
|
|
|
|
|
const day = Number.parseInt(birthStr.substring(6, 8), 10);
|
|
|
|
|
|
|
|
|
|
|
|
// 简单合法性校验
|
|
|
|
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
|
|
if (
|
|
|
|
|
|
year < 1900
|
|
|
|
|
|
|| year > currentYear + 1 // 允许未来1年(录入误差)
|
|
|
|
|
|
|| month < 1
|
|
|
|
|
|
|| month > 12
|
|
|
|
|
|
|| day < 1
|
|
|
|
|
|
|| day > 31
|
|
|
|
|
|
) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
export function useDebounce(value, delay = 500) {
|
|
|
|
|
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
|
|
|
|
const timerRef = useRef(null);
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
// 清除上一次的定时器
|
|
|
|
|
|
if (timerRef.current) {
|
|
|
|
|
|
clearTimeout(timerRef.current);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置新的定时器
|
|
|
|
|
|
timerRef.current = setTimeout(() => {
|
|
|
|
|
|
setDebouncedValue(value);
|
|
|
|
|
|
}, delay);
|
|
|
|
|
|
|
|
|
|
|
|
// 组件卸载或 delay/value 变化时清理
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
if (timerRef.current) {
|
|
|
|
|
|
clearTimeout(timerRef.current);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [value, delay]);
|
|
|
|
|
|
return debouncedValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 手机号脱敏
|
|
|
|
|
|
export function maskPhone(phone) {
|
|
|
|
|
|
if (!phone)
|
|
|
|
|
|
return "";
|
|
|
|
|
|
const str = String(phone).replace(/\s+/g, ""); // 去除空格
|
|
|
|
|
|
if (!/^1[3-9]\d{9}$/.test(str)) {
|
|
|
|
|
|
return phone; // 非标准手机号,原样返回
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${str.substring(0, 3)}****${str.substring(7)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 身份证号脱敏
|
|
|
|
|
|
export function maskIdCard(idCard) {
|
|
|
|
|
|
if (!idCard)
|
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
|
|
// 转为字符串并去除空格
|
|
|
|
|
|
const str = String(idCard).replace(/\s+/g, "");
|
|
|
|
|
|
|
|
|
|
|
|
// 判断是否为 18 位身份证(支持末尾 X/x)
|
|
|
|
|
|
if (!/^\d{17}[\dX]$/i.test(str)) {
|
|
|
|
|
|
return idCard; // 非标准身份证,原样返回
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return `${str.substring(0, 6)}********${str.substring(14)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 属地
|
|
|
|
|
|
export const getAreaNamePath = (item) => {
|
|
|
|
|
|
const names = [
|
|
|
|
|
|
item.provinceName,
|
|
|
|
|
|
item.cityName,
|
|
|
|
|
|
item.countryName,
|
|
|
|
|
|
item.streetName,
|
|
|
|
|
|
item.villageName,
|
|
|
|
|
|
|
|
|
|
|
|
].filter(name => name != null && name !== "");
|
|
|
|
|
|
return names.join("/");
|
|
|
|
|
|
};
|
|
|
|
|
|
// 行业类型
|
|
|
|
|
|
export const getCorpTypeNamePath = (item) => {
|
|
|
|
|
|
const names = [
|
|
|
|
|
|
item.corpTypeName,
|
|
|
|
|
|
item.corpType2Name,
|
|
|
|
|
|
item.corpType3Name,
|
|
|
|
|
|
item.corpType4Name,
|
|
|
|
|
|
].filter(name => name != null && name !== "");
|
|
|
|
|
|
return names.join("/");
|
|
|
|
|
|
};
|
|
|
|
|
|
// 身份证解码
|
|
|
|
|
|
export const UseDecodeIdCard = (userIdCard) => {
|
|
|
|
|
|
if (!userIdCard)
|
|
|
|
|
|
return userIdCard;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const decoded = atob(userIdCard);
|
|
|
|
|
|
if (/^\d{17}[\dX]$/.test(decoded)) {
|
|
|
|
|
|
return decoded;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch {
|
|
|
|
|
|
console.warn("Not a valid Base64 string, keep as is:", userIdCard);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return userIdCard; // fallback
|
|
|
|
|
|
};
|
2026-06-23 18:07:30 +08:00
|
|
|
|
|
|
|
|
|
|
/** 列表请求失败时返回空数据,避免 loading 无法结束 */
|
|
|
|
|
|
export function safeListRequest(request) {
|
|
|
|
|
|
return (params) => Promise.resolve(request(params)).catch((err) => {
|
|
|
|
|
|
console.warn("[safeListRequest]", err);
|
|
|
|
|
|
return { data: [], totalCount: 0 };
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const FILE_FETCH_TIMEOUT_MS = 3000;
|
|
|
|
|
|
const REQUEST_TIMEOUT_MS = 15000;
|
|
|
|
|
|
|
|
|
|
|
|
/** 详情/单条请求,失败或超时返回 null */
|
|
|
|
|
|
export async function safeRequest(request, params, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
|
|
|
if (typeof request !== "function") {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await Promise.race([
|
|
|
|
|
|
Promise.resolve(request(params)),
|
|
|
|
|
|
new Promise((_, reject) => {
|
|
|
|
|
|
setTimeout(() => reject(new Error("request timeout")), timeoutMs);
|
|
|
|
|
|
}),
|
|
|
|
|
|
]);
|
|
|
|
|
|
return res ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (err) {
|
|
|
|
|
|
console.warn("[safeRequest]", err);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 附件查询:本地无文件服务时不阻塞弹窗(超时后回退 mock 数据) */
|
|
|
|
|
|
export async function safeGetFiles(getFile, params, fallback = []) {
|
|
|
|
|
|
if (typeof getFile !== "function") {
|
|
|
|
|
|
return fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const files = await Promise.race([
|
|
|
|
|
|
Promise.resolve(getFile(params)).then((res) => (Array.isArray(res) ? res : [])),
|
|
|
|
|
|
new Promise((_, reject) => {
|
|
|
|
|
|
setTimeout(() => reject(new Error("getFile timeout")), FILE_FETCH_TIMEOUT_MS);
|
|
|
|
|
|
}),
|
|
|
|
|
|
]);
|
|
|
|
|
|
return files.length ? files : fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (err) {
|
|
|
|
|
|
console.warn("[safeGetFiles]", err);
|
|
|
|
|
|
return fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|