From 668fb7e99cdff15b66dae893b9bd4a3bb597ec19 Mon Sep 17 00:00:00 2001 From: Linzp Date: Wed, 13 May 2026 19:02:20 +0800 Subject: [PATCH 1/2] =?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 --- .../Task/Dashboard/HistorySection.js | 9 +- .../Task/Dashboard/RealtimeSection.js | 131 +++++++++++------- src/components/Task/Dashboard/constants.js | 35 +++++ .../Task/Dashboard/dashboard.module.scss | 25 ++-- src/components/Task/Dashboard/index.js | 2 +- .../Dashboard/useRealtimeStatisticsSSE.js | 13 +- src/components/Task/locale/en-US.js | 1 + src/components/Task/locale/zh-CN.js | 1 + src/components/Task/utils.js | 8 ++ src/mockPreset/index.js | 22 +++ 10 files changed, 174 insertions(+), 73 deletions(-) create mode 100644 src/components/Task/utils.js diff --git a/src/components/Task/Dashboard/HistorySection.js b/src/components/Task/Dashboard/HistorySection.js index faf5882..0556a08 100644 --- a/src/components/Task/Dashboard/HistorySection.js +++ b/src/components/Task/Dashboard/HistorySection.js @@ -10,9 +10,10 @@ import { PALETTE, RANGE_OPTIONS, TASK_STATUS_LIST, STATUS_COLOR_MAP, TASK_TYPE_COLOR_MAP, tooltipStyle, legendCenterStyle, lineChartGrid, lineChartGridWithRotatedLabels, lineSmooth, - axisLineStyle, axisLabelStyle, splitLineStyle, formatDuration + axisLineStyle, axisLabelStyle, splitLineStyle, formatDuration, sanitizeStatisticsDurationMs } from './constants'; import SectionHeader from './SectionHeader'; +import { getClientIanaTimezone } from '../utils'; import style from './dashboard.module.scss'; const HistorySection = createWithRemoteLoader({ @@ -47,7 +48,7 @@ const HistorySection = createWithRemoteLoader({ { reloadRef.current = reload; @@ -194,8 +195,8 @@ const HistorySection = createWithRemoteLoader({ const buildWaitExecDurationOption = rows => { if (!rows || rows.length === 0) return null; const dates = rows.map(item => item.date); - const avgWait = rows.map(item => item.avgWaitingTime || 0); - const avgExec = rows.map(item => item.avgExecutionTime || 0); + const avgWait = rows.map(item => sanitizeStatisticsDurationMs(item.avgWaitingTime) ?? 0); + const avgExec = rows.map(item => sanitizeStatisticsDurationMs(item.avgExecutionTime) ?? 0); const manyPoints = dates.length > 14; return { color: [PALETTE.waiting, PALETTE.running], diff --git a/src/components/Task/Dashboard/RealtimeSection.js b/src/components/Task/Dashboard/RealtimeSection.js index 363a5c1..ce3c7f6 100644 --- a/src/components/Task/Dashboard/RealtimeSection.js +++ b/src/components/Task/Dashboard/RealtimeSection.js @@ -1,5 +1,6 @@ import { createWithRemoteLoader } from '@kne/remote-loader'; -import { useMemo } from 'react'; +import { useMemo, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Col, Row, Space, Tag } from 'antd'; import { BarChartOutlined, CheckCircleOutlined, CloseCircleOutlined, ClockCircleOutlined, ThunderboltOutlined } from '@ant-design/icons'; import { Card as BoxCard, ColorfulCard } from '@kne/react-box'; @@ -17,7 +18,9 @@ import { axisLineStyle, axisLabelStyle, splitLineStyle, - formatDuration + formatDuration, + pickStatisticsDurationMs, + sanitizeStatisticsDurationMs } from './constants'; import useRealtimeStatisticsSSE from './useRealtimeStatisticsSSE'; import style from './dashboard.module.scss'; @@ -43,42 +46,40 @@ const buildTodayHourlySlots = raw => { slots[h].byType[item.type] = (slots[h].byType[item.type] || 0) + n; }); - const hasByTypeHourly = Object.values(slots).some(item => Object.keys(item.byType).length > 0); - // 兼容后端只返回 records 的场景:从 records 反推每小时按类型统计 - if (!hasByTypeHourly && Array.isArray(raw?.records) && raw.records.length > 0) { - raw.records.forEach(record => { - const createdAt = record?.createdAt ? new Date(record.createdAt) : null; - if (!createdAt || Number.isNaN(createdAt.getTime())) return; - const h = createdAt.getHours(); - if (!Number.isFinite(h) || h < 0 || h >= HOURS_IN_DAY) return; - const typeKey = String(record?.type); - slots[h].byType[typeKey] = (slots[h].byType[typeKey] || 0) + 1; - if (!slots[h].hasTotalFromApi) { - slots[h].total += 1; - } - }); - } - return slots; }; const hasHourlyInput = raw => { const a = raw?.hourlyTrend; const b = raw?.hourlyTrendByType; - const r = raw?.records; - return ( - (Array.isArray(a) && a.length > 0) || - (Array.isArray(b) && b.length > 0) || - (Array.isArray(r) && r.length > 0) - ); + return (Array.isArray(a) && a.length > 0) || (Array.isArray(b) && b.length > 0); }; const RealtimeSection = createWithRemoteLoader({ modules: ['components-thirdparty:Echart', 'components-core:Enum'] })( - withLocale(({ remoteModules, apis }) => { + withLocale(({ remoteModules, apis, baseUrl }) => { const [Echart, Enum] = remoteModules; const { formatMessage } = useIntl(); + const navigate = useNavigate(); + const myTaskPath = useMemo(() => { + if (baseUrl == null || baseUrl === '') return null; + const prefix = String(baseUrl).replace(/\/$/, ''); + return `${prefix}/task/my`; + }, [baseUrl]); + const goMyTasks = useCallback(() => { + if (myTaskPath) navigate(myTaskPath); + }, [navigate, myTaskPath]); + const manualPanelKeyHandler = useCallback( + e => { + if (!myTaskPath) return; + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + goMyTasks(); + } + }, + [goMyTasks, myTaskPath] + ); const sseUrl = apis?.task?.statistics?.sse?.url; const { realtimeData, isConnected, lastUpdatedAt } = useRealtimeStatisticsSSE(sseUrl); @@ -90,23 +91,25 @@ const RealtimeSection = createWithRemoteLoader({ const runningCount = Number(byStatus.running) || 0; const waitingCount = Number(byStatus.waiting) || 0; const canceledCount = Number(byStatus.canceled) || 0; - const manualTaskCount = Number(byRunnerType.manual) || 0; - const manualPendingCount = useMemo(() => { - const records = Array.isArray(realtimeData?.records) ? realtimeData.records : []; - return records.reduce((acc, item) => { - const isManual = String(item?.runnerType || '') === 'manual'; - const isPending = String(item?.status || '') === 'pending'; - return isManual && isPending ? acc + 1 : acc; - }, 0); - }, [realtimeData]); - const manualExecutedCount = Math.max(0, manualTaskCount - manualPendingCount); + const manualRt = realtimeData?.runnerTypeStats?.manual; + const hasManualRunnerStats = manualRt && typeof manualRt === 'object'; + const pendingByRunnerType = realtimeData?.pendingByRunnerType || {}; + const manualTaskCount = hasManualRunnerStats + ? Number(manualRt.total) || 0 + : Number(byRunnerType.manual) || 0; + const manualPendingCount = hasManualRunnerStats + ? Number(manualRt.pending) || 0 + : Number(pendingByRunnerType.manual) || 0; + const manualExecutedCount = hasManualRunnerStats + ? Number(manualRt.executed) || 0 + : Math.max(0, manualTaskCount - (Number(pendingByRunnerType.manual) || 0)); const manualDurationDisplay = useMemo(() => { const manualDur = realtimeData?.todayDuration?.byRunnerType?.manual; if (!manualDur || typeof manualDur !== 'object') return null; return { - avgWaitingTime: formatDuration(manualDur.avgWaitingTime), - avgExecutionTime: formatDuration(manualDur.avgExecutionTime), - avgTotalTime: formatDuration(manualDur.avgTotalTime) + avgWaitingTime: formatDuration(sanitizeStatisticsDurationMs(manualDur.avgWaitingTime)), + avgExecutionTime: formatDuration(sanitizeStatisticsDurationMs(manualDur.avgExecutionTime)), + avgTotalTime: formatDuration(sanitizeStatisticsDurationMs(manualDur.avgTotalTime)) }; }, [realtimeData]); @@ -228,17 +231,39 @@ const RealtimeSection = createWithRemoteLoader({ const dur = realtimeData?.todayDuration; if (!dur) return null; return { - avgWaitingTime: formatDuration(dur.avgWaitingTime), - avgExecutionTime: formatDuration(dur.avgExecutionTime), - avgTotalTime: formatDuration(dur.avgTotalTime) + avgWaitingTime: formatDuration(pickStatisticsDurationMs(dur, 'avgWaitingTime')), + avgExecutionTime: formatDuration(pickStatisticsDurationMs(dur, 'avgExecutionTime')), + avgTotalTime: formatDuration(pickStatisticsDurationMs(dur, 'avgTotalTime')) }; }, [realtimeData]); const durationByTypeOption = useMemo(() => { - const byType = realtimeData?.todayDuration?.byType; - if (!byType || typeof byType !== 'object') return null; - const entries = Object.entries(byType).filter(([, value]) => value && typeof value === 'object'); - if (entries.length === 0) return null; + const todayDur = realtimeData?.todayDuration; + if (!todayDur || typeof todayDur !== 'object') return null; + + const durationByType = + todayDur.byType && typeof todayDur.byType === 'object' ? todayDur.byType : {}; + const countByType = + realtimeData.byType && typeof realtimeData.byType === 'object' ? realtimeData.byType : {}; + + const typeSet = new Set([ + ...Object.keys(countByType).filter(k => (Number(countByType[k]) || 0) > 0), + ...Object.keys(durationByType).filter(k => durationByType[k] && typeof durationByType[k] === 'object') + ]); + if (typeSet.size === 0) return null; + + const entries = Array.from(typeSet) + .map(type => { + const d = durationByType[type]; + if (d && typeof d === 'object') return [type, d]; + const c = Number(countByType[type]) || 0; + return [type, { count: c }]; + }) + .sort((a, b) => { + const ca = Number(a[1]?.count) || Number(countByType[a[0]]) || 0; + const cb = Number(b[1]?.count) || Number(countByType[b[0]]) || 0; + return cb - ca; + }); return { color: [PALETTE.running, PALETTE.waiting, PALETTE.total], @@ -277,21 +302,21 @@ const RealtimeSection = createWithRemoteLoader({ { name: formatMessage({ id: 'AvgExecutionTime' }), type: 'bar', - data: entries.map(([, value]) => Number(value.avgExecutionTime) || 0), + data: entries.map(([, value]) => sanitizeStatisticsDurationMs(value.avgExecutionTime) ?? 0), barMaxWidth: 28, itemStyle: { color: PALETTE.running, borderRadius: [4, 4, 0, 0] } }, { name: formatMessage({ id: 'AvgWaitingTime' }), type: 'bar', - data: entries.map(([, value]) => Number(value.avgWaitingTime) || 0), + data: entries.map(([, value]) => sanitizeStatisticsDurationMs(value.avgWaitingTime) ?? 0), barMaxWidth: 28, itemStyle: { color: PALETTE.waiting, borderRadius: [4, 4, 0, 0] } }, { name: formatMessage({ id: 'AvgTotalTime' }), type: 'bar', - data: entries.map(([, value]) => Number(value.avgTotalTime) || 0), + data: entries.map(([, value]) => sanitizeStatisticsDurationMs(value.avgTotalTime) ?? 0), barMaxWidth: 28, itemStyle: { color: PALETTE.total, borderRadius: [4, 4, 0, 0] } } @@ -368,10 +393,16 @@ const RealtimeSection = createWithRemoteLoader({ return ( <> -
+
{formatMessage({ id: 'ManualExecutionStats' })} - {manualPendingCount}
{ + const n = Number(value); + if (!Number.isFinite(n) || n < 0) return null; + if (n > MAX_STATISTICS_DURATION_MS) return null; + return n; +}; + +/** + * 优先使用容器顶层字段(经 sanitize);无效时按 count 对 byType、byRunnerType 做加权回算(毫秒)。 + */ +export const pickStatisticsDurationMs = (container, field, breakdownKeys = ['byType', 'byRunnerType']) => { + const direct = sanitizeStatisticsDurationMs(container?.[field]); + if (direct != null) return direct; + for (const key of breakdownKeys) { + const obj = container?.[key]; + if (!obj || typeof obj !== 'object') continue; + let sum = 0; + let w = 0; + for (const v of Object.values(obj)) { + if (!v || typeof v !== 'object') continue; + const c = Number(v.count) || 0; + if (c <= 0) continue; + const x = sanitizeStatisticsDurationMs(v[field]); + if (x == null) continue; + sum += x * c; + w += c; + } + if (w > 0) return sum / w; + } + return null; +}; + export const formatDuration = ms => { if (ms == null || !Number.isFinite(ms) || ms < 0) return '-'; if (ms === 0) return '0ms'; diff --git a/src/components/Task/Dashboard/dashboard.module.scss b/src/components/Task/Dashboard/dashboard.module.scss index 30972bf..3ba3d10 100644 --- a/src/components/Task/Dashboard/dashboard.module.scss +++ b/src/components/Task/Dashboard/dashboard.module.scss @@ -152,10 +152,24 @@ border-radius: 10px; } +.manualExecutionPanelClickable { + cursor: pointer; + transition: box-shadow 0.15s ease, border-color 0.15s ease; + + &:hover { + border-color: rgba(239, 68, 68, 0.38); + box-shadow: 0 6px 18px rgba(239, 68, 68, 0.12); + } + + &:focus-visible { + outline: 2px solid rgba(185, 28, 28, 0.45); + outline-offset: 2px; + } +} + .manualExecutionHeader { display: flex; - align-items: baseline; - justify-content: space-between; + align-items: center; margin-bottom: 8px; } @@ -166,13 +180,6 @@ letter-spacing: 0.02em; } -.manualExecutionValue { - font-size: 1.2rem; - color: #dc2626; - font-weight: 750; - font-variant-numeric: tabular-nums; -} - .manualPendingCard { border: 1px solid rgba(239, 68, 68, 0.28); box-shadow: 0 8px 18px rgba(239, 68, 68, 0.1); diff --git a/src/components/Task/Dashboard/index.js b/src/components/Task/Dashboard/index.js index a43356d..cbc91b9 100644 --- a/src/components/Task/Dashboard/index.js +++ b/src/components/Task/Dashboard/index.js @@ -24,7 +24,7 @@ const Dashboard = createWithRemoteLoader({ children={
- +
diff --git a/src/components/Task/Dashboard/useRealtimeStatisticsSSE.js b/src/components/Task/Dashboard/useRealtimeStatisticsSSE.js index 3fd4d50..11b99ce 100644 --- a/src/components/Task/Dashboard/useRealtimeStatisticsSSE.js +++ b/src/components/Task/Dashboard/useRealtimeStatisticsSSE.js @@ -1,14 +1,7 @@ import { useEffect, useState } from 'react'; import { getToken } from '@kne/token-storage'; import { buildUrlWithParams } from './constants'; - -const getClientIanaTimezone = () => { - try { - return Intl.DateTimeFormat().resolvedOptions().timeZone || ''; - } catch { - return ''; - } -}; +import { getClientIanaTimezone } from '../utils'; const isLikelyTaskStatisticsPayload = obj => obj && @@ -19,7 +12,9 @@ const isLikelyTaskStatisticsPayload = obj => 'hourlyTrend' in obj || 'hourlyTrendByStatus' in obj || 'byType' in obj || - 'todayDuration' in obj); + 'todayDuration' in obj || + 'pendingByRunnerType' in obj || + 'runnerTypeStats' in obj); const unwrapStatisticsPayload = parsed => { if (parsed == null) return null; diff --git a/src/components/Task/locale/en-US.js b/src/components/Task/locale/en-US.js index 13ffbb7..a485bfd 100644 --- a/src/components/Task/locale/en-US.js +++ b/src/components/Task/locale/en-US.js @@ -74,6 +74,7 @@ const locale = { AvgTotalTime: 'Avg Total Time', RealtimeTaskOverview: 'Task Execution Overview', ManualExecutionStats: 'Manual Execution Tasks', + ManualExecutionGoMyTaskTitle: 'Click here to open My Tasks', ManualExecutionTasks: 'Manual Task Count', ManualPendingTasks: 'Manual Pending Count', ManualExecutedTasks: 'Manual Executed Count', diff --git a/src/components/Task/locale/zh-CN.js b/src/components/Task/locale/zh-CN.js index 8ed2aac..3614b0e 100644 --- a/src/components/Task/locale/zh-CN.js +++ b/src/components/Task/locale/zh-CN.js @@ -74,6 +74,7 @@ const locale = { AvgTotalTime: '平均总耗时', RealtimeTaskOverview: '任务执行概览', ManualExecutionStats: '手动执行任务', + ManualExecutionGoMyTaskTitle: '点击此处查看我的任务', ManualExecutionTasks: '手动执行数量', ManualPendingTasks: '手动待执行数', ManualExecutedTasks: '手动已执行数', diff --git a/src/components/Task/utils.js b/src/components/Task/utils.js new file mode 100644 index 0000000..4b96319 --- /dev/null +++ b/src/components/Task/utils.js @@ -0,0 +1,8 @@ +/** 浏览器 IANA 时区,传给 statistics 接口与 SSE,与后端「今日」划界一致 */ +export const getClientIanaTimezone = () => { + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone || ''; + } catch { + return ''; + } +}; diff --git a/src/mockPreset/index.js b/src/mockPreset/index.js index 846cfbe..50e8132 100644 --- a/src/mockPreset/index.js +++ b/src/mockPreset/index.js @@ -147,6 +147,28 @@ const apis = merge({}, getApis(), { hourlyTrend, hourlyTrendByStatus, hourlyTrendByType, + pendingByRunnerType: { + manual: Math.max(0, Math.floor(totalPending * 0.45)), + system: Math.max(0, totalPending - Math.floor(totalPending * 0.45)) + }, + runnerTypeStats: (() => { + const manualTotal = Math.floor(totalTasks * 0.4); + const systemTotal = Math.ceil(totalTasks * 0.6); + const manualPending = Math.max(0, Math.floor(totalPending * 0.45)); + const systemPending = Math.max(0, totalPending - Math.floor(totalPending * 0.45)); + return { + manual: { + total: manualTotal, + pending: manualPending, + executed: Math.max(0, manualTotal - manualPending) + }, + system: { + total: systemTotal, + pending: systemPending, + executed: Math.max(0, systemTotal - systemPending) + } + }; + })(), todayDuration: { completedCount: totalSuccess + totalFailed + totalCanceled, successCount: totalSuccess, From 748eeeb21e62ad8c24a1e0acee5f9a3987f6ff56 Mon Sep 17 00:00:00 2001 From: Linzp Date: Wed, 13 May 2026 19:03:29 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a771dd..d37c9a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kne-components/components-admin", - "version": "1.1.38", + "version": "1.1.39", "description": "用于实现一个后台管理系统的必要组件", "scripts": { "init": "husky",