Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kne-components/components-admin",
"version": "1.1.37",
"version": "1.1.38",
"description": "用于实现一个后台管理系统的必要组件",
"scripts": {
"init": "husky",
Expand Down Expand Up @@ -85,9 +85,11 @@
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@kne/app-children-router": "^0.1.7",
"@kne/column-split": "^1.0.5",
"@kne/count-down": "^0.2.2",
"@kne/is-empty": "^1.0.1",
"@kne/json-view": "^0.1.1",
"@kne/react-box": "^0.1.9",
"@kne/react-icon": "^0.1.4",
"@kne/react-intl": "^0.1.9",
"@kne/react-org-chart": "^0.1.6",
Expand Down
20 changes: 20 additions & 0 deletions src/components/Apis/getApis.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,16 @@ const getApis = options => {
retry: {
url: `${prefix}/task/retry`,
method: 'POST'
},
statistics: {
getOverview: {
url: `${prefix}/task/statistics`,
method: 'GET'
},
sse: {
url: `${prefix}/task/statistics/sse`,
method: 'GET'
}
}
},
tenantAdmin: {
Expand Down Expand Up @@ -506,6 +516,16 @@ const getApis = options => {
url: `${prefix}/message/templates/send`,
method: 'POST'
}
},
statistics: {
getOverview: {
url: `${prefix}/message/statistics`,
method: 'GET'
},
sse: {
url: `${prefix}/message/statistics/sse`,
method: 'GET'
}
}
},
mq: {
Expand Down
267 changes: 267 additions & 0 deletions src/components/MessageManger/Dashboard/HistorySection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import { createWithRemoteLoader } from '@kne/remote-loader';
import Fetch from '@kne/react-fetch';
import { useState, useRef } from 'react';
import { Button, Col, Row, Space, Segmented } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import { Card as BoxCard } from '@kne/react-box';
import withLocale from '../withLocale';
import { useIntl } from '@kne/react-intl';
import {
PALETTE,
RANGE_OPTIONS,
tooltipStyle,
itemTooltipStyle,
pieSeries,
legendPieTypeStyle,
legendPieCodeStyle,
legendCenterStyle,
lineChartGrid,
lineChartGridWithRotatedLabels,
lineSmooth,
axisLineStyle,
axisLabelStyle,
splitLineStyle
} from './constants';
import SectionHeader from './SectionHeader';
import { getClientIanaTimezone } from '../utils';
import style from './dashboard.module.scss';

const HistorySection = createWithRemoteLoader({
modules: ['components-thirdparty:Echart']
})(
withLocale(({ remoteModules, apis }) => {
const [Echart] = remoteModules;
const { formatMessage } = useIntl();
const [range, setRange] = useState('7d');
const reloadRef = useRef(() => {});

return (
<>
<SectionHeader
title={formatMessage({ id: 'HistoricalData' })}
extra={
<Space>
<Segmented
options={RANGE_OPTIONS.map(r => ({
label: formatMessage({ id: `Range_${r}` }),
value: r
}))}
value={range}
onChange={setRange}
/>
<Button type="link" icon={<ReloadOutlined />} onClick={() => reloadRef.current()}>
{formatMessage({ id: 'Refresh' })}
</Button>
</Space>
}
/>

<Fetch
{...Object.assign({}, apis.messageManger.statistics.getOverview, {
params: { range, timezone: getClientIanaTimezone() }
})}
render={({ data, reload }) => {
reloadRef.current = reload;

const templateStats = data?.templateStats || {};
const byType = data?.byType || {};

// 发送趋势折线图
const trendOption = (() => {
const recentTrend = data?.recentTrend || [];
const recentTrendByType = data?.recentTrendByType || [];
if (recentTrend.length === 0) return null;

const dateMap = {};
recentTrend.forEach(item => {
dateMap[item.date] = { date: item.date, total: item.count };
});
recentTrendByType.forEach(item => {
if (!dateMap[item.date]) {
dateMap[item.date] = { date: item.date, total: 0 };
}
const key = item.type === 0 ? 'email' : 'sms';
dateMap[item.date][key] = item.count;
});

const sorted = Object.values(dateMap).sort((a, b) => a.date.localeCompare(b.date));
const dates = sorted.map(item => item.date);
const totals = sorted.map(item => item.total);
const emails = sorted.map(item => item.email || 0);
const smsList = sorted.map(item => item.sms || 0);
const manyPoints = dates.length > 14;

return {
color: [PALETTE.total, PALETTE.email, PALETTE.sms],
tooltip: tooltipStyle,
legend: { ...legendCenterStyle, data: [formatMessage({ id: 'TotalCount' }), formatMessage({ id: 'Email' }), formatMessage({ id: 'SMS' })] },
grid: manyPoints ? lineChartGridWithRotatedLabels : lineChartGrid,
xAxis: {
type: 'category',
boundaryGap: false,
data: dates,
axisLine: axisLineStyle,
axisTick: { show: false },
axisLabel: {
...axisLabelStyle,
fontSize: 11,
rotate: manyPoints ? 28 : 0,
hideOverlap: true
}
},
yAxis: { type: 'value', ...splitLineStyle, axisLabel: axisLabelStyle, minInterval: 1 },
series: [
{
name: formatMessage({ id: 'TotalCount' }),
type: 'line',
smooth: lineSmooth,
symbol: 'circle',
symbolSize: 6,
data: totals,
lineStyle: { width: 2.5, color: PALETTE.total },
itemStyle: { color: PALETTE.total, borderColor: '#fff', borderWidth: 2 },
emphasis: { focus: 'series' },
areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: 'rgba(59,130,246,0.18)' }, { offset: 1, color: 'rgba(59,130,246,0.02)' }] } }
},
{
name: formatMessage({ id: 'Email' }),
type: 'line',
smooth: lineSmooth,
symbol: 'circle',
symbolSize: 6,
data: emails,
lineStyle: { width: 2.5, color: PALETTE.email },
itemStyle: { color: PALETTE.email, borderColor: '#fff', borderWidth: 2 },
emphasis: { focus: 'series' },
areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: 'rgba(99,102,241,0.18)' }, { offset: 1, color: 'rgba(99,102,241,0.02)' }] } }
},
{
name: formatMessage({ id: 'SMS' }),
type: 'line',
smooth: lineSmooth,
symbol: 'circle',
symbolSize: 6,
data: smsList,
lineStyle: { width: 2.5, color: PALETTE.sms },
itemStyle: { color: PALETTE.sms, borderColor: '#fff', borderWidth: 2 },
emphasis: { focus: 'series' },
areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: 'rgba(6,182,212,0.16)' }, { offset: 1, color: 'rgba(6,182,212,0.02)' }] } }
}
]
};
})();

// 编码饼图
const codePieOption = (() => {
const byCode = data?.byCode || {};
const entries = Object.entries(byCode);
if (entries.length === 0) return null;
return {
color: PALETTE.pie,
tooltip: itemTooltipStyle,
legend: legendPieCodeStyle,
series: [
{
...pieSeries(['50%', '42%'], ['44%', '62%']),
data: entries.map(([code, count]) => ({ name: code, value: count }))
}
]
};
})();

// 类型饼图
const typePieOption = {
color: [PALETTE.email, PALETTE.sms],
tooltip: itemTooltipStyle,
legend: legendPieTypeStyle,
series: [
{
...pieSeries(['50%', '44%'], ['46%', '66%']),
data: [
{ name: formatMessage({ id: 'Email' }), value: Number(byType['0']) || 0 },
{ name: formatMessage({ id: 'SMS' }), value: Number(byType['1']) || 0 }
]
}
]
};

const historyStatItems = [
{
label: formatMessage({ id: 'TotalRecords' }),
value: data?.totalRecords || 0,
color: PALETTE.total
},
{
label: `${formatMessage({ id: 'Email' })}${formatMessage({ id: 'TotalRecords' })}`,
value: Number(byType['0']) || 0,
color: PALETTE.email
},
{
label: `${formatMessage({ id: 'SMS' })}${formatMessage({ id: 'TotalRecords' })}`,
value: Number(byType['1']) || 0,
color: PALETTE.sms
},
{
label: formatMessage({ id: 'TemplateTotal' }),
value: templateStats.total || 0,
color: '#d97706'
},
{
label: formatMessage({ id: 'EnabledTemplates' }),
value: Number(templateStats.byStatus?.['0']) || 0,
color: PALETTE.enabled
},
{
label: formatMessage({ id: 'DisabledTemplates' }),
value: Number(templateStats.byStatus?.['1']) || 0,
color: PALETTE.disabled
}
];

return (
<>
<div className={style.historyStatsGrid}>
{historyStatItems.map(item => (
<div key={item.label} className={style.historyStatCell} style={{ borderLeftColor: item.color }}>
<div className={style.historyStatLabel}>{item.label}</div>
<div className={style.historyStatValue} style={{ color: item.color }}>
{item.value}
</div>
</div>
))}
</div>

<BoxCard className={`${style.chartCardSurface} ${style.historyTrendCard}`}>
{trendOption ? (
<Echart style={{ height: 380 }} option={trendOption} />
) : (
<div className={style.emptyState}>{formatMessage({ id: 'NoData' })}</div>
)}
</BoxCard>

<Row gutter={[20, 24]} className={style.historyPiesRow}>
<Col xs={24} lg={12} className={style.chartCol}>
<BoxCard className={style.chartCardSurface} title={formatMessage({ id: 'TypeDistribution' })} style={{ height: '100%' }}>
{typePieOption ? <Echart style={{ height: 320 }} option={typePieOption} /> : null}
</BoxCard>
</Col>
<Col xs={24} lg={12} className={style.chartCol}>
<BoxCard className={style.chartCardSurface} title={formatMessage({ id: 'ByCodeStats' })} style={{ height: '100%' }}>
{codePieOption ? (
<Echart style={{ height: 320 }} option={codePieOption} />
) : (
<div className={style.emptyState}>{formatMessage({ id: 'NoData' })}</div>
)}
</BoxCard>
</Col>
</Row>
</>
);
}}
/>
</>
);
})
);

export default HistorySection;
Loading
Loading