diff --git a/src/renderer/src/components/CleanerOperationHistoryModal.tsx b/src/renderer/src/components/CleanerOperationHistoryModal.tsx index 3630b18..c6f9859 100644 --- a/src/renderer/src/components/CleanerOperationHistoryModal.tsx +++ b/src/renderer/src/components/CleanerOperationHistoryModal.tsx @@ -6,7 +6,7 @@ * Admin users see all users' records, regular users see only their own. */ -import React, { useState, useEffect, useCallback } from 'react' +import React, { useState, useEffect, useCallback, useTransition } from 'react' import { Modal } from './ui/Modal' import { useLogger } from '../hooks/useLogger' import { @@ -69,6 +69,8 @@ interface BatchItemProps { onDelete: (batchId: string) => void } +const BATCH_PAGE_SIZE = 5 + const statusStyles: Record = { success: 'bg-green-100 text-green-700', partial: 'bg-amber-100 text-amber-700', @@ -286,12 +288,15 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => { } } + const filteredOrders = + currentAttempt !== undefined ? orders.filter((order) => order.attemptNumber === currentAttempt) : orders + const visibleExecutions = + currentAttempt !== undefined + ? executions.filter((execution) => execution.attemptNumber === currentAttempt) + : executions + const handleCopyColumn = (field: keyof CleanerHistoryOrderRecord) => { - const filtered = - currentAttempt !== undefined - ? orders.filter((o) => o.attemptNumber === currentAttempt) - : orders - const values = filtered + const values = filteredOrders .map((o) => String(o[field] ?? '')) .filter((v) => v && v !== '-') .join('\n') @@ -307,9 +312,6 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => { .catch(() => showError('复制失败,请手动复制')) } - const filteredOrders = - currentAttempt !== undefined ? orders.filter((o) => o.attemptNumber === currentAttempt) : orders - return (
{/* Batch summary */} @@ -420,32 +422,30 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
)}
- {executions - .filter((e) => currentAttempt === undefined || e.attemptNumber === currentAttempt) - .map((exec) => ( - - 耗时:{formatDuration(exec.operationTime, exec.endTime)} - - 订单:{exec.ordersProcessed}/{exec.totalOrders} + {visibleExecutions.map((exec) => ( + + 耗时:{formatDuration(exec.operationTime, exec.endTime)} + + 订单:{exec.ordersProcessed}/{exec.totalOrders} + + 删除:{exec.totalMaterialsDeleted} + {exec.totalMaterialsFailed > 0 && ( + 失败:{exec.totalMaterialsFailed} + )} + {exec.totalUncertainDeletions > 0 && ( + + 不确定:{exec.totalUncertainDeletions} - 删除:{exec.totalMaterialsDeleted} - {exec.totalMaterialsFailed > 0 && ( - 失败:{exec.totalMaterialsFailed} - )} - {exec.totalUncertainDeletions > 0 && ( - - 不确定:{exec.totalUncertainDeletions} - - )} - {exec.errorMessage && ( - - 错误:{exec.errorMessage.substring(0, 80)} - {exec.errorMessage.length > 80 ? '...' : ''} - - )} - {exec.appVersion && v{exec.appVersion}} - - ))} + )} + {exec.errorMessage && ( + + 错误:{exec.errorMessage.substring(0, 80)} + {exec.errorMessage.length > 80 ? '...' : ''} + + )} + {exec.appVersion && v{exec.appVersion}} + + ))}
)} @@ -711,9 +711,13 @@ export const CleanerOperationHistoryModal: React.FC(null) const [allUsers, setAllUsers] = useState([]) const [selectedUsers, setSelectedUsers] = useState([]) + const [currentPage, setCurrentPage] = useState(0) + const [isFilterPending, startFilterTransition] = useTransition() const logger = useLogger('CleanerOperationHistory') const isAdmin = user?.userType === 'Admin' + const hasPreviousPage = currentPage > 0 + const hasNextPage = batches.length === BATCH_PAGE_SIZE const fetchBatches = useCallback(async () => { setLoading(true) @@ -721,8 +725,12 @@ export const CleanerOperationHistoryModal: React.FC 0 - ? { limit: 100, usernames: selectedUsers } - : { limit: 100 } + ? { + limit: BATCH_PAGE_SIZE, + offset: currentPage * BATCH_PAGE_SIZE, + usernames: selectedUsers + } + : { limit: BATCH_PAGE_SIZE, offset: currentPage * BATCH_PAGE_SIZE } const result = await window.electron.cleaner.getHistoryBatches(options) if (result.success && result.data) { @@ -735,7 +743,7 @@ export const CleanerOperationHistoryModal: React.FC { try { @@ -765,13 +773,28 @@ export const CleanerOperationHistoryModal: React.FC { - setSelectedUsers((prev) => - prev.includes(username) ? prev.filter((u) => u !== username) : [...prev, username] - ) + startFilterTransition(() => { + setCurrentPage(0) + setSelectedUsers((prev) => + prev.includes(username) ? prev.filter((u) => u !== username) : [...prev, username] + ) + }) } const clearUserFilters = () => { - setSelectedUsers([]) + startFilterTransition(() => { + setCurrentPage(0) + setSelectedUsers([]) + }) + } + + const goToPreviousPage = () => { + setCurrentPage((prev) => Math.max(0, prev - 1)) + } + + const goToNextPage = () => { + if (!hasNextPage) return + setCurrentPage((prev) => prev + 1) } if (!isOpen) return null @@ -833,8 +856,9 @@ export const CleanerOperationHistoryModal: React.FC {batches.length > 0 && ( - 共 {batches.length} 条批次 + 本页 {batches.length} 条批次 )} + {isFilterPending && 正在更新筛选...} +
+
+ +
+ 第 {currentPage + 1} 页 +
+ +
diff --git a/src/renderer/src/components/ExecutionReportDialog.tsx b/src/renderer/src/components/ExecutionReportDialog.tsx index 421ed2d..4bf5c9b 100644 --- a/src/renderer/src/components/ExecutionReportDialog.tsx +++ b/src/renderer/src/components/ExecutionReportDialog.tsx @@ -11,6 +11,7 @@ import React from 'react' import { CheckCircle, XCircle, SkipForward, Package, Loader2, AlertTriangle } from 'lucide-react' import { Modal } from './ui/Modal' import type { CleanerProgress } from '../hooks/cleaner/types' +import { getExecutionReportState } from './execution-report-state' interface ExecutionReportDialogProps { isOpen: boolean @@ -56,6 +57,12 @@ export const ExecutionReportDialog: React.FC = ({ const hasFailedMaterials = materialsFailed > 0 || uncertainDeletions > 0 const showProgress = isExecuting && progress const isProgressing = !!showProgress + const reportState = getExecutionReportState({ + dryRun, + errors, + materialsFailed, + uncertainDeletions + }) // Update timer during progress React.useEffect(() => { @@ -123,11 +130,7 @@ export const ExecutionReportDialog: React.FC = ({ title={ isProgressing ? '正在执行清理...' - : dryRun - ? '预览执行报告' - : hasErrors - ? '执行完成 (有错误)' - : '执行完成' + : reportState.title } size={isProgressing ? 'lg' : 'md'} showCloseButton={!isExecuting} @@ -161,19 +164,17 @@ export const ExecutionReportDialog: React.FC = ({
{dryRun ? ( - ) : hasErrors ? ( + ) : reportState.state === 'failure' ? ( + ) : reportState.state === 'partial_success' ? ( + + ) : reportState.state === 'manual_review' ? ( + ) : ( )}
-

- {dryRun - ? '预览模式 - 未实际删除数据' - : hasErrors - ? '部分操作未能完成,请查看下方错误信息' - : '所有操作已成功完成'} -

+

{reportState.summary}

)} @@ -405,19 +406,33 @@ export const ExecutionReportDialog: React.FC = ({ )} - {!hasErrors && !dryRun && ( + {reportState.showSuccessBanner && (
操作已成功完成,数据已同步到 ERP 系统
)} - {!hasErrors && dryRun && ( + {reportState.showPreviewBanner && (
预览模式结束,数据未实际修改。确认无误后可正式执行。
)} + + {reportState.state === 'partial_success' && ( +
+ + 存在删除失败的物料,本次执行未完全成功,建议结合历史记录继续排查。 +
+ )} + + {reportState.state === 'manual_review' && ( +
+ + 存在不确定删除结果,请人工确认 ERP 中的最终状态后再继续后续操作。 +
+ )} )} diff --git a/src/renderer/src/components/execution-report-state.ts b/src/renderer/src/components/execution-report-state.ts new file mode 100644 index 0000000..e58000c --- /dev/null +++ b/src/renderer/src/components/execution-report-state.ts @@ -0,0 +1,80 @@ +export type ExecutionReportResultState = + | 'preview' + | 'success' + | 'partial_success' + | 'manual_review' + | 'failure' + +export interface ExecutionReportStateInput { + dryRun?: boolean + errors?: string[] + materialsFailed?: number + uncertainDeletions?: number +} + +export interface ExecutionReportStateDescriptor { + state: ExecutionReportResultState + title: string + summary: string + showSuccessBanner: boolean + showPreviewBanner: boolean +} + +export function getExecutionReportState( + input: ExecutionReportStateInput +): ExecutionReportStateDescriptor { + const { + dryRun = false, + errors = [], + materialsFailed = 0, + uncertainDeletions = 0 + } = input + + if (dryRun) { + return { + state: 'preview', + title: '预览执行报告', + summary: '预览模式 - 未实际删除数据', + showSuccessBanner: false, + showPreviewBanner: true + } + } + + if (errors.length > 0) { + return { + state: 'failure', + title: '执行完成 (失败)', + summary: '执行过程中出现错误,请先处理错误后再继续。', + showSuccessBanner: false, + showPreviewBanner: false + } + } + + if (materialsFailed > 0) { + return { + state: 'partial_success', + title: '执行完成 (部分成功)', + summary: '部分物料删除失败,请结合下方统计和历史记录继续排查。', + showSuccessBanner: false, + showPreviewBanner: false + } + } + + if (uncertainDeletions > 0) { + return { + state: 'manual_review', + title: '执行完成 (需人工确认)', + summary: '存在不确定删除结果,请人工复核后再判断是否完成。', + showSuccessBanner: false, + showPreviewBanner: false + } + } + + return { + state: 'success', + title: '执行完成', + summary: '所有操作已成功完成', + showSuccessBanner: true, + showPreviewBanner: false + } +} diff --git a/tests/unit/execution-report-state.test.ts b/tests/unit/execution-report-state.test.ts new file mode 100644 index 0000000..d0839c7 --- /dev/null +++ b/tests/unit/execution-report-state.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from 'vitest' +import { getExecutionReportState } from '../../src/renderer/src/components/execution-report-state' + +describe('execution report state helpers', () => { + it('treats dry-run as preview regardless of counters', () => { + const state = getExecutionReportState({ + dryRun: true, + errors: ['should be ignored'], + materialsFailed: 1, + uncertainDeletions: 1 + }) + + expect(state.state).toBe('preview') + expect(state.title).toBe('预览执行报告') + }) + + it('treats runtime errors as failure', () => { + const state = getExecutionReportState({ + errors: ['boom'], + materialsFailed: 0, + uncertainDeletions: 0 + }) + + expect(state.state).toBe('failure') + expect(state.title).toBe('执行完成 (失败)') + }) + + it('treats failed materials as partial success when there are no runtime errors', () => { + const state = getExecutionReportState({ + errors: [], + materialsFailed: 3, + uncertainDeletions: 0 + }) + + expect(state.state).toBe('partial_success') + expect(state.showSuccessBanner).toBe(false) + }) + + it('treats uncertain deletions as manual review when everything else succeeded', () => { + const state = getExecutionReportState({ + errors: [], + materialsFailed: 0, + uncertainDeletions: 2 + }) + + expect(state.state).toBe('manual_review') + expect(state.showSuccessBanner).toBe(false) + }) + + it('treats clean completion as success', () => { + const state = getExecutionReportState({ + errors: [], + materialsFailed: 0, + uncertainDeletions: 0 + }) + + expect(state.state).toBe('success') + expect(state.showSuccessBanner).toBe(true) + }) +})