diff --git a/src/main/ipc/cleaner-handler.ts b/src/main/ipc/cleaner-handler.ts index eb39150..0cbd41b 100644 --- a/src/main/ipc/cleaner-handler.ts +++ b/src/main/ipc/cleaner-handler.ts @@ -3,10 +3,16 @@ import { ErpAuthService } from '../services/erp/erp-auth' import { CleanerService } from '../services/erp/cleaner' import { OrderNumberResolver } from '../services/erp/order-resolver' import { MySqlService } from '../services/database/mysql' +import { ResultExporter } from '../services/excel/result-exporter' import { createLogger } from '../services/logger' import { withErrorHandling, type IpcResult } from './index' import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors' -import type { CleanerInput, CleanerResult } from '../types/cleaner.types' +import type { + CleanerInput, + CleanerResult, + ExportResultItem, + ExportResultResponse +} from '../types/cleaner.types' const log = createLogger('CleanerHandler') @@ -152,4 +158,35 @@ export function registerCleanerHandlers(): void { }, 'cleaner:run') } ) + + /** + * Export validation results to Excel + */ + ipcMain.handle( + 'cleaner:exportResults', + async (_event, items: ExportResultItem[]): Promise => { + try { + log.info('Exporting validation results', { count: items.length }) + + if (!items || items.length === 0) { + return { + success: false, + error: '没有数据可导出' + } + } + + const exporter = new ResultExporter() + const result = await exporter.exportValidationResults(items) + + return result + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + log.error('Export handler failed', { error: errorMessage }) + return { + success: false, + error: errorMessage + } + } + } + ) } diff --git a/src/main/services/excel/result-exporter.ts b/src/main/services/excel/result-exporter.ts new file mode 100644 index 0000000..e8756b6 --- /dev/null +++ b/src/main/services/excel/result-exporter.ts @@ -0,0 +1,113 @@ +import ExcelJS from 'exceljs' +import path from 'path' +import { app } from 'electron' +import fs from 'fs' +import { createLogger } from '../logger' +import type { ExportResultItem, ExportResultResponse } from '../../types/cleaner.types' + +const log = createLogger('ResultExporter') + +/** + * Excel exporter for validation results + * Exports filtered validation results to Excel file + */ +export class ResultExporter { + private readonly exportDir: string + private readonly fileName: string = '校验结果.xlsx' + + constructor() { + // Export to app directory/exports + this.exportDir = path.join(app.getPath('userData'), 'exports') + this.ensureExportDir() + } + + /** + * Ensure export directory exists + */ + private ensureExportDir(): void { + if (!fs.existsSync(this.exportDir)) { + fs.mkdirSync(this.exportDir, { recursive: true }) + log.info('Created export directory', { path: this.exportDir }) + } + } + + /** + * Export validation results to Excel + * @param items - Validation result items to export + * @returns Export result with file path or error + */ + async exportValidationResults(items: ExportResultItem[]): Promise { + try { + const filePath = path.join(this.exportDir, this.fileName) + log.info('Exporting validation results', { count: items.length, path: filePath }) + + const workbook = new ExcelJS.Workbook() + const worksheet = workbook.addWorksheet('校验结果') + + // Define columns + worksheet.columns = [ + { header: '材料名称', key: 'materialName', width: 30 }, + { header: '材料代码', key: 'materialCode', width: 20 }, + { header: '规格', key: 'specification', width: 25 }, + { header: '型号', key: 'model', width: 20 }, + { header: '负责人', key: 'managerName', width: 15 }, + { header: '勾选状态', key: 'isSelectedText', width: 12 }, + { header: '是否标记删除', key: 'isMarkedForDeletionText', width: 14 } + ] + + // Style header row + const headerRow = worksheet.getRow(1) + headerRow.font = { bold: true } + headerRow.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE0E0E0' } + } + headerRow.alignment = { horizontal: 'center' } + + // Add data rows + for (const item of items) { + worksheet.addRow({ + materialName: item.materialName || '', + materialCode: item.materialCode || '', + specification: item.specification || '', + model: item.model || '', + managerName: item.managerName || '', + isSelectedText: item.isSelected ? '是' : '否', + isMarkedForDeletionText: item.isMarkedForDeletion ? '是' : '否' + }) + } + + // Style data rows + for (let i = 2; i <= worksheet.rowCount; i++) { + const row = worksheet.getRow(i) + row.alignment = { vertical: 'middle' } + + // Highlight selected items + if (items[i - 2]?.isSelected) { + row.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE6F3FF' } + } + } + } + + // Save file + await workbook.xlsx.writeFile(filePath) + log.info('Export completed', { path: filePath, rows: items.length }) + + return { + success: true, + filePath + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + log.error('Export failed', { error: errorMessage }) + return { + success: false, + error: errorMessage + } + } + } +} diff --git a/src/main/types/cleaner.types.ts b/src/main/types/cleaner.types.ts index 4dbb150..cad992c 100644 --- a/src/main/types/cleaner.types.ts +++ b/src/main/types/cleaner.types.ts @@ -19,3 +19,32 @@ export interface OrderCleanDetail { materialsSkipped: number errors: string[] } + +/** + * Single validation result item for export + */ +export interface ExportResultItem { + materialName: string + materialCode: string + specification: string + model: string + managerName: string + isMarkedForDeletion: boolean + isSelected: boolean +} + +/** + * Request payload for exporting validation results + */ +export interface ExportResultRequest { + items: ExportResultItem[] +} + +/** + * Response for export operation + */ +export interface ExportResultResponse { + success: boolean + filePath?: string + error?: string +} diff --git a/src/main/types/ipc-api.types.ts b/src/main/types/ipc-api.types.ts index 737ca46..2f6feeb 100644 --- a/src/main/types/ipc-api.types.ts +++ b/src/main/types/ipc-api.types.ts @@ -4,7 +4,12 @@ */ import type { ExtractorInput, ExtractorResult } from './extractor.types' -import type { CleanerInput, CleanerResult } from './cleaner.types' +import type { + CleanerInput, + CleanerResult, + ExportResultItem, + ExportResultResponse +} from './cleaner.types' /** * MySQL connection configuration @@ -104,6 +109,12 @@ export interface CleanerAPI { runCleaner: ( input: CleanerInput ) => Promise<{ success: boolean; data?: CleanerResult; error?: string }> + + /** + * Export validation results to Excel + * @param items - Validation result items to export + */ + exportResults: (items: ExportResultItem[]) => Promise } /** diff --git a/src/preload/index.ts b/src/preload/index.ts index e81a2c5..8f0cb1a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -2,7 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron' import { electronAPI } from '@electron-toolkit/preload' import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types' import type { ExtractorInput } from '../main/types/extractor.types' -import type { CleanerInput } from '../main/types/cleaner.types' +import type { CleanerInput, ExportResultItem } from '../main/types/cleaner.types' import type { ResolverInput } from '../main/ipc/resolver-handler' import type { LoginRequest } from '../main/ipc/auth-handler' import type { UserInfo } from '../main/types/user.types' @@ -27,7 +27,8 @@ const api = { // Cleaner service cleaner: { - runCleaner: (input: CleanerInput) => ipcRenderer.invoke('cleaner:run', input) + runCleaner: (input: CleanerInput) => ipcRenderer.invoke('cleaner:run', input), + exportResults: (items: ExportResultItem[]) => ipcRenderer.invoke('cleaner:exportResults', items) }, // Order number resolver diff --git a/src/renderer/src/pages/CleanerPage.tsx b/src/renderer/src/pages/CleanerPage.tsx index 8fad336..92b9ff9 100644 --- a/src/renderer/src/pages/CleanerPage.tsx +++ b/src/renderer/src/pages/CleanerPage.tsx @@ -55,6 +55,7 @@ const CleanerPage: React.FC = () => { // Execution state const [isRunning, setIsRunning] = useState(false) const [isValidationRunning, setIsValidationRunning] = useState(false) + const [isExporting, setIsExporting] = useState(false) // Shared Production IDs state const [sharedProductionIdsCount, setSharedProductionIdsCount] = useState(0) @@ -267,6 +268,39 @@ const CleanerPage: React.FC = () => { } } + const handleExportResults = async () => { + if (filteredResults.length === 0) { + alert('没有数据可导出') + return + } + + setIsExporting(true) + try { + // Prepare export data from filtered results + const exportItems = filteredResults.map((result) => ({ + materialName: result.materialName, + materialCode: result.materialCode, + specification: result.specification || '', + model: result.model || '', + managerName: result.managerName || '', + isMarkedForDeletion: result.isMarkedForDeletion, + isSelected: selectedItems.has(result.materialCode) + })) + + const response = await window.electron.cleaner.exportResults(exportItems) + + if (response.success) { + alert(`导出成功!\n文件已保存到:${response.filePath}`) + } else { + throw new Error(response.error || '导出失败') + } + } catch (err) { + alert(err instanceof Error ? err.message : '导出过程中发生错误') + } finally { + setIsExporting(false) + } + } + return (
{/* 左栏:数据源与执行控制区 */} @@ -473,8 +507,12 @@ const CleanerPage: React.FC = () => { -
diff --git a/src/shared/ipc-channels.ts b/src/shared/ipc-channels.ts index 829d91f..7de76d9 100644 --- a/src/shared/ipc-channels.ts +++ b/src/shared/ipc-channels.ts @@ -15,6 +15,7 @@ export const IPC_CHANNELS = { // Cleaner service CLEANER_RUN: 'cleaner:run', + CLEANER_EXPORT_RESULTS: 'cleaner:exportResults', // Database service - MySQL DATABASE_MYSQL_CONNECT: 'database:mysql:connect',