feat: usage export data
This commit is contained in:
@@ -9,6 +9,7 @@ const SimpleCardItemWrapper = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
margin-bottom: 24px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const useStyles = createStyles(({ css, token }) => ({
|
const useStyles = createStyles(({ css, token }) => ({
|
||||||
@@ -25,6 +26,9 @@ const useStyles = createStyles(({ css, token }) => ({
|
|||||||
gap: ${token.padding}px;
|
gap: ${token.padding}px;
|
||||||
.title {
|
.title {
|
||||||
font-size: ${token.fontSize}px;
|
font-size: ${token.fontSize}px;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
font-weight: var(--font-weight-500);
|
font-weight: var(--font-weight-500);
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@@ -48,11 +52,12 @@ export const SimpleCardItem: React.FC<{
|
|||||||
|
|
||||||
export const SimpleCard: React.FC<{
|
export const SimpleCard: React.FC<{
|
||||||
dataList: { label: string; value: React.ReactNode }[];
|
dataList: { label: string; value: React.ReactNode }[];
|
||||||
|
height?: string | number;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const { dataList } = props;
|
const { dataList } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleCardItemWrapper>
|
<SimpleCardItemWrapper style={{ height: props.height || '100%' }}>
|
||||||
{dataList.map((item, index) => (
|
{dataList.map((item, index) => (
|
||||||
<SimpleCardItem
|
<SimpleCardItem
|
||||||
key={index}
|
key={index}
|
||||||
|
|||||||
@@ -48,11 +48,65 @@ const Chart: React.FC<{
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const handleOnFinished = () => {
|
||||||
|
if (!chart.current) return;
|
||||||
|
|
||||||
|
const currentChart = chart.current;
|
||||||
|
const optionsYAxis = currentChart.getOption()?.yAxis;
|
||||||
|
if (
|
||||||
|
!optionsYAxis ||
|
||||||
|
!Array.isArray(optionsYAxis) ||
|
||||||
|
optionsYAxis.length < 2
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
// @ts-ignore
|
||||||
|
const model = currentChart.getModel();
|
||||||
|
|
||||||
|
const yAxisModels = [
|
||||||
|
model.getComponent('yAxis', 0),
|
||||||
|
model.getComponent('yAxis', 1)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!yAxisModels[0] || !yAxisModels[1]) return;
|
||||||
|
|
||||||
|
const axes = yAxisModels.map((m) => m.axis);
|
||||||
|
|
||||||
|
const intervals = axes.map((axis) => axis.scale.getInterval());
|
||||||
|
const ticksList = axes.map((axis) => axis.scale.getTicks());
|
||||||
|
const counts = ticksList.map((t) => t.length);
|
||||||
|
|
||||||
|
if (counts[0] === counts[1]) return;
|
||||||
|
|
||||||
|
const unifiedCount = Math.max(counts[0], counts[1]);
|
||||||
|
|
||||||
|
const newMax0 = intervals[0] * (unifiedCount - 1);
|
||||||
|
const newMax1 = intervals[1] * (unifiedCount - 1);
|
||||||
|
|
||||||
|
const yAxis: any[] = [{}, {}];
|
||||||
|
|
||||||
|
if (counts[0] < unifiedCount) {
|
||||||
|
yAxis[0].max = newMax0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (counts[1] < unifiedCount) {
|
||||||
|
yAxis[1].max = newMax1;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
currentChart.setOption({
|
||||||
|
yAxis: yAxis
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
if (container.current) {
|
if (container.current) {
|
||||||
init();
|
init();
|
||||||
|
chart.current?.on('finished', handleOnFinished);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
chart.current?.dispose();
|
chart.current?.dispose();
|
||||||
|
chart.current?.off('finished', handleOnFinished);
|
||||||
};
|
};
|
||||||
}, [init]);
|
}, [init]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,9 @@
|
|||||||
import useUserSettings from '@/hooks/use-user-settings';
|
import useUserSettings from '@/hooks/use-user-settings';
|
||||||
|
import { formatLargeNumber } from '@/utils';
|
||||||
import { theme } from 'antd';
|
import { theme } from 'antd';
|
||||||
import { isFunction } from 'lodash';
|
import { isFunction } from 'lodash';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
const formatLargeNumber = (value: number) => {
|
|
||||||
if (typeof value !== 'number' || isNaN(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value >= 1e9) {
|
|
||||||
return (value / 1e9).toFixed(1).replace(/\.0$/, '') + 'B';
|
|
||||||
} else if (value >= 1e6) {
|
|
||||||
return (value / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
|
|
||||||
} else if (value >= 1e3) {
|
|
||||||
return (value / 1e3).toFixed(1).replace(/\.0$/, '') + 'K';
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const grid = {
|
export const grid = {
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 20,
|
right: 20,
|
||||||
|
|||||||
@@ -62,12 +62,7 @@ const MixLineBarChart: React.FC<
|
|||||||
yAxis,
|
yAxis,
|
||||||
legend: {
|
legend: {
|
||||||
...legend,
|
...legend,
|
||||||
data: legendData.map((item: any) => {
|
data: legendData
|
||||||
return {
|
|
||||||
name: item,
|
|
||||||
icon: 'circle'
|
|
||||||
};
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
series: []
|
series: []
|
||||||
@@ -112,16 +107,10 @@ const MixLineBarChart: React.FC<
|
|||||||
},
|
},
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
...options.yAxis,
|
...options.yAxis
|
||||||
min: 0,
|
|
||||||
|
|
||||||
splitNumber: 7
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...options.yAxis,
|
...options.yAxis,
|
||||||
min: 0,
|
|
||||||
max: 70,
|
|
||||||
splitNumber: 7,
|
|
||||||
nameTextStyle: {
|
nameTextStyle: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
align: 'right'
|
align: 'right'
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import type { LegendComponentOption } from 'echarts/components';
|
||||||
export interface ChartProps {
|
export interface ChartProps {
|
||||||
seriesData: any[];
|
seriesData: any[];
|
||||||
showEmpty?: boolean;
|
showEmpty?: boolean;
|
||||||
xAxisData: string[];
|
xAxisData: string[];
|
||||||
legendData?: string[];
|
legendData?: LegendComponentOption['data'];
|
||||||
labelFormatter?: (val?: any) => string;
|
labelFormatter?: (val?: any) => string;
|
||||||
tooltipValueFormatter?: (val: any) => string;
|
tooltipValueFormatter?: (val: any) => string;
|
||||||
height: string | number;
|
height: string | number;
|
||||||
|
|||||||
@@ -5,3 +5,7 @@ export const DASHBOARD_API = '/dashboard';
|
|||||||
export async function queryDashboardData() {
|
export async function queryDashboardData() {
|
||||||
return request(DASHBOARD_API);
|
return request(DASHBOARD_API);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function queryDashboardUsageData() {
|
||||||
|
return request(`${DASHBOARD_API}/usage`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import ModalFooter from '@/components/modal-footer';
|
||||||
|
import ScrollerModal from '@/components/scroller-modal';
|
||||||
|
import useTableFetch from '@/hooks/use-table-fetch';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { DatePicker, Select, Space, Table } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { queryDashboardUsageData } from '../../apis';
|
||||||
|
import { exportTableColumns } from '../../config';
|
||||||
|
|
||||||
|
const ExportData: React.FC<{
|
||||||
|
open: boolean;
|
||||||
|
onCancel: () => void;
|
||||||
|
}> = (props) => {
|
||||||
|
const {
|
||||||
|
dataSource,
|
||||||
|
rowSelection,
|
||||||
|
queryParams,
|
||||||
|
modalRef,
|
||||||
|
handleDelete,
|
||||||
|
handleDeleteBatch,
|
||||||
|
fetchData,
|
||||||
|
handlePageChange,
|
||||||
|
handleTableChange,
|
||||||
|
handleSearch,
|
||||||
|
handleNameChange
|
||||||
|
} = useTableFetch({
|
||||||
|
fetchAPI: queryDashboardUsageData
|
||||||
|
});
|
||||||
|
const { open, onCancel } = props || {};
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const handleSubmit = () => {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollerModal
|
||||||
|
title={'Export Data'}
|
||||||
|
open={open}
|
||||||
|
centered={false}
|
||||||
|
onCancel={onCancel}
|
||||||
|
destroyOnClose={true}
|
||||||
|
closeIcon={true}
|
||||||
|
maskClosable={false}
|
||||||
|
keyboard={false}
|
||||||
|
width={800}
|
||||||
|
styles={{
|
||||||
|
content: {
|
||||||
|
padding: '0px'
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
padding: 'var(--ant-modal-content-padding)',
|
||||||
|
paddingBottom: '0'
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
padding: '0 var(--ant-modal-content-padding)'
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
padding: '0 var(--ant-modal-content-padding)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
footer={
|
||||||
|
<ModalFooter
|
||||||
|
onOk={handleSubmit}
|
||||||
|
onCancel={onCancel}
|
||||||
|
okText={intl.formatMessage({ id: 'common.button.export' })}
|
||||||
|
></ModalFooter>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<DatePicker.RangePicker></DatePicker.RangePicker>
|
||||||
|
<Select placeholder="Select user"></Select>
|
||||||
|
<Select placeholder="Select model"></Select>
|
||||||
|
</Space>
|
||||||
|
<Table
|
||||||
|
columns={exportTableColumns}
|
||||||
|
tableLayout={dataSource.loadend ? 'auto' : 'fixed'}
|
||||||
|
style={{ width: '100%', marginTop: '16px' }}
|
||||||
|
dataSource={dataSource.dataList}
|
||||||
|
loading={dataSource.loading}
|
||||||
|
rowKey="id"
|
||||||
|
onChange={handleTableChange}
|
||||||
|
pagination={{
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSize: queryParams.perPage,
|
||||||
|
current: queryParams.page,
|
||||||
|
total: dataSource.total,
|
||||||
|
hideOnSinglePage: queryParams.perPage === 10,
|
||||||
|
onChange: handlePageChange
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
</Table>
|
||||||
|
</ScrollerModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExportData;
|
||||||
@@ -1,21 +1,45 @@
|
|||||||
import PageTools from '@/components/page-tools';
|
import PageTools from '@/components/page-tools';
|
||||||
|
import { ExportOutlined } from '@ant-design/icons';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Col, Row } from 'antd';
|
import { Button, Col, DatePicker, Row, Select } from 'antd';
|
||||||
import { FC, memo, useContext } from 'react';
|
import { FC, memo, useContext, useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
import { DashboardContext } from '../../config/dashboard-context';
|
import { DashboardContext } from '../../config/dashboard-context';
|
||||||
|
import ExportData from './export-data';
|
||||||
import RequestTokenInner from './request-token-inner';
|
import RequestTokenInner from './request-token-inner';
|
||||||
import TopUser from './top-user';
|
import TopUser from './top-user';
|
||||||
import useUsageData from './use-usage-data';
|
import useUsageData from './use-usage-data';
|
||||||
|
|
||||||
|
const FilterWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
.selection {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const UsageInner: FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
const UsageInner: FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const { model_usage } = useContext(DashboardContext);
|
const { model_usage } = useContext(DashboardContext);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { requestTokenData, topUserData } = useUsageData(model_usage || {});
|
const { requestTokenData, topUserData } = useUsageData(model_usage || {});
|
||||||
|
|
||||||
|
const handleOnCancel = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<PageTools
|
<PageTools
|
||||||
style={{ margin: '26px 0px' }}
|
style={{ margin: '26px 0px' }}
|
||||||
left={
|
left={
|
||||||
@@ -33,6 +57,20 @@ const UsageInner: FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
|||||||
xl={16}
|
xl={16}
|
||||||
style={{ paddingRight: paddingRight }}
|
style={{ paddingRight: paddingRight }}
|
||||||
>
|
>
|
||||||
|
<FilterWrapper>
|
||||||
|
<div className="selection">
|
||||||
|
<DatePicker.RangePicker></DatePicker.RangePicker>
|
||||||
|
<Select placeholder="Select user"></Select>
|
||||||
|
<Select placeholder="Select model"></Select>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<ExportOutlined />}
|
||||||
|
onClick={handleExport}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: 'common.button.export' })}
|
||||||
|
</Button>
|
||||||
|
</FilterWrapper>
|
||||||
<RequestTokenInner
|
<RequestTokenInner
|
||||||
requestData={requestTokenData.requestData}
|
requestData={requestTokenData.requestData}
|
||||||
xAxisData={requestTokenData.xAxisData}
|
xAxisData={requestTokenData.xAxisData}
|
||||||
@@ -46,7 +84,8 @@ const UsageInner: FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
|||||||
></TopUser>
|
></TopUser>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
<ExportData open={open} onCancel={handleOnCancel}></ExportData>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import CardWrapper from '@/components/card-wrapper';
|
import CardWrapper from '@/components/card-wrapper';
|
||||||
import BarChart from '@/components/echarts/bar-chart';
|
import { SimpleCard } from '@/components/card-wrapper/simple-card';
|
||||||
import LineChart from '@/components/echarts/line-chart';
|
import MixLineBar from '@/components/echarts/mix-line-bar';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Col, Row } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
@@ -29,13 +28,19 @@ const dataList = [
|
|||||||
{ label: '120K', value: 'API Requests' }
|
{ label: '120K', value: 'API Requests' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const legendData = [
|
||||||
|
{ name: 'Completion tokens', icon: 'roundRect' },
|
||||||
|
{ name: 'Prompt tokens', icon: 'roundRect' },
|
||||||
|
{ name: 'API requests', icon: 'circle' }
|
||||||
|
];
|
||||||
|
|
||||||
const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => {
|
const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => {
|
||||||
const { requestData, tokenData, xAxisData } = props;
|
const { requestData, tokenData, xAxisData } = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardWrapper style={{ width: '100%' }}>
|
<CardWrapper style={{ width: '100%' }}>
|
||||||
<Row style={{ width: '100%' }}>
|
{/* <Row style={{ width: '100%' }}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<LineChart
|
<LineChart
|
||||||
title={intl.formatMessage({ id: 'dashboard.apirequest' })}
|
title={intl.formatMessage({ id: 'dashboard.apirequest' })}
|
||||||
@@ -54,7 +59,20 @@ const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => {
|
|||||||
labelFormatter={labelFormatter}
|
labelFormatter={labelFormatter}
|
||||||
></BarChart>
|
></BarChart>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row> */}
|
||||||
|
<SimpleCard dataList={dataList} height={80}></SimpleCard>
|
||||||
|
<MixLineBar
|
||||||
|
chartData={{
|
||||||
|
line: requestData,
|
||||||
|
bar: tokenData
|
||||||
|
}}
|
||||||
|
seriesData={[]}
|
||||||
|
xAxisData={xAxisData}
|
||||||
|
height={360}
|
||||||
|
smooth={true}
|
||||||
|
legendData={legendData}
|
||||||
|
labelFormatter={labelFormatter}
|
||||||
|
></MixLineBar>
|
||||||
</CardWrapper>
|
</CardWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const TopUser: React.FC<TopUserProps> = (props) => {
|
|||||||
title={intl.formatMessage({ id: 'dashboard.topusers' })}
|
title={intl.formatMessage({ id: 'dashboard.topusers' })}
|
||||||
seriesData={userData}
|
seriesData={userData}
|
||||||
xAxisData={topUserList}
|
xAxisData={topUserList}
|
||||||
height={360}
|
height={520}
|
||||||
></HBar>
|
></HBar>
|
||||||
</CardWrapper>
|
</CardWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -92,8 +92,10 @@ export default function useUseageData(data: any) {
|
|||||||
data: { time: string; value: number }[];
|
data: { time: string; value: number }[];
|
||||||
} = {
|
} = {
|
||||||
name: 'API requests',
|
name: 'API requests',
|
||||||
areaStyle: {},
|
areaStyle: {
|
||||||
color: baseColorMap.base,
|
color: 'rgba(13,171,219,0.15)'
|
||||||
|
},
|
||||||
|
color: baseColorMap.baseR1,
|
||||||
data: generateData(dateRange, generateValueMap(apiRequestHistory))
|
data: generateData(dateRange, generateValueMap(apiRequestHistory))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { formatLargeNumber } from '@/utils';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
export const overviewConfigs = [
|
export const overviewConfigs = [
|
||||||
{
|
{
|
||||||
key: 'worker_count',
|
key: 'worker_count',
|
||||||
@@ -21,3 +24,49 @@ export const overviewConfigs = [
|
|||||||
backgroundColor: 'var(--color-white-1)'
|
backgroundColor: 'var(--color-white-1)'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const exportTableColumns = [
|
||||||
|
{
|
||||||
|
title: 'Date',
|
||||||
|
dataIndex: 'date',
|
||||||
|
render: (text: string) => {
|
||||||
|
return dayjs(text).format('YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'User',
|
||||||
|
dataIndex: 'user',
|
||||||
|
render: (text: string) => {
|
||||||
|
return text || 'admin';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Model',
|
||||||
|
dataIndex: 'model',
|
||||||
|
render: (text: string) => {
|
||||||
|
return text || 'All Models';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: 'Completion Tokens',
|
||||||
|
dataIndex: 'completion_tokens',
|
||||||
|
render: (text: number) => {
|
||||||
|
return formatLargeNumber(text) || 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Prompt Tokens',
|
||||||
|
dataIndex: 'prompt_tokens',
|
||||||
|
render: (text: number) => {
|
||||||
|
return formatLargeNumber(text) || 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'API Requests',
|
||||||
|
dataIndex: 'request_count',
|
||||||
|
render: (text: number) => {
|
||||||
|
return formatLargeNumber(text) || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|||||||
@@ -87,6 +87,22 @@ export const formatNumber = (num: number) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatLargeNumber = (value: number) => {
|
||||||
|
if (typeof value !== 'number' || isNaN(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value >= 1e9) {
|
||||||
|
return (value / 1e9).toFixed(1).replace(/\.0$/, '') + 'B';
|
||||||
|
} else if (value >= 1e6) {
|
||||||
|
return (value / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
|
||||||
|
} else if (value >= 1e3) {
|
||||||
|
return (value / 1e3).toFixed(1).replace(/\.0$/, '') + 'K';
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export function loadLanguageConfig(language: string) {
|
export function loadLanguageConfig(language: string) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const requireContext = require.context(`./${language}`, false, /\.ts$/);
|
const requireContext = require.context(`./${language}`, false, /\.ts$/);
|
||||||
|
|||||||
Reference in New Issue
Block a user