Merge branch 'feature/export-cleaner-data' into dev

This commit is contained in:
Misaka
2026-03-03 22:52:43 +08:00
7 changed files with 236 additions and 6 deletions

View File

@@ -3,10 +3,16 @@ import { ErpAuthService } from '../services/erp/erp-auth'
import { CleanerService } from '../services/erp/cleaner' import { CleanerService } from '../services/erp/cleaner'
import { OrderNumberResolver } from '../services/erp/order-resolver' import { OrderNumberResolver } from '../services/erp/order-resolver'
import { MySqlService } from '../services/database/mysql' import { MySqlService } from '../services/database/mysql'
import { ResultExporter } from '../services/excel/result-exporter'
import { createLogger } from '../services/logger' import { createLogger } from '../services/logger'
import { withErrorHandling, type IpcResult } from './index' import { withErrorHandling, type IpcResult } from './index'
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors' 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') const log = createLogger('CleanerHandler')
@@ -152,4 +158,35 @@ export function registerCleanerHandlers(): void {
}, 'cleaner:run') }, '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
}
}
}
)
} }

View 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
}
}
}
}

View File

@@ -19,3 +19,32 @@ export interface OrderCleanDetail {
materialsSkipped: number materialsSkipped: number
errors: string[] 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
}

View File

@@ -4,7 +4,12 @@
*/ */
import type { ExtractorInput, ExtractorResult } from './extractor.types' 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 * MySQL connection configuration
@@ -104,6 +109,12 @@ export interface CleanerAPI {
runCleaner: ( runCleaner: (
input: CleanerInput input: CleanerInput
) => Promise<{ success: boolean; data?: CleanerResult; error?: string }> ) => Promise<{ success: boolean; data?: CleanerResult; error?: string }>
/**
* Export validation results to Excel
* @param items - Validation result items to export
*/
exportResults: (items: ExportResultItem[]) => Promise<ExportResultResponse>
} }
/** /**

View File

@@ -2,7 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload' import { electronAPI } from '@electron-toolkit/preload'
import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types' import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types'
import type { ExtractorInput } from '../main/types/extractor.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 { ResolverInput } from '../main/ipc/resolver-handler'
import type { LoginRequest } from '../main/ipc/auth-handler' import type { LoginRequest } from '../main/ipc/auth-handler'
import type { UserInfo } from '../main/types/user.types' import type { UserInfo } from '../main/types/user.types'
@@ -31,7 +31,8 @@ const api = {
// Cleaner service // Cleaner service
cleaner: { 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 // Order number resolver

View File

@@ -56,6 +56,7 @@ const CleanerPage: React.FC = () => {
// Execution state // Execution state
const [isRunning, setIsRunning] = useState(false) const [isRunning, setIsRunning] = useState(false)
const [isValidationRunning, setIsValidationRunning] = useState(false) const [isValidationRunning, setIsValidationRunning] = useState(false)
const [isExporting, setIsExporting] = useState(false)
// Shared Production IDs state // Shared Production IDs state
const [sharedProductionIdsCount, setSharedProductionIdsCount] = useState(0) 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 ( return (
<div className="h-full flex flex-col xl:flex-row gap-6 animate-in fade-in slide-in-from-bottom-4 duration-500"> <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} /> <Settings2 size={14} />
</button> </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"> <button
<FileSpreadsheet size={14} /> 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> </button>
</div> </div>
</div> </div>

View File

@@ -15,6 +15,7 @@ export const IPC_CHANNELS = {
// Cleaner service // Cleaner service
CLEANER_RUN: 'cleaner:run', CLEANER_RUN: 'cleaner:run',
CLEANER_EXPORT_RESULTS: 'cleaner:exportResults',
// Database service - MySQL // Database service - MySQL
DATABASE_MYSQL_CONNECT: 'database:mysql:connect', DATABASE_MYSQL_CONNECT: 'database:mysql:connect',