refactor: extract cleaner hook helpers and api

This commit is contained in:
Misaka
2026-03-21 09:02:45 +08:00
parent b979b73ba1
commit 7b57545127
7 changed files with 463 additions and 210 deletions

View File

@@ -10,17 +10,7 @@
import React from 'react'
import { CheckCircle, XCircle, SkipForward, Package, Loader2 } from 'lucide-react'
import { Modal } from './ui/Modal'
interface CleanerProgress {
message: string
progress: number
currentOrderIndex: number
totalOrders: number
currentMaterialIndex: number
totalMaterialsInOrder: number
currentOrderNumber?: string
phase: 'login' | 'processing' | 'complete'
}
import type { CleanerProgress } from '../hooks/cleaner/types'
interface ExecutionReportDialogProps {
isOpen: boolean

View File

@@ -0,0 +1,159 @@
import type {
CleanerConfigResult,
CleanerInitializationResult,
CleanerReportData,
ValidationRequest,
ValidationResponsePayload
} from './types'
import type { CleanerExportItem, MaterialBatchChange } from './helpers'
export async function initializeCleanerPage(): Promise<CleanerInitializationResult> {
const adminResult = await window.electron.auth.isAdmin()
const userResult = await window.electron.auth.getCurrentUser()
const isAdmin = adminResult.success && Boolean(adminResult.data)
const user = userResult.success ? userResult.data : undefined
const currentUsername = user?.userInfo?.username ?? ''
let managers: string[] = []
if (isAdmin) {
const response = await window.electron.materials.getManagers()
const payload = response.success
? (response.data as { managers?: string[] } | undefined)
: undefined
managers = payload?.managers ?? []
}
const sharedIdsResult = await window.electron.validation.getSharedProductionIds()
const sharedIdsPayload = sharedIdsResult.success
? (sharedIdsResult.data as { productionIds?: string[] } | undefined)
: undefined
return {
isAdmin,
currentUsername,
managers,
sharedProductionIdsCount: sharedIdsPayload?.productionIds?.length ?? 0
}
}
export async function loadCleanerConfig(): Promise<CleanerConfigResult | null> {
const result = await window.electron.config.getCleaner()
if (!result.success || !result.data) {
return null
}
return {
queryBatchSize: result.data.queryBatchSize,
processConcurrency: result.data.processConcurrency
}
}
export async function runValidationRequest(
request: ValidationRequest
): Promise<ValidationResponsePayload | null> {
const response = await window.electron.validation.validate(request)
if (!response.success || !response.data) {
return response.success ? null : { success: false, error: response.error }
}
return response.data as ValidationResponsePayload
}
export async function saveDeletionPlan(params: {
materialsToUpsert: MaterialBatchChange[]
materialsToDelete: string[]
}): Promise<{ upserted: number; deleted: number }> {
const { materialsToUpsert, materialsToDelete } = params
let upserted = 0
let deleted = 0
if (materialsToUpsert.length > 0) {
const response = await window.electron.materials.upsertBatch(materialsToUpsert)
const payload = response.success
? (response.data as { stats?: { success?: number } } | undefined)
: undefined
if (!response.success) {
throw new Error(response.error || '写入物料失败')
}
upserted = payload?.stats?.success || 0
}
if (materialsToDelete.length > 0) {
const response = await window.electron.materials.delete(materialsToDelete)
const payload = response.success ? (response.data as { count?: number } | undefined) : undefined
if (!response.success) {
throw new Error(response.error || '删除物料失败')
}
deleted = payload?.count || 0
}
return { upserted, deleted }
}
export async function reloadManagers(): Promise<string[]> {
const response = await window.electron.materials.getManagers()
const payload = response.success
? (response.data as { managers?: string[] } | undefined)
: undefined
return payload?.managers ?? []
}
export async function runCleanerExecution(params: {
dryRun: boolean
headless: boolean
queryBatchSize: number
processConcurrency: number
}): Promise<CleanerReportData> {
const cleanerDataResult = await window.electron.validation.getCleanerData()
const cleanerData = cleanerDataResult.success ? (cleanerDataResult.data as any) : null
if (!cleanerDataResult.success || cleanerData?.success === false) {
throw new Error(cleanerDataResult.error || '获取清理数据失败')
}
const orderNumberList = cleanerData?.orderNumbers || []
const materialCodeList = cleanerData?.materialCodes || []
if (orderNumberList.length === 0) {
throw new Error('没有订单号数据。请先到数据提取页面输入 Production ID。')
}
if (materialCodeList.length === 0) {
throw new Error('没有物料代码数据。请确认已在物料清理界面确认要删除的物料。')
}
const response = await window.electron.cleaner.runCleaner({
orderNumbers: orderNumberList,
materialCodes: materialCodeList,
dryRun: params.dryRun,
headless: params.headless,
queryBatchSize: params.queryBatchSize,
processConcurrency: params.processConcurrency
})
const cleanerRunData = response.success ? (response.data as any) : null
if (!response.success || !cleanerRunData) {
throw new Error(response.error || '清理失败')
}
return {
ordersProcessed: cleanerRunData.ordersProcessed,
materialsDeleted: cleanerRunData.materialsDeleted,
materialsSkipped: cleanerRunData.materialsSkipped,
errors: cleanerRunData.errors,
retriedOrders: cleanerRunData.retriedOrders,
successfulRetries: cleanerRunData.successfulRetries
}
}
export async function exportCleanerResults(items: CleanerExportItem[]): Promise<string> {
const response = await window.electron.cleaner.exportResults(items)
const exportData = response.success
? (response.data as { success?: boolean; filePath?: string; error?: string } | null)
: null
if (!response.success || exportData?.success === false) {
throw new Error(response.error || exportData?.error || '导出失败')
}
return exportData?.filePath ?? ''
}

View File

@@ -0,0 +1,107 @@
import type { ValidationResult } from './types'
export interface MaterialBatchChange {
materialCode: string
managerName: string
}
export interface DeletionPlan {
materialsToUpsert: MaterialBatchChange[]
materialsToDelete: string[]
missingManager: string[]
}
export interface CleanerExportItem {
materialName: string
materialCode: string
specification: string
model: string
managerName: string
isMarkedForDeletion: boolean
isSelected: boolean
}
export function getStoredBoolean(key: string, fallback: boolean): boolean {
const saved = sessionStorage.getItem(key)
return saved ? saved === 'true' : fallback
}
export function getStoredValidationMode(): 'full' | 'filtered' {
const saved = sessionStorage.getItem('cleaner_validationMode')
return saved === 'full' ? 'full' : 'filtered'
}
export function filterValidationResults(params: {
validationResults: ValidationResult[]
isAdmin: boolean
currentUsername: string
managers: string[]
selectedManagers: Set<string>
hiddenItems: Set<string>
}): ValidationResult[] {
const { validationResults, isAdmin, currentUsername, managers, selectedManagers, hiddenItems } =
params
let results = validationResults
if (!isAdmin && currentUsername) {
results = results.filter((result) => {
return result.managerName === currentUsername || !result.managerName
})
} else if (managers.length > 0 && selectedManagers.size > 0) {
results = results.filter((result) => {
return selectedManagers.has(result.managerName) || !result.managerName
})
}
return results.filter((result) => !hiddenItems.has(result.materialCode))
}
export function buildDeletionPlan(
resultsToProcess: ValidationResult[],
selectedItems: Set<string>
): DeletionPlan {
const materialsToUpsert: MaterialBatchChange[] = []
const materialsToDelete: string[] = []
const missingManager: string[] = []
for (const result of resultsToProcess) {
if (!result.materialCode?.trim()) {
continue
}
const code = result.materialCode.trim()
if (selectedItems.has(code)) {
if (!result.managerName?.trim()) {
missingManager.push(code)
} else {
materialsToUpsert.push({
materialCode: code,
managerName: result.managerName.trim()
})
}
} else {
materialsToDelete.push(code)
}
}
return {
materialsToUpsert,
materialsToDelete,
missingManager
}
}
export function buildExportItems(
filteredResults: ValidationResult[],
selectedItems: Set<string>
): CleanerExportItem[] {
return 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)
}))
}

View File

@@ -0,0 +1,60 @@
export interface ValidationRequest {
mode: 'database_full' | 'database_filtered'
productionIdFile?: string
useSharedProductionIds?: boolean
}
export interface ValidationResult {
materialName: string
materialCode: string
specification: string
model: string
managerName: string
isMarkedForDeletion: boolean
matchedTypeKeyword?: string
}
export interface ValidationStats {
totalRecords: number
matchedCount: number
markedCount: number
}
export interface ValidationResponsePayload {
success: boolean
results?: ValidationResult[]
stats?: ValidationStats
error?: string
}
export interface CleanerProgress {
message: string
progress: number
currentOrderIndex: number
totalOrders: number
currentMaterialIndex: number
totalMaterialsInOrder: number
currentOrderNumber?: string
phase: 'login' | 'processing' | 'complete'
}
export interface CleanerReportData {
ordersProcessed: number
materialsDeleted: number
materialsSkipped: number
errors: string[]
retriedOrders?: number
successfulRetries?: number
}
export interface CleanerInitializationResult {
isAdmin: boolean
currentUsername: string
managers: string[]
sharedProductionIdsCount: number
}
export interface CleanerConfigResult {
queryBatchSize: number
processConcurrency: number
}

View File

@@ -1,27 +1,23 @@
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
import { showSuccess, showError, showWarning, formatListMessage } from '../stores/useAppStore'
import { ConfirmDialogProps } from '../components/ui/ConfirmDialog'
export interface ValidationResult {
materialName: string
materialCode: string
specification: string
model: string
managerName: string
isMarkedForDeletion: boolean
matchedTypeKeyword?: string
}
export interface CleanerProgress {
message: string
progress: number
currentOrderIndex: number
totalOrders: number
currentMaterialIndex: number
totalMaterialsInOrder: number
currentOrderNumber?: string
phase: 'login' | 'processing' | 'complete'
}
import {
buildDeletionPlan,
buildExportItems,
filterValidationResults,
getStoredBoolean,
getStoredValidationMode
} from './cleaner/helpers'
import {
exportCleanerResults,
initializeCleanerPage,
loadCleanerConfig as fetchCleanerConfig,
reloadManagers,
runCleanerExecution,
runValidationRequest,
saveDeletionPlan
} from './cleaner/api'
import type { CleanerProgress, CleanerReportData, ValidationResult } from './cleaner/types'
export function useCleaner() {
// Authentication & permissions
@@ -29,15 +25,8 @@ export function useCleaner() {
const [currentUsername, setCurrentUsername] = useState<string>('')
// State with sessionStorage persistence
const [dryRun, setDryRun] = useState(() => {
const saved = sessionStorage.getItem('cleaner_dryRun')
return saved ? saved === 'true' : false
})
const [valMode, setValMode] = useState<'full' | 'filtered'>(() => {
const saved = sessionStorage.getItem('cleaner_validationMode')
return saved === 'full' ? 'full' : 'filtered'
})
const [dryRun, setDryRun] = useState(() => getStoredBoolean('cleaner_dryRun', false))
const [valMode, setValMode] = useState<'full' | 'filtered'>(() => getStoredValidationMode())
// Validation state
const [validationResults, setValidationResults] = useState<ValidationResult[]>([])
@@ -60,24 +49,14 @@ export function useCleaner() {
// Execution report dialog state
const [isReportDialogOpen, setIsReportDialogOpen] = useState(false)
const [reportData, setReportData] = useState<{
ordersProcessed: number
materialsDeleted: number
materialsSkipped: number
errors: string[]
retriedOrders?: number
successfulRetries?: number
} | null>(null)
const [reportData, setReportData] = useState<CleanerReportData | null>(null)
// Progress state
const [progress, setProgress] = useState<CleanerProgress | null>(null)
const [startTime, setStartTime] = useState<number | null>(null)
// Execution settings state
const [headless, setHeadless] = useState(() => {
const saved = sessionStorage.getItem('cleaner_headless')
return saved ? saved === 'true' : true
})
const [headless, setHeadless] = useState(() => getStoredBoolean('cleaner_headless', true))
const [queryBatchSize, setQueryBatchSize] = useState(100)
const [processConcurrency, setProcessConcurrency] = useState(1)
const [showSettingsMenu, setShowSettingsMenu] = useState(false)
@@ -117,35 +96,17 @@ export function useCleaner() {
useEffect(() => {
const initializePage = async () => {
try {
const adminResult = await window.electron.auth.isAdmin()
const userResult = await window.electron.auth.getCurrentUser()
const admin = adminResult.success && Boolean(adminResult.data)
const user = userResult.success ? userResult.data : undefined
setIsAdmin(admin)
if (user && user.userInfo) {
setCurrentUsername(user.userInfo.username)
if (!admin) {
setSelectedManagers(new Set([user.userInfo.username]))
}
}
const result = await initializeCleanerPage()
setIsAdmin(result.isAdmin)
setCurrentUsername(result.currentUsername)
setManagers(result.managers)
setSharedProductionIdsCount(result.sharedProductionIdsCount)
// Load managers
if (admin) {
const resp = await window.electron.materials.getManagers()
const managersPayload = resp.success
? (resp.data as { managers: string[] } | undefined)
: undefined
const managerList = managersPayload?.managers ?? []
setManagers(managerList)
setSelectedManagers(new Set(managerList))
if (result.isAdmin) {
setSelectedManagers(new Set(result.managers))
} else if (result.currentUsername) {
setSelectedManagers(new Set([result.currentUsername]))
}
// Get shared Production IDs
const result = await window.electron.validation.getSharedProductionIds()
const idsPayload = result.success
? (result.data as { productionIds?: string[] } | undefined)
: undefined
setSharedProductionIdsCount(idsPayload?.productionIds?.length ?? 0)
} catch (err) {
console.error('Initialization failed:', err)
}
@@ -168,10 +129,10 @@ export function useCleaner() {
useEffect(() => {
const loadCleanerConfig = async () => {
try {
const result = await window.electron.config.getCleaner()
if (result.success && result.data) {
setQueryBatchSize(result.data.queryBatchSize)
setProcessConcurrency(result.data.processConcurrency)
const result = await fetchCleanerConfig()
if (result) {
setQueryBatchSize(result.queryBatchSize)
setProcessConcurrency(result.processConcurrency)
}
} catch (err) {
console.error('Failed to load cleaner config:', err)
@@ -204,14 +165,14 @@ export function useCleaner() {
// Filter validation results by selected managers and hidden items
const filteredResults = useMemo(() => {
let results = validationResults
if (!isAdmin && currentUsername) {
results = results.filter((r) => r.managerName === currentUsername || !r.managerName)
} else if (managers.length > 0 && selectedManagers.size > 0) {
results = results.filter((r) => selectedManagers.has(r.managerName) || !r.managerName)
}
results = results.filter((r) => !hiddenItems.has(r.materialCode))
return results
return filterValidationResults({
validationResults,
isAdmin,
currentUsername,
managers,
selectedManagers,
hiddenItems
})
}, [validationResults, isAdmin, currentUsername, managers, selectedManagers, hiddenItems])
// Handlers
@@ -222,13 +183,12 @@ export function useCleaner() {
setHiddenItems(new Set())
try {
const response = await window.electron.validation.validate({
const validationData = await runValidationRequest({
mode: valMode === 'full' ? 'database_full' : 'database_filtered',
useSharedProductionIds: valMode === 'filtered'
})
const validationData = response.success ? (response.data as any) : null
if (response.success && validationData?.success && validationData.results) {
if (validationData?.success && validationData.results) {
setValidationResults(validationData.results)
const markedCodes = new Set<string>(
validationData.results
@@ -248,7 +208,7 @@ export function useCleaner() {
}
showSuccess('校验完成')
} else {
showError(response.error || validationData?.error || '校验失败')
showError(validationData?.error || '校验失败')
}
} catch (err) {
showError(err instanceof Error ? err.message : '校验过程中发生未知错误')
@@ -324,21 +284,10 @@ export function useCleaner() {
return
}
const materialsToUpsert: { materialCode: string; managerName: string }[] = []
const materialsToDelete: string[] = []
const missingManager: string[] = []
for (const result of resultsToProcess) {
if (!result.materialCode?.trim()) continue
const code = result.materialCode.trim()
if (selectedItems.has(code)) {
if (!result.managerName?.trim()) missingManager.push(code)
else materialsToUpsert.push({ materialCode: code, managerName: result.managerName.trim() })
} else {
materialsToDelete.push(code)
}
}
const { materialsToUpsert, materialsToDelete, missingManager } = buildDeletionPlan(
resultsToProcess,
selectedItems
)
if (missingManager.length > 0) {
showError(
@@ -368,31 +317,15 @@ export function useCleaner() {
setIsRunning(true)
const msgParts: string[] = []
if (materialsToUpsert.length > 0) {
const res = await window.electron.materials.upsertBatch(materialsToUpsert)
const payload = res.success
? (res.data as { stats?: { success?: number } } | undefined)
: undefined
if (!res.success) throw new Error(res.error || '写入物料失败')
msgParts.push(`写入/更新成功:${payload?.stats?.success || 0}`)
}
if (materialsToDelete.length > 0) {
const res = await window.electron.materials.delete(materialsToDelete)
const payload = res.success ? (res.data as { count?: number } | undefined) : undefined
if (!res.success) throw new Error(res.error || '删除物料失败')
msgParts.push(`删除成功:${payload?.count || 0}`)
}
const result = await saveDeletionPlan({ materialsToUpsert, materialsToDelete })
if (materialsToUpsert.length > 0) msgParts.push(`写入/更新成功:${result.upserted}`)
if (materialsToDelete.length > 0) msgParts.push(`删除成功:${result.deleted}`)
showSuccess(`操作完成!\n\n${msgParts.join('\n')}`)
// Reload managers if admin
if (isAdmin) {
const resp = await window.electron.materials.getManagers()
const payload = resp.success
? (resp.data as { managers?: string[] } | undefined)
: undefined
setManagers(payload?.managers ?? [])
setManagers(await reloadManagers())
}
} catch (err) {
showError(err instanceof Error ? err.message : '操作失败')
@@ -429,42 +362,13 @@ export function useCleaner() {
setIsReportDialogOpen(true)
try {
const cleanerDataResult = await window.electron.validation.getCleanerData()
const cleanerData = cleanerDataResult.success ? (cleanerDataResult.data as any) : null
if (!cleanerDataResult.success || cleanerData?.success === false) {
throw new Error(cleanerDataResult.error || '获取清理数据失败')
}
const orderNumberList = cleanerData?.orderNumbers || []
const materialCodeList = cleanerData?.materialCodes || []
if (orderNumberList.length === 0)
throw new Error('没有订单号数据。请先到数据提取页面输入 Production ID。')
if (materialCodeList.length === 0)
throw new Error('没有物料代码数据。请确认已在物料清理界面确认要删除的物料。')
const response = await window.electron.cleaner.runCleaner({
orderNumbers: orderNumberList,
materialCodes: materialCodeList,
const result = await runCleanerExecution({
dryRun,
headless,
queryBatchSize,
processConcurrency
})
const cleanerRunData = response.success ? (response.data as any) : null
if (response.success && cleanerRunData) {
setReportData({
ordersProcessed: cleanerRunData.ordersProcessed,
materialsDeleted: cleanerRunData.materialsDeleted,
materialsSkipped: cleanerRunData.materialsSkipped,
errors: cleanerRunData.errors,
retriedOrders: cleanerRunData.retriedOrders,
successfulRetries: cleanerRunData.successfulRetries
})
} else {
throw new Error(response.error || '清理失败')
}
setReportData(result)
} catch (err) {
showError(err instanceof Error ? err.message : '发生未知错误')
} finally {
@@ -488,25 +392,8 @@ export function useCleaner() {
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)
const exportData = response.success ? (response.data as any) : null
if (response.success && exportData?.success !== false) {
showSuccess(`导出成功!\n文件已保存到${exportData?.filePath ?? ''}`)
} else {
throw new Error(response.error || exportData?.error || '导出失败')
}
const filePath = await exportCleanerResults(buildExportItems(filteredResults, selectedItems))
showSuccess(`导出成功!\n文件已保存到${filePath}`)
} catch (err) {
showError(err instanceof Error ? err.message : '导出过程中发生错误')
} finally {

View File

@@ -1,33 +1,10 @@
import { useState, useCallback } from 'react'
interface ValidationRequest {
mode: 'database_full' | 'database_filtered'
productionIdFile?: string
useSharedProductionIds?: boolean
}
interface ValidationResult {
materialName: string
materialCode: string
specification: string
model: string
managerName: string
isMarkedForDeletion: boolean
matchedTypeKeyword?: string
}
interface ValidationStats {
totalRecords: number
matchedCount: number
markedCount: number
}
interface ValidationResponse {
success: boolean
results?: ValidationResult[]
stats?: ValidationStats
error?: string
}
import type {
ValidationRequest,
ValidationResponsePayload as ValidationResponse,
ValidationResult,
ValidationStats
} from './cleaner/types'
interface UseValidationState {
loading: boolean