From a6e845b0cfe3a931e07b8958bd17acd67bfdd7f4 Mon Sep 17 00:00:00 2001 From: Linzp Date: Tue, 9 Jun 2026 18:33:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=B8=80=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/components/Apis/getApis.js | 2 +- .../MessageManger/Dashboard/HistorySection.js | 59 +++- .../MessageManger/Dashboard/constants.js | 2 +- src/components/MessageManger/locale/en-US.js | 1 + src/components/MessageManger/locale/zh-CN.js | 1 + .../Task/Dashboard/HistorySection.js | 281 ++++++++++++++---- src/components/Task/Dashboard/constants.js | 2 +- .../Task/Dashboard/dashboard.module.scss | 28 ++ src/components/Task/README.md | 2 +- src/components/Task/doc/api.md | 2 +- src/components/Task/locale/en-US.js | 2 + src/components/Task/locale/zh-CN.js | 2 + src/mockPreset/index.js | 18 +- src/preset.js | 4 +- 15 files changed, 330 insertions(+), 78 deletions(-) diff --git a/package.json b/package.json index 9aaaf43..6222de5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kne-components/components-admin", - "version": "1.1.47", + "version": "1.1.48", "description": "用于实现一个后台管理系统的必要组件", "scripts": { "init": "husky", diff --git a/src/components/Apis/getApis.js b/src/components/Apis/getApis.js index 076f7cb..dc5e603 100644 --- a/src/components/Apis/getApis.js +++ b/src/components/Apis/getApis.js @@ -166,7 +166,7 @@ const getApis = options => { statistics: { /** * 历史统计看板(GET)。 - * Query:`range`(7d|1m|1y)、`timezone`(IANA,与「今日」划界一致)。 + * Query:`range`(7d|1m|3m|1y)、`timezone`(IANA,与「今日」划界一致)。 * 响应字段约定见 `src/components/Task/doc/api.md`「任务统计 HTTP」。 */ getOverview: { diff --git a/src/components/MessageManger/Dashboard/HistorySection.js b/src/components/MessageManger/Dashboard/HistorySection.js index 3698f57..84a2b2a 100644 --- a/src/components/MessageManger/Dashboard/HistorySection.js +++ b/src/components/MessageManger/Dashboard/HistorySection.js @@ -26,10 +26,47 @@ import SectionHeader from './SectionHeader'; import { getClientIanaTimezone } from '../utils'; import style from './dashboard.module.scss'; -const RANGE_DAY_COUNT = { '7d': 7, '1m': 30, '1y': 365 }; +const RANGE_DAY_COUNT = { '7d': 7, '1m': 30, '3m': 90, '1y': 365 }; +const RANGE_AXIS_UNIT = { '7d': 'day', '1m': 'month', '3m': 'month', '1y': 'year' }; +const RANGE_MONTH_COUNT = { '1m': 1, '3m': 3 }; + +const getAxisUnit = rangeKey => RANGE_AXIS_UNIT[rangeKey] || 'day'; + +const formatAxisKey = (date, unit) => { + const value = String(date || ''); + if (unit === 'year') return value.slice(0, 4); + if (unit === 'month') return value.slice(0, 7); + return value.slice(0, 10); +}; + +/** 本地日历上从 range 起点到今天的时间轴,随视图切换日/月/年粒度 */ +const buildLocalAxisForRange = rangeKey => { + const axisUnit = getAxisUnit(rangeKey); + if (axisUnit === 'year') { + const now = new Date(); + const startYear = now.getFullYear() - 1; + return Array.from({ length: now.getFullYear() - startYear + 1 }, (_, i) => String(startYear + i)); + } + + if (axisUnit === 'month') { + const cursor = new Date(); + cursor.setHours(0, 0, 0, 0); + cursor.setDate(1); + cursor.setMonth(cursor.getMonth() - (RANGE_MONTH_COUNT[rangeKey] || 1)); + const end = new Date(); + end.setHours(0, 0, 0, 0); + end.setDate(1); + + const months = []; + while (cursor <= end) { + const y = cursor.getFullYear(); + const m = String(cursor.getMonth() + 1).padStart(2, '0'); + months.push(`${y}-${m}`); + cursor.setMonth(cursor.getMonth() + 1); + } + return months; + } -/** 本地日历上从 range 起点到今天的日期轴(YYYY-MM-DD) */ -const buildLocalDateAxisForRange = rangeKey => { const days = RANGE_DAY_COUNT[rangeKey] || 7; const dates = []; for (let i = days - 1; i >= 0; i -= 1) { @@ -88,12 +125,17 @@ const HistorySection = createWithRemoteLoader({ const trendOption = (() => { const recentTrend = data?.recentTrend || []; const recentTrendByType = data?.recentTrendByType || []; - const axisDates = recentTrend.length > 0 ? null : buildLocalDateAxisForRange(range); + const axisUnit = getAxisUnit(range); + const axisDates = recentTrend.length > 0 ? null : buildLocalAxisForRange(range); const dateMap = {}; if (recentTrend.length > 0) { recentTrend.forEach(item => { - dateMap[item.date] = { date: item.date, total: item.count }; + const date = formatAxisKey(item.date, axisUnit); + if (!dateMap[date]) { + dateMap[date] = { date, total: 0 }; + } + dateMap[date].total += Number(item.count) || 0; }); } else { axisDates.forEach(d => { @@ -101,11 +143,12 @@ const HistorySection = createWithRemoteLoader({ }); } recentTrendByType.forEach(item => { - if (!dateMap[item.date]) { - dateMap[item.date] = { date: item.date, total: 0 }; + const date = formatAxisKey(item.date, axisUnit); + if (!dateMap[date]) { + dateMap[date] = { date, total: 0 }; } const key = item.type === 0 ? 'email' : 'sms'; - dateMap[item.date][key] = item.count; + dateMap[date][key] = (dateMap[date][key] || 0) + (Number(item.count) || 0); }); const sorted = Object.values(dateMap).sort((a, b) => a.date.localeCompare(b.date)); diff --git a/src/components/MessageManger/Dashboard/constants.js b/src/components/MessageManger/Dashboard/constants.js index 934e17c..6253856 100644 --- a/src/components/MessageManger/Dashboard/constants.js +++ b/src/components/MessageManger/Dashboard/constants.js @@ -1,4 +1,4 @@ -export const RANGE_OPTIONS = ['7d', '1m', '1y']; +export const RANGE_OPTIONS = ['7d', '1m', '3m', '1y']; export const buildUrlWithParams = (url, params = {}) => { const query = Object.keys(params) diff --git a/src/components/MessageManger/locale/en-US.js b/src/components/MessageManger/locale/en-US.js index e38dffa..df149f4 100644 --- a/src/components/MessageManger/locale/en-US.js +++ b/src/components/MessageManger/locale/en-US.js @@ -54,6 +54,7 @@ const messages = { TotalCount: 'Total Count', Range_7d: 'Last 7 Days', Range_1m: 'Last Month', + Range_3m: 'Last 3 Months', Range_1y: 'Last Year', Refresh: 'Refresh', RealtimeConnected: 'Realtime Connected', diff --git a/src/components/MessageManger/locale/zh-CN.js b/src/components/MessageManger/locale/zh-CN.js index 1772921..309e2d4 100644 --- a/src/components/MessageManger/locale/zh-CN.js +++ b/src/components/MessageManger/locale/zh-CN.js @@ -54,6 +54,7 @@ const messages = { TotalCount: '总数', Range_7d: '近7天', Range_1m: '近1个月', + Range_3m: '近3个月', Range_1y: '近1年', Refresh: '刷新', RealtimeConnected: '实时已连接', diff --git a/src/components/Task/Dashboard/HistorySection.js b/src/components/Task/Dashboard/HistorySection.js index 2819e50..992fe0d 100644 --- a/src/components/Task/Dashboard/HistorySection.js +++ b/src/components/Task/Dashboard/HistorySection.js @@ -1,7 +1,7 @@ import { createWithRemoteLoader } from '@kne/remote-loader'; import Fetch from '@kne/react-fetch'; import { useState, useRef } from 'react'; -import { Button, Col, Row, Space, Segmented, Tag } from 'antd'; +import { Button, Col, Row, Select, Space, Segmented, Tag } from 'antd'; import { ReloadOutlined } from '@ant-design/icons'; import { Card as BoxCard } from '@kne/react-box'; import withLocale from '../withLocale'; @@ -16,7 +16,9 @@ import SectionHeader from './SectionHeader'; import { getClientIanaTimezone } from '../utils'; import style from './dashboard.module.scss'; -const RANGE_DAY_COUNT = { '7d': 7, '1m': 30, '1y': 365 }; +const RANGE_DAY_COUNT = { '7d': 7, '1m': 30, '3m': 90, '1y': 365 }; +const RANGE_AXIS_UNIT = { '7d': 'day', '1m': 'month', '3m': 'month', '1y': 'year' }; +const RANGE_MONTH_COUNT = { '1m': 1, '3m': 3 }; /** 每小时趋势「按状态」折线:避免 running 使用琥珀色易与其它图表橙色混淆 */ const HOURLY_STATUS_LINE_COLOR_MAP = { @@ -24,8 +26,43 @@ const HOURLY_STATUS_LINE_COLOR_MAP = { running: '#7c3aed' }; -/** 本地日历上从 range 起点到今天的日期轴(YYYY-MM-DD) */ -const buildLocalDateAxisForRange = rangeKey => { +const getAxisUnit = rangeKey => RANGE_AXIS_UNIT[rangeKey] || 'day'; + +const formatAxisKey = (date, unit) => { + const value = String(date || ''); + if (unit === 'year') return value.slice(0, 4); + if (unit === 'month') return value.slice(0, 7); + return value.slice(0, 10); +}; + +/** 本地日历上从 range 起点到今天的时间轴,随视图切换日/月/年粒度 */ +const buildLocalAxisForRange = rangeKey => { + const axisUnit = getAxisUnit(rangeKey); + if (axisUnit === 'year') { + const now = new Date(); + const startYear = now.getFullYear() - 1; + return Array.from({ length: now.getFullYear() - startYear + 1 }, (_, i) => String(startYear + i)); + } + + if (axisUnit === 'month') { + const cursor = new Date(); + cursor.setHours(0, 0, 0, 0); + cursor.setDate(1); + cursor.setMonth(cursor.getMonth() - (RANGE_MONTH_COUNT[rangeKey] || 1)); + const end = new Date(); + end.setHours(0, 0, 0, 0); + end.setDate(1); + + const months = []; + while (cursor <= end) { + const y = cursor.getFullYear(); + const m = String(cursor.getMonth() + 1).padStart(2, '0'); + months.push(`${y}-${m}`); + cursor.setMonth(cursor.getMonth() + 1); + } + return months; + } + const days = RANGE_DAY_COUNT[rangeKey] || 7; const dates = []; for (let i = days - 1; i >= 0; i -= 1) { @@ -40,6 +77,75 @@ const buildLocalDateAxisForRange = rangeKey => { return dates; }; +const buildRangeDateSet = rangeKey => { + const days = RANGE_DAY_COUNT[rangeKey] || 7; + const dates = []; + for (let i = days - 1; i >= 0; i -= 1) { + const d = new Date(); + d.setHours(0, 0, 0, 0); + d.setDate(d.getDate() - i); + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + dates.push(`${y}-${m}-${day}`); + } + return new Set(dates); +}; + +const buildBucketedDurationRows = ({ rangeKey, durationTrend, runnerType, selectedTypes = [] }) => { + const axisKeys = durationTrend.length > 0 ? null : buildLocalAxisForRange(rangeKey); + const axisUnit = getAxisUnit(rangeKey); + const bucketMap = {}; + const typeFilters = selectedTypes.map(String).filter(Boolean); + + if (axisKeys) { + axisKeys.forEach(date => { + bucketMap[date] = { date, waitingSum: 0, executionSum: 0, weight: 0 }; + }); + } + + durationTrend.forEach(item => { + const date = formatAxisKey(item.date, axisUnit); + if (!bucketMap[date]) bucketMap[date] = { date, waitingSum: 0, executionSum: 0, weight: 0 }; + + const byTypeForRunner = item?.byTypeByRunnerType?.[runnerType]; + const durationItems = typeFilters.length > 0 + ? typeFilters.map(type => byTypeForRunner?.[type]).filter(Boolean) + : [item?.byRunnerType?.[runnerType] || null].filter(Boolean); + + durationItems.forEach(durationItem => { + const avgWaitingTime = Number(durationItem?.avgWaitingTime) || 0; + const avgExecutionTime = Number(durationItem?.avgExecutionTime) || 0; + const count = Number(durationItem?.count) || 0; + const weight = count > 0 ? count : avgWaitingTime > 0 || avgExecutionTime > 0 ? 1 : 0; + + bucketMap[date].waitingSum += avgWaitingTime * weight; + bucketMap[date].executionSum += avgExecutionTime * weight; + bucketMap[date].weight += weight; + }); + }); + + return Object.values(bucketMap) + .sort((a, b) => a.date.localeCompare(b.date)) + .map(item => ({ + date: item.date, + avgWaitingTime: item.weight > 0 ? item.waitingSum / item.weight : 0, + avgExecutionTime: item.weight > 0 ? item.executionSum / item.weight : 0 + })); +}; + +const getDurationTaskTypesByRunner = (durationTrend, runnerType) => { + const typeSet = new Set(); + durationTrend.forEach(item => { + Object.keys(item?.byTypeByRunnerType?.[runnerType] || {}).forEach(type => { + typeSet.add(String(type)); + }); + }); + return Array.from(typeSet).sort((a, b) => + a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }) + ); +}; + const buildEmptyHorizontalBarOption = (placeholder, splitLineStyle, axisLineStyle, axisLabelStyle) => ({ tooltip: { show: false }, grid: { left: 6, right: 28, top: 10, bottom: 10, containLabel: true }, @@ -78,6 +184,8 @@ const HistorySection = createWithRemoteLoader({ const [range, setRange] = useState('7d'); /** 历史每小时趋势:按任务类型 | 按任务状态 */ const [hourlyHistoryDim, setHourlyHistoryDim] = useState('type'); + const [manualDurationTypes, setManualDurationTypes] = useState([]); + const [systemDurationTypes, setSystemDurationTypes] = useState([]); const reloadRef = useRef(() => {}); return ( @@ -115,12 +223,17 @@ const HistorySection = createWithRemoteLoader({ const trendOption = (() => { const recentTrend = data?.recentTrend || []; const recentTrendByType = data?.recentTrendByType || []; - const axisDates = recentTrend.length > 0 ? null : buildLocalDateAxisForRange(range); + const axisUnit = getAxisUnit(range); + const axisDates = recentTrend.length > 0 ? null : buildLocalAxisForRange(range); const dateMap = {}; if (recentTrend.length > 0) { recentTrend.forEach(item => { - dateMap[item.date] = { date: item.date, total: item.count }; + const date = formatAxisKey(item.date, axisUnit); + if (!dateMap[date]) { + dateMap[date] = { date, total: 0 }; + } + dateMap[date].total += Number(item.count) || 0; }); } else { axisDates.forEach(d => { @@ -128,10 +241,11 @@ const HistorySection = createWithRemoteLoader({ }); } recentTrendByType.forEach(item => { - if (!dateMap[item.date]) { - dateMap[item.date] = { date: item.date, total: 0 }; + const date = formatAxisKey(item.date, axisUnit); + if (!dateMap[date]) { + dateMap[date] = { date, total: 0 }; } - dateMap[item.date][item.type] = item.count; + dateMap[date][item.type] = (dateMap[date][item.type] || 0) + (Number(item.count) || 0); }); const sorted = Object.values(dateMap).sort((a, b) => a.date.localeCompare(b.date)); @@ -251,6 +365,14 @@ const HistorySection = createWithRemoteLoader({ const typeTotal = typeEntries.reduce((sum, { count }) => sum + count, 0); const durationTrend = data?.durationTrend || []; + const manualDurationTaskTypes = getDurationTaskTypesByRunner(durationTrend, 'manual'); + const systemDurationTaskTypes = getDurationTaskTypesByRunner(durationTrend, 'system'); + const effectiveManualDurationTypes = manualDurationTypes.filter(type => + manualDurationTaskTypes.includes(String(type)) + ); + const effectiveSystemDurationTypes = systemDurationTypes.filter(type => + systemDurationTaskTypes.includes(String(type)) + ); const buildWaitExecDurationOption = rows => { const dates = rows.map(item => item.date); @@ -312,35 +434,18 @@ const HistorySection = createWithRemoteLoader({ }; }; - const durationAxisDates = buildLocalDateAxisForRange(range); - const manualDurationRows = durationTrend.length - ? durationTrend.map(day => { - const b = day?.byRunnerType?.manual; - return { - date: day.date, - avgWaitingTime: b?.avgWaitingTime ?? 0, - avgExecutionTime: b?.avgExecutionTime ?? 0 - }; - }) - : durationAxisDates.map(date => ({ - date, - avgWaitingTime: 0, - avgExecutionTime: 0 - })); - const systemDurationRows = durationTrend.length - ? durationTrend.map(day => { - const b = day?.byRunnerType?.system; - return { - date: day.date, - avgWaitingTime: b?.avgWaitingTime ?? 0, - avgExecutionTime: b?.avgExecutionTime ?? 0 - }; - }) - : durationAxisDates.map(date => ({ - date, - avgWaitingTime: 0, - avgExecutionTime: 0 - })); + const manualDurationRows = buildBucketedDurationRows({ + rangeKey: range, + durationTrend, + runnerType: 'manual', + selectedTypes: effectiveManualDurationTypes + }); + const systemDurationRows = buildBucketedDurationRows({ + rangeKey: range, + durationTrend, + runnerType: 'system', + selectedTypes: effectiveSystemDurationTypes + }); const manualDurationOption = buildWaitExecDurationOption(manualDurationRows); const systemDurationOption = buildWaitExecDurationOption(systemDurationRows); @@ -524,24 +629,84 @@ const HistorySection = createWithRemoteLoader({ - - - - - - - - - - - - + + {taskTypeList => { + const typeLabelMap = {}; + (taskTypeList || []).forEach(item => { + const value = String(item.value); + typeLabelMap[value] = item.label || item.description || value; + }); + const buildTaskTypeOptions = types => types.map((type, i) => ({ + value: type, + label: typeLabelMap[type] || type, + color: TASK_TYPE_COLOR_MAP[i % TASK_TYPE_COLOR_MAP.length] + })); + const manualTaskTypeOptions = buildTaskTypeOptions(manualDurationTaskTypes); + const systemTaskTypeOptions = buildTaskTypeOptions(systemDurationTaskTypes); + const renderTaskTypeSelect = (value, onChange, options) => { + const optionColorMap = options.reduce((result, option) => { + result[option.value] = option.color; + return result; + }, {}); + return ( +