import compose from 'ramda/es/compose'; import filter from 'ramda/es/filter'; import isNil from 'ramda/es/isNil'; import map from 'ramda/es/map'; import split from 'ramda/es/split'; import trim from 'ramda/es/trim'; /** * 移除字符串首尾的指定字符(仅各移除一个,若首/尾等于 char)。 * 支持柯里化:仅传 char 时返回 (str) => string。 * @param char 要移除的单个字符 * @param str 原字符串;省略时返回柯里化函数 * @returns 处理后的字符串,或柯里化后的函数 */ function trimChar(char: string, str: string): string; function trimChar(char: string): (str: string) => string; function trimChar(char: string, str?: string) { if (isNil(str)) { return (str1: string) => { let result = str1; if (result.charAt(0) === char) { result = result.slice(1); } if (result.charAt(result.length - 1) === char) { result = result.slice(0, -1); } return result; }; } else { let result = str; if (result.charAt(0) === char) { result = result.slice(1); } if (result.charAt(result.length - 1) === char) { result = result.slice(0, -1); } return result; } } export { trimChar }; /** * 将字符串的首字母转为小写,其余不变;空字符串原样返回。 * @param str 原字符串 * @returns 首字母小写后的字符串 */ export function toLowerCaseFirstLetter(str: string) { if (!str) return str; return str.charAt(0).toLowerCase() + str.slice(1); } /** * 将字符串的首字母转为大写,其余不变;空字符串原样返回。 * @param str 原字符串 * @returns 首字母大写后的字符串 */ export function toUpperCaseFirstLetter(str: string) { if (!str) return str; return str.charAt(0).toUpperCase() + str.slice(1); } /** * 账号脱敏:前二后四保留,中间用 '**' 代替;长度 ≤8 时仅保留后四位并前缀 '**'。空串返回 ''。 * @param str 原始账号字符串 * @returns 脱敏后的字符串,如 'us**com.'、'**5678' */ export function maskAccount(str: string): string { if (!str) return ''; const len = str.length; if (len <= 8) return '**' + str.slice(-4); return str.slice(0, 2) + '**' + str.slice(-4); } /** * 判断字符串是否为常见邮箱格式(本地部分 + @ + 域名 + 至少两位 TLD)。 * @param str 待检测字符串 * @returns 符合邮箱格式返回 true,否则 false */ export function isEmail(str: string): boolean { const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; return regex.test(str); } /** * 判断字符串是否为 Gmail 邮箱(xxx@gmail.com)。 * @param str 待检测字符串 * @returns 为 Gmail 格式返回 true,否则 false */ export function isGmail(str: string): boolean { const regex = /^[a-zA-Z0-9._%+-]+@gmail([.])com$/; return regex.test(str); } /** * 判断字符串是否为合法 IPv4 地址(四段、每段 0–255)。 * @param str 待检测字符串 * @returns 合法 IPv4 返回 true,否则 false */ export function isIp(str: string): boolean { const octet = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])'; const regex = new RegExp(`^${octet}\\.${octet}\\.${octet}\\.${octet}$`); return regex.test(str); } /** * 生成具备加密强度的唯一标识(基于 crypto.getRandomValues + 时间戳,base36)。 * 同一时刻多次调用不重复、不可预测,但较慢(依赖底层加密 API)。 * @returns 唯一字符串,适合用作防篡改或安全场景的 sign */ export function getCryptoUniqueSign() { const v = Array.from(crypto.getRandomValues(new Uint32Array(3))) .map((num) => num.toString().padStart(10, '0')) .join(''); return BigInt(`${Date.now()}${v}`).toString(36); } /** * 生成唯一标识(基于 Math.random + 时间戳,base36)。同一时刻多次调用不重复,可预测、效率较高,不适用于安全场景。 * @returns 唯一字符串,适合用作非安全场景的 ID 或 sign */ export function getUniqueSign() { const v = [Math.floor(Math.random() * 2 ** 32), Math.floor(Math.random() * 2 ** 32)] .map((num) => num.toString().padStart(10, '0')) .join(''); return BigInt(`${Date.now()}${v}`).toString(36); } /** * 将 IPv4 字符串转为 32 位无符号整数,语义与 MySQL INET_ATON 一致。非法或空串返回 0。 * @param ip 点分十进制 IPv4 字符串 * @returns 无符号整数,或 0(格式错误/空) */ export function ipToNum(ip: string): number { if (!ip) return 0; const parts = ip.split('.'); if (parts.length !== 4) return 0; const num0 = parseInt(parts[0], 10); const num1 = parseInt(parts[1], 10); const num2 = parseInt(parts[2], 10); const num3 = parseInt(parts[3], 10); if ( isNaN(num0) || num0 < 0 || num0 > 255 || isNaN(num1) || num1 < 0 || num1 > 255 || isNaN(num2) || num2 < 0 || num2 > 255 || isNaN(num3) || num3 < 0 || num3 > 255 ) { return 0; } return ((num0 << 24) | (num1 << 16) | (num2 << 8) | num3) >>> 0; } /** * 将 32 位无符号整数转为 IPv4 点分十进制字符串,语义与 MySQL INET_NTOA 一致。超出 [0, 0xffffffff] 返回 ''。 * @param num 无符号整数 * @returns 点分十进制 IP 字符串,或 ''(越界) */ export function numToIp(num: number): string { if (num < 0 || num > 0xffffffff) { return ''; } const byte3 = (num >>> 24) & 0xff; const byte2 = (num >>> 16) & 0xff; const byte1 = (num >>> 8) & 0xff; const byte0 = num & 0xff; return `${byte3}.${byte2}.${byte1}.${byte0}`; } /** * 将字符串按逗号、空白(含换行)拆分为非空字符串数组;先 trim 再按 /[,\s]+/ 拆分,并对每项 trim、过滤空串。 * @param str 原字符串(由 compose 管道传入,调用方式 toStrArray(str)) * @returns 非空字符串数组 */ export const toStrArray = compose( filter((v) => v !== ''), map((v) => trim(v)), split(/[,\s]+/), trimChar(','), trim ); /** * 根据字符组成检测文本主要语言:中文(含 CJK 等)与英文(拉丁字母)数量比较,空或仅空白返回 'unknown'。 * @param text 待检测文本 * @returns 'zh-CN' | 'en' | 'unknown' */ export function detectLanguage(text: string): 'zh-CN' | 'en' | 'unknown' { if (!text || !text.trim()) { return 'unknown'; } const trimmedText = text.trim(); const chineseRegex = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\u30a0-\u30ff\uff00-\uffef]/; const englishRegex = /[a-zA-Z]/; const hasChineseChars = chineseRegex.test(trimmedText); const hasEnglishChars = englishRegex.test(trimmedText); const chineseMatches = trimmedText.match(/[\u4e00-\u9fff]/g) || []; const englishMatches = trimmedText.match(/[a-zA-Z]/g) || []; const chineseCount = chineseMatches.length; const englishCount = englishMatches.length; if (hasChineseChars && (!hasEnglishChars || chineseCount > englishCount)) { return 'zh-CN'; } if (hasEnglishChars && (!hasChineseChars || englishCount > chineseCount)) { return 'en'; } return 'unknown'; } /** * 根据标题文本检测语言并返回源语言代码;与 TranslateSourceLang 的 value 对应,未知时默认 'zh-CN'。 * @param title 标题文本 * @returns 'zh-CN' | 'en'(检测为英文时返回 'en',其余返回 'zh-CN') */ export function getSourceLanguageFromTitle(title: string): string { const detectedLang = detectLanguage(title); switch (detectedLang) { case 'zh-CN': return 'zh-CN'; case 'en': return 'en'; default: return 'zh-CN'; } }