newCloud/src/utils/format.ts
2025-11-26 11:06:25 +08:00

1209 lines
32 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 ProTable from '@ant-design/pro-table'
import numeral from 'numeral'
import './printBox.less'
import { CurrentUser } from 'umi'
import session from './session'
import moment from 'moment'
import { synchroBehaviorRecord } from '@/services/user'
export type TreeSelectModel = {
value: number | string,
id: number | string,
title: string,
pId: number | string,
disabled?: boolean
}
export type TreeNodeDto = {
node: any;
children: TreeNodeDto[];
}
/**
* @description: [{node:{},chidlren:[]}] => [{...node,children}]
* @param {any} node 需要拼接的父节点
* @param {any} chidlren 需要遍历的平级节点 合并节点中的
* @return {*} 返回[node:{...,children:[]}]
*/
export function wrapTreeNode(data: TreeNodeDto[]) {
const wrapData: any = data.map((item: TreeNodeDto) => {
const node = { ...item.node };
if (item.children && item.children.length > 0) {
node.children = wrapTreeNode(item.children);
}
return node
});
return wrapData;
}
export function formatTreeForProTable(data: TreeNodeDto[]) {
const wrapData = wrapTreeNode(data);
return {
data: wrapData,
current: 1,
pageSize: 100,
total: 100,
success: true,
};
}
export function tableList(list: any) {
return {
data: list.List || [],
current: list.PageIndex || 1,
pageSize: list.pageSize || 10,
total: list.TotalCount || 0,
otherData: list?.OtherData || '',
success: true,
};
}
/**
* @description:生产的数据仅提供 treeSelect组件treeDataSimpleMode模式使用的 数据,
* @param fields 需要转化的数据,
* @param treeKeys fields 与 TreeSelectModel 数据键值 如 fields as EnumItem ,treeKeys: { title: 'fieldEnumName', value: 'id', id:'id', pId:'parentId' }
* @returns TreeSelectModel[]
*/
export const getTreeSelectOption = async (treeKeys: TreeSelectModel, fields: any[]) => {
const option: TreeSelectModel[] = []
const { value, title, pId, id } = treeKeys
fields.forEach(async (item: any) => {
option.push({ value: item[value], title: item[title], pId: item[pId], id: item[id] })
if (item.children) {
const children: TreeSelectModel[] = await getTreeSelectOption(treeKeys, item.children)
option.push(...children)
}
})
return option
}
// 转换数据为option格式数据
export function formateOptions(list: [], rules: { name: string; value: string; other?: string }) {
// let options: { label: string; value: number | string; other?: string | number }[] = [];
const { name, value, other } = rules;
if (list && other) {
return list.map((n) => {
return {
label: n[name],
value: n[value],
other: n[other],
};
});
} if (list) {
return list.map((n) => {
return {
label: n[name],
value: n[value],
};
});
}
return [];
}
// 转换options数据value类型为 number
export function formateField(list: { label: string; value: string | number }[]) {
const valueNumber: { label: string; value: number }[] = [];
list.map((n: any) => {
if (!isNaN(Number(n.value))) {
valueNumber.push({
label: n.label,
value: numeral(n.value).value(),
});
}
});
return valueNumber.length > 0 ? valueNumber : list;
}
// 转换树节点的value类型为string
export function formateTreeField(list: TreeNodeDto[]) {
const valueNumber: any[] = list.map((item: TreeNodeDto) => {
const node = { label: item.node.label, value: item.node.value.toString(), type: item.node.type, ico: item.node.ico };
if (item.children && item.children.length > 0) {
node.children = formateTreeField(item.children);
}
return node
});
return valueNumber.length > 0 ? valueNumber : list;
}
// 把图片格式转化为 ui框架需要的数据格式
export const transferImg = (orgIamges: any[]) => {
const newImages = orgIamges.map((n: any) => {
if (typeof n === 'object') {
return {
uid: n.ImageId, // 注意这个uid一定不能少否则上传失败
name: n.ImageName,
status: 'done',
url: n.ImageUrl, // url 是展示在页面上的绝对链接
imgUrl: n.ImagePath,
}
}
const [name] = [...n.split("/")].reverse()
return {
uid: new Date().getTime(), // 注意这个uid一定不能少否则上传失败
name,
status: 'done',
url: n, // url 是展示在页面上的绝对链接
imgUrl: n,
}
})
return newImages
}
// 合计方法
export const handleGetSumRow = (data: any[], fieldList: string[], sumTitle: string, avgFiled?: string) => {
// data 表格数据 fieldList 要算合计的字段 sumTitle 显示合计两个字的字段 avgFiled 要算均值的字段(这个字段也必须在算合计的字段里面)
if (data && data.length > 0) {
if (data.length >= 2) {
const res: any = {}
if (fieldList && fieldList.length > 0) {
fieldList.forEach((item: any) => {
res[item] = 0
})
}
data.forEach((item: any) => {
if (res) {
for (const key in res) {
if (item[key]) {
res[key] += item[key]
}
}
}
})
if (avgFiled) {
res[avgFiled] = Number((res[avgFiled] / data.length).toFixed(2))
}
console.log('res', res);
res[sumTitle] = "合计"
data.unshift(res)
return data
}
// 一个片区的时候 判断是不是一个服务区 一个服务区的话 就单单显示一个服务区就好
if (data && data.length === 1) {
const obj: any = data[0]
if (obj.children && obj.children.length === 1) {
return obj.children
}
return data
}
return data
}
return []
}
// 自定义打印的内容的打印方法
export const handleNewPrint = (printName: string, title: string, neckBox?: any, styles?: any, tableDom?: any, footer?: any) => {
// printName 打印出来文件的名称
// title 打印内容的标题
// neckBox 标题下面可能会要求的打印的内容 数组 {label,value}格式 样式已写死
// styles 获取页面的样式
// tableDom 要打印显示的表格 直接dom元素拿进来(处理好的)
// footer 打印内容底部的自定义样式 需求不一样 样式也不一样 外面写好样式和标签直接传入
const printWindow = window.open('', '_blank', 'width=1400,height=800');
if (printWindow) {
printWindow.document.open();
printWindow.document.write(`
<html>
<head>
<title>${printName || ''}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
position: relative;
overflow: hidden;
}
${styles}
.handlePrintBox{
width: 100%;
height: 100%;
position: relative;
z-index: 0;
box-sizing: border-box;
padding: 20px 0 0 0;
}
.handlePrintBox .custom-header {
font-size: 24px;
text-align: center;
margin-bottom: 20px;
font-weight: 600;
}
.handlePrintBox .neckBox{
width: 100%;
box-sizing: border-box;
padding: 12px 12px 12px 20px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.handlePrintBox .neckBox .neckBoxItem{
width: calc(100% / 3);
display: flex;
align-items: center;
box-sizing: border-box;
padding-left: 5%;
}
.handlePrintBox .neckBox .bigNeckBoxItem{
width: calc((100% / 3) * 2);
display: flex;
align-items: center;
box-sizing: border-box;
padding-left: 5%;
}
.handlePrintBox .neckBox .bigNeckBoxItem .itemLabel{
font-size: 14px;
font-weight: 600;
margin-right: 2px;
}
.handlePrintBox .neckBox .bigNeckBoxItem .itemValue{
font-size: 12px;
}
.handlePrintBox .neckBox .neckBoxItem .itemLabel{
font-size: 14px;
font-weight: 600;
margin-right: 2px;
}
.handlePrintBox .neckBox .neckBoxItem .itemValue{
font-size: 12px;
}
.handlePrintBox .tableBox{
width: 100%;
box-sizing: border-box;
padding: 12px;
display: flex;
justify-content: center;
border-collapse: separate;
border-spacing: 0;
}
.handlePrintBox .tableBox .ant-table-thead tr th{
border: 2px solid #000;
}
.handlePrintBox .tableBox .ant-table-tbody tr td{
border: 2px solid #000;
}
.handlePrintBox .tableBox .ant-table-summary tr td{
border: 2px solid #000;
}
.handlePrintBox .tableUnit{
width: 100%;
display: flex;
justify-content: flex-end;
box-sizing: border-box;
font-size: 14px;
}
.handlePrintBox .tableUnit .tableRight{
width: calc(100% / 3);
font-size: 14px;
box-sizing: border-box;
padding-left: 5%;
}
.pro-table {
width: 100%;
border-collapse: collapse;
}
.pro-table th, .pro-table td {
border: 1px solid #ddd;
padding: 8px;
}
.pro-table th {
background-color: #f2f2f2;
text-align: left;
}
/* 水印样式 */
.watermark {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: grid;
grid-template-rows: repeat(4, 1fr); /* 水平行数 */
grid-template-columns: repeat(4, 1fr); /* 垂直列数 */
opacity: 0.2;
pointer-events: none;
z-index: 1;
overflow: hidden;
}
.watermark span {
display: flex;
justify-content: center;
align-items: center;
transform: rotate(-45deg); /* 水印旋转 */
font-size: 14px;
color: rgba(0, 0, 0); /* 半透明水印 */
user-select: none;
}
</style>
</head>
<body>
<div class="handlePrintBox">
<div class="custom-header">${title}</div>
<div class="neckBox">
${neckBox && neckBox.length > 0
? neckBox
.map((item: any) => {
return `<div class="${item.label === '门店名称' ? 'bigNeckBoxItem' : 'neckBoxItem'}">
<span class="itemLabel">${item.label ? item.label + '' : ''}</span>
<span class="itemValue">${item.value}</span>
</div>`;
})
.join('') // 连接成单一字符串,避免逗号
: ''}
</div>
<div class="tableUnit">
<div class="tableRight">单位:元</div>
</div>
<div class="tableBox">
${tableDom}
</div>
<div>
${footer}
</div>
</div>
<div class="watermark">
${Array(16) // 5x5 网格的水印内容
.fill('<span>安徽省驿达高速公路服务区经营管理有限公司</span>')
.join('')}
</div>
</body>
</html>
`)
printWindow.document.close();
printWindow.print();
// 使用定时器检测打印窗口是否关闭
const closeCheckInterval = setInterval(() => {
if (printWindow.closed) {
clearInterval(closeCheckInterval);
}
}, 500);
// 监听窗口焦点事件,若打印窗口失去焦点,关闭窗口
printWindow.onfocus = () => {
printWindow.close();
clearInterval(closeCheckInterval);
};
printWindow.onafterprint = () => printWindow.close();
}
}
// 自定义打印的内容的打印方法
export const handleNewPrintAHJG = (printName: string, title: string, neckBox?: any, styles?: any, tableDom?: any, footer?: any) => {
// printName 打印出来文件的名称
// title 打印内容的标题
// neckBox 标题下面可能会要求的打印的内容 数组 {label,value}格式 样式已写死
// styles 获取页面的样式
// tableDom 要打印显示的表格 直接dom元素拿进来(处理好的)
// footer 打印内容底部的自定义样式 需求不一样 样式也不一样 外面写好样式和标签直接传入
const printWindow = window.open('', '_blank', 'width=1400,height=800');
if (printWindow) {
printWindow.document.open();
printWindow.document.write(`
<html>
<head>
<title>${printName || ''}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
position: relative;
overflow: hidden;
}
${styles}
.handlePrintBox{
width: 100%;
height: 100%;
position: relative;
z-index: 0;
box-sizing: border-box;
padding: 20px 0 0 0;
}
.handlePrintBox .custom-header {
font-size: 24px;
text-align: center;
margin-bottom: 20px;
font-weight: 600;
}
.handlePrintBox .neckBox{
width: 100%;
box-sizing: border-box;
padding: 12px 5% 12px 5%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.handlePrintBox .neckBox .neckBoxItem{
width: calc(100% / 4);
display: flex;
align-items: center;
box-sizing: border-box;
}
.handlePrintBox .neckBox .bigNeckBoxItem{
width: calc((100% / 4) * 2);
display: flex;
align-items: center;
box-sizing: border-box;
}
.handlePrintBox .neckBox .bigNeckBoxItem .itemLabel{
font-size: 14px;
font-weight: 600;
margin-right: 2px;
}
.handlePrintBox .neckBox .bigNeckBoxItem .itemValue{
font-size: 12px;
}
.handlePrintBox .neckBox .neckBoxItem .itemLabel{
font-size: 14px;
font-weight: 600;
margin-right: 2px;
}
.handlePrintBox .neckBox .neckBoxItem .itemValue{
font-size: 12px;
}
.handlePrintBox .tableBox{
width: 100%;
box-sizing: border-box;
padding: 12px;
display: flex;
justify-content: center;
border-collapse: separate;
border-spacing: 0;
}
.handlePrintBox .tableBox .ant-table-thead tr th{
border: 2px solid #000;
}
.handlePrintBox .tableBox .ant-table-tbody tr td{
border: 2px solid #000;
}
.handlePrintBox .tableBox .ant-table-summary tr td{
border: 2px solid #000;
}
.handlePrintBox .tableUnit{
width: 100%;
display: flex;
justify-content: flex-end;
box-sizing: border-box;
font-size: 14px;
}
.handlePrintBox .tableUnit .tableRight{
width: calc(100% / 3);
font-size: 14px;
box-sizing: border-box;
padding-left: 5%;
}
.pro-table {
width: 100%;
border-collapse: collapse;
}
.pro-table th, .pro-table td {
border: 1px solid #ddd;
padding: 8px;
}
.pro-table th {
background-color: #f2f2f2;
text-align: left;
}
/* 水印样式 */
.watermark {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: grid;
grid-template-rows: repeat(4, 1fr); /* 水平行数 */
grid-template-columns: repeat(4, 1fr); /* 垂直列数 */
opacity: 0.2;
pointer-events: none;
z-index: 1;
overflow: hidden;
}
.watermark span {
display: flex;
justify-content: center;
align-items: center;
transform: rotate(-45deg); /* 水印旋转 */
font-size: 14px;
color: rgba(0, 0, 0); /* 半透明水印 */
user-select: none;
}
</style>
</head>
<body>
<div class="handlePrintBox">
<div class="custom-header">${title}</div>
<div class="neckBox">
${neckBox && neckBox.length > 0
? neckBox
.map((item: any) => {
return `<div class="${item.label === '门店名称' ? 'bigNeckBoxItem' : 'neckBoxItem'}">
<span class="itemLabel">${item.label ? item.label + '' : ''}</span>
<span class="itemValue">${item.value}</span>
</div>`;
})
.join('') // 连接成单一字符串,避免逗号
: ''}
</div>
<div class="tableBox">
${tableDom}
</div>
<div>
${footer}
</div>
</div>
<div class="watermark">
${Array(16) // 5x5 网格的水印内容
.fill('<span>安徽建工集团投资运营管理有限公司</span>')
.join('')}
</div>
</body>
</html>
`)
printWindow.document.close();
printWindow.print();
// 使用定时器检测打印窗口是否关闭
const closeCheckInterval = setInterval(() => {
if (printWindow.closed) {
clearInterval(closeCheckInterval);
}
}, 500);
// 监听窗口焦点事件,若打印窗口失去焦点,关闭窗口
printWindow.onfocus = () => {
printWindow.close();
clearInterval(closeCheckInterval);
};
printWindow.onafterprint = () => printWindow.close();
}
}
// 打印图片
export const handlePrintImg = (url: any) => {
const printWindow = window.open('', '_blank', 'width=1400,height=800');
if (printWindow) {
printWindow.document.open();
printWindow.document.close();
printWindow.document.write(`
<html>
<head>
<style>
</style>
<body>
<div style="width:100%;height:100%;display:flex;justify-content: center;">
<img style="max-width:100%;max-height:100%" src=${url}>
</div>
</body>
</head>
</html>
`)
printWindow.print();
// 使用定时器检测打印窗口是否关闭
const closeCheckInterval = setInterval(() => {
if (printWindow.closed) {
clearInterval(closeCheckInterval);
}
}, 500);
printWindow.onfocus = () => {
printWindow.close();
clearInterval(closeCheckInterval);
};
printWindow.onafterprint = () => printWindow.close();
}
}
// 记录每一个按钮的 调用了的方法
export const handleSetPublicLog = (obj: any) => {
// desc 一个数组 入参内容说明 startTime 开始时间 endTime 结束时间 时间戳格式 buttonType 1 为点击了查询按钮
const currentUser: CurrentUser = session.get('currentUser');
let nowMenu = session.get("currentMenu")
let basicInfo = session.get("basicInfo")
let systemBasin = session.get("systemBasin")
let browserVersion = session.get("browserVersion")
if (obj.desc && obj.desc.length > 0) {
obj.desc.forEach((item: any) => {
item.url = 'https://api.eshangtech.com' + item.url
})
}
const req: any = {
USER_ID: currentUser.ID,
USER_NAME: currentUser.Name,
BEHAVIORRECORD_TYPE: "2000", // 1000 浏览页面 2000 行为记录
BEHAVIORRECORD_EXPLAIN: `在页面${nowMenu.name}${obj.buttonType === 1 ? '点击了查询按钮' : ''}`, // 操作行为说明
BEHAVIORRECORD_ROUT: nowMenu.pathname,
BEHAVIORRECORD_ROUTNAME: nowMenu.name,
REQUEST_INFO: JSON.stringify(obj.desc),// 存的是一个JSON 存完整点的内容 在什么页面点击了什么 调用了什么接口 入参是什么 多个接口的时候是数组格式
// BEHAVIORRECORD_DESC: obj.desc, // 入参
BEHAVIORRECORD_TIME: moment(new Date(obj.startTime)).format('YYYY-MM-DD HH:mm:ss'),
BEHAVIORRECORD_LEAVETIME: moment(new Date(obj.endTime)).format('YYYY-MM-DD HH:mm:ss'),
BEHAVIORRECORD_DURATION: (obj.endTime - obj.startTime) / 1000,
OWNERUNIT_ID: currentUser.OwnerUnitId,
OWNERUNIT_NAME: currentUser.OwnerUnitName,
BUSINESSMAN_ID: currentUser.BusinessManID,
BUSINESSMAN_NAME: currentUser.BusinessManName,
SOURCE_PLATFORM: '驿商云平台',
USER_LOGINIP: basicInfo.ip,
USER_LOGINPLACE: `${basicInfo?.prov ? basicInfo?.prov : ''}${basicInfo?.prov && basicInfo?.city ? '-' : ''}${basicInfo?.city ? basicInfo?.city : ''}${basicInfo?.prov && basicInfo?.city && basicInfo?.district ? '-' : ''}${basicInfo?.district ? basicInfo?.district : ''}`,
BROWSER_VERSION: browserVersion,
OPERATING_SYSTEM: systemBasin
}
console.log('reqreqreqreqreq', req);
synchroBehaviorRecord(req)
}
// 将一个对象的key值变为 中文字
export const handleChangeKeyTo = async (params: any, keyObj: any) => {
// params 需要变的对象 keyObj 中文内容
let newObj: any = {}
for (let key in params) {
newObj[keyObj[key]] = params[key]
}
return newObj
}
// 传入秒 返回时分
export const secondsToHuman = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const parts = [];
if (hours > 0) parts.push(`${hours}小时`);
if (minutes > 0) parts.push(`${minutes}分钟`);
return parts.join('') || '0分钟';
}
// 封装一个只要传入操作事项的就可以记录日志
export const handleSetlogSave = async (str?: string) => {
const currentUser = session.get('currentUser')
const basicInfo = session.get('basicInfo')
const browserVersion = session.get('browserVersion')
const systemBasin = session.get('systemBasin')
let nowMenu = session.get("currentMenu")
synchroBehaviorRecord({
USER_ID: currentUser?.ID,
USER_NAME: currentUser?.Name,
BEHAVIORRECORD_TYPE: "2000",
BEHAVIORRECORD_EXPLAIN: str || "",
BEHAVIORRECORD_ROUT: nowMenu.pathname,
BEHAVIORRECORD_ROUTNAME: nowMenu.name,
BEHAVIORRECORD_TIME: moment().format('YYYY-MM-DD HH:mm:ss'),
// REQUEST_INFO: str || "",
OWNERUNIT_ID: currentUser?.OwnerUnitId,
OWNERUNIT_NAME: currentUser?.OwnerUnitName,
BUSINESSMAN_ID: currentUser?.BUSINESSMAN_ID,
BUSINESSMAN_NAME: currentUser?.BUSINESSMAN_NAME,
SOURCE_PLATFORM: "出行平台",
USER_LOGINIP: basicInfo?.ip,
USER_LOGINPLACE: `${basicInfo?.prov ? basicInfo?.prov : ''}${basicInfo?.prov && basicInfo?.city ? '-' : ''}${basicInfo?.city ? basicInfo?.city : ''}${basicInfo?.prov && basicInfo?.city && basicInfo?.district ? '-' : ''}${basicInfo?.district ? basicInfo?.district : ''}`,
BROWSER_VERSION: browserVersion,
OPERATING_SYSTEM: systemBasin
})
}
export function convertTreeToLabelValue<T>(
tree: T[],
labelKey: keyof T,
valueKey: keyof T,
childrenKey: keyof T = 'children' as keyof T
): any[] {
return tree.map((item: any) => {
const node: any = {
label: item[labelKey],
value: item[valueKey]
};
if (Array.isArray(item[childrenKey]) && item[childrenKey].length > 0) {
node.children = convertTreeToLabelValue(item[childrenKey], labelKey, valueKey, childrenKey);
}
return node;
});
}
export function convertTreeFieldToNumber(tree: any[], key: string): any[] {
return tree.map(node => {
const newNode = {
...node,
[key]: typeof node[key] === 'number' ? node[key].toString() : node[key],
};
if (Array.isArray(node.children) && node.children.length > 0) {
newNode.children = convertTreeFieldToNumber(node.children, key);
}
return newNode;
});
}
// 登录密码的正则校验 判断是否符合规则
export function validatePassword(password: string) {
if (!password || password.length < 8) {
return false;
}
const hasDigit = /\d/.test(password);
const hasLower = /[a-z]/.test(password);
const hasUpper = /[A-Z]/.test(password);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>_\-+=~`[\]\\;]/.test(password);
// 统计符合的种类
let count = 0;
if (hasDigit) count++;
if (hasLower || hasUpper) count++; // 大小写都算作字母
if (hasSpecial) count++;
return count >= 2;
}
/**
* 对多层级树形数据按指定字段进行排序
* @param data 树形数据数组
* @param sortField 排序字段,格式: "字段名 排序方式" (如: "Total_Count desc"、"TotalRevenue.Revenue_Amount asc")
* @param childrenKey 子节点的key名称默认为 'children'
* @returns 排序后的树形数据
*/
export function sortTreeData(data: any[], sortField: string, childrenKey: string = 'children'): any[] {
if (!data || !Array.isArray(data) || data.length === 0 || !sortField) {
return data;
}
const parts = sortField.trim().split(/\s+/);
if (parts.length !== 2) {
console.warn('排序字段格式错误,应为: "字段名 排序方式",如: "Total_Count desc" 或 "TotalRevenue.Revenue_Amount asc"');
return data;
}
const [fieldPath, sortOrder] = parts;
const isDesc = sortOrder.toLowerCase() === 'desc';
/**
* 根据字段路径获取对象的值
* @param obj 目标对象
* @param path 字段路径,如 "TotalRevenue.Revenue_Amount"
* @returns 字段值
*/
function getNestedValue(obj: any, path: string): any {
if (!obj || typeof obj !== 'object') {
return null;
}
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current === null || current === undefined) {
return null;
}
current = current[key];
}
return current;
}
function sortRecursive(nodes: any[]): any[] {
// 对当前层级进行排序
const sortedNodes = [...nodes].sort((a, b) => {
const aValue = getNestedValue(a, fieldPath);
const bValue = getNestedValue(b, fieldPath);
// 处理 null, undefined 的情况,将它们排到最后
if (aValue == null && bValue == null) return 0;
if (aValue == null) return 1;
if (bValue == null) return -1;
// 数值比较
if (typeof aValue === 'number' && typeof bValue === 'number') {
return isDesc ? bValue - aValue : aValue - bValue;
}
// 字符串比较(先尝试转换为数字)
const aNum = Number(aValue);
const bNum = Number(bValue);
if (!isNaN(aNum) && !isNaN(bNum)) {
return isDesc ? bNum - aNum : aNum - bNum;
}
// 字符串比较
const aStr = String(aValue);
const bStr = String(bValue);
if (isDesc) {
return bStr.localeCompare(aStr);
} else {
return aStr.localeCompare(bStr);
}
});
// 递归排序子节点
return sortedNodes.map(node => {
if (node[childrenKey] && Array.isArray(node[childrenKey]) && node[childrenKey].length > 0) {
return {
...node,
[childrenKey]: sortRecursive(node[childrenKey])
};
}
return node;
});
}
return sortRecursive(data);
}
/**
* 格式化多层级树形数据 - 千分号格式化和枚举解析
* @param data 多层级数组数据
* @param formatFields 需要格式化的字段数组 (支持嵌套字段如 "obj.fieldName")
* @param enumFields 需要解析枚举的字段数组 (支持嵌套字段)
* @param enumData 枚举对应的数据数组,按顺序对应 enumFields
* @param childrenKey 子节点的key名称默认为 'children'
* @returns 格式化后的数组数据
*/
export function formatTreeData(
data: any[],
formatFields: string[] = [],
enumFields: string[] = [],
enumData: any[] = [],
percentFields: string[] = [],
childrenKey: string = 'children'
): any[] {
if (!data || !Array.isArray(data) || data.length === 0) {
return data;
}
/**
* 根据字段路径获取对象的值
* @param obj 目标对象
* @param path 字段路径,如 "obj.fieldName"
* @returns 字段值
*/
function getNestedValue(obj: any, path: string): any {
if (!obj || typeof obj !== 'object') {
return undefined;
}
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current === null || current === undefined) {
return undefined;
}
current = current[key];
}
return current;
}
/**
* 根据字段路径设置对象的值
* @param obj 目标对象
* @param path 字段路径,如 "obj.fieldName"
* @param value 要设置的值
*/
function setNestedValue(obj: any, path: string, value: any): void {
if (!obj || typeof obj !== 'object') {
return;
}
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (current[key] === null || current[key] === undefined || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
}
/**
* 将数字格式化为千分号字符串
* @param value 要格式化的值
* @returns 格式化后的字符串
*/
function formatNumber(value: any): string {
if (value === null || value === undefined || value === '') {
return '';
}
const num = Number(value);
if (isNaN(num)) {
return String(value);
}
return num.toLocaleString();
}
/**
* 根据枚举数据解析值
* @param value 要解析的值
* @param enumData 枚举数据,支持对象格式 {1000:"枚举1",2000:"枚举2"} 或数组格式
* @returns 解析后的值
*/
function parseEnumValue(value: any, enumData: any): any {
if (!enumData) {
return value;
}
// 支持对象格式 {1000:"枚举1",2000:"枚举2"}
if (typeof enumData === 'object' && !Array.isArray(enumData)) {
// 直接匹配
let enumValue = enumData[value];
if (enumValue !== undefined) {
return enumValue;
}
// 类型转换匹配:尝试字符串和数字的相互转换
const valueStr = String(value);
const valueNum = Number(value);
// 如果原值是数字,尝试用字符串匹配
if (!isNaN(valueNum)) {
enumValue = enumData[valueStr];
if (enumValue !== undefined) {
return enumValue;
}
}
// 如果原值是字符串,尝试用数字匹配
if (!isNaN(valueNum)) {
enumValue = enumData[valueNum];
if (enumValue !== undefined) {
return enumValue;
}
}
// 遍历所有键进行宽松匹配
for (const key in enumData) {
if (String(key) === String(value) || Number(key) === Number(value)) {
return enumData[key];
}
}
return value;
}
// 支持数组格式(保持向下兼容)
if (Array.isArray(enumData) && enumData.length > 0) {
const enumItem = enumData.find(item => {
// 支持多种匹配方式,包括类型转换
const itemValue = item.value || item.id || item.key || item.code;
return itemValue === value ||
String(itemValue) === String(value) ||
Number(itemValue) === Number(value);
});
if (enumItem) {
// 返回枚举项的显示文本
return enumItem.label || enumItem.name || enumItem.text || enumItem.title || value;
}
}
return value;
}
function formatRecursive(nodes: any[]): any[] {
return nodes.map(node => {
// 深拷贝节点,避免修改原数据
const newNode = JSON.parse(JSON.stringify(node));
// 处理格式化字段
formatFields.forEach(fieldPath => {
const value = getNestedValue(newNode, fieldPath);
if (value !== undefined) {
const formattedValue = formatNumber(value);
setNestedValue(newNode, fieldPath, formattedValue);
}
});
// 处理枚举字段
enumFields.forEach((fieldPath, index) => {
const value = getNestedValue(newNode, fieldPath);
if (value !== undefined && enumData[index]) {
const parsedValue = parseEnumValue(value, enumData[index]);
setNestedValue(newNode, fieldPath, parsedValue);
}
});
// 处理百分号字段
percentFields.forEach((fieldPath) => {
const value = getNestedValue(newNode, fieldPath);
if (value !== undefined) {
// 如果值是null设置为空字符串
if (value === null) {
setNestedValue(newNode, fieldPath, '');
return;
}
const currentValueStr = String(value);
// 如果值后面还没有百分号,则添加
if (!currentValueStr.endsWith('%')) {
const percentValue = currentValueStr + '%';
setNestedValue(newNode, fieldPath, percentValue);
}
}
});
// 递归处理子节点
if (newNode[childrenKey] && Array.isArray(newNode[childrenKey]) && newNode[childrenKey].length > 0) {
newNode[childrenKey] = formatRecursive(newNode[childrenKey]);
}
return newNode;
});
}
return formatRecursive(data);
}
export async function getUserIP(): Promise<string> {
try {
// 尝试多个第三方API服务
const apis = [
'https://api.ipify.org?format=json',
'https://ipapi.co/json/',
'https://jsonip.com/',
'https://httpbin.org/ip'
];
for (const api of apis) {
try {
const response = await fetch(api, {
method: 'GET',
timeout: 5000, // 5秒超时
});
if (!response.ok) {
continue;
}
const data = await response.json();
// 根据不同API返回格式获取IP
if (data.ip) {
return data.ip;
} else if (data.origin) {
// httpbin返回格式
return data.origin;
}
} catch (error) {
console.warn(`IP API ${api} 请求失败:`, error);
continue;
}
}
throw new Error('所有IP服务都无法访问');
} catch (error) {
console.error('获取IP地址失败:', error);
return '获取失败';
}
}
/**
* 根据IP地址获取地理位置信息
* @param ip IP地址
* @returns Promise<object> 地理位置信息
*/
export async function getLocationByIP(ip: string, ak: string): Promise<{
country?: string;
province?: string;
city?: string;
district?: string;
isp?: string;
success: boolean;
message?: string;
}> {
if (!ip || ip === '获取失败') {
return {
success: false,
message: 'IP地址无效'
};
}
if (!ak) {
return {
success: false,
message: '百度地图API密钥(ak)不能为空'
};
}
try {
const response = await fetch(`/baidu-api/location/ip?ip=${ip}&ak=${ak}`, {
method: 'GET',
timeout: 8000,
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
// 检查百度API返回状态
if (data.status !== 0) {
return {
success: false,
message: `百度API错误: ${data.message || '未知错误'}`
};
}
// 解析百度地图返回的数据
const content = data.content;
const addressDetail = content.address_detail;
return {
country: '中国', // 百度地图IP定位默认中国
province: addressDetail.province || '',
city: addressDetail.city || '',
district: addressDetail.district || '',
isp: content.address || '',
success: true
};
} catch (error) {
console.error('获取地理位置失败:', error);
return {
success: false,
message: '获取地理位置失败'
};
}
}