mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-11 12:07:42 +00:00
333 lines
9.1 KiB
TypeScript
333 lines
9.1 KiB
TypeScript
import moment, { Moment } from 'moment';
|
|
import { NumericUnit } from '../common/constants';
|
|
import { isString } from 'lodash';
|
|
|
|
export function formatByDecimalPlaces(value: number | string, decimalPlaces: number) {
|
|
if (isNaN(+value) || decimalPlaces < 0 || decimalPlaces > 100) {
|
|
return value;
|
|
}
|
|
let strValue = (+value).toFixed(decimalPlaces);
|
|
if (!/^[0-9.]+$/g.test(strValue)) {
|
|
return '0';
|
|
}
|
|
while (strValue.includes('.') && (strValue.endsWith('.') || strValue.endsWith('0'))) {
|
|
strValue = strValue.slice(0, -1);
|
|
}
|
|
return strValue;
|
|
}
|
|
|
|
export function formatByThousandSeperator(value: number | string) {
|
|
if (isNaN(+value)) {
|
|
return value;
|
|
}
|
|
const partValues = value.toString().split('.');
|
|
partValues[0] = partValues[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
return partValues.join('.');
|
|
}
|
|
|
|
export function formatMetric(value: number | string) {
|
|
return formatByThousandSeperator(formatByDecimalPlaces(value, 4));
|
|
}
|
|
|
|
export function formatByUnit(value: number | string, unit: NumericUnit) {
|
|
const numericValue = +value;
|
|
if (isNaN(numericValue) || unit === NumericUnit.None) {
|
|
return value;
|
|
}
|
|
let exponentValue = 0;
|
|
switch (unit) {
|
|
case NumericUnit.TenThousand:
|
|
case NumericUnit.EnTenThousand:
|
|
exponentValue = 4;
|
|
break;
|
|
case NumericUnit.OneHundredMillion:
|
|
exponentValue = 8;
|
|
break;
|
|
case NumericUnit.Thousand:
|
|
exponentValue = 3;
|
|
break;
|
|
case NumericUnit.Million:
|
|
exponentValue = 6;
|
|
break;
|
|
case NumericUnit.Giga:
|
|
exponentValue = 9;
|
|
break;
|
|
}
|
|
return numericValue / Math.pow(10, exponentValue);
|
|
}
|
|
|
|
export const getFormattedValue = (value: number | string, remainZero?: boolean) => {
|
|
if (remainZero && (value === undefined || +value === 0)) {
|
|
return 0;
|
|
}
|
|
if (value === undefined) {
|
|
return '-';
|
|
}
|
|
if (!isFinite(+value)) {
|
|
return value;
|
|
}
|
|
const unit =
|
|
+value >= 100000000
|
|
? NumericUnit.OneHundredMillion
|
|
: +value >= 10000
|
|
? NumericUnit.TenThousand
|
|
: NumericUnit.None;
|
|
|
|
let formattedValue = formatByUnit(value, unit);
|
|
formattedValue = formatByDecimalPlaces(
|
|
formattedValue,
|
|
unit === NumericUnit.OneHundredMillion ? 2 : +value < 1 ? 3 : 1
|
|
);
|
|
formattedValue = formatByThousandSeperator(formattedValue);
|
|
if ((typeof formattedValue === 'number' && isNaN(formattedValue)) || +formattedValue === 0) {
|
|
return '-';
|
|
}
|
|
return `${formattedValue}${unit === NumericUnit.None ? '' : unit}`;
|
|
};
|
|
|
|
export const formatNumberWithCN = (num: number) => {
|
|
if (isNaN(num)) return '-';
|
|
if (num >= 10000) {
|
|
return (num / 10000).toFixed(1) + '万';
|
|
} else {
|
|
return formatByDecimalPlaces(num, 2);
|
|
}
|
|
};
|
|
|
|
export const groupByColumn = (data: any[], column: string) => {
|
|
return data.reduce((result, item) => {
|
|
const resultData = { ...result };
|
|
const key = item[column];
|
|
if (!resultData[key]) {
|
|
resultData[key] = [];
|
|
}
|
|
resultData[key].push(item);
|
|
return resultData;
|
|
}, {});
|
|
};
|
|
|
|
// 获取任意两个日期中的所有日期
|
|
export function enumerateDaysBetweenDates(startDate: Moment, endDate: Moment, dateType?: any) {
|
|
let daysList: any[] = [];
|
|
const day = endDate.diff(startDate, dateType || 'days');
|
|
const format = dateType === 'months' ? 'YYYY-MM' : 'YYYY-MM-DD';
|
|
daysList.push(startDate.format(format));
|
|
for (let i = 1; i <= day; i++) {
|
|
daysList.push(startDate.add(1, dateType || 'days').format(format));
|
|
}
|
|
return daysList;
|
|
}
|
|
|
|
export const normalizeTrendData = (
|
|
resultList: any[],
|
|
dateColumnName: string,
|
|
valueColumnName: string,
|
|
startDate: string,
|
|
endDate: string,
|
|
dateType?: string
|
|
) => {
|
|
const dateList = enumerateDaysBetweenDates(moment(startDate), moment(endDate), dateType);
|
|
const result = dateList.map(date => {
|
|
const item = resultList.find(
|
|
result =>
|
|
moment(result[dateColumnName]).format(dateType === 'months' ? 'YYYY-MM' : 'YYYY-MM-DD') ===
|
|
date
|
|
);
|
|
return {
|
|
...(item || {}),
|
|
[dateColumnName]: date,
|
|
[valueColumnName]: item ? item[valueColumnName] : 0,
|
|
};
|
|
});
|
|
return result;
|
|
};
|
|
|
|
export const getMinMaxDate = (resultList: any[], dateColumnName: string) => {
|
|
const dateList = resultList.map(item => moment(item[dateColumnName]));
|
|
return [moment.min(dateList).format('YYYY-MM-DD'), moment.max(dateList).format('YYYY-MM-DD')];
|
|
};
|
|
|
|
export function hexToRgbObj(hex) {
|
|
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
return result
|
|
? {
|
|
r: parseInt(result[1], 16),
|
|
g: parseInt(result[2], 16),
|
|
b: parseInt(result[3], 16),
|
|
}
|
|
: null;
|
|
}
|
|
|
|
export function getLightenDarkenColor(col, amt) {
|
|
let result;
|
|
if (col?.includes('rgb')) {
|
|
const [r, g, b, a] = col.match(/\d+/g).map(Number);
|
|
result = { r, g, b, a };
|
|
} else {
|
|
result = hexToRgbObj(col) || {};
|
|
}
|
|
return `rgba(${result.r + amt},${result.g + amt},${result.b + amt}${
|
|
result.a ? `,${result.a}` : ''
|
|
})`;
|
|
}
|
|
|
|
export function getChartLightenColor(col) {
|
|
return getLightenDarkenColor(col, 80);
|
|
}
|
|
|
|
export const isMobile = window.navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i);
|
|
|
|
export const isIOS = window.navigator.userAgent.match(/(iPhone|iPod|ios)/i);
|
|
|
|
export const isAndroid = window.navigator.userAgent.match(/(Android)/i);
|
|
|
|
export function isProd() {
|
|
return process.env.NODE_ENV === 'production';
|
|
}
|
|
|
|
export function setToken(token: string) {
|
|
localStorage.setItem('SUPERSONIC_TOKEN', token);
|
|
}
|
|
|
|
export function getToken() {
|
|
return localStorage.getItem('SUPERSONIC_TOKEN');
|
|
}
|
|
|
|
export const updateMessageContainerScroll = (nodeId?: string) => {
|
|
setTimeout(() => {
|
|
const ele: any = document.getElementById('messageContainer');
|
|
if (ele && ele.scrollHeight > ele.clientHeight) {
|
|
if (nodeId) {
|
|
const node = document.getElementById(nodeId);
|
|
if (node) {
|
|
ele.scrollTop = ele.scrollHeight - node.clientHeight - 130;
|
|
}
|
|
} else {
|
|
ele.scrollTop = ele.scrollHeight;
|
|
}
|
|
}
|
|
}, 100);
|
|
};
|
|
|
|
/**
|
|
* UUID生成器
|
|
* @param len 长度 number
|
|
* @param radix 随机数基数 number
|
|
* @returns {string}
|
|
*/
|
|
export const uuid = (len: number = 8, radix: number = 62) => {
|
|
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
|
|
const uuid: string[] = [];
|
|
let i;
|
|
|
|
if (len) {
|
|
// Compact form
|
|
for (i = 0; i < len; i++) {
|
|
uuid[i] = chars[Math.floor(Math.random() * radix)];
|
|
}
|
|
} else {
|
|
// rfc4122, version 4 form
|
|
let r;
|
|
|
|
// rfc4122 requires these characters
|
|
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
|
|
uuid[14] = '4';
|
|
|
|
// Fill in random data. At i==19 set the high bits of clock sequence as
|
|
// per rfc4122, sec. 4.1.5
|
|
for (i = 0; i < 36; i++) {
|
|
if (!uuid[i]) {
|
|
r = Math.floor(Math.random() * 16);
|
|
uuid[i] = chars[i === 19 ? ((r % 4) % 8) + 8 : r];
|
|
}
|
|
}
|
|
}
|
|
return uuid.join('');
|
|
};
|
|
|
|
let utilCanvas: any = null;
|
|
|
|
export const getTextWidth = (
|
|
text: string,
|
|
fontSize: string = '16px',
|
|
fontWeight: string = 'normal',
|
|
fontFamily: string = 'DINPro Medium'
|
|
): number => {
|
|
const canvas = utilCanvas || (utilCanvas = document.createElement('canvas'));
|
|
const context = canvas.getContext('2d');
|
|
context.font = `${fontWeight} ${fontSize} ${fontFamily}`;
|
|
const metrics = context.measureText(text);
|
|
return Math.ceil(metrics.width);
|
|
};
|
|
|
|
export function jsonParse(config: any, defaultReturn?: any) {
|
|
if (!isString(config)) {
|
|
return config;
|
|
}
|
|
if (!config) {
|
|
return defaultReturn;
|
|
}
|
|
try {
|
|
return JSON.parse(config);
|
|
} catch (error) {
|
|
console.log(error);
|
|
return defaultReturn;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 导出文本文件的函数
|
|
* @param content - 要导出的文本内容
|
|
* @param fileName - 导出的文件名
|
|
* @param mimeType - 文件的 MIME 类型,默认为 'text/plain'
|
|
*/
|
|
export function exportTextFile(content: string, fileName: string, mimeType: string = 'text/plain') {
|
|
// 创建一个 Blob 对象
|
|
const blob = new Blob([content], { type: mimeType });
|
|
|
|
// 创建一个 URL 对象
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
// 创建一个 <a> 元素
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = fileName;
|
|
|
|
// 触发下载
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
|
|
// 移除 <a> 元素
|
|
document.body.removeChild(a);
|
|
|
|
// 释放 URL 对象
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function replacer(key: string, value: any) {
|
|
return value === null ? '' : value; // 将null值转换为空字符串
|
|
}
|
|
|
|
export function exportCsvFile(data: any[]) {
|
|
// 生成CSV内容
|
|
const csvRows: any[] = [];
|
|
const headers = Object.keys(data[0]);
|
|
csvRows.push(headers.join(',')); // 添加表头
|
|
|
|
for (const row of data) {
|
|
csvRows.push(headers.map(header => JSON.stringify(row[header], replacer)).join(','));
|
|
}
|
|
|
|
// 创建Blob并下载文件
|
|
const csvString = '\ufeff' + csvRows.join('\n');
|
|
const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'data.csv'; // 指定下载文件名
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
window.URL.revokeObjectURL(url); // 释放Blob URL
|
|
}
|