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 { 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user