1209 lines
32 KiB
TypeScript
1209 lines
32 KiB
TypeScript
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: '获取地理位置失败'
|
||
};
|
||
}
|
||
} |