diff --git a/src/renderer/src/components/ReportAnalysisDialog.tsx b/src/renderer/src/components/ReportAnalysisDialog.tsx index 7a7b07e..8788ebd 100644 --- a/src/renderer/src/components/ReportAnalysisDialog.tsx +++ b/src/renderer/src/components/ReportAnalysisDialog.tsx @@ -41,11 +41,26 @@ interface DailyMetrics { retriedOrders: number successfulRetries: number executionTimeSecs: number + avgExecutionTimeSecs: number users: string[] // Unique users who ran reports on this day reportCount: number } -type MetricKey = keyof Omit +// User-specific daily metrics for comparison view +interface UserDailyMetrics { + date: string + user: string + processedOrders: number + deletedMaterials: number + skippedMaterials: number + errors: number + retriedOrders: number + successfulRetries: number + executionTimeSecs: number + reportCount: number +} + +type MetricKey = keyof Omit const METRIC_LABELS: Record = { processedOrders: '处理订单数', @@ -54,7 +69,7 @@ const METRIC_LABELS: Record = { errors: '错误数量', retriedOrders: '重试订单数', successfulRetries: '成功重试数', - executionTimeSecs: '执行耗时(秒)' + executionTimeSecs: '每订单平均耗时(秒)' } const METRIC_COLORS: Record = { @@ -64,7 +79,24 @@ const METRIC_COLORS: Record = { errors: '#000000', // black retriedOrders: '#8b5cf6', // violet-500 successfulRetries: '#10b981', // emerald-500 - executionTimeSecs: '#64748b' // slate-500 + executionTimeSecs: '#f97316' // orange-500 +} + +// User colors for comparison view +const USER_COLORS = [ + '#3b82f6', // blue-500 + '#10b981', // emerald-500 + '#f59e0b', // amber-500 + '#ef4444', // red-500 + '#8b5cf6', // violet-500 + '#ec4899', // pink-500 + '#06b6d4', // cyan-500 + '#84cc16' // lime-500 +] + +const getUserColor = (user: string, users: string[]): string => { + const index = users.indexOf(user) + return USER_COLORS[index % USER_COLORS.length] } export const ReportAnalysisDialog: React.FC = ({ @@ -81,6 +113,12 @@ export const ReportAnalysisDialog: React.FC = ({ new Set(['processedOrders', 'deletedMaterials', 'errors']) ) + // View mode: aggregated (by date) or comparison (by user) + const [viewMode, setViewMode] = useState<'aggregated' | 'comparison'>('aggregated') + + // Selected users for comparison view + const [selectedUsers, setSelectedUsers] = useState>(new Set()) + const parseDurationToSeconds = (durationStr: string): number => { // Handle empty or zero case if (!durationStr || durationStr === '0秒' || durationStr === '0分0秒') { @@ -265,6 +303,7 @@ export const ReportAnalysisDialog: React.FC = ({ retriedOrders: 0, successfulRetries: 0, executionTimeSecs: 0, + avgExecutionTimeSecs: 0, users: [], reportCount: 0 }) @@ -285,6 +324,16 @@ export const ReportAnalysisDialog: React.FC = ({ } } + // Calculate average execution time per order for each day + // Formula: total execution time / total processed orders (efficiency metric) + for (const day of dailyMap.values()) { + day.avgExecutionTimeSecs = day.processedOrders > 0 + ? day.executionTimeSecs / day.processedOrders + : 0 + // Replace executionTimeSecs with avgExecutionTimeSecs for chart display + day.executionTimeSecs = day.avgExecutionTimeSecs + } + // Convert map to array and sort by date const sortedData = Array.from(dailyMap.values()).sort((a, b) => { // Basic string comparison works for YYYY-MM-DD @@ -294,6 +343,99 @@ export const ReportAnalysisDialog: React.FC = ({ return sortedData }, [reportData]) + // Extract all unique users from report data + const allUsers = useMemo(() => { + const userSet = new Set() + reportData.forEach(data => userSet.add(data.user)) + return Array.from(userSet).sort() + }, [reportData]) + + // Aggregate data by date AND user for comparison view + const comparisonData = useMemo(() => { + if (!reportData.length) return [] + + // Filter by selected users if any + const filteredData = selectedUsers.size > 0 + ? reportData.filter(data => selectedUsers.has(data.user)) + : reportData + + // Group by date + user + const keyMap = new Map() + + for (const data of filteredData) { + const key = `${data.date}|${data.user}` + + if (!keyMap.has(key)) { + keyMap.set(key, { + date: data.date, + user: data.user, + processedOrders: 0, + deletedMaterials: 0, + skippedMaterials: 0, + errors: 0, + retriedOrders: 0, + successfulRetries: 0, + executionTimeSecs: 0, + reportCount: 0 + }) + } + + const entry = keyMap.get(key)! + entry.processedOrders += data.processedOrders + entry.deletedMaterials += data.deletedMaterials + entry.skippedMaterials += data.skippedMaterials + entry.errors += data.errors + entry.retriedOrders += data.retriedOrders + entry.successfulRetries += data.successfulRetries + entry.executionTimeSecs += data.executionTimeSecs + entry.reportCount += 1 + } + + // Calculate average execution time per order for each entry + // Formula: total execution time / total processed orders (efficiency metric) + for (const entry of keyMap.values()) { + entry.executionTimeSecs = entry.processedOrders > 0 + ? entry.executionTimeSecs / entry.processedOrders + : 0 + } + + return Array.from(keyMap.values()) + .sort((a, b) => { + const dateCompare = a.date.localeCompare(b.date) + if (dateCompare !== 0) return dateCompare + return a.user.localeCompare(b.user) + }) + }, [reportData, selectedUsers]) + + // Format comparison data for chart rendering + const comparisonChartData = useMemo(() => { + if (!comparisonData.length) return [] + + const dates = [...new Set(comparisonData.map(d => d.date))].sort() + const users = [...new Set(comparisonData.map(d => d.user))] + .filter(user => selectedUsers.size === 0 || selectedUsers.has(user)) + .sort() + + const lookup = new Map() + comparisonData.forEach(d => { + lookup.set(`${d.date}|${d.user}`, d) + }) + + return dates.map(date => { + const point: any = { date } + users.forEach(user => { + const key = `${date}|${user}` + const data = lookup.get(key) + + Array.from(selectedMetrics).forEach(metric => { + const userKey = `${user}_${metric}` as any + point[userKey] = data ? (data as any)[metric] : 0 + }) + }) + return point + }) + }, [comparisonData, selectedMetrics, selectedUsers]) + const handleMetricToggle = (metric: MetricKey) => { const next = new Set(selectedMetrics) if (next.has(metric)) { @@ -340,6 +482,7 @@ export const ReportAnalysisDialog: React.FC = ({

操作用户: {dailyData.users.join(', ')}

报告总数: {dailyData.reportCount}

+

每订单平均耗时: {dailyData.avgExecutionTimeSecs.toFixed(1)} 秒

)} @@ -348,6 +491,40 @@ export const ReportAnalysisDialog: React.FC = ({ return null } + // Comparison Tooltip for user-specific data + const ComparisonTooltip = ({ active, payload, label, users, selectedUsers }: any) => { + if (active && payload && payload.length) { + const displayUsers = selectedUsers.size === 0 ? users : Array.from(selectedUsers) + const firstMetric = Array.from(selectedMetrics)[0] + + return ( +
+

{label}

+ +
+ {displayUsers.map((user: string) => { + const userEntry = payload.find((p: any) => p.name === (user || '未分配')) + if (!userEntry) return null + + return ( +
+ + + {user || '未分配'}: + + + {userEntry.value} {firstMetric === 'executionTimeSecs' ? '秒' : ''} + +
+ ) + })} +
+
+ ) + } + return null + } + if (!isOpen) return null if (!isAdmin) return null @@ -424,48 +601,199 @@ export const ReportAnalysisDialog: React.FC = ({ + {/* View Mode Toggle */} +
+

视图模式

+
+ + +
+
+ + {/* User Filter Chips - Only in comparison mode */} + {viewMode === 'comparison' && ( +
+
+

筛选用户

+
+ + +
+
+ +
+ {allUsers.map((user) => { + const isSelected = selectedUsers.has(user) + const color = getUserColor(user, allUsers) + + return ( + + ) + })} +
+ + {selectedUsers.size === 0 && ( +

+ 未选择用户时将显示所有用户数据 +

+ )} +
+ )} + {/* Chart */}
- - - - - - } /> - - - {/* Render selected metrics as lines or bars */} - {Array.from(selectedMetrics).map((metric) => ( - + + + - ))} - - + + } /> + + + {Array.from(selectedMetrics).map((metric) => ( + + ))} + + + ) : ( + + + + + + } /> + + + {selectedUsers.size === 0 || selectedUsers.size > 1 + ? // Multiple users: show first metric for each user + allUsers.filter(user => selectedUsers.size === 0 || selectedUsers.has(user)).map((user) => ( + + )) + : // Single user: show all metrics for that user + Array.from(selectedMetrics).map((metric) => { + const user = Array.from(selectedUsers)[0] + return ( + + ) + }) + } + + + )}
- 数据以天为单位进行聚合统计。展示的是选定时间段内的总量。 + {viewMode === 'aggregated' + ? '数据以天为单位进行聚合统计。展示的是选定时间段内的总量。' + : selectedUsers.size === 0 + ? '展示所有用户的数据对比。未选择用户时显示全部。' + : '展示选定用户的数据对比。' + }
)}