| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- 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 邮箱([email protected])。
- * @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';
- }
- }
|