chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,544 @@
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,
};