newCloud/src/utils/utils.ts
2025-06-13 19:18:28 +08:00

828 lines
25 KiB
TypeScript
Raw 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 { parse } from 'querystring';
import moment from 'moment';
import numeral from 'numeral';
import ExportJsonExcel from 'js-export-excel';
import type { RangePickerDateProps } from 'antd/es/date-picker/generatePicker';
import type { ProColumns } from '@ant-design/pro-table';
type RangePickerValue = RangePickerDateProps<moment.Moment>['value']
/* eslint no-useless-escape:0 import/prefer-default-export:0 */
const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
export const isUrl = (path: string): boolean => reg.test(path);
export const isAntDesignPro = (): boolean => {
if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
return true;
}
return window.location.hostname === 'preview.pro.ant.design';
};
// For the official demo site, it is used to turn off features that are not needed in the real development environment
export const isAntDesignProOrDev = (): boolean => {
const { NODE_ENV } = process.env;
if (NODE_ENV === 'development') {
return true;
}
return isAntDesignPro();
};
export const getPageQuery = () => parse(window.location.href.split('?')[1]);
export function getTimeDistance(type: 'today' | 'week' | 'month' | 'year'): RangePickerValue {
const now = new Date();
const oneDay = 1000 * 60 * 60 * 24;
if (type === 'today') {
now.setHours(0);
now.setMinutes(0);
now.setSeconds(0);
return [moment(now), moment(now.getTime() + (oneDay - 1000))];
}
if (type === 'week') {
let day = now.getDay();
now.setHours(0);
now.setMinutes(0);
now.setSeconds(0);
if (day === 0) {
day = 6;
} else {
day -= 1;
}
const beginTime = now.getTime() - day * oneDay;
return [moment(beginTime), moment(beginTime + (7 * oneDay - 1000))];
}
const year = now.getFullYear();
if (type === 'month') {
const month = now.getMonth();
const nextDate = moment(now).add(1, 'months');
const nextYear = nextDate.year();
const nextMonth = nextDate.month();
return [
moment(`${year}-${fixedZero(month + 1)}-01 00:00:00`),
moment(moment(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).valueOf() - 1000),
];
}
return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)];
}
export function fixedZero(val: number) {
return val * 1 < 10 ? `0${val}` : val;
}
// 拆分小数点前后数字
export const splitAmount = (value: any, fixedType?: string) => {
const formatType = fixedType || '0,0'
const stringValue = numeral(value).format(formatType)
const [intValue, floatValue] = stringValue.split(".")
return [intValue, floatValue]
}
// 报表导出excel 无多级表头
export const exportExcel = async (
outColumn: ProColumns<any>[],
reqDetailList: any[],
fileName?: string,
) => {
// 网络请求命名空间
// const outColumn: ProColumns<any>[] = columns.slice(3); // 需要放在state里边,TableColumns
const option: { fileName: string; datas: any[] } = {
fileName: fileName || '新建文件', // : '收营员报表' + moment().format('YYYYMMDDHHmmss'),
datas: [],
};
if (reqDetailList?.length) {
const getList = (data = [], index: number) => {
const wrapData: any = data.map((item: any) => {
const node = { ...item };
if (item.children && item.children.length > 0) {
const childNode = getList(item.children, index)
return [node, ...childNode.data]
}
return node
});
return { data: wrapData, index: index + 1 };
}
const data = getList(reqDetailList as [], 0)
const outList = data.data.flat(data.index + 1)
option.datas = [
{
sheetData: outList.map((item: any, index: number) => {
const result = {};
outColumn.forEach((c: any, i) => {
if (c.hideInTable) return;
if (c.dataIndex === 'index') {
result[c.dataIndex] = index + 1;
} else if (c.render) {
result[c.dataIndex] = typeof c.render(item[c.dataIndex], item) === 'object' ?
item[c.dataIndex] : c.render(item[c.dataIndex], item)
} else {
result[c.dataIndex] = item[c.dataIndex] ? item[c.dataIndex] : item[c.dataIndex] === 0 ? 0 : ''
}
});
// if(item.children) {
// const childrenColums = item.children.map((child, index) => {
// const res = {};
// outColumn.forEach((c: any, i) => {
// if (c.hideInTable) return;
// if (i !== 0) {
// res[c.dataIndex] = c.render ? c.render(child[c.dataIndex],item) : child[c.dataIndex] ;
// } else {
// res[c.dataIndex] = index + 1;
// }
// });
// return res
// })
// console.log(childrenColums)
// // return {childrenColums}
// }
// console.log('result',result)
return result;
}),
sheetName: 'sheet1', // Excel文件名称
sheetFilter: outColumn.map((item) => item.dataIndex),
sheetHeader: outColumn.map((item) => item.title),
columnWidths: outColumn.map(() => 10),
},
];
// function warpNode(data) {
// data.map((item, index) => {
// const result = {};
// if(item.children) {
// }
// outColumn.forEach((c: any, i) => {
// if (c.hideInTable) return;
// if (i !== 0) {
// result[c.dataIndex] = item[c.dataIndex];
// } else {
// result[c.dataIndex] = index + 1;
// }
// });
// return result;
// })
// }
// }
const toExcel = new ExportJsonExcel(option);
toExcel.saveExcel();
return { message: 'ok' };
}
return { message: '没有可导出的数据' };
};
// 合作商户资金到账汇总报表导出的修改
export const exportExcelReceivedSum = (
outColumn: ProColumns<any>[],
reqDetailList: any[],
fileName?: string,) => {
const option: { fileName: string; datas: any[] } = {
fileName: fileName || '新建文件', // : '收营员报表' + moment().format('YYYYMMDDHHmmss'),
datas: [],
};
if (reqDetailList?.length) {
const getList = (data = [], index: number) => {
const wrapData: any = data.map((item: any) => {
const node = { ...item };
if (item.children && item.children.length > 0) {
const childNode = getList(item.children, index)
return [node, ...childNode.data]
}
return node
});
return { data: wrapData, index: index + 1 };
}
const data = getList(reqDetailList as [], 0)
const outList = data.data.flat(data.index + 1)
console.log('outList', outList)
option.datas = [
{
sheetData: outList.map((item: any, index: number) => {
const result = {};
// 判断这个对象是否都有值
outColumn.forEach((c: any, i) => {
if (c.hideInTable) return;
if (c.render) {
result[c.dataIndex] = typeof c.render(item[c.dataIndex], item) === 'object' ?
item[c.dataIndex] : c.render(item[c.dataIndex], item)
} else {
result[c.dataIndex] = item[c.dataIndex] ? item[c.dataIndex] : item[c.dataIndex] === 0 ? 0 : ''
}
});
// if(item.children) {
// const childrenColums = item.children.map((child, index) => {
// const res = {};
// outColumn.forEach((c: any, i) => {
// if (c.hideInTable) return;
// if (i !== 0) {
// res[c.dataIndex] = c.render ? c.render(child[c.dataIndex],item) : child[c.dataIndex] ;
// } else {
// res[c.dataIndex] = index + 1;
// }
// });
// return res
// })
// // return {childrenColums}
// }
return result;
}),
sheetName: 'sheet1', // Excel文件名称
sheetFilter: outColumn.map((item) => item.dataIndex),
sheetHeader: outColumn.map((item) => item.title),
columnWidths: outColumn.map(() => 10),
},
];
console.log('option.datas', option.datas)
// function warpNode(data) {
// data.map((item, index) => {
// const result = {};
// if(item.children) {
// }
// outColumn.forEach((c: any, i) => {
// if (c.hideInTable) return;
// if (i !== 0) {
// result[c.dataIndex] = item[c.dataIndex];
// } else {
// result[c.dataIndex] = index + 1;
// }
// });
// return result;
// })
// }
// }
const toExcel = new ExportJsonExcel(option);
toExcel.saveExcel();
return { message: 'ok' };
}
return { message: '没有可导出的数据' };
}
// 处理好数据 可作为公共的导出方法
// outColumn 排除hideInTable和 再设置中隐藏的字段 变成一个数组
// reqDetailList 无论多少级 转成一层数组用于导出
// 合作商户资金到账汇总报表导出的修改
export const exportExcelaccountMonthly = (
outColumn: ProColumns<any>[],
reqDetailList: any[],
fileName?: string,) => {
const option: { fileName: string; datas: any[] } = {
fileName: fileName || '新建文件', // : '收营员报表' + moment().format('YYYYMMDDHHmmss'),
datas: [],
};
if (reqDetailList?.length) {
const data = reqDetailList
const outList = data.flat(data.index + 1)
option.datas = [
{
sheetData: outList.map((item: any, index: number) => {
const result = {};
// 判断这个对象是否都有值
outColumn.forEach((c: any, i) => {
if (c.hideInTable) return;
if (c.render) {
result[c.dataIndex] = typeof c.render(item[c.dataIndex], item) === 'object' ?
item[c.dataIndex] : c.render(item[c.dataIndex], item)
} else {
result[c.dataIndex] = item[c.dataIndex] ? item[c.dataIndex] : item[c.dataIndex] === 0 ? 0 : ''
}
});
return result;
}),
sheetName: 'sheet1', // Excel文件名称
sheetFilter: outColumn.map((item) => item.dataIndex),
sheetHeader: outColumn.map((item) => item.title),
columnWidths: outColumn.map(() => 10),
},
];
const toExcel = new ExportJsonExcel(option);
toExcel.saveExcel();
return { message: 'ok' };
}
return { message: '没有可导出的数据' };
}
// 打印页面配置内容
export const printOutBody = (ele: any, content: string) => {
const dateEle = document.createElement('div');
const printConent = document.createElement('div');
if (typeof ele === 'object' && ele.length > 1) {
ele.forEach(n => {
if (n) {
printConent.appendChild(n)
}
})
} else {
const title = ele?.getElementsByClassName('ant-table-wrapper')[0];
printConent.appendChild(ele)
if (title) {
dateEle.setAttribute('style', 'text-align:right;font-size:20px;margin-bottom: 20px');
dateEle.innerHTML = content;
ele.insertBefore(dateEle, title);
}
}
// dateEle.setAttribute('style', 'text-align:right;font-size:20px;margin-bottom: 20px;');
// dateEle.innerHTML = content;
return printConent;
};
// 打印合同配置的内容
export const printContract = (ele: any, content: string) => {
const dateEle = document.createElement('div');
const printConent = document.createElement('div');
if (typeof ele === 'object' && ele.length > 1) {
ele.forEach(n => printConent.appendChild(n))
} else {
const title = ele
printConent.appendChild(ele)
if (title) {
dateEle.setAttribute('style', 'text-align:center;font-size:20px;margin-bottom: 20px;');
dateEle.innerHTML = content;
ele.insertBefore(dateEle, title);
}
}
return printConent
}
// 打印低代码生成页面配置内容
// 由于不同页面表单所在区域的dom元素不同 此方法建议只在低代码生成的页面中 做打印功能
export const printOutInternal = (ele: any, content: string) => {
// 先创建一个包裹在最外层的dom 再在这里面新建一个dom用来放需要打印的内容
const dateEle = document.createElement('div');
const printConent = document.createElement('div');
// 增加一些样式
printConent.setAttribute('style', 'width:100%;padding:25px')
// 顶部内容dom 用于放标题内容
const top = document.createElement('div');
// 顶部dom框样式
top.setAttribute('style', 'width:100%;display:flex;just-content:center;align-items:center;margin-bottom:20px')
// 找title的文本信息dom拿到值
const title = ele?.getElementsByClassName('ant-pro-table-list-toolbar-title')[0];
// 标题的样式
title.setAttribute('style', 'font-size:20px;margin: 0 auto')
// 把title的dom放到顶部dom中 顶部dom加入到需要打印内容的dom中 添加的顺序就是显示的顺序
top.appendChild(title)
dateEle.appendChild(top)
// 拿到表格的整体dom
const body = ele?.getElementsByClassName('ant-table')[0]
body.setAttribute('style', 'height:auto')
const colgroup = body.getElementsByTagName('colgroup')[0]
const colList = colgroup.getElementsByTagName('col')
for (let i = colList.length - 1; i >= 0; i--) {
colList[i].remove()
}
// 表格数据的dom
// const listDom = body.getElementsByClassName('ant-table-body')[0]
// listDom.setAttribute('style', '')
// 拿到表头
const th = body.getElementsByTagName('th')
// 判断表头里面有没有操作列 有操作列的话 每行数据的最后一列要去除
let isOption = false
th.forEach((n: any) => {
if (n.innerHTML === '操作') {
n.remove()
isOption = true
}
})
th.forEach((n: any, index: number) => {
if (index === th.length - 1) {
n.setAttribute('style', 'width:1px')
// n.remove()
}
// else {
// n.setAttribute('style',`width:calc((100% - 1px)/${th.length - 1}),text-align:center`)
// }
})
// 拿到表格数据的dom
const tbody = body.getElementsByClassName('ant-table-tbody')[0]
const list = tbody.getElementsByTagName('tr')
// 删除操作列的数据
if (isOption) {
list.forEach((n: any, index: number) => {
const td = n.getElementsByTagName('td')
const { length } = td
td[length - 1].remove()
})
}
// 判断是否有传入时间 有时间 显示在表格和头部中间偏右
if (content) {
const time = document.createElement('div')
time.innerText = content
time.setAttribute('style', 'width:100%;text-align:right')
dateEle.appendChild(time)
}
// 添加处理过的数据 放到打印内容中
dateEle.appendChild(body)
printConent.appendChild(dateEle)
return printConent;
};
// 封装导出的方法 尽量适配报表的全部页面
export const handlePrint = (ele: any) => {
const printContent = document.createElement('div');
const cloneNode = ele.cloneNode(true)
const tableContent = cloneNode.getElementsByClassName('ant-card')[0]
printContent.appendChild(tableContent)
return printContent
}
// 获取临时查看图片的base4码
export const getBase64 = async (file: any): Promise<any> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
// 打印合作商户合同期结算明细表 有流程进度的特殊情况
export const printSettlementDetails = (ele: any, content: string) => {
console.log('ele', ele)
const dateEle = document.createElement('div');
const printConent = document.createElement('div');
if (typeof ele === 'object' && ele.length > 1) {
ele.forEach(n => printConent.appendChild(n))
} else {
const title = ele
printConent.appendChild(ele)
if (title) {
dateEle.setAttribute('style', 'text-align:center;font-size:20px;margin-bottom: 20px;');
dateEle.innerHTML = content;
ele.insertBefore(dateEle, title);
}
}
console.log('printConent', printConent)
return printConent
}
// 处理高精度问题
export const handleHighPrecision = (num: number) => {
// 处理负数情况
const isNegative = num < 0;
const newNumber = Number(Math.abs(num).toFixed(2))
const str: string = newNumber.toString(); // 取绝对值进行处理
// 取整数部分
let integer: string = str.split('.')[0];
// 确保整数部分至少两位(前面补 0
if (integer.length < 3) {
integer = integer.padStart(3, '0');
}
// 取最后两位作为小数部分
const lastTwo: string = integer.substring(integer.length - 2);
// 取前面的整数部分
const startStr: string = integer.substring(0, integer.length - 2);
// 组合字符串,转换为数字
const result = parseFloat(`${startStr}.${lastTwo}`);
// 如果原数是负数,返回负值
return isNegative ? -result : result;
}
// 将多层级树组 按照label value 变为单层的对象
export const handleGetListObj = (list: any) => {
let res: any = {};
if (!list || list.length === 0) {
return res;
}
list.forEach((item: any) => {
if (item.children?.length) {
const childObj = handleGetListObj(item.children);
res = { ...res, ...childObj };
}
if (item.value !== undefined && item.label !== undefined) {
res[item.value] = item.label;
}
});
return res;
}
// 将多层的columns 按照key value 变为单层对象
export const handleColumnsToObj = (list: any, parentTitle?: string) => {
let res: any = {}
if (!list || list.length === 0) {
return res;
}
list.forEach((item: any) => {
const currentTitle = typeof item.title === 'string' ? item.title : item.aiTitle || '';
const fullTitle = parentTitle ? `${currentTitle}${parentTitle} ` : currentTitle;
if (item.children && item.children.length > 0) {
// 如果有子节点,递归处理子节点,并传入当前拼接后的标题
const childResults = handleColumnsToObj(item.children, fullTitle);
res = { ...res, ...childResults };
} else {
// 如果没有子节点,添加到结果对象中
if(item.renderHaveText){
res[item.renderHaveText] = fullTitle;
}else if (item.aiDataIndex) {
res[item.aiDataIndex] = fullTitle;
}
}
})
return res;
}
// 将多层的表格数据 他里面的对象数据 全部解构出来 变层父级key拼上当级key的格式 与原先的父级同级
export const handleExportListToObj = (data: any) => {
if (!Array.isArray(data)) return [];
const processNode = (node: any) => {
// 处理当前节点的数据
const processedNode: any = {};
// 处理所有字段
Object.entries(node).forEach(([key, value]) => {
// 保留children和ShopINCList结构
if (key === 'children' || key === 'ShopINCList') {
return;
}
// 处理对象类型字段
if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
Object.entries(value).forEach(([innerKey, innerValue]) => {
processedNode[`${key}${innerKey}`] = innerValue;
});
} else {
processedNode[key] = value;
}
});
// 处理子节点
const children = node.children && node.children.length > 0 ? node.children : node.ShopINCList && node.ShopINCList.length > 0 ? node.ShopINCList : [];
if (children.length > 0) {
processedNode.children = children.map((child: any) => processNode(child));
}
return processedNode;
};
return data.map(node => processNode(node));
};
// 有一个对象 key的值为跟表格的每一层对象的key对应 value值为这个key所代表的意思
export const handleChangeItemKeyToValue = (list: any, keyMapping: Record<string, any>) => {
if (!Array.isArray(list)) return [];
const processNode = (node: any) => {
const newNode: any = {};
// 处理当前节点的所有属性
Object.entries(node).forEach(([key, value]) => {
// 特殊处理children数组
if (key === 'children') {
const processedChildren = value.map((child: any) => processNode(child));
if (processedChildren.length > 0) {
newNode.children = processedChildren;
}
return;
}
// 检查是否有映射关系
if (key in keyMapping) {
const newKey = keyMapping[key];
// 处理值
if (value && typeof value === 'object' && !Array.isArray(value)) {
// 如果是对象,递归处理
const processedValue = processNode(value);
if (Object.keys(processedValue).length > 0) {
newNode[newKey] = processedValue;
}
} else {
// 普通值直接赋值
newNode[newKey] = value;
}
}
});
return newNode;
};
return list.map(node => processNode(node)).filter(node => Object.keys(node).length > 0);
};
// export const handleChangeItemKeyToValue = (list: any, keyMapping: any) => {
// if (!Array.isArray(list)) return [];
// const processNode = (node: any) => {
// const newNode: any = {};
// // 处理当前节点的所有属性
// Object.entries(node).forEach(([key, value]: any) => {
// // 特殊处理children数组
// if (key === 'children') {
// newNode.children = value.map((child: any) => processNode(child));
// return;
// }
// // 检查是否有映射关系
// const newKey = keyMapping[key] || key;
// // 处理值
// if (value && typeof value === 'object' && !Array.isArray(value)) {
// // 如果是对象,递归处理
// newNode[newKey] = processNode(value);
// } else {
// // 普通值直接赋值
// newNode[newKey] = value;
// }
// });
// return newNode;
// };
// return list.map(node => processNode(node));
// }
// 多层级数组 变为同一级
export const handleFlattenTree = (treeData: any) => {
const result: any = [];
function traverse(nodes) {
if (!Array.isArray(nodes)) return;
nodes.forEach(node => {
// 将当前节点加入结果(排除嵌套字段)
const { children, ShopINCList, ...rest } = node;
result.push(rest);
// 递归处理嵌套节点
if (children && children.length > 0) traverse(children);
if (ShopINCList && ShopINCList.length > 0) traverse(ShopINCList);
});
}
traverse(treeData);
return result;
}
export const findNodeByPath = (treeData: any[], targetPath: string): any | undefined => {
// 边界检查
if (!Array.isArray(treeData) || !targetPath) return undefined;
for (const node of treeData) {
// 检查当前节点是否匹配
if (node.SYSTEMMODULE_URL === targetPath) {
return node;
}
// 如果有子节点且不是空数组,递归查找
if (node.SystemModuleList?.length > 0) {
const found = findNodeByPath(node.SystemModuleList, targetPath);
if (found) return found;
}
}
return undefined; // 未找到
}
// 封装一个方法 将传入的树形字段 分别变为 片区层 服务区层 门店层
export const handleGetSpecifyFormatLevelRes = (data: any) => {
const result = {
regions: [], // 片区层(最高级)
serviceAreas: [], // 服务区层
shops: [] // 门店层
};
if (!Array.isArray(data)) return result;
const cleanItem = (item: any): any => {
const cleaned: any = {};
Object.keys(item).forEach(key => {
const value = item[key];
// 保留非对象且非数组的字段
if (typeof value !== 'object' || value === null) {
cleaned[key] = value;
}
});
return cleaned;
};
// 递归处理每一项
const processItem = (item: any) => {
const keys = Object.keys(item);
const lowerCaseKeys: any = keys.map(key => key.toLowerCase());
// 获取字段值(不区分大小写)
const spRegionTypeId = keys.find(k => k.toLowerCase() === 'spregiontypeid');
const serverpartId = keys.find(k => k.toLowerCase() === 'serverpartid');
const serverpartShopId = keys.find(k => k.toLowerCase() === 'serverpartshopid');
const hasSPRegionTypeId = spRegionTypeId != null && item[spRegionTypeId] != null;
const hasServerpartId = serverpartId != null && item[serverpartId] != null;
const hasServerpartShopId = serverpartShopId != null && item[serverpartShopId] != null;
// 严格层级判断
if (hasServerpartShopId) {
result.shops.push(cleanItem(item)); // 规则3只要 ServerpartShopId 有值就是门店层
} else if (hasSPRegionTypeId && hasServerpartId) {
result.serviceAreas.push(cleanItem(item)); // 规则2SPRegionTypeId + ServerpartId无 ServerpartShopId
} else if (hasSPRegionTypeId && !hasServerpartId && !hasServerpartShopId) {
result.regions.push(cleanItem(item)); // 规则1仅 SPRegionTypeId
}
// 递归处理子集children 或 ShopINCList字段名大小写敏感
if (item.children && Array.isArray(item.children)) {
item.children.forEach(child => processItem(child));
}
if (item.ShopINCList && Array.isArray(item.ShopINCList)) {
item.ShopINCList.forEach(child => processItem(child));
}
};
// 遍历初始数组
data.forEach(item => processItem(item));
return result;
}
export const checkAndCompleteFields = (targetObj, sourceObj) => {
// 获取目标对象所有键(转换为小写)
const targetKeys = Object.keys(targetObj).map(key => key.toLowerCase());
// 遍历源对象的所有键
for (const sourceKey in sourceObj) {
if (sourceObj.hasOwnProperty(sourceKey)) {
const lowerSourceKey = sourceKey.toLowerCase();
// 检查目标对象是否包含当前键(不区分大小写)
if (!targetKeys.includes(lowerSourceKey)) {
// 找到源对象中原始键名(保留原始大小写)
const originalKey = Object.keys(sourceObj).find(
key => key.toLowerCase() === lowerSourceKey
);
// 将缺失的字段添加到目标对象
targetObj[originalKey] = sourceObj[originalKey];
}
}
}
return targetObj;
}
// 字符串计数 控制跟word一样
export const countLikeWordAccurate = (text: string) => {
if (!text) return 0;
// 将文本标准化:合并连续空格,去除首尾空格
const normalizedText = text.replace(/\s+/g, ' ').trim();
if (!normalizedText) return 0;
// 使用Unicode属性转义更准确地匹配中文字符
const chineseRegex = /(\p{Script=Han}|\p{sc=Hani})/gu;
const chineseCount = [...normalizedText.matchAll(chineseRegex)].length;
// 统计非中文部分
const nonChineseText = normalizedText.replace(chineseRegex, ' ');
// 统计英文单词和数字序列
const wordCount = nonChineseText.split(/\s+/)
.filter(word => word.length > 0)
.length;
return chineseCount + wordCount;
}