545 lines
14 KiB
Plaintext
545 lines
14 KiB
Plaintext
import { t } from "@/utils/i18n";
|
||
|
||
// 过滤非数字
|
||
const getNumbersOnly = (text: string) => {
|
||
return text?.replace(/[^0-9]/g, "");
|
||
};
|
||
|
||
// 判断字符串是否全是数字
|
||
const isNumber = (value: string) => {
|
||
return !Number.isNaN(Number(value));
|
||
};
|
||
|
||
const isWeakNumber = (value: any) => {
|
||
if (typeof value === "number") {
|
||
return true;
|
||
}
|
||
if (value && typeof value === "string") {
|
||
return isNumber(value);
|
||
}
|
||
return false;
|
||
};
|
||
|
||
// 校验手机号是否合法
|
||
function isValidPhone(phone: string) {
|
||
const reg = /^1[3456789]\d{9}$/;
|
||
return reg.test(phone);
|
||
}
|
||
|
||
// 校验邮箱地址是否合法
|
||
function isValidEmail(email: string) {
|
||
const reg = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,}$/;
|
||
return reg.test(email);
|
||
}
|
||
|
||
// 校验数据库表名是否合法
|
||
// 1. 表名必须以字母开头,后面可以跟字母、数字或下划线。
|
||
function validateTableName(tableName: string) {
|
||
const reg = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||
return reg.test(tableName);
|
||
}
|
||
|
||
// 检测字符串是否为有效的JSON格式
|
||
function isValidJSON(str: string): boolean {
|
||
if (!str || typeof str !== "string") {
|
||
return false;
|
||
}
|
||
|
||
// 去除首尾空白字符
|
||
const trimmedStr = str.trim();
|
||
|
||
// 检查基本结构:对象必须以 { 开头, 以} 结尾;
|
||
if (!/^{/.test(trimmedStr) || !/}$/.test(trimmedStr)) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
JSON.parse(trimmedStr);
|
||
return true;
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 检测字符串是否为有效的JSON格式,并返回解析结果
|
||
function parseJSON<T = any>(
|
||
str: string,
|
||
): { isValid: boolean; data?: T; error?: string } {
|
||
if (!str || typeof str !== "string") {
|
||
return { isValid: false, error: t("Mobile.Common.invalidInputString") };
|
||
}
|
||
|
||
try {
|
||
const data = JSON.parse(str) as T;
|
||
return { isValid: true, data };
|
||
} catch (error) {
|
||
return {
|
||
isValid: false,
|
||
error:
|
||
error instanceof Error
|
||
? error.message
|
||
: t("Mobile.Common.invalidJsonFormat"),
|
||
};
|
||
}
|
||
}
|
||
|
||
// 校验登录密码
|
||
function validatePassword(password: string) {
|
||
return password?.length >= 6;
|
||
// const reg = /^(?=.*\d)(?=.*[a-zA-Z])[\da-zA-Z~!@#$%^&*]{8,16}$/;
|
||
// return reg.test(password);
|
||
}
|
||
|
||
// 获取url地址search中的参数
|
||
const getURLParams = () => {
|
||
// #ifdef H5
|
||
const searchParams = window.location.search.substring(1).split("&");
|
||
// 为了解决元素隐式具有 "any" 类型的问题,将 params 的类型定义为 Record<string, string>
|
||
// 这样就可以使用 string 类型的 key 来索引 params 对象
|
||
const params: Record<string, string> = {};
|
||
for (const param of searchParams) {
|
||
const [key, value] = param.split("=");
|
||
params[key] = value;
|
||
}
|
||
return params;
|
||
// #endif
|
||
// #ifndef H5
|
||
// 非H5平台从页面参数中获取
|
||
const pages = getCurrentPages();
|
||
const currentPage = pages[pages.length - 1];
|
||
const params: Record<string, string> = {};
|
||
if (currentPage?.options) {
|
||
for (const key in currentPage.options) {
|
||
params[key] = currentPage.options[key] || "";
|
||
}
|
||
}
|
||
return params;
|
||
// #endif
|
||
};
|
||
|
||
// 给页面head添加base标签: <base target="_blank">
|
||
const addBaseTarget = () => {
|
||
// #ifdef H5
|
||
if (!document.head.querySelector("base")) {
|
||
const base = document.createElement("base");
|
||
base.target = "_blank";
|
||
document.head.append(base);
|
||
}
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
// 非H5平台无需处理
|
||
// #endif
|
||
};
|
||
|
||
// 判断对象是否为空
|
||
const isEmptyObject = (obj: Record<string, any>) => {
|
||
return obj && typeof obj === "object" && Object.keys(obj).length === 0;
|
||
};
|
||
|
||
// 格式化时间
|
||
function formatTimeAgo(targetTime: string) {
|
||
if (!targetTime) {
|
||
return "";
|
||
}
|
||
const now = new Date().getTime(); // 当前时间戳,单位为毫秒
|
||
const target = new Date(targetTime).getTime();
|
||
|
||
const diff = now - target; // 时间差(毫秒)
|
||
const diffSeconds = Math.floor(diff / 1000); // 转换为秒
|
||
const diffMinutes = Math.floor(diffSeconds / 60); // 转换为分钟
|
||
const diffHours = Math.floor(diffMinutes / 60); // 转换为小时
|
||
const diffDays = Math.floor(diffHours / 24); // 转换为天
|
||
|
||
if (diffDays > 365 * 2) {
|
||
return t("Mobile.Time.yearsAgo", {
|
||
count: Math.floor(diffDays / 365),
|
||
});
|
||
} else if (diffDays > 365) {
|
||
return t("Mobile.Time.lastYear");
|
||
} else if (diffDays > 30) {
|
||
const currentDate = new Date();
|
||
const inputDate = new Date(targetTime);
|
||
// 计算月份差
|
||
let monthsDifference =
|
||
(currentDate.getFullYear() - inputDate.getFullYear()) * 12;
|
||
monthsDifference += currentDate.getMonth() - inputDate.getMonth();
|
||
|
||
// 如果当前日期的日小于输入日期的日,月份差减 1
|
||
if (currentDate.getDate() < inputDate.getDate()) {
|
||
monthsDifference--;
|
||
}
|
||
|
||
if (monthsDifference >= 1) {
|
||
return t("Mobile.Time.monthsAgo", {
|
||
count: monthsDifference,
|
||
});
|
||
}
|
||
return null;
|
||
} else if (diffDays > 6) {
|
||
return t("Mobile.Time.daysAgo", {
|
||
count: diffDays,
|
||
});
|
||
} else if (diffDays > 2) {
|
||
let date = new Date(targetTime);
|
||
let month = date.getMonth() + 1;
|
||
let day = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate();
|
||
return `${month}-${day}`;
|
||
} else if (diffDays === 2) {
|
||
return t("Mobile.Time.dayBeforeYesterday");
|
||
} else if (diffDays === 1) {
|
||
return t("Mobile.Time.yesterday");
|
||
} else if (diffHours > 1) {
|
||
return t("Mobile.Time.hoursAgo", {
|
||
count: diffHours,
|
||
});
|
||
} else if (diffMinutes > 0) {
|
||
return t("Mobile.Time.minutesAgo", {
|
||
count: diffMinutes,
|
||
});
|
||
} else {
|
||
return t("Mobile.Time.justNow");
|
||
}
|
||
}
|
||
|
||
// html自定义转义
|
||
function encodeHTML(str: string) {
|
||
return str
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
// 检查第一个数组每个元素是否都存在于第二个数组中。
|
||
const arraysContainSameItems = (arr1: string[], arr2: string[]) => {
|
||
// 使用 Set 去重
|
||
const set1 = new Set(arr1);
|
||
const set2 = new Set(arr2);
|
||
|
||
// 检查 set1 中的每个元素是否都在 set2 中
|
||
for (let item of set1) {
|
||
if (!set2.has(item)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
};
|
||
|
||
// 向上查找元素
|
||
const findParentElement = (element: HTMLElement, className: string) => {
|
||
// #ifdef H5
|
||
let currentElement = element;
|
||
while (currentElement.parentElement) {
|
||
if (currentElement.parentElement.classList.contains(className)) {
|
||
return currentElement.parentElement;
|
||
}
|
||
currentElement = currentElement.parentElement;
|
||
}
|
||
return null;
|
||
// #endif
|
||
// #ifndef H5
|
||
// 非H5平台不支持DOM操作,返回null
|
||
return null;
|
||
// #endif
|
||
};
|
||
|
||
const findClassElement = (currentElement: HTMLElement, className: string) => {
|
||
// #ifdef H5
|
||
if (currentElement.classList.contains(className)) {
|
||
return currentElement;
|
||
}
|
||
return findParentElement(currentElement, className);
|
||
// #endif
|
||
// #ifndef H5
|
||
// 非H5平台不支持DOM操作,返回null
|
||
return null;
|
||
// #endif
|
||
};
|
||
const noop = () => {};
|
||
|
||
/**
|
||
* 生成唯一ID(同步方法,适用于所有平台)
|
||
* 格式:时间戳(13位) + 随机数(7位) = 20位字符串
|
||
* @returns 唯一ID字符串
|
||
*/
|
||
function generateUniqueId(): string {
|
||
// 时间戳(毫秒)
|
||
const timestamp = Date.now().toString(36);
|
||
|
||
// 随机数部分 - 使用多个随机源提高唯一性
|
||
const random1 = Math.random().toString(36).substring(2, 9);
|
||
const random2 = Math.random().toString(36).substring(2, 5);
|
||
|
||
// 组合成唯一ID
|
||
return `${timestamp}${random1}${random2}`;
|
||
}
|
||
|
||
/**
|
||
* 生成类似 UUID 格式的ID(同步方法)
|
||
* 格式:xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||
* @returns UUID格式的字符串
|
||
*/
|
||
function generateUUID(): string {
|
||
const timestamp = Date.now();
|
||
const random = () => Math.floor(Math.random() * 16).toString(16);
|
||
|
||
// 使用时间戳和随机数组合
|
||
let uuid = "";
|
||
for (let i = 0; i < 32; i++) {
|
||
if (i === 8 || i === 12 || i === 16 || i === 20) {
|
||
uuid += "-";
|
||
}
|
||
if (i === 12) {
|
||
uuid += "4"; // UUID version 4
|
||
} else if (i === 16) {
|
||
uuid += ((Math.random() * 4) | 8).toString(16); // UUID variant
|
||
} else {
|
||
// 混合时间戳和随机数
|
||
const useTime = i < 8;
|
||
uuid += useTime
|
||
? ((timestamp >> ((7 - i) * 4)) & 0xf).toString(16)
|
||
: random();
|
||
}
|
||
}
|
||
return uuid;
|
||
}
|
||
|
||
/**
|
||
* 替换路径模板中的动态变量
|
||
* @param template - 路径模板,例如 /view/detail/{id}
|
||
* @param params - 参数对象,例如 { id: 123 }
|
||
* @returns 替换后的路径,例如 /view/detail/123
|
||
*/
|
||
const fillPathParams = (
|
||
template: string,
|
||
params: Record<string, string | number>,
|
||
): string => {
|
||
return template.replace(/{(\w+)}/g, (_, key) => {
|
||
if (params[key] === undefined) {
|
||
throw new Error(
|
||
t("Mobile.Common.missingPathParam", {
|
||
key,
|
||
}),
|
||
);
|
||
}
|
||
return String(params[key]);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 检查路径模板中的变量是否在 data 中存在且值有效
|
||
* @param template 路径模板,例如 /user/{id}/{name}
|
||
* @param data 参数对象,例如 { id: 1, name: 'Tom' }
|
||
* @returns 是否全部存在且有效
|
||
*/
|
||
const checkPathParams = (
|
||
template: string,
|
||
data: Record<string, any>,
|
||
): boolean => {
|
||
const keys = [...template.matchAll(/{(\w+)}/g)].map((m) => m[1]);
|
||
return (
|
||
keys.length === 0 ||
|
||
keys.every(
|
||
(k) => data[k] !== undefined && data[k] !== null && data[k] !== "",
|
||
)
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 将对象转换为 URL 查询字符串
|
||
* 用于替代 URLSearchParams(微信小程序不支持)
|
||
* @param params 参数对象
|
||
* @returns URL 查询字符串,例如 "key1=value1&key2=value2"
|
||
*/
|
||
const objectToQueryString = (
|
||
params: Record<string, any> | null | undefined,
|
||
): string => {
|
||
if (!params || typeof params !== "object") {
|
||
return "";
|
||
}
|
||
|
||
const queryParts: string[] = [];
|
||
|
||
for (const key in params) {
|
||
if (params.hasOwnProperty(key)) {
|
||
const value = params[key];
|
||
|
||
// 跳过 undefined 和 null 值
|
||
if (value === undefined || value === null || value === "") {
|
||
continue;
|
||
}
|
||
|
||
// 对键和值进行 URL 编码
|
||
const encodedKey = encodeURIComponent(key);
|
||
const encodedValue = encodeURIComponent(String(value));
|
||
|
||
queryParts.push(`${encodedKey}=${encodedValue}`);
|
||
}
|
||
}
|
||
|
||
return queryParts.join("&");
|
||
};
|
||
|
||
/**
|
||
* 从 URL 中移除指定 query 参数(支持多个、支持 hash、支持不存在的参数)
|
||
* @param url
|
||
* @param keys
|
||
* @returns
|
||
*/
|
||
const removeQueryCompat = (url: string, keys: string | string[]): string => {
|
||
const arr = Array.isArray(keys) ? keys : [keys];
|
||
|
||
const [base, hash = ""] = url.split("#");
|
||
const [path, query = ""] = base.split("?");
|
||
|
||
if (!query) return url; // 没 query 无需处理
|
||
|
||
const params = query.split("&").filter((item) => {
|
||
const [key] = item.split("=");
|
||
return !arr.includes(key);
|
||
});
|
||
|
||
const newUrl = params.length ? `${path}?${params.join("&")}` : path;
|
||
|
||
return hash ? `${newUrl}#${hash}` : newUrl;
|
||
};
|
||
|
||
/**
|
||
* 登录后跳转页面,如果redirectUrl存在,则跳转到redirectUrl,否则跳转到/pages/index/index
|
||
* @param redirectUrl
|
||
* @returns
|
||
*/
|
||
const redirectTo = (redirectUrl: string) => {
|
||
if (!redirectUrl) {
|
||
uni.reLaunch({ url: "/pages/index/index" });
|
||
return;
|
||
}
|
||
|
||
// #ifdef H5 || WEB
|
||
location.replace(redirectUrl);
|
||
// #endif
|
||
|
||
// #ifdef MP-WEIXIN
|
||
// 先解码URL,避免路径被错误拼接(如 %2Fpages%2Findex 会被拼接到当前路径后)
|
||
const decodedUrl = decodeURIComponent(redirectUrl);
|
||
uni.reLaunch({ url: decodedUrl });
|
||
// 清空全局变量
|
||
globalThis.appConfig.redirectUrl = null;
|
||
// #endif
|
||
};
|
||
|
||
/**
|
||
* 获取当前页面的路径
|
||
*/
|
||
const getCurrentPagePath = () => {
|
||
// #ifdef H5 || WEB
|
||
return window.location.href;
|
||
// #endif
|
||
|
||
// #ifdef MP-WEIXIN
|
||
return getCurrentPages()[getCurrentPages().length - 1]?.route;
|
||
// #endif
|
||
};
|
||
|
||
/**
|
||
* 获取当前页面的参数
|
||
*/
|
||
const getCurrentPageParams = () => {
|
||
// #ifdef H5 || WEB
|
||
// 从 hash 中提取查询参数(适配 hash 路由模式)
|
||
const hash = window.location.hash;
|
||
const questionMarkIndex = hash.indexOf("?");
|
||
|
||
if (questionMarkIndex === -1) {
|
||
return {};
|
||
}
|
||
|
||
const queryString = hash.substring(questionMarkIndex + 1);
|
||
const params: Record<string, any> = {};
|
||
|
||
queryString.split("&").forEach((param) => {
|
||
const [key, value] = param.split("=");
|
||
if (key) {
|
||
params[key] = value ? decodeURIComponent(value) : "";
|
||
}
|
||
});
|
||
|
||
return params;
|
||
// #endif
|
||
|
||
// #ifdef MP-WEIXIN
|
||
return getCurrentPages()[getCurrentPages().length - 1]?.options;
|
||
// #endif
|
||
};
|
||
|
||
/**
|
||
* 获取当前页面的完整路径
|
||
* @param fullPath 是否包含参数
|
||
* @returns 当前页面的完整路径
|
||
*/
|
||
const getCurrentPageFullPath = (fullPath: boolean = true) => {
|
||
const path = getCurrentPagePath();
|
||
const params = objectToQueryString(getCurrentPageParams());
|
||
return fullPath && params ? `${path}?${params}` : path;
|
||
};
|
||
|
||
/**
|
||
* 跳转至上一页,如果页面栈中有多个页面,正常返回,如果页面栈只有一个页面(浏览器刷新后),使用浏览器历史记录返回 如果没有历史记录,跳转到首页
|
||
*/
|
||
const jumpNavigateBack = () => {
|
||
// 获取当前页面栈
|
||
const pages = getCurrentPages();
|
||
// 如果页面栈中有多个页面,正常返回
|
||
if (pages.length > 1) {
|
||
uni.navigateBack({
|
||
delta: 1,
|
||
});
|
||
} else {
|
||
// 如果页面栈只有一个页面(浏览器刷新后),使用浏览器历史记录返回 如果没有历史记录,跳转到首页
|
||
uni.switchTab({
|
||
url: "/pages/index/index",
|
||
});
|
||
}
|
||
};
|
||
|
||
// 判断 url 地址是否是 http 或者 https 开头
|
||
const isHttpUrl = (url: string) => {
|
||
return url.startsWith("http://") || url.startsWith("https://");
|
||
};
|
||
|
||
export {
|
||
addBaseTarget,
|
||
arraysContainSameItems,
|
||
encodeHTML,
|
||
findClassElement,
|
||
findParentElement,
|
||
formatTimeAgo,
|
||
generateUniqueId,
|
||
generateUUID,
|
||
getNumbersOnly,
|
||
getURLParams,
|
||
isEmptyObject,
|
||
isNumber,
|
||
isValidEmail,
|
||
isValidJSON,
|
||
isValidPhone,
|
||
isWeakNumber,
|
||
noop,
|
||
parseJSON,
|
||
validatePassword,
|
||
validateTableName,
|
||
fillPathParams,
|
||
checkPathParams,
|
||
objectToQueryString,
|
||
removeQueryCompat,
|
||
redirectTo,
|
||
getCurrentPagePath,
|
||
getCurrentPageParams,
|
||
getCurrentPageFullPath,
|
||
jumpNavigateBack,
|
||
isHttpUrl,
|
||
};
|