Files
qiming/qiming-mobile/utils/common.uts

545 lines
14 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// 检查第一个数组每个元素是否都存在于第二个数组中。
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,
};