Merge branch 'feature/export-cleaner-data' into dev
This commit is contained in:
@@ -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<ExportResultResponse> => {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
113
src/main/services/excel/result-exporter.ts
Normal file
113
src/main/services/excel/result-exporter.ts
Normal file
@@ -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<ExportResultResponse> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<ExportResultResponse>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'
|
||||
@@ -31,7 +31,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
|
||||
|
||||
@@ -56,6 +56,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)
|
||||
@@ -271,6 +272,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 (
|
||||
<div className="h-full flex flex-col xl:flex-row gap-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
{/* 左栏:数据源与执行控制区 */}
|
||||
@@ -480,8 +514,12 @@ const CleanerPage: React.FC = () => {
|
||||
>
|
||||
<Settings2 size={14} /> 类型管理
|
||||
</button>
|
||||
<button className="text-xs bg-blue-50 border border-blue-200 text-blue-700 px-3 py-1.5 rounded shadow-sm hover:bg-blue-100 flex items-center gap-1.5 font-medium">
|
||||
<FileSpreadsheet size={14} /> 导出结果
|
||||
<button
|
||||
onClick={handleExportResults}
|
||||
disabled={isExporting || filteredResults.length === 0}
|
||||
className="text-xs bg-blue-50 border border-blue-200 text-blue-700 px-3 py-1.5 rounded shadow-sm hover:bg-blue-100 flex items-center gap-1.5 font-medium disabled:opacity-50"
|
||||
>
|
||||
<FileSpreadsheet size={14} /> {isExporting ? '导出中...' : '导出结果'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user