Compare commits
2 Commits
fad08796f7
...
9caa38caa8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9caa38caa8 | ||
|
|
0f1e0a8908 |
@@ -9,6 +9,8 @@ import { registerCleanerHandlers } from './cleaner-handler'
|
||||
import { registerDatabaseHandlers } from './database-handler'
|
||||
import { registerResolverHandlers } from './resolver-handler'
|
||||
import { registerAuthHandlers } from './auth-handler'
|
||||
import { registerValidationHandlers } from './validation-handler'
|
||||
import { registerSettingsHandlers } from './settings-handler'
|
||||
|
||||
/**
|
||||
* Register all IPC handlers
|
||||
@@ -20,4 +22,6 @@ export function registerIpcHandlers(): void {
|
||||
registerDatabaseHandlers()
|
||||
registerResolverHandlers()
|
||||
registerAuthHandlers()
|
||||
registerValidationHandlers()
|
||||
registerSettingsHandlers()
|
||||
}
|
||||
|
||||
255
src/main/ipc/settings-handler.ts
Normal file
255
src/main/ipc/settings-handler.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* Settings IPC Handler
|
||||
*
|
||||
* Provides IPC handlers for settings management:
|
||||
* - Get/set settings
|
||||
* - Reset to defaults
|
||||
* - Test ERP connection
|
||||
* - Test database connection
|
||||
*/
|
||||
|
||||
import { ipcMain } from 'electron'
|
||||
import { ConfigManager } from '../services/config/config-manager'
|
||||
import { SessionManager } from '../services/user/session-manager'
|
||||
import { ErpAuthService } from '../services/erp/erp-auth'
|
||||
import { MySqlService } from '../services/database/mysql'
|
||||
import { SqlServerService } from '../services/database/sql-server'
|
||||
import type {
|
||||
SettingsData,
|
||||
UserType,
|
||||
ConnectionTestResult,
|
||||
SaveSettingsResult
|
||||
} from '../types/settings.types'
|
||||
|
||||
/**
|
||||
* Filter settings by user type
|
||||
* Admin users get all settings, User users get limited settings
|
||||
*/
|
||||
function filterSettingsByUserType(
|
||||
settings: SettingsData,
|
||||
userType: UserType
|
||||
): SettingsData {
|
||||
if (userType === 'Admin') {
|
||||
return settings // Return all settings for Admin
|
||||
}
|
||||
|
||||
// User users get limited settings
|
||||
return {
|
||||
erp: {
|
||||
username: settings.erp.username,
|
||||
password: settings.erp.password,
|
||||
headless: settings.erp.headless,
|
||||
url: settings.erp.url,
|
||||
ignoreHttpsErrors: settings.erp.ignoreHttpsErrors,
|
||||
autoCloseBrowser: settings.erp.autoCloseBrowser
|
||||
},
|
||||
paths: settings.paths,
|
||||
execution: settings.execution,
|
||||
// Include minimal required fields for other sections
|
||||
database: settings.database,
|
||||
extraction: settings.extraction,
|
||||
validation: settings.validation,
|
||||
ui: settings.ui
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register IPC handlers for settings management
|
||||
*/
|
||||
export function registerSettingsHandlers(): void {
|
||||
const configManager = ConfigManager.getInstance()
|
||||
const sessionManager = SessionManager.getInstance()
|
||||
|
||||
/**
|
||||
* Get current user type
|
||||
*/
|
||||
ipcMain.handle('settings:getUserType', async (): Promise<UserType> => {
|
||||
return (sessionManager.getUserType() as UserType) || 'Guest'
|
||||
})
|
||||
|
||||
/**
|
||||
* Get settings (filtered by user type)
|
||||
*/
|
||||
ipcMain.handle('settings:getSettings', async (): Promise<SettingsData> => {
|
||||
const userType = (sessionManager.getUserType() as UserType) || 'Guest'
|
||||
const settings = configManager.getAllSettings()
|
||||
return filterSettingsByUserType(settings, userType)
|
||||
})
|
||||
|
||||
/**
|
||||
* Save settings
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'settings:saveSettings',
|
||||
async (_event, settings: SettingsData): Promise<SaveSettingsResult> => {
|
||||
try {
|
||||
const success = await configManager.saveAllSettings(settings)
|
||||
if (success) {
|
||||
return { success: true }
|
||||
} else {
|
||||
return { success: false, error: '保存设置失败' }
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
return { success: false, error: `保存设置失败:${message}` }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Reset to default settings (Admin only)
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'settings:resetDefaults',
|
||||
async (): Promise<SaveSettingsResult> => {
|
||||
try {
|
||||
const userType = sessionManager.getUserType()
|
||||
if (userType !== 'Admin') {
|
||||
return { success: false, error: '只有管理员可以恢复默认设置' }
|
||||
}
|
||||
|
||||
configManager.resetToDefaults()
|
||||
const success = await configManager.save()
|
||||
if (success) {
|
||||
return { success: true }
|
||||
} else {
|
||||
return { success: false, error: '恢复默认设置失败' }
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
return { success: false, error: `恢复默认设置失败:${message}` }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Test ERP connection
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'settings:testErpConnection',
|
||||
async (): Promise<ConnectionTestResult> => {
|
||||
try {
|
||||
const settings = configManager.getAllSettings()
|
||||
const erpConfig = settings.erp
|
||||
|
||||
if (!erpConfig.url || !erpConfig.username || !erpConfig.password) {
|
||||
return {
|
||||
success: false,
|
||||
message: '请先配置 ERP URL、用户名和密码'
|
||||
}
|
||||
}
|
||||
|
||||
// Create ERP auth service and try to login
|
||||
const erpAuthService = new ErpAuthService(erpConfig)
|
||||
|
||||
try {
|
||||
await erpAuthService.login()
|
||||
// Login successful, close browser
|
||||
await erpAuthService.close()
|
||||
return {
|
||||
success: true,
|
||||
message: 'ERP 连接测试成功!'
|
||||
}
|
||||
} catch (loginError) {
|
||||
const errorMessage = loginError instanceof Error ? loginError.message : '登录失败'
|
||||
return {
|
||||
success: false,
|
||||
message: `ERP 连接测试失败:${errorMessage}`
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
return {
|
||||
success: false,
|
||||
message: `ERP 连接测试失败:${message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Test database connection
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'settings:testDbConnection',
|
||||
async (): Promise<ConnectionTestResult> => {
|
||||
try {
|
||||
const settings = configManager.getAllSettings()
|
||||
const dbConfig = settings.database
|
||||
|
||||
if (dbConfig.dbType === 'mysql') {
|
||||
// Test MySQL connection
|
||||
if (!dbConfig.mysqlHost || !dbConfig.database || !dbConfig.username) {
|
||||
return {
|
||||
success: false,
|
||||
message: '请先配置 MySQL 主机、数据库名和用户名'
|
||||
}
|
||||
}
|
||||
|
||||
const mysqlService = new MySqlService({
|
||||
host: dbConfig.mysqlHost,
|
||||
port: dbConfig.mysqlPort,
|
||||
user: dbConfig.username,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.database
|
||||
})
|
||||
|
||||
try {
|
||||
await mysqlService.connect()
|
||||
await mysqlService.disconnect()
|
||||
return {
|
||||
success: true,
|
||||
message: 'MySQL 数据库连接测试成功!'
|
||||
}
|
||||
} catch (connError) {
|
||||
const errorMessage = connError instanceof Error ? connError.message : '连接失败'
|
||||
return {
|
||||
success: false,
|
||||
message: `MySQL 数据库连接测试失败:${errorMessage}`
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Test SQL Server connection
|
||||
if (!dbConfig.server || !dbConfig.database || !dbConfig.username) {
|
||||
return {
|
||||
success: false,
|
||||
message: '请先配置 SQL Server 服务器、数据库名和用户名'
|
||||
}
|
||||
}
|
||||
|
||||
const sqlServerService = new SqlServerService({
|
||||
server: dbConfig.server,
|
||||
port: 1433, // Default SQL Server port
|
||||
user: dbConfig.username,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.database,
|
||||
options: {
|
||||
trustServerCertificate: true
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
await sqlServerService.connect()
|
||||
await sqlServerService.disconnect()
|
||||
return {
|
||||
success: true,
|
||||
message: 'SQL Server 数据库连接测试成功!'
|
||||
}
|
||||
} catch (connError) {
|
||||
const errorMessage = connError instanceof Error ? connError.message : '连接失败'
|
||||
return {
|
||||
success: false,
|
||||
message: `SQL Server 数据库连接测试失败:${errorMessage}`
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
return {
|
||||
success: false,
|
||||
message: `数据库连接测试失败:${message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
714
src/main/ipc/validation-handler.ts
Normal file
714
src/main/ipc/validation-handler.ts
Normal file
@@ -0,0 +1,714 @@
|
||||
/**
|
||||
* IPC handlers for material validation operations
|
||||
*
|
||||
* Provides endpoints for:
|
||||
* - Running material validation from database
|
||||
* - Getting/setting materials to be deleted
|
||||
* - Manager-based filtering
|
||||
*/
|
||||
|
||||
import { ipcMain } from 'electron'
|
||||
import { MySqlService } from '../services/database/mysql'
|
||||
import { MaterialsToBeDeletedDAO } from '../services/database/materials-to-be-deleted-dao'
|
||||
import { DiscreteMaterialPlanDAO } from '../services/database/discrete-material-plan-dao'
|
||||
import type {
|
||||
ValidationRequest,
|
||||
ValidationResponse,
|
||||
MaterialUpsertBatchRequest,
|
||||
MaterialDeleteRequest,
|
||||
MaterialOperationResponse,
|
||||
ValidationResult,
|
||||
MaterialRecordSummary
|
||||
} from '../types/validation.types'
|
||||
|
||||
/**
|
||||
* Shared state for Production IDs from extractor page
|
||||
* This is a simple in-memory store for sharing Production IDs between pages
|
||||
*/
|
||||
const sharedProductionIds = new Set<string>()
|
||||
|
||||
/**
|
||||
* Set shared Production IDs
|
||||
*/
|
||||
export function setSharedProductionIds(ids: string[]): void {
|
||||
ids.forEach(id => sharedProductionIds.add(id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shared Production IDs
|
||||
*/
|
||||
export function getSharedProductionIds(): string[] {
|
||||
return [...sharedProductionIds]
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear shared Production IDs
|
||||
*/
|
||||
export function clearSharedProductionIds(): void {
|
||||
sharedProductionIds.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MySQL service for validation operations
|
||||
*/
|
||||
async function getValidationMySqlService(): Promise<MySqlService> {
|
||||
const mysqlService = new MySqlService({
|
||||
host: process.env.DB_MYSQL_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
|
||||
user: process.env.DB_USERNAME || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || ''
|
||||
})
|
||||
|
||||
await mysqlService.connect()
|
||||
return mysqlService
|
||||
}
|
||||
|
||||
/**
|
||||
* Read Production IDs from file
|
||||
*/
|
||||
function readProductionIds(filePath: string): string[] {
|
||||
const fs = require('fs')
|
||||
const content = fs.readFileSync(filePath, 'utf-8')
|
||||
return content
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify input type (production ID or order number)
|
||||
*/
|
||||
function identifyInputType(input: string): 'production_id' | 'order_number' | 'unknown' {
|
||||
// Order number: SC + 14 digits
|
||||
if (/^SC\d{14}$/.test(input)) {
|
||||
return 'order_number'
|
||||
}
|
||||
// Production ID: 2 digits + 1 letter + 1-6 digits
|
||||
if (/^\d{2}[A-Za-z]\d{1,6}$/.test(input)) {
|
||||
return 'production_id'
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source numbers from inputs
|
||||
*/
|
||||
async function getSourceNumbersFromInputs(
|
||||
inputs: string[],
|
||||
mysqlService: MySqlService
|
||||
): Promise<string[]> {
|
||||
const productionIds: string[] = []
|
||||
const orderNumbers: string[] = []
|
||||
|
||||
for (const item of inputs) {
|
||||
const type = identifyInputType(item)
|
||||
if (type === 'order_number') {
|
||||
orderNumbers.push(item)
|
||||
} else if (type === 'production_id') {
|
||||
productionIds.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
// Query production contract data for production IDs
|
||||
// Table name in MySQL: productionContractData_26年压力表合同数据
|
||||
// Column name: 生产订单号 (SourceNumber)
|
||||
if (productionIds.length > 0) {
|
||||
const placeholders = productionIds.map(() => '?').join(',')
|
||||
const contractSql = `
|
||||
SELECT DISTINCT 生产订单号
|
||||
FROM productionContractData_26年压力表合同数据
|
||||
WHERE 总排号 IN (${placeholders})
|
||||
`
|
||||
const contractResult = await mysqlService.query(contractSql, productionIds)
|
||||
const dbOrderNumbers = contractResult.rows.map(
|
||||
row => row.生产订单号 as string
|
||||
)
|
||||
orderNumbers.push(...dbOrderNumbers)
|
||||
}
|
||||
|
||||
// Deduplicate
|
||||
return [...new Set(orderNumbers)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Register IPC handlers for validation operations
|
||||
*/
|
||||
export function registerValidationHandlers(): void {
|
||||
// ==================== VALIDATION ====================
|
||||
|
||||
/**
|
||||
* Run material validation from database
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'validation:validate',
|
||||
async (
|
||||
_event,
|
||||
request: ValidationRequest
|
||||
): Promise<ValidationResponse> => {
|
||||
let mysqlService: MySqlService | null = null
|
||||
|
||||
try {
|
||||
console.log('[Validation] Starting validation:', request)
|
||||
|
||||
// Connect to MySQL
|
||||
mysqlService = await getValidationMySqlService()
|
||||
|
||||
let sourceNumbers: string[] | null = null
|
||||
|
||||
// Get source numbers based on mode
|
||||
if (request.mode === 'database_filtered') {
|
||||
if (request.useSharedProductionIds) {
|
||||
// Use shared Production IDs from extractor page
|
||||
const sharedIds = getSharedProductionIds()
|
||||
console.log(`[Validation] Using ${sharedIds.length} shared Production IDs`)
|
||||
|
||||
if (sharedIds.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: '没有可用的共享 Production ID。请在数据提取页面输入 Production ID。',
|
||||
stats: {
|
||||
totalRecords: 0,
|
||||
matchedCount: 0,
|
||||
markedCount: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceNumbers = await getSourceNumbersFromInputs(sharedIds, mysqlService)
|
||||
console.log(
|
||||
`[Validation] Got ${sourceNumbers.length} source numbers from shared Production IDs`
|
||||
)
|
||||
} else if (request.productionIdFile) {
|
||||
// Read from file
|
||||
const inputs = readProductionIds(request.productionIdFile)
|
||||
console.log(
|
||||
`[Validation] Read ${inputs.length} inputs from file`
|
||||
)
|
||||
sourceNumbers = await getSourceNumbersFromInputs(inputs, mysqlService)
|
||||
console.log(
|
||||
`[Validation] Got ${sourceNumbers.length} source numbers`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Get material records from DiscreteMaterialPlanData
|
||||
const materialDao = new DiscreteMaterialPlanDAO()
|
||||
// Inject the mysqlService instance
|
||||
;(materialDao as any).mysqlService = mysqlService
|
||||
|
||||
let materialRecords: any[] = []
|
||||
|
||||
if (request.mode === 'database_full') {
|
||||
// Full table query with deduplication by MaterialCode
|
||||
materialRecords = await materialDao.queryAllDistinctByMaterialCode()
|
||||
} else if (sourceNumbers && sourceNumbers.length > 0) {
|
||||
// Filtered query by source numbers
|
||||
materialRecords = await materialDao.queryBySourceNumbersDistinct(
|
||||
sourceNumbers
|
||||
)
|
||||
}
|
||||
|
||||
if (materialRecords.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'No material records found',
|
||||
stats: {
|
||||
totalRecords: 0,
|
||||
matchedCount: 0,
|
||||
markedCount: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get type keywords from MaterialsTypeToBeDeleted
|
||||
const typeKeywordSql = `
|
||||
SELECT MaterialName, ManagerName
|
||||
FROM dbo_MaterialsTypeToBeDeleted
|
||||
WHERE MaterialName IS NOT NULL
|
||||
`
|
||||
const typeKeywordResult = await mysqlService.query(typeKeywordSql)
|
||||
const typeKeywords = typeKeywordResult.rows.map(row => ({
|
||||
materialName: row.MaterialName as string,
|
||||
managerName: row.ManagerName as string
|
||||
}))
|
||||
|
||||
// Get marked material codes from MaterialsToBeDeleted
|
||||
const markedSql = `
|
||||
SELECT MaterialCode, ManagerName
|
||||
FROM dbo_MaterialsToBeDeleted
|
||||
WHERE MaterialCode IS NOT NULL AND ManagerName IS NOT NULL
|
||||
`
|
||||
const markedResult = await mysqlService.query(markedSql)
|
||||
const markedCodesDict = new Map<string, string>()
|
||||
for (const row of markedResult.rows) {
|
||||
markedCodesDict.set(
|
||||
row.MaterialCode as string,
|
||||
row.ManagerName as string
|
||||
)
|
||||
}
|
||||
|
||||
// Match materials
|
||||
const results: ValidationResult[] = []
|
||||
for (const record of materialRecords) {
|
||||
const materialName = (record.MaterialName as string) || ''
|
||||
const materialCode = (record.MaterialCode as string) || ''
|
||||
const specification = (record.Specification as string) || ''
|
||||
const model = (record.Model as string) || ''
|
||||
|
||||
// Priority 1: Check MaterialsToBeDeleted (MaterialCode exact match)
|
||||
let managerName = markedCodesDict.get(materialCode) || null
|
||||
const isMarkedForDeletion = managerName !== null
|
||||
let matchedTypeKeyword: string | undefined = undefined
|
||||
|
||||
// Priority 2: Match with MaterialsTypeToBeDeleted (MaterialName contains)
|
||||
if (!managerName) {
|
||||
for (const typeKeyword of typeKeywords) {
|
||||
if (
|
||||
typeKeyword.materialName &&
|
||||
typeKeyword.materialName.includes(materialName)
|
||||
) {
|
||||
matchedTypeKeyword = typeKeyword.materialName
|
||||
managerName = typeKeyword.managerName
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.push({
|
||||
materialName,
|
||||
materialCode,
|
||||
specification,
|
||||
model,
|
||||
managerName: managerName || '',
|
||||
isMarkedForDeletion,
|
||||
matchedTypeKeyword
|
||||
})
|
||||
}
|
||||
|
||||
const markedCount = results.filter(r => r.isMarkedForDeletion).length
|
||||
const matchedCount = results.filter(r => r.managerName).length
|
||||
|
||||
return {
|
||||
success: true,
|
||||
results,
|
||||
stats: {
|
||||
totalRecords: results.length,
|
||||
matchedCount,
|
||||
markedCount
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
console.error('[Validation] Validation error:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: `Validation failed: ${message}`
|
||||
}
|
||||
} finally {
|
||||
if (mysqlService) {
|
||||
try {
|
||||
await mysqlService.disconnect()
|
||||
} catch (closeError) {
|
||||
console.warn('[Validation] Error disconnecting MySQL:', closeError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ==================== MATERIAL OPERATIONS ====================
|
||||
|
||||
/**
|
||||
* Upsert batch materials to MaterialsToBeDeleted
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materials:upsertBatch',
|
||||
async (
|
||||
_event,
|
||||
request: MaterialUpsertBatchRequest
|
||||
): Promise<MaterialOperationResponse> => {
|
||||
let mysqlService: MySqlService | null = null
|
||||
|
||||
try {
|
||||
mysqlService = await getValidationMySqlService()
|
||||
const dao = new MaterialsToBeDeletedDAO()
|
||||
;(dao as any).mysqlService = mysqlService
|
||||
|
||||
const stats = await dao.upsertBatch(request.materials)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stats
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
console.error('[Materials] Upsert batch error:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: `Upsert failed: ${message}`
|
||||
}
|
||||
} finally {
|
||||
if (mysqlService) {
|
||||
try {
|
||||
await mysqlService.disconnect()
|
||||
} catch (closeError) {
|
||||
console.warn('[Materials] Error disconnecting MySQL:', closeError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Delete materials by material codes
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materials:delete',
|
||||
async (
|
||||
_event,
|
||||
request: MaterialDeleteRequest
|
||||
): Promise<MaterialOperationResponse> => {
|
||||
let mysqlService: MySqlService | null = null
|
||||
|
||||
try {
|
||||
mysqlService = await getValidationMySqlService()
|
||||
const dao = new MaterialsToBeDeletedDAO()
|
||||
;(dao as any).mysqlService = mysqlService
|
||||
|
||||
const count = await dao.deleteByMaterialCodes(request.materialCodes)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
count
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
console.error('[Materials] Delete error:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: `Delete failed: ${message}`
|
||||
}
|
||||
} finally {
|
||||
if (mysqlService) {
|
||||
try {
|
||||
await mysqlService.disconnect()
|
||||
} catch (closeError) {
|
||||
console.warn('[Materials] Error disconnecting MySQL:', closeError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Get unique manager names
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materials:getManagers',
|
||||
async (_event): Promise<{ managers: string[] }> => {
|
||||
let mysqlService: MySqlService | null = null
|
||||
|
||||
try {
|
||||
mysqlService = await getValidationMySqlService()
|
||||
const dao = new MaterialsToBeDeletedDAO()
|
||||
;(dao as any).mysqlService = mysqlService
|
||||
|
||||
const managers = await dao.getManagers()
|
||||
return { managers }
|
||||
} catch (error) {
|
||||
console.error('[Materials] Get managers error:', error)
|
||||
return { managers: [] }
|
||||
} finally {
|
||||
if (mysqlService) {
|
||||
try {
|
||||
await mysqlService.disconnect()
|
||||
} catch (closeError) {
|
||||
console.warn('[Materials] Error disconnecting MySQL:', closeError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Get materials by manager
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materials:getByManager',
|
||||
async (
|
||||
_event,
|
||||
managerName: string
|
||||
): Promise<{ materials: MaterialRecordSummary[] }> => {
|
||||
let mysqlService: MySqlService | null = null
|
||||
|
||||
try {
|
||||
mysqlService = await getValidationMySqlService()
|
||||
const dao = new MaterialsToBeDeletedDAO()
|
||||
;(dao as any).mysqlService = mysqlService
|
||||
|
||||
const materials = await dao.getMaterialsByManager(managerName)
|
||||
|
||||
// Get material codes set for quick lookup
|
||||
const markedCodes = await dao.getAllMaterialCodes()
|
||||
|
||||
// Enrich with material details from DiscreteMaterialPlanData
|
||||
const enrichedMaterials: MaterialRecordSummary[] = []
|
||||
|
||||
for (const mat of materials) {
|
||||
const detailSql = `
|
||||
SELECT MaterialName, Specification, Model
|
||||
FROM dbo_DiscreteMaterialPlanData
|
||||
WHERE MaterialCode = ?
|
||||
LIMIT 1
|
||||
`
|
||||
const detailResult = await mysqlService.query(detailSql, [
|
||||
mat.materialCode
|
||||
])
|
||||
|
||||
enrichedMaterials.push({
|
||||
materialCode: mat.materialCode,
|
||||
materialName:
|
||||
detailResult.rows.length > 0
|
||||
? (detailResult.rows[0].MaterialName as string)
|
||||
: '',
|
||||
specification:
|
||||
detailResult.rows.length > 0
|
||||
? (detailResult.rows[0].Specification as string)
|
||||
: '',
|
||||
model:
|
||||
detailResult.rows.length > 0
|
||||
? (detailResult.rows[0].Model as string)
|
||||
: '',
|
||||
managerName: mat.managerName,
|
||||
isMarked: markedCodes.has(mat.materialCode)
|
||||
})
|
||||
}
|
||||
|
||||
return { materials: enrichedMaterials }
|
||||
} catch (error) {
|
||||
console.error('[Materials] Get by manager error:', error)
|
||||
return { materials: [] }
|
||||
} finally {
|
||||
if (mysqlService) {
|
||||
try {
|
||||
await mysqlService.disconnect()
|
||||
} catch (closeError) {
|
||||
console.warn('[Materials] Error disconnecting MySQL:', closeError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Get all material records
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materials:getAll',
|
||||
async (_event): Promise<{ materials: MaterialRecordSummary[] }> => {
|
||||
let mysqlService: MySqlService | null = null
|
||||
|
||||
try {
|
||||
mysqlService = await getValidationMySqlService()
|
||||
const dao = new MaterialsToBeDeletedDAO()
|
||||
;(dao as any).mysqlService = mysqlService
|
||||
|
||||
const materials = await dao.getAllRecords()
|
||||
const markedCodes = await dao.getAllMaterialCodes()
|
||||
|
||||
const enrichedMaterials: MaterialRecordSummary[] = []
|
||||
|
||||
for (const mat of materials) {
|
||||
const detailSql = `
|
||||
SELECT MaterialName, Specification, Model
|
||||
FROM dbo_DiscreteMaterialPlanData
|
||||
WHERE MaterialCode = ?
|
||||
LIMIT 1
|
||||
`
|
||||
const detailResult = await mysqlService.query(detailSql, [
|
||||
mat.materialCode
|
||||
])
|
||||
|
||||
enrichedMaterials.push({
|
||||
materialCode: mat.materialCode,
|
||||
materialName:
|
||||
detailResult.rows.length > 0
|
||||
? (detailResult.rows[0].MaterialName as string)
|
||||
: '',
|
||||
specification:
|
||||
detailResult.rows.length > 0
|
||||
? (detailResult.rows[0].Specification as string)
|
||||
: '',
|
||||
model:
|
||||
detailResult.rows.length > 0
|
||||
? (detailResult.rows[0].Model as string)
|
||||
: '',
|
||||
managerName: mat.managerName,
|
||||
isMarked: markedCodes.has(mat.materialCode)
|
||||
})
|
||||
}
|
||||
|
||||
return { materials: enrichedMaterials }
|
||||
} catch (error) {
|
||||
console.error('[Materials] Get all error:', error)
|
||||
return { materials: [] }
|
||||
} finally {
|
||||
if (mysqlService) {
|
||||
try {
|
||||
await mysqlService.disconnect()
|
||||
} catch (closeError) {
|
||||
console.warn('[Materials] Error disconnecting MySQL:', closeError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Get statistics
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materials:getStatistics',
|
||||
async (_event): Promise<{ stats: any }> => {
|
||||
let mysqlService: MySqlService | null = null
|
||||
|
||||
try {
|
||||
mysqlService = await getValidationMySqlService()
|
||||
const dao = new MaterialsToBeDeletedDAO()
|
||||
;(dao as any).mysqlService = mysqlService
|
||||
|
||||
const stats = await dao.getStatistics()
|
||||
return { stats }
|
||||
} catch (error) {
|
||||
console.error('[Materials] Get statistics error:', error)
|
||||
return { stats: null }
|
||||
} finally {
|
||||
if (mysqlService) {
|
||||
try {
|
||||
await mysqlService.disconnect()
|
||||
} catch (closeError) {
|
||||
console.warn('[Materials] Error disconnecting MySQL:', closeError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Set shared Production IDs from extractor page
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'validation:setSharedProductionIds',
|
||||
async (_event, productionIds: string[]): Promise<void> => {
|
||||
console.log(`[Validation] Received ${productionIds.length} shared Production IDs`)
|
||||
setSharedProductionIds(productionIds)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Get shared Production IDs
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'validation:getSharedProductionIds',
|
||||
async (): Promise<{ productionIds: string[] }> => {
|
||||
return { productionIds: getSharedProductionIds() }
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Get cleaner data (order numbers from shared Production IDs + material codes from MaterialsToBeDeleted)
|
||||
* Filters materials by current user (admin sees all, regular users see only their own)
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'validation:getCleanerData',
|
||||
async (_event): Promise<{
|
||||
success: boolean
|
||||
orderNumbers?: string[]
|
||||
materialCodes?: string[]
|
||||
error?: string
|
||||
}> => {
|
||||
let mysqlService: MySqlService | null = null
|
||||
const sessionManager = (await import('../services/user/session-manager')).SessionManager.getInstance()
|
||||
|
||||
try {
|
||||
// Get current user
|
||||
const userInfo = sessionManager.getUserInfo()
|
||||
if (!userInfo) {
|
||||
return {
|
||||
success: false,
|
||||
error: '用户未登录'
|
||||
}
|
||||
}
|
||||
|
||||
const isAdmin = userInfo.userType === 'Admin'
|
||||
const username = userInfo.username
|
||||
|
||||
console.log(`[CleanerData] User: ${username}, isAdmin: ${isAdmin}`)
|
||||
|
||||
// Connect to MySQL
|
||||
mysqlService = await getValidationMySqlService()
|
||||
|
||||
// 1. Get order numbers from shared Production IDs
|
||||
const sharedIds = getSharedProductionIds()
|
||||
let orderNumbers: string[] = []
|
||||
|
||||
if (sharedIds.length > 0) {
|
||||
console.log(`[CleanerData] Using ${sharedIds.length} shared Production IDs`)
|
||||
orderNumbers = await getSourceNumbersFromInputs(sharedIds, mysqlService)
|
||||
console.log(`[CleanerData] Got ${orderNumbers.length} order numbers`)
|
||||
}
|
||||
|
||||
// 2. Get material codes from MaterialsToBeDeleted table
|
||||
let materialCodes: string[] = []
|
||||
|
||||
if (isAdmin) {
|
||||
// Admin sees all materials
|
||||
const allCodesSql = `
|
||||
SELECT MaterialCode
|
||||
FROM dbo_MaterialsToBeDeleted
|
||||
WHERE MaterialCode IS NOT NULL
|
||||
`
|
||||
const result = await mysqlService.query(allCodesSql)
|
||||
materialCodes = result.rows
|
||||
.map(row => row.MaterialCode as string)
|
||||
.filter(Boolean)
|
||||
console.log(`[CleanerData] Admin user: got ${materialCodes.length} materials`)
|
||||
} else {
|
||||
// Regular users only see their own materials
|
||||
const userMaterialsSql = `
|
||||
SELECT MaterialCode
|
||||
FROM dbo_MaterialsToBeDeleted
|
||||
WHERE ManagerName = ? AND MaterialCode IS NOT NULL
|
||||
`
|
||||
const result = await mysqlService.query(userMaterialsSql, [username])
|
||||
materialCodes = result.rows
|
||||
.map(row => row.MaterialCode as string)
|
||||
.filter(Boolean)
|
||||
console.log(`[CleanerData] Regular user: got ${materialCodes.length} materials`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
orderNumbers,
|
||||
materialCodes
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
console.error('[CleanerData] Error:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: `获取清理数据失败:${message}`
|
||||
}
|
||||
} finally {
|
||||
if (mysqlService) {
|
||||
try {
|
||||
await mysqlService.disconnect()
|
||||
} catch (closeError) {
|
||||
console.warn('[CleanerData] Error disconnecting MySQL:', closeError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
446
src/main/services/config/config-manager.ts
Normal file
446
src/main/services/config/config-manager.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
/**
|
||||
* Configuration Manager
|
||||
*
|
||||
* Manages application configuration stored in .env file
|
||||
* Provides methods for reading, writing, and saving configuration values
|
||||
*/
|
||||
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname } from 'path'
|
||||
import type {
|
||||
SettingsData,
|
||||
ErpConfig,
|
||||
DatabaseConfig,
|
||||
PathsConfig,
|
||||
ExtractionConfig,
|
||||
ValidationConfig,
|
||||
UiConfig,
|
||||
ExecutionConfig,
|
||||
DatabaseType,
|
||||
MatchMode,
|
||||
ValidationDataSource
|
||||
} from '../../types/settings.types'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
/**
|
||||
* Default settings values
|
||||
*/
|
||||
const DEFAULT_SETTINGS: SettingsData = {
|
||||
erp: {
|
||||
url: 'https://68.11.34.30:8082/',
|
||||
username: '',
|
||||
password: '',
|
||||
headless: true,
|
||||
ignoreHttpsErrors: true,
|
||||
autoCloseBrowser: true
|
||||
},
|
||||
database: {
|
||||
dbType: 'mysql',
|
||||
server: '',
|
||||
mysqlHost: '192.168.31.83',
|
||||
mysqlPort: 3306,
|
||||
database: 'BLD_DB',
|
||||
username: 'remote_user',
|
||||
password: ''
|
||||
},
|
||||
paths: {
|
||||
dataDir: 'D:/python/playwrite/data/',
|
||||
defaultOutput: '离散备料计划维护_合并.xlsx',
|
||||
validationOutput: '物料状态校验结果.xlsx'
|
||||
},
|
||||
extraction: {
|
||||
batchSize: 100,
|
||||
verbose: true,
|
||||
autoConvert: true,
|
||||
mergeBatches: true,
|
||||
enableDbPersistence: true
|
||||
},
|
||||
validation: {
|
||||
dataSource: 'database_full',
|
||||
batchSize: 2000,
|
||||
matchMode: 'substring',
|
||||
enableCrud: false,
|
||||
defaultManager: ''
|
||||
},
|
||||
ui: {
|
||||
fontFamily: 'Microsoft YaHei UI',
|
||||
fontSize: 10,
|
||||
productionIdInputWidth: 20
|
||||
},
|
||||
execution: {
|
||||
dryRun: false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration Manager Class
|
||||
*/
|
||||
export class ConfigManager {
|
||||
private static instance: ConfigManager | null = null
|
||||
private envPath: string
|
||||
private configCache: Map<string, string> = new Map()
|
||||
private initialized: boolean = false
|
||||
|
||||
private constructor() {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
this.envPath = path.resolve(__dirname, '../../.env')
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance
|
||||
*/
|
||||
public static getInstance(): ConfigManager {
|
||||
if (ConfigManager.instance === null) {
|
||||
ConfigManager.instance = new ConfigManager()
|
||||
}
|
||||
return ConfigManager.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize configuration from .env file
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
await this.loadEnvFile()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load .env file into cache
|
||||
*/
|
||||
private async loadEnvFile(): Promise<void> {
|
||||
try {
|
||||
if (fs.existsSync(this.envPath)) {
|
||||
const content = fs.readFileSync(this.envPath, 'utf-8')
|
||||
const lines = content.split('\n')
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim()
|
||||
// Skip empty lines and comments
|
||||
if (!trimmedLine || trimmedLine.startsWith('#')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const [key, ...valueParts] = trimmedLine.split('=')
|
||||
if (key && valueParts.length > 0) {
|
||||
const value = valueParts.join('=').trim()
|
||||
this.configCache.set(key.trim(), value)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ConfigManager] Failed to load .env file:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a configuration value
|
||||
* @param key - Configuration key
|
||||
* @param defaultValue - Default value if key doesn't exist
|
||||
*/
|
||||
public get(key: string, defaultValue?: string): string | undefined {
|
||||
return this.configCache.get(key) ?? defaultValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a boolean configuration value
|
||||
*/
|
||||
public getBoolean(key: string, defaultValue: boolean = false): boolean {
|
||||
const value = this.get(key)
|
||||
if (value === undefined) return defaultValue
|
||||
return value.toLowerCase() === 'true'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a number configuration value
|
||||
*/
|
||||
public getNumber(key: string, defaultValue: number = 0): number {
|
||||
const value = this.get(key)
|
||||
if (value === undefined) return defaultValue
|
||||
const parsed = parseInt(value, 10)
|
||||
return isNaN(parsed) ? defaultValue : parsed
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a configuration value in cache
|
||||
*/
|
||||
public set(key: string, value: string | number | boolean): void {
|
||||
this.configCache.set(key, String(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* Save configuration to .env file
|
||||
*/
|
||||
public async save(): Promise<boolean> {
|
||||
try {
|
||||
// Build .env content from cache
|
||||
const lines: string[] = []
|
||||
|
||||
// ERP Configuration
|
||||
lines.push('# ===========================')
|
||||
lines.push('# ERP 系统配置')
|
||||
lines.push('# ===========================')
|
||||
lines.push(`ERP_URL=${this.configCache.get('erp.url') || DEFAULT_SETTINGS.erp.url}`)
|
||||
lines.push(`ERP_USERNAME=${this.configCache.get('erp.username') || DEFAULT_SETTINGS.erp.username}`)
|
||||
lines.push(`ERP_PASSWORD=${this.configCache.get('erp.password') || DEFAULT_SETTINGS.erp.password}`)
|
||||
lines.push(`ERP_HEADLESS=${this.configCache.get('erp.headless') || DEFAULT_SETTINGS.erp.headless}`)
|
||||
lines.push(`ERP_IGNORE_HTTPS_ERRORS=${this.configCache.get('erp.ignoreHttpsErrors') || DEFAULT_SETTINGS.erp.ignoreHttpsErrors}`)
|
||||
lines.push(`ERP_AUTO_CLOSE_BROWSER=${this.configCache.get('erp.autoCloseBrowser') || DEFAULT_SETTINGS.erp.autoCloseBrowser}`)
|
||||
lines.push('')
|
||||
|
||||
// Database Configuration - SQL Server
|
||||
lines.push('# ===========================')
|
||||
lines.push('# 数据库配置 - SQL Server')
|
||||
lines.push('# ===========================')
|
||||
lines.push(`# DB_TYPE=sqlserver`)
|
||||
lines.push(`# DB_SERVER=${this.configCache.get('database.server') || ''}`)
|
||||
lines.push(`# DB_NAME=${this.configCache.get('database.database') || ''}`)
|
||||
lines.push(`# DB_USERNAME=${this.configCache.get('database.username') || ''}`)
|
||||
lines.push(`# DB_PASSWORD=${this.configCache.get('database.password') || ''}`)
|
||||
lines.push(`DB_SQLSERVER_DRIVER=ODBC Driver 18 for SQL Server`)
|
||||
lines.push(`DB_TRUST_SERVER_CERTIFICATE=yes`)
|
||||
lines.push('')
|
||||
|
||||
// Database Configuration - MySQL
|
||||
lines.push('# ===========================')
|
||||
lines.push('# 数据库配置 - MySQL (切换时使用)')
|
||||
lines.push('# ===========================')
|
||||
lines.push(`DB_TYPE=${this.configCache.get('database.dbType') || DEFAULT_SETTINGS.database.dbType}`)
|
||||
lines.push(`DB_NAME=${this.configCache.get('database.database') || DEFAULT_SETTINGS.database.database}`)
|
||||
lines.push(`DB_USERNAME=${this.configCache.get('database.username') || DEFAULT_SETTINGS.database.username}`)
|
||||
lines.push(`DB_PASSWORD=${this.configCache.get('database.password') || DEFAULT_SETTINGS.database.password}`)
|
||||
lines.push(`DB_MYSQL_HOST=${this.configCache.get('database.mysqlHost') || DEFAULT_SETTINGS.database.mysqlHost}`)
|
||||
lines.push(`DB_MYSQL_PORT=${this.configCache.get('database.mysqlPort') || DEFAULT_SETTINGS.database.mysqlPort}`)
|
||||
lines.push(`DB_MYSQL_CHARSET=utf8mb4`)
|
||||
lines.push('')
|
||||
|
||||
// Order number parsing table configuration
|
||||
lines.push('# 订单号解析表配置')
|
||||
lines.push('# 表名:包含 productionID 和 生产订单号 映射关系的表')
|
||||
lines.push(`DB_TABLE_NAME=productionContractData_26 年压力表合同数据`)
|
||||
lines.push('# 字段名:总排号 (对应 productionID)')
|
||||
lines.push(`DB_FIELD_PRODUCTION_ID=总排号`)
|
||||
lines.push('# 字段名:生产订单号 (对应生产订单号)')
|
||||
lines.push(`DB_FIELD_ORDER_NUMBER=生产订单号`)
|
||||
lines.push('')
|
||||
|
||||
// Path Configuration
|
||||
lines.push('# ===========================')
|
||||
lines.push('# 路径配置')
|
||||
lines.push('# ===========================')
|
||||
lines.push(`PATH_DATA_DIR=${this.configCache.get('paths.dataDir') || DEFAULT_SETTINGS.paths.dataDir}`)
|
||||
lines.push(`PATH_PRODUCTION_ID_FILE=ProductionID.txt`)
|
||||
lines.push(`PATH_DEFAULT_OUTPUT=${this.configCache.get('paths.defaultOutput') || DEFAULT_SETTINGS.paths.defaultOutput}`)
|
||||
lines.push(`PATH_VALIDATION_OUTPUT=${this.configCache.get('paths.validationOutput') || DEFAULT_SETTINGS.paths.validationOutput}`)
|
||||
lines.push('')
|
||||
|
||||
// Data Extraction Configuration
|
||||
lines.push('# ===========================')
|
||||
lines.push('# 数据提取配置')
|
||||
lines.push('# ===========================')
|
||||
lines.push(`EXTRACTION_BATCH_SIZE=${this.configCache.get('extraction.batchSize') || DEFAULT_SETTINGS.extraction.batchSize}`)
|
||||
lines.push(`EXTRACTION_VERBOSE=${this.configCache.get('extraction.verbose') || DEFAULT_SETTINGS.extraction.verbose}`)
|
||||
lines.push(`EXTRACTION_AUTO_CONVERT=${this.configCache.get('extraction.autoConvert') || DEFAULT_SETTINGS.extraction.autoConvert}`)
|
||||
lines.push(`EXTRACTION_MERGE_BATCHES=${this.configCache.get('extraction.mergeBatches') || DEFAULT_SETTINGS.extraction.mergeBatches}`)
|
||||
lines.push(`EXTRACTION_ENABLE_DB_PERSISTENCE=${this.configCache.get('extraction.enableDbPersistence') || DEFAULT_SETTINGS.extraction.enableDbPersistence}`)
|
||||
lines.push('')
|
||||
|
||||
// Validation Configuration
|
||||
lines.push('# ===========================')
|
||||
lines.push('# 校验配置')
|
||||
lines.push('# ===========================')
|
||||
lines.push(`VALIDATION_DATA_SOURCE=${this.configCache.get('validation.dataSource') || DEFAULT_SETTINGS.validation.dataSource}`)
|
||||
lines.push(`VALIDATION_USE_DATABASE=${this.configCache.get('validation.useDatabase') || true}`)
|
||||
lines.push(`VALIDATION_BATCH_SIZE=${this.configCache.get('validation.batchSize') || DEFAULT_SETTINGS.validation.batchSize}`)
|
||||
lines.push(`VALIDATION_ENABLE_CRUD=${this.configCache.get('validation.enableCrud') || DEFAULT_SETTINGS.validation.enableCrud}`)
|
||||
lines.push(`VALIDATION_DEFAULT_MANAGER=${this.configCache.get('validation.defaultManager') || DEFAULT_SETTINGS.validation.defaultManager}`)
|
||||
lines.push(`VALIDATION_MATCH_MODE=${this.configCache.get('validation.matchMode') || DEFAULT_SETTINGS.validation.matchMode}`)
|
||||
lines.push('')
|
||||
|
||||
// UI Configuration
|
||||
lines.push('# ===========================')
|
||||
lines.push('# UI 配置')
|
||||
lines.push('# ===========================')
|
||||
lines.push(`UI_FONT_FAMILY=${this.configCache.get('ui.fontFamily') || DEFAULT_SETTINGS.ui.fontFamily}`)
|
||||
lines.push(`UI_FONT_SIZE=${this.configCache.get('ui.fontSize') || DEFAULT_SETTINGS.ui.fontSize}`)
|
||||
lines.push(`UI_PRODUCTION_ID_INPUT_WIDTH=${this.configCache.get('ui.productionIdInputWidth') || DEFAULT_SETTINGS.ui.productionIdInputWidth}`)
|
||||
lines.push('')
|
||||
|
||||
// Execution Configuration
|
||||
lines.push('# ===========================')
|
||||
lines.push('# 执行配置')
|
||||
lines.push('# ===========================')
|
||||
lines.push(`EXECUTION_DRYRUN=${this.configCache.get('execution.dryRun') || DEFAULT_SETTINGS.execution.dryRun}`)
|
||||
|
||||
const content = lines.join('\n')
|
||||
fs.writeFileSync(this.envPath, content, 'utf-8')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('[ConfigManager] Failed to save .env file:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings as SettingsData object
|
||||
*/
|
||||
public getAllSettings(): SettingsData {
|
||||
return {
|
||||
erp: {
|
||||
url: this.get('ERP_URL', DEFAULT_SETTINGS.erp.url),
|
||||
username: this.get('ERP_USERNAME', DEFAULT_SETTINGS.erp.username),
|
||||
password: this.get('ERP_PASSWORD', DEFAULT_SETTINGS.erp.password),
|
||||
headless: this.getBoolean('ERP_HEADLESS', DEFAULT_SETTINGS.erp.headless),
|
||||
ignoreHttpsErrors: this.getBoolean('ERP_IGNORE_HTTPS_ERRORS', DEFAULT_SETTINGS.erp.ignoreHttpsErrors),
|
||||
autoCloseBrowser: this.getBoolean('ERP_AUTO_CLOSE_BROWSER', DEFAULT_SETTINGS.erp.autoCloseBrowser)
|
||||
},
|
||||
database: {
|
||||
dbType: (this.get('DB_TYPE', DEFAULT_SETTINGS.database.dbType) as DatabaseType) || DEFAULT_SETTINGS.database.dbType,
|
||||
server: this.get('DB_SERVER', DEFAULT_SETTINGS.database.server),
|
||||
mysqlHost: this.get('DB_MYSQL_HOST', DEFAULT_SETTINGS.database.mysqlHost),
|
||||
mysqlPort: this.getNumber('DB_MYSQL_PORT', DEFAULT_SETTINGS.database.mysqlPort),
|
||||
database: this.get('DB_NAME', DEFAULT_SETTINGS.database.database),
|
||||
username: this.get('DB_USERNAME', DEFAULT_SETTINGS.database.username),
|
||||
password: this.get('DB_PASSWORD', DEFAULT_SETTINGS.database.password)
|
||||
},
|
||||
paths: {
|
||||
dataDir: this.get('PATH_DATA_DIR', DEFAULT_SETTINGS.paths.dataDir),
|
||||
defaultOutput: this.get('PATH_DEFAULT_OUTPUT', DEFAULT_SETTINGS.paths.defaultOutput),
|
||||
validationOutput: this.get('PATH_VALIDATION_OUTPUT', DEFAULT_SETTINGS.paths.validationOutput)
|
||||
},
|
||||
extraction: {
|
||||
batchSize: this.getNumber('EXTRACTION_BATCH_SIZE', DEFAULT_SETTINGS.extraction.batchSize),
|
||||
verbose: this.getBoolean('EXTRACTION_VERBOSE', DEFAULT_SETTINGS.extraction.verbose),
|
||||
autoConvert: this.getBoolean('EXTRACTION_AUTO_CONVERT', DEFAULT_SETTINGS.extraction.autoConvert),
|
||||
mergeBatches: this.getBoolean('EXTRACTION_MERGE_BATCHES', DEFAULT_SETTINGS.extraction.mergeBatches),
|
||||
enableDbPersistence: this.getBoolean('EXTRACTION_ENABLE_DB_PERSISTENCE', DEFAULT_SETTINGS.extraction.enableDbPersistence)
|
||||
},
|
||||
validation: {
|
||||
dataSource: (this.get('VALIDATION_DATA_SOURCE', DEFAULT_SETTINGS.validation.dataSource) as ValidationDataSource) || DEFAULT_SETTINGS.validation.dataSource,
|
||||
batchSize: this.getNumber('VALIDATION_BATCH_SIZE', DEFAULT_SETTINGS.validation.batchSize),
|
||||
matchMode: (this.get('VALIDATION_MATCH_MODE', DEFAULT_SETTINGS.validation.matchMode) as MatchMode) || DEFAULT_SETTINGS.validation.matchMode,
|
||||
enableCrud: this.getBoolean('VALIDATION_ENABLE_CRUD', DEFAULT_SETTINGS.validation.enableCrud),
|
||||
defaultManager: this.get('VALIDATION_DEFAULT_MANAGER', DEFAULT_SETTINGS.validation.defaultManager)
|
||||
},
|
||||
ui: {
|
||||
fontFamily: this.get('UI_FONT_FAMILY', DEFAULT_SETTINGS.ui.fontFamily),
|
||||
fontSize: this.getNumber('UI_FONT_SIZE', DEFAULT_SETTINGS.ui.fontSize),
|
||||
productionIdInputWidth: this.getNumber('UI_PRODUCTION_ID_INPUT_WIDTH', DEFAULT_SETTINGS.ui.productionIdInputWidth)
|
||||
},
|
||||
execution: {
|
||||
dryRun: this.getBoolean('EXECUTION_DRYRUN', DEFAULT_SETTINGS.execution.dryRun)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings from SettingsData object
|
||||
*/
|
||||
public async saveAllSettings(settings: SettingsData): Promise<boolean> {
|
||||
// ERP settings
|
||||
this.set('erp.url', settings.erp.url)
|
||||
this.set('erp.username', settings.erp.username)
|
||||
this.set('erp.password', settings.erp.password)
|
||||
this.set('erp.headless', settings.erp.headless)
|
||||
this.set('erp.ignoreHttpsErrors', settings.erp.ignoreHttpsErrors)
|
||||
this.set('erp.autoCloseBrowser', settings.erp.autoCloseBrowser)
|
||||
|
||||
// Database settings
|
||||
this.set('database.dbType', settings.database.dbType)
|
||||
this.set('database.server', settings.database.server)
|
||||
this.set('database.mysqlHost', settings.database.mysqlHost)
|
||||
this.set('database.mysqlPort', settings.database.mysqlPort)
|
||||
this.set('database.database', settings.database.database)
|
||||
this.set('database.username', settings.database.username)
|
||||
this.set('database.password', settings.database.password)
|
||||
|
||||
// Path settings
|
||||
this.set('paths.dataDir', settings.paths.dataDir)
|
||||
this.set('paths.defaultOutput', settings.paths.defaultOutput)
|
||||
this.set('paths.validationOutput', settings.paths.validationOutput)
|
||||
|
||||
// Extraction settings
|
||||
this.set('extraction.batchSize', settings.extraction.batchSize)
|
||||
this.set('extraction.verbose', settings.extraction.verbose)
|
||||
this.set('extraction.autoConvert', settings.extraction.autoConvert)
|
||||
this.set('extraction.mergeBatches', settings.extraction.mergeBatches)
|
||||
this.set('extraction.enableDbPersistence', settings.extraction.enableDbPersistence)
|
||||
|
||||
// Validation settings
|
||||
this.set('validation.dataSource', settings.validation.dataSource)
|
||||
this.set('validation.batchSize', settings.validation.batchSize)
|
||||
this.set('validation.matchMode', settings.validation.matchMode)
|
||||
this.set('validation.enableCrud', settings.validation.enableCrud)
|
||||
this.set('validation.defaultManager', settings.validation.defaultManager)
|
||||
|
||||
// UI settings
|
||||
this.set('ui.fontFamily', settings.ui.fontFamily)
|
||||
this.set('ui.fontSize', settings.ui.fontSize)
|
||||
this.set('ui.productionIdInputWidth', settings.ui.productionIdInputWidth)
|
||||
|
||||
// Execution settings
|
||||
this.set('execution.dryRun', settings.execution.dryRun)
|
||||
|
||||
return this.save()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to default settings
|
||||
*/
|
||||
public resetToDefaults(): SettingsData {
|
||||
// Clear cache and reload from defaults
|
||||
this.configCache.clear()
|
||||
|
||||
// Set all defaults
|
||||
this.set('erp.url', DEFAULT_SETTINGS.erp.url)
|
||||
this.set('erp.username', DEFAULT_SETTINGS.erp.username)
|
||||
this.set('erp.password', DEFAULT_SETTINGS.erp.password)
|
||||
this.set('erp.headless', DEFAULT_SETTINGS.erp.headless)
|
||||
this.set('erp.ignoreHttpsErrors', DEFAULT_SETTINGS.erp.ignoreHttpsErrors)
|
||||
this.set('erp.autoCloseBrowser', DEFAULT_SETTINGS.erp.autoCloseBrowser)
|
||||
|
||||
this.set('database.dbType', DEFAULT_SETTINGS.database.dbType)
|
||||
this.set('database.server', DEFAULT_SETTINGS.database.server)
|
||||
this.set('database.mysqlHost', DEFAULT_SETTINGS.database.mysqlHost)
|
||||
this.set('database.mysqlPort', DEFAULT_SETTINGS.database.mysqlPort)
|
||||
this.set('database.database', DEFAULT_SETTINGS.database.database)
|
||||
this.set('database.username', DEFAULT_SETTINGS.database.username)
|
||||
this.set('database.password', DEFAULT_SETTINGS.database.password)
|
||||
|
||||
this.set('paths.dataDir', DEFAULT_SETTINGS.paths.dataDir)
|
||||
this.set('paths.defaultOutput', DEFAULT_SETTINGS.paths.defaultOutput)
|
||||
this.set('paths.validationOutput', DEFAULT_SETTINGS.paths.validationOutput)
|
||||
|
||||
this.set('extraction.batchSize', DEFAULT_SETTINGS.extraction.batchSize)
|
||||
this.set('extraction.verbose', DEFAULT_SETTINGS.extraction.verbose)
|
||||
this.set('extraction.autoConvert', DEFAULT_SETTINGS.extraction.autoConvert)
|
||||
this.set('extraction.mergeBatches', DEFAULT_SETTINGS.extraction.mergeBatches)
|
||||
this.set('extraction.enableDbPersistence', DEFAULT_SETTINGS.extraction.enableDbPersistence)
|
||||
|
||||
this.set('validation.dataSource', DEFAULT_SETTINGS.validation.dataSource)
|
||||
this.set('validation.batchSize', DEFAULT_SETTINGS.validation.batchSize)
|
||||
this.set('validation.matchMode', DEFAULT_SETTINGS.validation.matchMode)
|
||||
this.set('validation.enableCrud', DEFAULT_SETTINGS.validation.enableCrud)
|
||||
this.set('validation.defaultManager', DEFAULT_SETTINGS.validation.defaultManager)
|
||||
|
||||
this.set('ui.fontFamily', DEFAULT_SETTINGS.ui.fontFamily)
|
||||
this.set('ui.fontSize', DEFAULT_SETTINGS.ui.fontSize)
|
||||
this.set('ui.productionIdInputWidth', DEFAULT_SETTINGS.ui.productionIdInputWidth)
|
||||
|
||||
this.set('execution.dryRun', DEFAULT_SETTINGS.execution.dryRun)
|
||||
|
||||
return DEFAULT_SETTINGS
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default settings
|
||||
*/
|
||||
public getDefaultSettings(): SettingsData {
|
||||
return DEFAULT_SETTINGS
|
||||
}
|
||||
}
|
||||
460
src/main/services/database/discrete-material-plan-dao.ts
Normal file
460
src/main/services/database/discrete-material-plan-dao.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
/**
|
||||
* Data Access Object for DiscreteMaterialPlanData table
|
||||
*
|
||||
* Mirrors the Python DiscreteMaterialPlanDAO functionality:
|
||||
* - Query operations for discrete material plan data
|
||||
* - Support for querying by PlanNumber, SourceNumber
|
||||
* - Deduplication by MaterialCode
|
||||
* - Statistics gathering
|
||||
*/
|
||||
|
||||
import { MySqlService } from './mysql'
|
||||
|
||||
/**
|
||||
* Material plan record interface
|
||||
*/
|
||||
export interface MaterialPlanRecord {
|
||||
id?: number
|
||||
factory: string
|
||||
materialStatus: string
|
||||
planNumber: string
|
||||
sourceNumber: string
|
||||
materialType: string
|
||||
productCode: string
|
||||
productName: string
|
||||
productUnit: string
|
||||
productPlanQuantity: number
|
||||
useDepartment: string
|
||||
remark: string
|
||||
creator: string
|
||||
createDate: Date
|
||||
approver: string
|
||||
approveDate: Date
|
||||
sequenceNumber: number
|
||||
materialCode: string
|
||||
materialName: string
|
||||
specification: string
|
||||
model: string
|
||||
drawingNumber: string
|
||||
materialQuality: string
|
||||
planQuantity: number
|
||||
unit: string
|
||||
requiredDate: Date
|
||||
warehouse: string
|
||||
unitUsage: number
|
||||
cumulativeOutputQuantity: number
|
||||
bomVersion: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for DiscreteMaterialPlanData table
|
||||
*/
|
||||
export const DISCRETE_MATERIAL_PLAN_CONFIG = {
|
||||
TABLE_NAME_MYSQL: 'dbo_DiscreteMaterialPlanData',
|
||||
COLUMNS: {
|
||||
ID: 'ID',
|
||||
FACTORY: 'Factory',
|
||||
MATERIAL_STATUS: 'MaterialStatus',
|
||||
PLAN_NUMBER: 'PlanNumber',
|
||||
SOURCE_NUMBER: 'SourceNumber',
|
||||
MATERIAL_TYPE: 'MaterialType',
|
||||
PRODUCT_CODE: 'ProductCode',
|
||||
PRODUCT_NAME: 'ProductName',
|
||||
PRODUCT_UNIT: 'ProductUnit',
|
||||
PRODUCT_PLAN_QUANTITY: 'ProductPlanQuantity',
|
||||
USE_DEPARTMENT: 'UseDepartment',
|
||||
REMARK: 'Remark',
|
||||
CREATOR: 'Creator',
|
||||
CREATE_DATE: 'CreateDate',
|
||||
APPROVER: 'Approver',
|
||||
APPROVE_DATE: 'ApproveDate',
|
||||
SEQUENCE_NUMBER: 'SequenceNumber',
|
||||
MATERIAL_CODE: 'MaterialCode',
|
||||
MATERIAL_NAME: 'MaterialName',
|
||||
SPECIFICATION: 'Specification',
|
||||
MODEL: 'Model',
|
||||
DRAWING_NUMBER: 'DrawingNumber',
|
||||
MATERIAL_QUALITY: 'MaterialQuality',
|
||||
PLAN_QUANTITY: 'PlanQuantity',
|
||||
UNIT: 'Unit',
|
||||
REQUIRED_DATE: 'RequiredDate',
|
||||
WAREHOUSE: 'Warehouse',
|
||||
UNIT_USAGE: 'UnitUsage',
|
||||
CUMULATIVE_OUTPUT_QUANTITY: 'CumulativeOutputQuantity',
|
||||
BOM_VERSION: 'BOMVersion'
|
||||
}
|
||||
} as const
|
||||
|
||||
/**
|
||||
* DiscreteMaterialPlanDAO Class
|
||||
*/
|
||||
export class DiscreteMaterialPlanDAO {
|
||||
private mysqlService: MySqlService | null = null
|
||||
|
||||
/**
|
||||
* Get MySQL service instance
|
||||
*/
|
||||
private async getMySqlService(): Promise<MySqlService> {
|
||||
if (this.mysqlService && this.mysqlService.isConnected()) {
|
||||
return this.mysqlService
|
||||
}
|
||||
|
||||
this.mysqlService = new MySqlService({
|
||||
host: process.env.DB_MYSQL_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
|
||||
user: process.env.DB_USERNAME || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || ''
|
||||
})
|
||||
|
||||
await this.mysqlService.connect()
|
||||
return this.mysqlService
|
||||
}
|
||||
|
||||
// ==================== QUERY ALL ====================
|
||||
|
||||
/**
|
||||
* Query all records from DiscreteMaterialPlanData table
|
||||
* @returns List of all records
|
||||
*/
|
||||
async queryAll(): Promise<any[]> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `SELECT * FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}`
|
||||
|
||||
const result = await mysqlService.query(sql)
|
||||
return result.rows
|
||||
} catch (error) {
|
||||
console.error('[DiscreteMaterialPlanDAO] Query all error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query all records with deduplication by MaterialCode
|
||||
* Strategy: Keep first record for each MaterialCode
|
||||
* Order: CreateDate ASC, SequenceNumber ASC
|
||||
* @returns List of deduplicated records
|
||||
*/
|
||||
async queryAllDistinctByMaterialCode(): Promise<any[]> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
WITH RankedRecords AS (
|
||||
SELECT
|
||||
*,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY MaterialCode
|
||||
ORDER BY CreateDate ASC, SequenceNumber ASC
|
||||
) AS rn
|
||||
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE MaterialCode IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType,
|
||||
ProductCode, ProductName, ProductUnit, ProductPlanQuantity,
|
||||
UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate,
|
||||
SequenceNumber, MaterialCode, MaterialName, Specification, Model,
|
||||
DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate,
|
||||
Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion
|
||||
FROM RankedRecords
|
||||
WHERE rn = 1
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql)
|
||||
return result.rows
|
||||
} catch (error) {
|
||||
console.error('[DiscreteMaterialPlanDAO] Query all distinct by material code error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== QUERY BY SOURCE NUMBER ====================
|
||||
|
||||
/**
|
||||
* Query records by SourceNumber list
|
||||
* @param sourceNumbers - List of SourceNumber values
|
||||
* @returns List of records
|
||||
*/
|
||||
async queryBySourceNumbers(sourceNumbers: string[]): Promise<any[]> {
|
||||
if (!sourceNumbers || sourceNumbers.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
const batchSize = 2000
|
||||
const allResults: any[] = []
|
||||
|
||||
for (let i = 0; i < sourceNumbers.length; i += batchSize) {
|
||||
const batch = sourceNumbers.slice(i, i + batchSize)
|
||||
const placeholders = batch.map(() => '?').join(',')
|
||||
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE SourceNumber IN (${placeholders})
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql, batch)
|
||||
allResults.push(...result.rows)
|
||||
}
|
||||
|
||||
return allResults
|
||||
} catch (error) {
|
||||
console.error('[DiscreteMaterialPlanDAO] Query by source numbers error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query records by SourceNumber with deduplication by MaterialCode
|
||||
* Strategy: Keep first record for each MaterialCode
|
||||
* Order: CreateDate ASC, SequenceNumber ASC
|
||||
* @param sourceNumbers - List of SourceNumber values
|
||||
* @returns List of deduplicated records
|
||||
*/
|
||||
async queryBySourceNumbersDistinct(sourceNumbers: string[]): Promise<any[]> {
|
||||
if (!sourceNumbers || sourceNumbers.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
const batchSize = 2000
|
||||
const allResults: any[] = []
|
||||
|
||||
for (let i = 0; i < sourceNumbers.length; i += batchSize) {
|
||||
const batch = sourceNumbers.slice(i, i + batchSize)
|
||||
const placeholders = batch.map(() => '?').join(',')
|
||||
|
||||
const sql = `
|
||||
WITH RankedRecords AS (
|
||||
SELECT
|
||||
*,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY MaterialCode
|
||||
ORDER BY CreateDate ASC, SequenceNumber ASC
|
||||
) AS rn
|
||||
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE SourceNumber IN (${placeholders})
|
||||
AND MaterialCode IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType,
|
||||
ProductCode, ProductName, ProductUnit, ProductPlanQuantity,
|
||||
UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate,
|
||||
SequenceNumber, MaterialCode, MaterialName, Specification, Model,
|
||||
DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate,
|
||||
Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion
|
||||
FROM RankedRecords
|
||||
WHERE rn = 1
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql, batch)
|
||||
allResults.push(...result.rows)
|
||||
}
|
||||
|
||||
return allResults
|
||||
} catch (error) {
|
||||
console.error('[DiscreteMaterialPlanDAO] Query by source numbers distinct error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query by production order number (SourceNumber)
|
||||
* @param sourceNumber - SourceNumber value
|
||||
* @returns List of records
|
||||
*/
|
||||
async queryBySourceNumber(sourceNumber: string): Promise<any[]> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE SourceNumber = ?
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql, [sourceNumber])
|
||||
return result.rows
|
||||
} catch (error) {
|
||||
console.error('[DiscreteMaterialPlanDAO] Query by source number error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== QUERY BY PLAN NUMBER ====================
|
||||
|
||||
/**
|
||||
* Query by plan number
|
||||
* @param planNumber - PlanNumber value
|
||||
* @returns List of records
|
||||
*/
|
||||
async queryByPlanNumber(planNumber: string): Promise<any[]> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE PlanNumber = ?
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql, [planNumber])
|
||||
return result.rows
|
||||
} catch (error) {
|
||||
console.error('[DiscreteMaterialPlanDAO] Query by plan number error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query by multiple plan numbers
|
||||
* @param planNumbers - List of PlanNumber values
|
||||
* @returns List of records
|
||||
*/
|
||||
async queryByPlanNumbers(planNumbers: string[]): Promise<any[]> {
|
||||
if (!planNumbers || planNumbers.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
const placeholders = planNumbers.map(() => '?').join(',')
|
||||
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE PlanNumber IN (${placeholders})
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql, planNumbers)
|
||||
return result.rows
|
||||
} catch (error) {
|
||||
console.error('[DiscreteMaterialPlanDAO] Query by plan numbers error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== UTILITY METHODS ====================
|
||||
|
||||
/**
|
||||
* Count all records
|
||||
* @returns Total number of records
|
||||
*/
|
||||
async countAll(): Promise<number> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `SELECT COUNT(*) as count FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}`
|
||||
|
||||
const result = await mysqlService.query(sql)
|
||||
return result.rows.length > 0 ? (result.rows[0].count as number) : 0
|
||||
} catch (error) {
|
||||
console.error('[DiscreteMaterialPlanDAO] Count all error:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count records by plan number
|
||||
* @param planNumber - PlanNumber value
|
||||
* @returns Number of records
|
||||
*/
|
||||
async countByPlanNumber(planNumber: string): Promise<number> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE PlanNumber = ?
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql, [planNumber])
|
||||
return result.rows.length > 0 ? (result.rows[0].count as number) : 0
|
||||
} catch (error) {
|
||||
console.error('[DiscreteMaterialPlanDAO] Count by plan number error:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique material names
|
||||
* @param sourceNumbers - Optional list of SourceNumber values to filter
|
||||
* @returns List of unique material names
|
||||
*/
|
||||
async getUniqueMaterialNames(sourceNumbers?: string[]): Promise<string[]> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
let sql: string
|
||||
let params: any[] = []
|
||||
|
||||
if (sourceNumbers && sourceNumbers.length > 0) {
|
||||
const placeholders = sourceNumbers.map(() => '?').join(',')
|
||||
sql = `
|
||||
SELECT DISTINCT MaterialName
|
||||
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE SourceNumber IN (${placeholders})
|
||||
AND MaterialName IS NOT NULL
|
||||
`
|
||||
params = sourceNumbers
|
||||
} else {
|
||||
sql = `
|
||||
SELECT DISTINCT MaterialName
|
||||
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE MaterialName IS NOT NULL
|
||||
`
|
||||
}
|
||||
|
||||
const result = await mysqlService.query(sql, params)
|
||||
return result.rows
|
||||
.map(row => row.MaterialName as string)
|
||||
.filter(Boolean)
|
||||
} catch (error) {
|
||||
console.error('[DiscreteMaterialPlanDAO] Get unique material names error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics
|
||||
* @returns Statistics object
|
||||
*/
|
||||
async getStatistics(): Promise<any> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
COUNT(*) as totalRecords,
|
||||
COUNT(DISTINCT PlanNumber) as uniquePlans,
|
||||
COUNT(DISTINCT SourceNumber) as uniqueOrders,
|
||||
MIN(CreateDate) as earliestRecord,
|
||||
MAX(CreateDate) as latestRecord
|
||||
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql)
|
||||
return result.rows.length > 0 ? result.rows[0] : {}
|
||||
} catch (error) {
|
||||
console.error('[DiscreteMaterialPlanDAO] Get statistics error:', error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from database
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.mysqlService) {
|
||||
await this.mysqlService.disconnect()
|
||||
this.mysqlService = null
|
||||
}
|
||||
}
|
||||
}
|
||||
516
src/main/services/database/materials-to-be-deleted-dao.ts
Normal file
516
src/main/services/database/materials-to-be-deleted-dao.ts
Normal file
@@ -0,0 +1,516 @@
|
||||
/**
|
||||
* Data Access Object for MaterialsToBeDeleted table
|
||||
*
|
||||
* Mirrors the Python MaterialsToBeDeletedDAO functionality:
|
||||
* - CRUD operations for materials identified by MaterialCode
|
||||
* - Batch upsert operations
|
||||
* - Manager-based filtering and queries
|
||||
* - Statistics gathering
|
||||
*/
|
||||
|
||||
import { MySqlService } from './mysql'
|
||||
|
||||
/**
|
||||
* Material record interface
|
||||
*/
|
||||
export interface MaterialRecord {
|
||||
id?: number
|
||||
materialCode: string
|
||||
managerName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert statistics
|
||||
*/
|
||||
export interface UpsertStats {
|
||||
total: number
|
||||
success: number
|
||||
failed: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Material statistics
|
||||
*/
|
||||
export interface MaterialStats {
|
||||
totalMaterials: number
|
||||
uniqueManagers: number
|
||||
materialsPerManager: Record<string, number>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for MaterialsToBeDeleted table
|
||||
*/
|
||||
export const MATERIALS_TO_BE_DELETED_CONFIG = {
|
||||
TABLE_NAME_MYSQL: 'dbo_MaterialsToBeDeleted',
|
||||
COLUMNS: {
|
||||
ID: 'ID',
|
||||
MATERIAL_CODE: 'MaterialCode',
|
||||
MANAGER_NAME: 'ManagerName'
|
||||
}
|
||||
} as const
|
||||
|
||||
/**
|
||||
* MaterialsToBeDeleted DAO Class
|
||||
*/
|
||||
export class MaterialsToBeDeletedDAO {
|
||||
private mysqlService: MySqlService | null = null
|
||||
|
||||
/**
|
||||
* Get MySQL service instance
|
||||
*/
|
||||
private async getMySqlService(): Promise<MySqlService> {
|
||||
if (this.mysqlService && this.mysqlService.isConnected()) {
|
||||
return this.mysqlService
|
||||
}
|
||||
|
||||
this.mysqlService = new MySqlService({
|
||||
host: process.env.DB_MYSQL_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
|
||||
user: process.env.DB_USERNAME || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || ''
|
||||
})
|
||||
|
||||
await this.mysqlService.connect()
|
||||
return this.mysqlService
|
||||
}
|
||||
|
||||
// ==================== UPSERT (MERGE) ====================
|
||||
|
||||
/**
|
||||
* Insert or update a single material record
|
||||
* @param materialCode - Material code (exact match key)
|
||||
* @param managerName - Manager name
|
||||
* @returns True if successful
|
||||
*/
|
||||
async upsertMaterial(materialCode: string, managerName: string): Promise<boolean> {
|
||||
if (!materialCode || !materialCode.trim()) {
|
||||
console.error('[MaterialsToBeDeletedDAO] MaterialCode cannot be empty')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
INSERT INTO ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} (MaterialCode, ManagerName)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName)
|
||||
`
|
||||
|
||||
await mysqlService.query(sql, [materialCode.trim(), managerName?.trim() || null])
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Upsert material error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update multiple material records in batch
|
||||
* @param materials - List of materials with materialCode and managerName
|
||||
* @returns Statistics object
|
||||
*/
|
||||
async upsertBatch(materials: { materialCode: string; managerName: string }[]): Promise<UpsertStats> {
|
||||
if (!materials || materials.length === 0) {
|
||||
return { total: 0, success: 0, failed: 0 }
|
||||
}
|
||||
|
||||
const stats: UpsertStats = {
|
||||
total: materials.length,
|
||||
success: 0,
|
||||
failed: 0
|
||||
}
|
||||
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
for (const material of materials) {
|
||||
const materialCode = material.materialCode?.trim()
|
||||
const managerName = material.managerName?.trim() || ''
|
||||
|
||||
if (!materialCode) {
|
||||
stats.failed++
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const sql = `
|
||||
INSERT INTO ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} (MaterialCode, ManagerName)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName)
|
||||
`
|
||||
|
||||
await mysqlService.query(sql, [materialCode, managerName || null])
|
||||
stats.success++
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Error upserting material:', materialCode, error)
|
||||
stats.failed++
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Batch upsert error:', error)
|
||||
stats.failed = stats.total - stats.success
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// ==================== READ ====================
|
||||
|
||||
/**
|
||||
* Get all material codes as a set
|
||||
* @returns Set of material codes
|
||||
*/
|
||||
async getAllMaterialCodes(): Promise<Set<string>> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
SELECT MaterialCode
|
||||
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE MaterialCode IS NOT NULL
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql)
|
||||
return new Set(result.rows.map(row => row.MaterialCode as string).filter(Boolean))
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Get all material codes error:', error)
|
||||
return new Set()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all material records
|
||||
* @returns List of all material records
|
||||
*/
|
||||
async getAllRecords(): Promise<MaterialRecord[]> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
SELECT ID, MaterialCode, ManagerName
|
||||
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE MaterialCode IS NOT NULL
|
||||
ORDER BY ManagerName, MaterialCode
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql)
|
||||
return result.rows.map(row => ({
|
||||
id: row.ID as number,
|
||||
materialCode: row.MaterialCode as string,
|
||||
managerName: row.ManagerName as string
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Get all records error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all materials for a specific manager
|
||||
* @param managerName - Manager name
|
||||
* @returns List of materials for the manager
|
||||
*/
|
||||
async getMaterialsByManager(managerName: string): Promise<MaterialRecord[]> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
SELECT ID, MaterialCode, ManagerName
|
||||
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE ManagerName = ? AND MaterialCode IS NOT NULL
|
||||
ORDER BY MaterialCode
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql, [managerName])
|
||||
return result.rows.map(row => ({
|
||||
id: row.ID as number,
|
||||
materialCode: row.MaterialCode as string,
|
||||
managerName: row.ManagerName as string
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Get materials by manager error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of unique manager names
|
||||
* @returns List of unique manager names
|
||||
*/
|
||||
async getManagers(): Promise<string[]> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
SELECT DISTINCT ManagerName
|
||||
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE ManagerName IS NOT NULL
|
||||
ORDER BY ManagerName
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql)
|
||||
return result.rows.map(row => row.ManagerName as string).filter(Boolean)
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Get managers error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific record by material code
|
||||
* @param materialCode - Material code
|
||||
* @returns Material record or null
|
||||
*/
|
||||
async getRecordByMaterialCode(materialCode: string): Promise<MaterialRecord | null> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
SELECT ID, MaterialCode, ManagerName
|
||||
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE MaterialCode = ?
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql, [materialCode.trim()])
|
||||
if (result.rows.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const row = result.rows[0]
|
||||
return {
|
||||
id: row.ID as number,
|
||||
materialCode: row.MaterialCode as string,
|
||||
managerName: row.ManagerName as string
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Get record by material code error:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== DELETE ====================
|
||||
|
||||
/**
|
||||
* Delete a specific material by material code
|
||||
* @param materialCode - Material code
|
||||
* @returns True if successful
|
||||
*/
|
||||
async deleteByMaterialCode(materialCode: string): Promise<boolean> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE MaterialCode = ?
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql, [materialCode.trim()])
|
||||
return result.rowCount > 0
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Delete by material code error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all materials for a specific manager
|
||||
* @param managerName - Manager name
|
||||
* @returns Number of records deleted
|
||||
*/
|
||||
async deleteByManager(managerName: string): Promise<number> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE ManagerName = ?
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql, [managerName])
|
||||
return result.rowCount
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Delete by manager error:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all material records
|
||||
* @returns Number of records deleted
|
||||
*/
|
||||
async deleteAllMaterials(): Promise<number> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}`
|
||||
|
||||
const result = await mysqlService.query(sql)
|
||||
return result.rowCount
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Delete all materials error:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple materials by material codes
|
||||
* @param materialCodes - List of material codes to delete
|
||||
* @returns Number of records deleted
|
||||
*/
|
||||
async deleteByMaterialCodes(materialCodes: string[]): Promise<number> {
|
||||
if (!materialCodes || materialCodes.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
let totalDeleted = 0
|
||||
const batchSize = 1000
|
||||
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
for (let i = 0; i < materialCodes.length; i += batchSize) {
|
||||
const batch = materialCodes.slice(i, i + batchSize)
|
||||
const placeholders = batch.map(() => '?').join(',')
|
||||
|
||||
const sql = `
|
||||
DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE MaterialCode IN (${placeholders})
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql, batch)
|
||||
totalDeleted += result.rowCount
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Delete by material codes error:', error)
|
||||
}
|
||||
|
||||
return totalDeleted
|
||||
}
|
||||
|
||||
// ==================== UTILITIES ====================
|
||||
|
||||
/**
|
||||
* Check if a material exists
|
||||
* @param materialCode - Material code
|
||||
* @returns True if material exists
|
||||
*/
|
||||
async materialExists(materialCode: string): Promise<boolean> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE MaterialCode = ?
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql, [materialCode.trim()])
|
||||
return result.rows.length > 0 && (result.rows[0].count as number) > 0
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Material exists error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count all material records
|
||||
* @returns Total number of records
|
||||
*/
|
||||
async countAll(): Promise<number> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `SELECT COUNT(*) as count FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}`
|
||||
|
||||
const result = await mysqlService.query(sql)
|
||||
return result.rows.length > 0 ? (result.rows[0].count as number) : 0
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Count all error:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count materials for a specific manager
|
||||
* @param managerName - Manager name
|
||||
* @returns Number of materials for the manager
|
||||
*/
|
||||
async countByManager(managerName: string): Promise<number> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
const sql = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE ManagerName = ?
|
||||
`
|
||||
|
||||
const result = await mysqlService.query(sql, [managerName])
|
||||
return result.rows.length > 0 ? (result.rows[0].count as number) : 0
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Count by manager error:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive statistics
|
||||
* @returns Statistics object
|
||||
*/
|
||||
async getStatistics(): Promise<MaterialStats> {
|
||||
try {
|
||||
const mysqlService = await this.getMySqlService()
|
||||
|
||||
// Get total and unique managers
|
||||
const statsSql = `
|
||||
SELECT
|
||||
COUNT(*) as totalMaterials,
|
||||
COUNT(DISTINCT ManagerName) as uniqueManagers
|
||||
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE MaterialCode IS NOT NULL
|
||||
`
|
||||
|
||||
const statsResult = await mysqlService.query(statsSql)
|
||||
const stats = statsResult.rows[0] || {}
|
||||
|
||||
// Get materials per manager
|
||||
const managerSql = `
|
||||
SELECT ManagerName, COUNT(*) as count
|
||||
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}
|
||||
WHERE ManagerName IS NOT NULL
|
||||
GROUP BY ManagerName
|
||||
ORDER BY count DESC
|
||||
`
|
||||
|
||||
const managerResult = await mysqlService.query(managerSql)
|
||||
const materialsPerManager = managerResult.rows.map(row => ({
|
||||
[row.ManagerName as string]: row.count as number
|
||||
}))
|
||||
|
||||
return {
|
||||
totalMaterials: (stats.totalMaterials as number) || 0,
|
||||
uniqueManagers: (stats.uniqueManagers as number) || 0,
|
||||
materialsPerManager
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MaterialsToBeDeletedDAO] Get statistics error:', error)
|
||||
return {
|
||||
totalMaterials: 0,
|
||||
uniqueManagers: 0,
|
||||
materialsPerManager: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from database
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.mysqlService) {
|
||||
await this.mysqlService.disconnect()
|
||||
this.mysqlService = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,17 +78,27 @@ export class MySqlService {
|
||||
}
|
||||
|
||||
try {
|
||||
const [rows, fields] = await this.connection.execute(sql, params)
|
||||
const [result, fields] = await this.connection.execute(sql, params)
|
||||
|
||||
// Convert rows to plain objects and extract column names
|
||||
const columns = fields.map((field) => field.name)
|
||||
const rowCount = Array.isArray(rows) ? rows.length : 0
|
||||
// Convert to plain objects and extract column names
|
||||
const columns = Array.isArray(fields) ? fields.map((field) => field.name) : []
|
||||
|
||||
// Type assertion for rows - mysql2 returns different types based on query
|
||||
const typedRows = Array.isArray(rows) ? (rows as Record<string, unknown>[]) : []
|
||||
// Handle different result types
|
||||
let rows: Record<string, unknown>[] = []
|
||||
let rowCount = 0
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
// SELECT query - result is an array of rows
|
||||
rows = result as Record<string, unknown>[]
|
||||
rowCount = rows.length
|
||||
} else if (typeof result === 'object' && result !== null) {
|
||||
// INSERT/UPDATE/DELETE query - result is OkPacket
|
||||
const okPacket = result as any
|
||||
rowCount = okPacket.affectedRows || okPacket.changedRows || 0
|
||||
}
|
||||
|
||||
return {
|
||||
rows: typedRows,
|
||||
rows,
|
||||
columns,
|
||||
rowCount
|
||||
}
|
||||
|
||||
185
src/main/types/settings.types.ts
Normal file
185
src/main/types/settings.types.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Settings types and interfaces
|
||||
*
|
||||
* Defines configuration structures for ERPAuto settings management
|
||||
*/
|
||||
|
||||
/**
|
||||
* User type for settings permission control
|
||||
*/
|
||||
export type UserType = 'Admin' | 'User' | 'Guest'
|
||||
|
||||
/**
|
||||
* Database type selection
|
||||
*/
|
||||
export type DatabaseType = 'sqlserver' | 'mysql'
|
||||
|
||||
/**
|
||||
* Validation match mode
|
||||
*/
|
||||
export type MatchMode = 'substring' | 'exact'
|
||||
|
||||
/**
|
||||
* Validation data source options
|
||||
*/
|
||||
export type ValidationDataSource = 'database_full' | 'database_filtered' | 'excel_existing' | 'excel_full'
|
||||
|
||||
/**
|
||||
* ERP configuration
|
||||
*/
|
||||
export interface ErpConfig {
|
||||
/** ERP system URL */
|
||||
url: string
|
||||
/** ERP username */
|
||||
username: string
|
||||
/** ERP password */
|
||||
password: string
|
||||
/** Headless browser mode */
|
||||
headless: boolean
|
||||
/** Ignore HTTPS certificate errors */
|
||||
ignoreHttpsErrors: boolean
|
||||
/** Auto close browser after operations */
|
||||
autoCloseBrowser: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Database configuration
|
||||
*/
|
||||
export interface DatabaseConfig {
|
||||
/** Database type */
|
||||
dbType: DatabaseType
|
||||
/** SQL Server server address */
|
||||
server: string
|
||||
/** MySQL host */
|
||||
mysqlHost: string
|
||||
/** MySQL port */
|
||||
mysqlPort: number
|
||||
/** Database name */
|
||||
database: string
|
||||
/** Database username */
|
||||
username: string
|
||||
/** Database password */
|
||||
password: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Path configuration
|
||||
*/
|
||||
export interface PathsConfig {
|
||||
/** Data directory */
|
||||
dataDir: string
|
||||
/** Default output file */
|
||||
defaultOutput: string
|
||||
/** Validation output file */
|
||||
validationOutput: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Data extraction configuration
|
||||
*/
|
||||
export interface ExtractionConfig {
|
||||
/** Batch size for data extraction */
|
||||
batchSize: number
|
||||
/** Enable verbose logging */
|
||||
verbose: boolean
|
||||
/** Auto convert to Excel */
|
||||
autoConvert: boolean
|
||||
/** Merge batches */
|
||||
mergeBatches: boolean
|
||||
/** Enable database persistence */
|
||||
enableDbPersistence: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Material validation configuration
|
||||
*/
|
||||
export interface ValidationConfig {
|
||||
/** Data source type */
|
||||
dataSource: ValidationDataSource
|
||||
/** Batch size for validation */
|
||||
batchSize: number
|
||||
/** Match mode */
|
||||
matchMode: MatchMode
|
||||
/** Enable CRUD operations */
|
||||
enableCrud: boolean
|
||||
/** Default manager name */
|
||||
defaultManager: string
|
||||
}
|
||||
|
||||
/**
|
||||
* UI configuration
|
||||
*/
|
||||
export interface UiConfig {
|
||||
/** Font family */
|
||||
fontFamily: string
|
||||
/** Font size */
|
||||
fontSize: number
|
||||
/** Production ID input width */
|
||||
productionIdInputWidth: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution configuration (User-only)
|
||||
*/
|
||||
export interface ExecutionConfig {
|
||||
/** Dry run mode */
|
||||
dryRun: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete settings data structure
|
||||
*/
|
||||
export interface SettingsData {
|
||||
/** ERP configuration */
|
||||
erp: ErpConfig
|
||||
/** Database configuration */
|
||||
database: DatabaseConfig
|
||||
/** Path configuration */
|
||||
paths: PathsConfig
|
||||
/** Extraction configuration */
|
||||
extraction: ExtractionConfig
|
||||
/** Validation configuration */
|
||||
validation: ValidationConfig
|
||||
/** UI configuration */
|
||||
ui: UiConfig
|
||||
/** Execution configuration */
|
||||
execution: ExecutionConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection test result
|
||||
*/
|
||||
export interface ConnectionTestResult {
|
||||
/** Whether connection was successful */
|
||||
success: boolean
|
||||
/** Status message */
|
||||
message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings result
|
||||
*/
|
||||
export interface SaveSettingsResult {
|
||||
/** Whether save was successful */
|
||||
success: boolean
|
||||
/** Error message if failed */
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings API interface for preload
|
||||
*/
|
||||
export interface SettingsAPI {
|
||||
/** Get current user type */
|
||||
getUserType: () => Promise<UserType>
|
||||
/** Get settings (filtered by user type) */
|
||||
getSettings: () => Promise<SettingsData>
|
||||
/** Save settings */
|
||||
saveSettings: (settings: SettingsData) => Promise<SaveSettingsResult>
|
||||
/** Reset to defaults (Admin only) */
|
||||
resetDefaults: () => Promise<SaveSettingsResult>
|
||||
/** Test ERP connection */
|
||||
testErpConnection: () => Promise<ConnectionTestResult>
|
||||
/** Test database connection */
|
||||
testDbConnection: () => Promise<ConnectionTestResult>
|
||||
}
|
||||
105
src/main/types/validation.types.ts
Normal file
105
src/main/types/validation.types.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Validation IPC API type definitions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Material validation result
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
materialName: string
|
||||
materialCode: string
|
||||
specification: string
|
||||
model: string
|
||||
managerName: string
|
||||
isMarkedForDeletion: boolean
|
||||
matchedTypeKeyword?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Material validation request
|
||||
*/
|
||||
export interface ValidationRequest {
|
||||
/** Validation mode: 'database_full' or 'database_filtered' */
|
||||
mode: 'database_full' | 'database_filtered'
|
||||
/** Production ID file path (for database_filtered mode) */
|
||||
productionIdFile?: string
|
||||
/** Use shared Production IDs from extractor page */
|
||||
useSharedProductionIds?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation response
|
||||
*/
|
||||
export interface ValidationResponse {
|
||||
success: boolean
|
||||
results?: ValidationResult[]
|
||||
error?: string
|
||||
stats?: {
|
||||
totalRecords: number
|
||||
matchedCount: number
|
||||
markedCount: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Material upsert request
|
||||
*/
|
||||
export interface MaterialUpsertRequest {
|
||||
materialCode: string
|
||||
managerName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Material upsert batch request
|
||||
*/
|
||||
export interface MaterialUpsertBatchRequest {
|
||||
materials: { materialCode: string; managerName: string }[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Material delete request
|
||||
*/
|
||||
export interface MaterialDeleteRequest {
|
||||
materialCodes: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Material query by manager request
|
||||
*/
|
||||
export interface MaterialQueryByManagerRequest {
|
||||
managerName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Material operations response
|
||||
*/
|
||||
export interface MaterialOperationResponse {
|
||||
success: boolean
|
||||
count?: number
|
||||
stats?: {
|
||||
total: number
|
||||
success: number
|
||||
failed: number
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Manager filter response
|
||||
*/
|
||||
export interface ManagerFilterResponse {
|
||||
managers: string[]
|
||||
materials?: MaterialRecordSummary[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Material record summary
|
||||
*/
|
||||
export interface MaterialRecordSummary {
|
||||
materialCode: string
|
||||
materialName: string
|
||||
specification: string
|
||||
model: string
|
||||
managerName: string
|
||||
isMarked: boolean
|
||||
}
|
||||
107
src/preload/index.d.ts
vendored
107
src/preload/index.d.ts
vendored
@@ -8,6 +8,8 @@ import type {
|
||||
UserSelectionResponse,
|
||||
CurrentUserResponse
|
||||
} from '../main/ipc/auth-handler'
|
||||
import type { ValidationRequest, ValidationResponse } from '../main/types/validation.types'
|
||||
import type { SettingsData, UserType, ConnectionTestResult, SaveSettingsResult } from '../main/types/settings.types'
|
||||
|
||||
/**
|
||||
* Order number resolver API
|
||||
@@ -69,6 +71,108 @@ export interface AuthAPI {
|
||||
isAdmin: () => Promise<boolean>
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation API
|
||||
*/
|
||||
export interface ValidationAPI {
|
||||
/**
|
||||
* Run material validation
|
||||
* @param request - Validation request
|
||||
*/
|
||||
validate: (request: ValidationRequest) => Promise<ValidationResponse>
|
||||
/**
|
||||
* Set shared Production IDs from extractor page
|
||||
* @param productionIds - List of Production IDs
|
||||
*/
|
||||
setSharedProductionIds: (productionIds: string[]) => Promise<void>
|
||||
/**
|
||||
* Get shared Production IDs
|
||||
*/
|
||||
getSharedProductionIds: () => Promise<{ productionIds: string[] }>
|
||||
/**
|
||||
* Get cleaner data (order numbers from shared Production IDs + material codes from MaterialsToBeDeleted)
|
||||
* Filters materials by current user (admin sees all, regular users see only their own)
|
||||
*/
|
||||
getCleanerData: () => Promise<{
|
||||
success: boolean
|
||||
orderNumbers?: string[]
|
||||
materialCodes?: string[]
|
||||
error?: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* Materials API
|
||||
*/
|
||||
export interface MaterialsAPI {
|
||||
/**
|
||||
* Upsert batch materials to MaterialsToBeDeleted
|
||||
* @param materials - List of materials with materialCode and managerName
|
||||
*/
|
||||
upsertBatch: (materials: { materialCode: string; managerName: string }[]) => Promise<{
|
||||
success: boolean
|
||||
stats?: { total: number; success: number; failed: number }
|
||||
error?: string
|
||||
}>
|
||||
/**
|
||||
* Delete materials by material codes
|
||||
* @param materialCodes - List of material codes to delete
|
||||
*/
|
||||
delete: (materialCodes: string[]) => Promise<{
|
||||
success: boolean
|
||||
count?: number
|
||||
error?: string
|
||||
}>
|
||||
/**
|
||||
* Get unique manager names
|
||||
*/
|
||||
getManagers: () => Promise<{ managers: string[] }>
|
||||
/**
|
||||
* Get materials by manager
|
||||
* @param managerName - Manager name
|
||||
*/
|
||||
getByManager: (managerName: string) => Promise<{ materials: unknown[] }>
|
||||
/**
|
||||
* Get all material records
|
||||
*/
|
||||
getAll: () => Promise<{ materials: unknown[] }>
|
||||
/**
|
||||
* Get statistics
|
||||
*/
|
||||
getStatistics: () => Promise<{ stats: unknown }>
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings API
|
||||
*/
|
||||
export interface SettingsAPI {
|
||||
/**
|
||||
* Get current user type
|
||||
*/
|
||||
getUserType: () => Promise<UserType>
|
||||
/**
|
||||
* Get settings (filtered by user type)
|
||||
*/
|
||||
getSettings: () => Promise<SettingsData>
|
||||
/**
|
||||
* Save settings
|
||||
* @param settings - Settings data to save
|
||||
*/
|
||||
saveSettings: (settings: SettingsData) => Promise<SaveSettingsResult>
|
||||
/**
|
||||
* Reset to defaults (Admin only)
|
||||
*/
|
||||
resetDefaults: () => Promise<SaveSettingsResult>
|
||||
/**
|
||||
* Test ERP connection
|
||||
*/
|
||||
testErpConnection: () => Promise<ConnectionTestResult>
|
||||
/**
|
||||
* Test database connection
|
||||
*/
|
||||
testDbConnection: () => Promise<ConnectionTestResult>
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: {
|
||||
@@ -93,6 +197,9 @@ declare global {
|
||||
database: DatabaseAPI
|
||||
resolver: ResolverAPI
|
||||
auth: AuthAPI
|
||||
validation: ValidationAPI
|
||||
materials: MaterialsAPI
|
||||
settings: SettingsAPI
|
||||
}
|
||||
api: unknown
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { CleanerInput } 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'
|
||||
import type { ValidationRequest } from '../main/types/validation.types'
|
||||
import type { SettingsData } from '../main/types/settings.types'
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
@@ -61,6 +63,40 @@ const api = {
|
||||
isSqlServerConnected: () => ipcRenderer.invoke('database:sqlserver:isConnected'),
|
||||
querySqlServer: (sql: string, params?: Record<string, unknown>) =>
|
||||
ipcRenderer.invoke('database:sqlserver:query', sql, params)
|
||||
},
|
||||
|
||||
// Validation service
|
||||
validation: {
|
||||
validate: (request: ValidationRequest) => ipcRenderer.invoke('validation:validate', request),
|
||||
setSharedProductionIds: (productionIds: string[]) =>
|
||||
ipcRenderer.invoke('validation:setSharedProductionIds', productionIds),
|
||||
getSharedProductionIds: () =>
|
||||
ipcRenderer.invoke('validation:getSharedProductionIds'),
|
||||
getCleanerData: () =>
|
||||
ipcRenderer.invoke('validation:getCleanerData')
|
||||
},
|
||||
|
||||
// Materials service
|
||||
materials: {
|
||||
upsertBatch: (materials: { materialCode: string; managerName: string }[]) =>
|
||||
ipcRenderer.invoke('materials:upsertBatch', { materials }),
|
||||
delete: (materialCodes: string[]) =>
|
||||
ipcRenderer.invoke('materials:delete', { materialCodes }),
|
||||
getManagers: () => ipcRenderer.invoke('materials:getManagers'),
|
||||
getByManager: (managerName: string) =>
|
||||
ipcRenderer.invoke('materials:getByManager', managerName),
|
||||
getAll: () => ipcRenderer.invoke('materials:getAll'),
|
||||
getStatistics: () => ipcRenderer.invoke('materials:getStatistics')
|
||||
},
|
||||
|
||||
// Settings service
|
||||
settings: {
|
||||
getUserType: () => ipcRenderer.invoke('settings:getUserType'),
|
||||
getSettings: () => ipcRenderer.invoke('settings:getSettings'),
|
||||
saveSettings: (settings: SettingsData) => ipcRenderer.invoke('settings:saveSettings', settings),
|
||||
resetDefaults: () => ipcRenderer.invoke('settings:resetDefaults'),
|
||||
testErpConnection: () => ipcRenderer.invoke('settings:testErpConnection'),
|
||||
testDbConnection: () => ipcRenderer.invoke('settings:testDbConnection')
|
||||
}
|
||||
} as const
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@ import LoginDialog from './components/LoginDialog'
|
||||
import UserSelectionDialog, { type UserInfo as SelectedUserInfo } from './components/UserSelectionDialog'
|
||||
import { ExtractorPage } from './pages/ExtractorPage'
|
||||
import { CleanerPage } from './pages/CleanerPage'
|
||||
import SettingsPage from './pages/SettingsPage'
|
||||
|
||||
type Page = 'home' | 'extractor' | 'cleaner'
|
||||
type Page = 'home' | 'extractor' | 'cleaner' | 'settings'
|
||||
|
||||
interface CurrentUser {
|
||||
username: string
|
||||
@@ -290,7 +291,7 @@ function App(): React.JSX.Element {
|
||||
minHeight: '100vh',
|
||||
backgroundColor: '#f5f7fa'
|
||||
}}>
|
||||
{/* Header with user info and logout */}
|
||||
{/* Header with user info, navigation and logout */}
|
||||
<header style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
@@ -300,13 +301,76 @@ function App(): React.JSX.Element {
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '24px' }}>
|
||||
<span style={{
|
||||
fontSize: '14px',
|
||||
color: '#666'
|
||||
}}>
|
||||
欢迎,{currentUser?.username} ({currentUser?.userType})
|
||||
</span>
|
||||
{/* Navigation tabs */}
|
||||
<nav style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={() => setCurrentPage('home')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: currentPage === 'home' ? '#1890ff' : '#f5f5f5',
|
||||
color: currentPage === 'home' ? '#fff' : '#666',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s'
|
||||
}}
|
||||
>
|
||||
主页
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('extractor')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: currentPage === 'extractor' ? '#1890ff' : '#f5f5f5',
|
||||
color: currentPage === 'extractor' ? '#fff' : '#666',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s'
|
||||
}}
|
||||
>
|
||||
数据提取
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('cleaner')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: currentPage === 'cleaner' ? '#1890ff' : '#f5f5f5',
|
||||
color: currentPage === 'cleaner' ? '#fff' : '#666',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s'
|
||||
}}
|
||||
>
|
||||
物料清理
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('settings')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: currentPage === 'settings' ? '#1890ff' : '#f5f5f5',
|
||||
color: currentPage === 'settings' ? '#fff' : '#666',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s'
|
||||
}}
|
||||
>
|
||||
系统设置
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div>
|
||||
{shouldShowLogout && (
|
||||
@@ -330,39 +394,15 @@ function App(): React.JSX.Element {
|
||||
{currentPage === 'home' && (
|
||||
<div>
|
||||
<h1 style={{ fontSize: '24px', color: '#333', marginBottom: '24px' }}>主页面</h1>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: '16px'
|
||||
}}>
|
||||
<div onClick={() => setCurrentPage('extractor')} style={{
|
||||
padding: '24px',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<h3 style={{ margin: '0 0 8px 0', color: '#1890ff' }}>数据提取</h3>
|
||||
<p style={{ margin: 0, color: '#666', fontSize: '14px' }}>提取 ERP 数据</p>
|
||||
</div>
|
||||
<div onClick={() => setCurrentPage('cleaner')} style={{
|
||||
padding: '24px',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<h3 style={{ margin: '0 0 8px 0', color: '#52c41a' }}>物料清理</h3>
|
||||
<p style={{ margin: 0, color: '#666', fontSize: '14px' }}>清理 ERP 物料</p>
|
||||
</div>
|
||||
</div>
|
||||
<p style={{ color: '#666', fontSize: '14px' }}>
|
||||
请使用顶部导航栏切换到 数据提取、物料清理 或 系统设置 页面
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentPage === 'extractor' && <ExtractorPage />}
|
||||
{currentPage === 'cleaner' && <CleanerPage />}
|
||||
{currentPage === 'settings' && <SettingsPage />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,64 +1,380 @@
|
||||
import React, { useState } from 'react'
|
||||
import { OrderNumberInput } from '../components/OrderNumberInput'
|
||||
import { MaterialCodeInput } from '../components/MaterialCodeInput'
|
||||
import React, { useState, useCallback, useEffect } from 'react'
|
||||
|
||||
interface CleanerResult {
|
||||
ordersProcessed: number
|
||||
materialsDeleted: number
|
||||
materialsSkipped: number
|
||||
errors: string[]
|
||||
details: Array<{
|
||||
orderNumber: string
|
||||
materialsDeleted: number
|
||||
materialsSkipped: number
|
||||
errors: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
interface CleanerProgress {
|
||||
message: string
|
||||
progress?: number
|
||||
/**
|
||||
* Material validation result interface
|
||||
*/
|
||||
interface ValidationResult {
|
||||
materialName: string
|
||||
materialCode: string
|
||||
specification: string
|
||||
model: string
|
||||
managerName: string
|
||||
isMarkedForDeletion: boolean
|
||||
matchedTypeKeyword?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* CleanerPage - Main page for ERP material cleaning
|
||||
* CleanerPage with material validation functionality
|
||||
*
|
||||
* This component provides the same functionality as the Python MaterialValidationTab:
|
||||
* - Material validation from database (full table or filtered by ProductionID)
|
||||
* - Checkbox selection for materials to delete
|
||||
* - Manager-based filtering (Admin only)
|
||||
* - Two-phase deletion: confirm (mark in DB) + execute (delete from ERP)
|
||||
*/
|
||||
export const CleanerPage: React.FC = () => {
|
||||
const [orderNumbers, setOrderNumbers] = useState('')
|
||||
const [materialCodes, setMaterialCodes] = useState('')
|
||||
const [dryRun, setDryRun] = useState(true)
|
||||
// State with sessionStorage persistence
|
||||
const [dryRun] = useState(() => {
|
||||
const saved = sessionStorage.getItem('cleaner_dryRun')
|
||||
return saved ? saved === 'true' : true
|
||||
})
|
||||
const [validationMode, setValidationMode] = useState<'database_full' | 'database_filtered'>(() => {
|
||||
const saved = sessionStorage.getItem('cleaner_validationMode') as 'database_full' | 'database_filtered'
|
||||
return saved || 'database_filtered'
|
||||
})
|
||||
const [useSharedProductionIds, setUseSharedProductionIds] = useState(() => {
|
||||
const saved = sessionStorage.getItem('cleaner_useSharedProductionIds')
|
||||
return saved ? saved === 'true' : true
|
||||
})
|
||||
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [progress, setProgress] = useState<CleanerProgress | null>(null)
|
||||
const [result, setResult] = useState<CleanerResult | null>(null)
|
||||
const [progress, setProgress] = useState<{ message: string; progress?: number } | null>(null)
|
||||
const [result, setResult] = useState<{
|
||||
ordersProcessed: number
|
||||
materialsDeleted: number
|
||||
materialsSkipped: number
|
||||
errors: string[]
|
||||
details: Array<{
|
||||
orderNumber: string
|
||||
materialsDeleted: number
|
||||
materialsSkipped: number
|
||||
errors: string[]
|
||||
}>
|
||||
} | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleClean = async () => {
|
||||
if (!orderNumbers.trim()) {
|
||||
setError('请输入至少一个订单号')
|
||||
// Validation state
|
||||
const [validationResults, setValidationResults] = useState<ValidationResult[]>([])
|
||||
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set())
|
||||
const [managers, setManagers] = useState<string[]>([])
|
||||
const [selectedManagers, setSelectedManagers] = useState<Set<string>>(new Set())
|
||||
const [isAdmin, setIsAdmin] = useState(false)
|
||||
const [currentUsername, setCurrentUsername] = useState<string>('')
|
||||
const [isValidationRunning, setIsValidationRunning] = useState(false)
|
||||
const [validationStats, setValidationStats] = useState<{
|
||||
totalRecords: number
|
||||
matchedCount: number
|
||||
markedCount: number
|
||||
} | null>(null)
|
||||
|
||||
// Hidden items state (for user mode hide checked feature)
|
||||
const [hiddenItems, setHiddenItems] = useState<Set<string>>(new Set())
|
||||
|
||||
// Filter validation results by selected managers and hidden items
|
||||
const filteredResults = React.useMemo(() => {
|
||||
let results = validationResults
|
||||
if (!isAdmin && currentUsername) {
|
||||
// Non-admin users only see their own data
|
||||
results = results.filter(r => r.managerName === currentUsername || !r.managerName)
|
||||
} else if (managers.length > 0 && selectedManagers.size > 0) {
|
||||
// Admin users see selected managers' data
|
||||
results = results.filter(
|
||||
r => selectedManagers.has(r.managerName) || !r.managerName
|
||||
)
|
||||
}
|
||||
// Filter out hidden items
|
||||
results = results.filter(r => !hiddenItems.has(r.materialCode))
|
||||
return results
|
||||
}, [validationResults, isAdmin, currentUsername, managers, selectedManagers, hiddenItems])
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem('cleaner_dryRun', dryRun.toString())
|
||||
}, [dryRun])
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem('cleaner_validationMode', validationMode)
|
||||
}, [validationMode])
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem('cleaner_useSharedProductionIds', useSharedProductionIds.toString())
|
||||
}, [useSharedProductionIds])
|
||||
|
||||
// Shared Production IDs state
|
||||
const [sharedProductionIdsCount, setSharedProductionIdsCount] = useState(0)
|
||||
|
||||
// Check admin status and get shared Production IDs on mount
|
||||
React.useEffect(() => {
|
||||
const initializePage = async () => {
|
||||
const admin = await window.electron.auth.isAdmin()
|
||||
const user = await window.electron.auth.getCurrentUser()
|
||||
setIsAdmin(admin)
|
||||
if (user && user.userInfo) {
|
||||
setCurrentUsername(user.userInfo.username)
|
||||
if (!admin) {
|
||||
// Non-admin users can only see their own data
|
||||
setSelectedManagers(new Set([user.userInfo.username]))
|
||||
}
|
||||
}
|
||||
|
||||
// Get shared Production IDs
|
||||
try {
|
||||
const result = await window.electron.validation.getSharedProductionIds()
|
||||
setSharedProductionIdsCount(result.productionIds.length)
|
||||
} catch (err) {
|
||||
console.error('Failed to get shared Production IDs:', err)
|
||||
}
|
||||
}
|
||||
initializePage()
|
||||
}, [])
|
||||
|
||||
// Load managers for filter (Admin only)
|
||||
const loadManagers = useCallback(async () => {
|
||||
if (!isAdmin) return
|
||||
try {
|
||||
const response = await window.electron.materials.getManagers()
|
||||
setManagers(response.managers)
|
||||
// Select all managers by default
|
||||
setSelectedManagers(new Set(response.managers))
|
||||
} catch (err) {
|
||||
console.error('Failed to load managers:', err)
|
||||
}
|
||||
}, [isAdmin])
|
||||
|
||||
// Handle validation
|
||||
const handleValidation = useCallback(async () => {
|
||||
setIsValidationRunning(true)
|
||||
setError(null)
|
||||
setValidationResults([])
|
||||
setSelectedItems(new Set())
|
||||
setValidationStats(null)
|
||||
|
||||
try {
|
||||
const response = await window.electron.validation.validate({
|
||||
mode: validationMode,
|
||||
useSharedProductionIds: validationMode === 'database_filtered' ? useSharedProductionIds : undefined
|
||||
})
|
||||
|
||||
if (response.success && response.results) {
|
||||
setValidationResults(response.results)
|
||||
setValidationStats(response.stats || null)
|
||||
|
||||
// Auto-select items that are already marked for deletion
|
||||
const markedCodes = new Set(
|
||||
response.results
|
||||
.filter(r => r.isMarkedForDeletion)
|
||||
.map(r => r.materialCode)
|
||||
)
|
||||
setSelectedItems(markedCodes)
|
||||
|
||||
// Auto-select managers for admin users
|
||||
if (isAdmin) {
|
||||
const uniqueManagers = new Set(
|
||||
response.results
|
||||
.map(r => r.managerName)
|
||||
.filter(Boolean)
|
||||
)
|
||||
setManagers([...uniqueManagers])
|
||||
setSelectedManagers(uniqueManagers)
|
||||
}
|
||||
} else {
|
||||
setError(response.error || '校验失败')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '校验过程中发生未知错误')
|
||||
} finally {
|
||||
setIsValidationRunning(false)
|
||||
}
|
||||
}, [validationMode, useSharedProductionIds, isAdmin])
|
||||
|
||||
// Handle checkbox toggle
|
||||
const handleCheckboxToggle = useCallback((materialCode: string) => {
|
||||
setSelectedItems(prev => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(materialCode)) {
|
||||
newSet.delete(materialCode)
|
||||
} else {
|
||||
newSet.add(materialCode)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Handle select all
|
||||
const handleSelectAll = useCallback(() => {
|
||||
setSelectedItems(new Set(filteredResults.map(r => r.materialCode)))
|
||||
}, [filteredResults])
|
||||
|
||||
// Handle deselect all
|
||||
const handleDeselectAll = useCallback(() => {
|
||||
setSelectedItems(new Set())
|
||||
}, [])
|
||||
|
||||
// Handle confirm deletion (mark materials in database)
|
||||
const handleConfirmDeletion = useCallback(async () => {
|
||||
if (validationResults.length === 0) {
|
||||
alert('没有可处理的数据')
|
||||
return
|
||||
}
|
||||
if (!materialCodes.trim()) {
|
||||
setError('请输入至少一个物料代码')
|
||||
|
||||
// Collect all data
|
||||
const materialsToUpsert: { materialCode: string; managerName: string }[] = []
|
||||
const materialsToDelete: string[] = []
|
||||
const missingManager: string[] = []
|
||||
|
||||
for (const result of validationResults) {
|
||||
// Skip empty material codes
|
||||
if (!result.materialCode || !result.materialCode.trim()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const materialCode = result.materialCode.trim()
|
||||
const isChecked = selectedItems.has(materialCode)
|
||||
|
||||
if (isChecked) {
|
||||
// Checked: needs to be upserted
|
||||
if (!result.managerName || !result.managerName.trim()) {
|
||||
missingManager.push(materialCode)
|
||||
} else {
|
||||
materialsToUpsert.push({
|
||||
materialCode,
|
||||
managerName: result.managerName.trim()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Unchecked: needs to be deleted
|
||||
materialsToDelete.push(materialCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate: checked items must have manager names
|
||||
if (missingManager.length > 0) {
|
||||
const msg = `以下已勾选的记录缺少负责人信息,无法保存:\n\n` +
|
||||
missingManager.slice(0, 10).join('\n') +
|
||||
(missingManager.length > 10 ? `\n... 共 ${missingManager.length} 条` : '')
|
||||
alert(msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there's anything to do
|
||||
if (materialsToUpsert.length === 0 && materialsToDelete.length === 0) {
|
||||
alert('没有需要处理的记录')
|
||||
return
|
||||
}
|
||||
|
||||
// Build confirmation message
|
||||
const confirmParts: string[] = []
|
||||
if (materialsToUpsert.length > 0) {
|
||||
confirmParts.push(`写入/更新 ${materialsToUpsert.length} 条记录`)
|
||||
}
|
||||
if (materialsToDelete.length > 0) {
|
||||
confirmParts.push(`删除 ${materialsToDelete.length} 条记录`)
|
||||
}
|
||||
|
||||
const confirmed = window.confirm(`确认以下操作吗?\n\n${confirmParts.join('\n')}`)
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
// Execute the operation
|
||||
try {
|
||||
setIsRunning(true)
|
||||
|
||||
// Upsert selected materials
|
||||
let upsertSuccess = 0
|
||||
let upsertFailed = 0
|
||||
if (materialsToUpsert.length > 0) {
|
||||
const upsertResult = await window.electron.materials.upsertBatch(materialsToUpsert)
|
||||
if (upsertResult.success) {
|
||||
upsertSuccess = upsertResult.stats?.success || 0
|
||||
upsertFailed = upsertResult.stats?.failed || 0
|
||||
} else {
|
||||
throw new Error(upsertResult.error || '写入物料失败')
|
||||
}
|
||||
}
|
||||
|
||||
// Delete unselected materials
|
||||
let deleteCount = 0
|
||||
if (materialsToDelete.length > 0) {
|
||||
const deleteResult = await window.electron.materials.delete(materialsToDelete)
|
||||
if (deleteResult.success) {
|
||||
deleteCount = deleteResult.count || 0
|
||||
} else {
|
||||
throw new Error(deleteResult.error || '删除物料失败')
|
||||
}
|
||||
}
|
||||
|
||||
// Show result message
|
||||
const msgParts: string[] = []
|
||||
if (upsertSuccess > 0) {
|
||||
msgParts.push(`写入/更新成功:${upsertSuccess} 条`)
|
||||
}
|
||||
if (upsertFailed > 0) {
|
||||
msgParts.push(`写入/更新失败:${upsertFailed} 条`)
|
||||
}
|
||||
if (deleteCount > 0) {
|
||||
msgParts.push(`删除成功:${deleteCount} 条`)
|
||||
}
|
||||
|
||||
const msg = `操作完成!\n\n${msgParts.join('\n')}`
|
||||
|
||||
if (upsertFailed > 0) {
|
||||
alert(`完成(部分失败)\n\n${msg}`)
|
||||
} else {
|
||||
alert(msg)
|
||||
}
|
||||
|
||||
// Reload managers to reflect changes
|
||||
await loadManagers()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '操作失败')
|
||||
} finally {
|
||||
setIsRunning(false)
|
||||
}
|
||||
}, [selectedItems, validationResults, loadManagers])
|
||||
|
||||
// Handle execute deletion (清理)
|
||||
const handleExecuteDeletion = useCallback(async () => {
|
||||
if (!dryRun) {
|
||||
const confirmed = window.confirm('警告:正式执行将删除 ERP 系统中的物料数据,是否继续?')
|
||||
if (!confirmed) return
|
||||
}
|
||||
|
||||
setIsRunning(true)
|
||||
setProgress(null)
|
||||
setResult(null)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const orderNumberList = orderNumbers
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
// Get cleaner data from backend (order numbers from shared Production IDs + material codes from DB)
|
||||
const cleanerDataResult = await window.electron.validation.getCleanerData()
|
||||
|
||||
const materialCodeList = materialCodes
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
if (!cleanerDataResult.success) {
|
||||
setError(cleanerDataResult.error || '获取清理数据失败')
|
||||
setIsRunning(false)
|
||||
return
|
||||
}
|
||||
|
||||
const orderNumberList = cleanerDataResult.orderNumbers || []
|
||||
const materialCodeList = cleanerDataResult.materialCodes || []
|
||||
|
||||
if (orderNumberList.length === 0) {
|
||||
setError('没有订单号数据。请先到数据提取页面输入 Production ID。')
|
||||
setIsRunning(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (materialCodeList.length === 0) {
|
||||
setError('没有物料代码数据。请确认已在物料清理界面勾选要删除的物料。')
|
||||
setIsRunning(false)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[CleanerPage] Starting cleaner with:', {
|
||||
orderNumbers: orderNumberList.length,
|
||||
materialCodes: materialCodeList.length,
|
||||
dryRun
|
||||
})
|
||||
|
||||
// Call cleaner API through electron
|
||||
const response = await window.electron.cleaner.runCleaner({
|
||||
orderNumbers: orderNumberList,
|
||||
materialCodes: materialCodeList,
|
||||
@@ -76,64 +392,228 @@ export const CleanerPage: React.FC = () => {
|
||||
setIsRunning(false)
|
||||
setProgress(null)
|
||||
}
|
||||
}
|
||||
}, [dryRun])
|
||||
|
||||
const handleReset = () => {
|
||||
setOrderNumbers('')
|
||||
setMaterialCodes('')
|
||||
setDryRun(true)
|
||||
setResult(null)
|
||||
setError(null)
|
||||
setProgress(null)
|
||||
}
|
||||
// Handle hide checked items (for user mode)
|
||||
const handleHideChecked = useCallback(() => {
|
||||
const checkedCodes = filteredResults
|
||||
.filter(r => selectedItems.has(r.materialCode))
|
||||
.map(r => r.materialCode)
|
||||
|
||||
if (checkedCodes.length === 0) {
|
||||
setError('没有已勾选的物料')
|
||||
return
|
||||
}
|
||||
|
||||
setHiddenItems(prev => new Set([...prev, ...checkedCodes]))
|
||||
}, [filteredResults, selectedItems])
|
||||
|
||||
// Handle show all items
|
||||
const handleShowAll = useCallback(() => {
|
||||
setHiddenItems(new Set())
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="cleaner-page">
|
||||
<h1 className="page-title">ERP 物料清理</h1>
|
||||
|
||||
<div className="cleaner-content">
|
||||
{/* Input Section */}
|
||||
<div className="input-section">
|
||||
<OrderNumberInput
|
||||
value={orderNumbers}
|
||||
onChange={setOrderNumbers}
|
||||
label="订单号列表"
|
||||
placeholder="请输入订单号,每行一个"
|
||||
/>
|
||||
{/* Validation Section (Admin only for mode selection) */}
|
||||
<div className="validation-section">
|
||||
<h2>物料校验</h2>
|
||||
|
||||
<MaterialCodeInput
|
||||
value={materialCodes}
|
||||
onChange={setMaterialCodes}
|
||||
label="物料代码列表"
|
||||
placeholder="请输入物料代码,每行一个"
|
||||
/>
|
||||
{isAdmin && (
|
||||
<div className="validation-mode-selector">
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
checked={validationMode === 'database_full'}
|
||||
onChange={() => setValidationMode('database_full')}
|
||||
disabled={isValidationRunning}
|
||||
/>
|
||||
数据库 - 全表校验
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
checked={validationMode === 'database_filtered'}
|
||||
onChange={() => setValidationMode('database_filtered')}
|
||||
disabled={isValidationRunning}
|
||||
/>
|
||||
数据库 - ProductionID 过滤
|
||||
</label>
|
||||
{validationMode === 'database_filtered' && (
|
||||
<div style={{ marginTop: '8px', marginLeft: '24px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useSharedProductionIds}
|
||||
onChange={(e) => setUseSharedProductionIds(e.target.checked)}
|
||||
disabled={isValidationRunning}
|
||||
/>
|
||||
使用数据提取页面的 Production ID
|
||||
</label>
|
||||
{useSharedProductionIds && (
|
||||
<p style={{ margin: '4px 0 0 0', fontSize: '12px', color: sharedProductionIdsCount > 0 ? '#52c41a' : '#fa8c16' }}>
|
||||
{sharedProductionIdsCount > 0
|
||||
? `当前有 ${sharedProductionIdsCount} 个共享的 Production ID`
|
||||
: '暂无共享的 Production ID,请先在数据提取页面输入'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="dry-run-option">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={dryRun}
|
||||
onChange={(e) => setDryRun(e.target.checked)}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
<span>干运行模式(不实际删除,仅预览)</span>
|
||||
</label>
|
||||
</div>
|
||||
{!isAdmin && (
|
||||
<p style={{ fontSize: '14px', color: '#666', marginBottom: '16px' }}>
|
||||
当前模式:数据库 - ProductionID 过滤(使用数据提取页面的数据)
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="button-group">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleClean}
|
||||
disabled={isRunning || !orderNumbers.trim() || !materialCodes.trim()}
|
||||
onClick={handleValidation}
|
||||
disabled={isValidationRunning}
|
||||
>
|
||||
{isRunning ? '清理中...' : '开始清理'}
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={handleReset} disabled={isRunning}>
|
||||
重置
|
||||
{isValidationRunning ? '校验中...' : '开始校验'}
|
||||
</button>
|
||||
{isAdmin && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={handleSelectAll}
|
||||
disabled={filteredResults.length === 0}
|
||||
>
|
||||
全选
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={handleDeselectAll}
|
||||
disabled={selectedItems.size === 0}
|
||||
>
|
||||
取消全选
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{validationStats && (
|
||||
<div className="validation-stats">
|
||||
<span>总记录数:{validationStats.totalRecords}</span>
|
||||
<span>匹配到负责人:{validationStats.matchedCount}</span>
|
||||
<span>已标记删除:{validationStats.markedCount}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Manager Filter (Admin only) */}
|
||||
{isAdmin && managers.length > 0 && (
|
||||
<div className="manager-filter-section">
|
||||
<h3>筛选(按负责人)</h3>
|
||||
<div className="manager-checkboxes">
|
||||
{managers.map(manager => (
|
||||
<label key={manager}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedManagers.has(manager)}
|
||||
onChange={(e) => {
|
||||
setSelectedManagers(prev => {
|
||||
const newSet = new Set(prev)
|
||||
if (e.target.checked) {
|
||||
newSet.add(manager)
|
||||
} else {
|
||||
newSet.delete(manager)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{manager}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Validation Results Table */}
|
||||
{filteredResults.length > 0 && (
|
||||
<div className="validation-results-section">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<h2 style={{ margin: 0 }}>校验结果</h2>
|
||||
{/* Action buttons: left group (Hide Checked, Show All, Confirm Delete) + right button (Execute Delete in red) */}
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
{!isAdmin && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={handleHideChecked}
|
||||
disabled={selectedItems.size === 0}
|
||||
>
|
||||
隐藏勾选
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={handleShowAll}
|
||||
disabled={hiddenItems.size === 0}
|
||||
>
|
||||
显示全部
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-warning"
|
||||
onClick={handleConfirmDeletion}
|
||||
disabled={selectedItems.size === 0 || isRunning}
|
||||
>
|
||||
确认删除
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={handleExecuteDeletion}
|
||||
disabled={isRunning || validationResults.length === 0}
|
||||
style={{ marginLeft: '16px' }}
|
||||
>
|
||||
开始清理
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table className="validation-results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>选择</th>
|
||||
<th>材料名称</th>
|
||||
<th>材料代码</th>
|
||||
<th>规格</th>
|
||||
<th>型号</th>
|
||||
<th>负责人</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredResults.map(result => (
|
||||
<tr key={result.materialCode}>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedItems.has(result.materialCode)}
|
||||
onChange={() => handleCheckboxToggle(result.materialCode)}
|
||||
/>
|
||||
</td>
|
||||
<td>{result.materialName}</td>
|
||||
<td>{result.materialCode}</td>
|
||||
<td>{result.specification}</td>
|
||||
<td>{result.model}</td>
|
||||
<td>{result.managerName || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress Section */}
|
||||
{progress && (
|
||||
<div className="progress-section">
|
||||
@@ -150,7 +630,13 @@ export const CleanerPage: React.FC = () => {
|
||||
{error && (
|
||||
<div className="error-section">
|
||||
<h3>错误</h3>
|
||||
<p>{error}</p>
|
||||
<div className="error-message" title="双击可选中复制" onDoubleClick={(e) => {
|
||||
const text = e.currentTarget.textContent
|
||||
navigator.clipboard.writeText(text || '')
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
<p className="error-hint">💡 提示:双击错误信息可选中复制</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -226,7 +712,7 @@ export const CleanerPage: React.FC = () => {
|
||||
<style>{`
|
||||
.cleaner-page {
|
||||
padding: 24px;
|
||||
max-width: 800px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.page-title {
|
||||
@@ -241,16 +727,23 @@ export const CleanerPage: React.FC = () => {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
}
|
||||
.input-section {
|
||||
.validation-section, .manager-filter-section {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
.dry-run-option {
|
||||
.validation-section h2, .manager-filter-section h3 {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.dry-run-option label {
|
||||
.validation-mode-selector {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.validation-mode-selector label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -258,17 +751,10 @@ export const CleanerPage: React.FC = () => {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.dry-run-option input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dry-run-option input:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 24px;
|
||||
@@ -297,6 +783,80 @@ export const CleanerPage: React.FC = () => {
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
.btn-warning {
|
||||
background: #fa8c16;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-warning:hover:not(:disabled) {
|
||||
background: #ffc069;
|
||||
}
|
||||
.btn-danger {
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #ff7875;
|
||||
}
|
||||
.validation-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 12px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #52c41a;
|
||||
}
|
||||
.validation-stats span {
|
||||
font-weight: 500;
|
||||
}
|
||||
.manager-filter-section {
|
||||
background: #fafafa;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.manager-checkboxes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.manager-checkboxes label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
background: #fff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
.validation-results-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.validation-results-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
.validation-results-table th, .validation-results-table td {
|
||||
padding: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
text-align: left;
|
||||
color: #000;
|
||||
}
|
||||
.validation-results-table th {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.validation-results-table tr:nth-child(even) {
|
||||
background: #fafafa;
|
||||
}
|
||||
.validation-results-table tr:hover {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
.progress-section {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
@@ -332,9 +892,27 @@ export const CleanerPage: React.FC = () => {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.error-section p {
|
||||
color: #666;
|
||||
margin: 0;
|
||||
.error-message {
|
||||
background: #fff;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
border: 1px solid #ffccc7;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
cursor: text;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.error-message:hover {
|
||||
background: #fffbfc;
|
||||
}
|
||||
.error-hint {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
.result-section {
|
||||
margin-top: 24px;
|
||||
@@ -434,6 +1012,11 @@ export const CleanerPage: React.FC = () => {
|
||||
font-size: 13px;
|
||||
color: #fa8c16;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.input-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { OrderNumberInput } from '../components/OrderNumberInput'
|
||||
|
||||
// Extractor result type (matches the type from main process)
|
||||
@@ -18,12 +18,60 @@ interface ExtractorProgress {
|
||||
* ExtractorPage - Main page for ERP data extraction
|
||||
*/
|
||||
export const ExtractorPage: React.FC = () => {
|
||||
const [orderNumbers, setOrderNumbers] = useState('')
|
||||
const [batchSize, setBatchSize] = useState(100)
|
||||
const [orderNumbers, setOrderNumbers] = useState(() => {
|
||||
// Restore from sessionStorage on mount
|
||||
return sessionStorage.getItem('extractor_orderNumbers') || ''
|
||||
})
|
||||
const [batchSize, setBatchSize] = useState(() => {
|
||||
const saved = sessionStorage.getItem('extractor_batchSize')
|
||||
return saved ? parseInt(saved, 10) : 100
|
||||
})
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [progress, setProgress] = useState<ExtractorProgress | null>(null)
|
||||
const [result, setResult] = useState<ExtractorResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [saveStatus, setSaveStatus] = useState<'saved' | 'unsaved' | 'saving'>('unsaved')
|
||||
|
||||
// Save to sessionStorage when orderNumbers changes
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem('extractor_orderNumbers', orderNumbers)
|
||||
// Update shared Production IDs when orderNumbers changes
|
||||
if (orderNumbers.trim()) {
|
||||
const orderNumberList = orderNumbers
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
window.electron.validation.setSharedProductionIds(orderNumberList)
|
||||
}
|
||||
}, [orderNumbers])
|
||||
|
||||
// Save to sessionStorage when batchSize changes
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem('extractor_batchSize', batchSize.toString())
|
||||
}, [batchSize])
|
||||
|
||||
const saveSharedProductionIds = async () => {
|
||||
if (!orderNumbers.trim()) {
|
||||
setError('请输入至少一个订单号')
|
||||
return
|
||||
}
|
||||
|
||||
setSaveStatus('saving')
|
||||
try {
|
||||
const orderNumberList = orderNumbers
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
|
||||
await window.electron.validation.setSharedProductionIds(orderNumberList)
|
||||
console.log(`[Extractor] Stored ${orderNumberList.length} Production IDs for sharing`)
|
||||
setSaveStatus('saved')
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '保存失败')
|
||||
setSaveStatus('unsaved')
|
||||
}
|
||||
}
|
||||
|
||||
const handleExtract = async () => {
|
||||
if (!orderNumbers.trim()) {
|
||||
@@ -42,6 +90,10 @@ export const ExtractorPage: React.FC = () => {
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
|
||||
// Store Production IDs for sharing with cleaner page (before extraction starts)
|
||||
await window.electron.validation.setSharedProductionIds(orderNumberList)
|
||||
console.log(`[Extractor] Stored ${orderNumberList.length} Production IDs for sharing`)
|
||||
|
||||
// Call extractor API through electron
|
||||
const response = await window.electron.extractor.runExtractor({
|
||||
orderNumbers: orderNumberList,
|
||||
@@ -104,6 +156,14 @@ export const ExtractorPage: React.FC = () => {
|
||||
>
|
||||
{isRunning ? '提取中...' : '开始提取'}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={saveSharedProductionIds}
|
||||
disabled={isRunning || !orderNumbers.trim() || saveStatus === 'saved'}
|
||||
title="保存订单号以便在清理页面使用"
|
||||
>
|
||||
{saveStatus === 'saving' ? '保存中...' : saveStatus === 'saved' ? '已保存' : '保存为共享 ID'}
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={handleReset} disabled={isRunning}>
|
||||
重置
|
||||
</button>
|
||||
@@ -254,6 +314,10 @@ export const ExtractorPage: React.FC = () => {
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
.btn-secondary[data-saved="true"] {
|
||||
background: #52c41a;
|
||||
color: #fff;
|
||||
}
|
||||
.progress-section {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
|
||||
975
src/renderer/src/pages/SettingsPage.tsx
Normal file
975
src/renderer/src/pages/SettingsPage.tsx
Normal file
@@ -0,0 +1,975 @@
|
||||
/**
|
||||
* Settings Page
|
||||
*
|
||||
* System settings configuration page with user type-based access control
|
||||
* Admin users see all settings, User users see limited settings
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import type {
|
||||
SettingsData,
|
||||
UserType,
|
||||
DatabaseType,
|
||||
MatchMode,
|
||||
ValidationDataSource
|
||||
} from '../../../main/types/settings.types'
|
||||
|
||||
// Default settings
|
||||
const DEFAULT_SETTINGS: SettingsData = {
|
||||
erp: {
|
||||
url: 'https://68.11.34.30:8082/',
|
||||
username: '',
|
||||
password: '',
|
||||
headless: true,
|
||||
ignoreHttpsErrors: true,
|
||||
autoCloseBrowser: true
|
||||
},
|
||||
database: {
|
||||
dbType: 'mysql',
|
||||
server: '',
|
||||
mysqlHost: '192.168.31.83',
|
||||
mysqlPort: 3306,
|
||||
database: 'BLD_DB',
|
||||
username: 'remote_user',
|
||||
password: ''
|
||||
},
|
||||
paths: {
|
||||
dataDir: 'D:/python/playwrite/data/',
|
||||
defaultOutput: '离散备料计划维护_合并.xlsx',
|
||||
validationOutput: '物料状态校验结果.xlsx'
|
||||
},
|
||||
extraction: {
|
||||
batchSize: 100,
|
||||
verbose: true,
|
||||
autoConvert: true,
|
||||
mergeBatches: true,
|
||||
enableDbPersistence: true
|
||||
},
|
||||
validation: {
|
||||
dataSource: 'database_full',
|
||||
batchSize: 2000,
|
||||
matchMode: 'substring',
|
||||
enableCrud: false,
|
||||
defaultManager: ''
|
||||
},
|
||||
ui: {
|
||||
fontFamily: 'Microsoft YaHei UI',
|
||||
fontSize: 10,
|
||||
productionIdInputWidth: 20
|
||||
},
|
||||
execution: {
|
||||
dryRun: false
|
||||
}
|
||||
}
|
||||
|
||||
const SettingsPage: React.FC = () => {
|
||||
// User type state
|
||||
const [userType, setUserType] = useState<UserType>('Guest')
|
||||
|
||||
// Settings state
|
||||
const [settings, setSettings] = useState<SettingsData>(DEFAULT_SETTINGS)
|
||||
|
||||
// UI state
|
||||
const [isModified, setIsModified] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
||||
|
||||
// Password visibility state for User ERP credentials
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
// Load settings on mount
|
||||
useEffect(() => {
|
||||
loadSettings()
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Load user type and settings from main process
|
||||
*/
|
||||
const loadSettings = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// Get user type
|
||||
const type = await window.electron.settings.getUserType()
|
||||
setUserType(type)
|
||||
|
||||
// Get settings (filtered by user type)
|
||||
const loadedSettings = await window.electron.settings.getSettings()
|
||||
setSettings(loadedSettings)
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error)
|
||||
showMessage('加载设置失败', 'error')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a message to user
|
||||
*/
|
||||
const showMessage = (text: string, type: 'success' | 'error') => {
|
||||
setMessage({ type, text })
|
||||
setTimeout(() => setMessage(null), 3000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update settings and mark as modified
|
||||
*/
|
||||
const updateSettings = (
|
||||
section: keyof SettingsData,
|
||||
key: string,
|
||||
value: unknown
|
||||
) => {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section],
|
||||
[key]: value
|
||||
}
|
||||
}))
|
||||
setIsModified(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test ERP connection
|
||||
*/
|
||||
const handleTestErpConnection = async () => {
|
||||
try {
|
||||
const result = await window.electron.settings.testErpConnection()
|
||||
if (result.success) {
|
||||
showMessage(result.message || 'ERP 连接测试成功!', 'success')
|
||||
} else {
|
||||
showMessage(result.message || 'ERP 连接测试失败', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('ERP connection test error:', error)
|
||||
showMessage('ERP 连接测试失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test database connection
|
||||
*/
|
||||
const handleTestDbConnection = async () => {
|
||||
try {
|
||||
const result = await window.electron.settings.testDbConnection()
|
||||
if (result.success) {
|
||||
showMessage(result.message || '数据库连接测试成功!', 'success')
|
||||
} else {
|
||||
showMessage(result.message || '数据库连接测试失败', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Database connection test error:', error)
|
||||
showMessage('数据库连接测试失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings
|
||||
*/
|
||||
const handleSaveSettings = async () => {
|
||||
try {
|
||||
const result = await window.electron.settings.saveSettings(settings)
|
||||
if (result.success) {
|
||||
showMessage('设置已保存', 'success')
|
||||
setIsModified(false)
|
||||
} else {
|
||||
showMessage(result.error || '保存设置失败', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save settings error:', error)
|
||||
showMessage('保存设置失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to default settings (Admin only)
|
||||
*/
|
||||
const handleResetDefaults = async () => {
|
||||
if (!window.confirm('确定要恢复默认设置吗?这将覆盖 .env 文件中的所有配置。')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await window.electron.settings.resetDefaults()
|
||||
if (result.success) {
|
||||
showMessage('已恢复默认设置', 'success')
|
||||
const loadedSettings = await window.electron.settings.getSettings()
|
||||
setSettings(loadedSettings)
|
||||
setIsModified(false)
|
||||
} else {
|
||||
showMessage(result.error || '恢复默认设置失败', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Reset defaults error:', error)
|
||||
showMessage('恢复默认设置失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<div className="loading-content">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
<style>{`
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
.loading-content {
|
||||
text-align: center;
|
||||
}
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #e8e8e8;
|
||||
border-top-color: #1890ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const isUserOnly = userType === 'User'
|
||||
|
||||
return (
|
||||
<div className="settings-page">
|
||||
{/* Header */}
|
||||
<div className="settings-header">
|
||||
<h1>系统设置</h1>
|
||||
<span className="user-type-badge">
|
||||
当前用户类型:{userType === 'Admin' ? '管理员' : userType === 'User' ? '普通用户' : '访客'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Message toast */}
|
||||
{message && (
|
||||
<div className={`toast toast-${message.type}`}>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings content */}
|
||||
<div className="settings-content">
|
||||
{/* Admin: ERP System Configuration */}
|
||||
{userType === 'Admin' && (
|
||||
<>
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">ERP 系统配置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">ERP URL:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.erp.url}
|
||||
onChange={(e) => updateSettings('erp', 'url', e.target.value)}
|
||||
placeholder="https://68.11.34.30:8082/"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">用户名:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.erp.username}
|
||||
onChange={(e) => updateSettings('erp', 'username', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">密码:</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-input"
|
||||
value={settings.erp.password}
|
||||
onChange={(e) => updateSettings('erp', 'password', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Database Configuration */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">数据库配置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">数据库类型:</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={settings.database.dbType}
|
||||
onChange={(e) => updateSettings('database', 'dbType', e.target.value as DatabaseType)}
|
||||
>
|
||||
<option value="sqlserver">SQL Server</option>
|
||||
<option value="mysql">MySQL</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{settings.database.dbType === 'mysql' ? (
|
||||
<>
|
||||
<div className="form-row">
|
||||
<label className="form-label">MySQL 主机:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.database.mysqlHost}
|
||||
onChange={(e) => updateSettings('database', 'mysqlHost', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">MySQL 端口:</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
value={settings.database.mysqlPort}
|
||||
onChange={(e) => updateSettings('database', 'mysqlPort', parseInt(e.target.value) || 3306)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="form-row">
|
||||
<label className="form-label">SQL Server 服务器:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.database.server}
|
||||
onChange={(e) => updateSettings('database', 'server', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-label">数据库名:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.database.database}
|
||||
onChange={(e) => updateSettings('database', 'database', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">数据库用户名:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.database.username}
|
||||
onChange={(e) => updateSettings('database', 'username', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">数据库密码:</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-input"
|
||||
value={settings.database.password}
|
||||
onChange={(e) => updateSettings('database', 'password', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Browser Settings */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">浏览器设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.erp.headless}
|
||||
onChange={(e) => updateSettings('erp', 'headless', e.target.checked)}
|
||||
/>
|
||||
<span>无头模式 (不显示浏览器)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.erp.ignoreHttpsErrors}
|
||||
onChange={(e) => updateSettings('erp', 'ignoreHttpsErrors', e.target.checked)}
|
||||
/>
|
||||
<span>忽略 HTTPS 错误</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.erp.autoCloseBrowser}
|
||||
onChange={(e) => updateSettings('erp', 'autoCloseBrowser', e.target.checked)}
|
||||
/>
|
||||
<span>操作完成后自动关闭浏览器</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Paths Configuration */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">路径设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">数据目录:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.paths.dataDir}
|
||||
onChange={(e) => updateSettings('paths', 'dataDir', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">默认输出文件:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.paths.defaultOutput}
|
||||
onChange={(e) => updateSettings('paths', 'defaultOutput', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">校验输出文件:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.paths.validationOutput}
|
||||
onChange={(e) => updateSettings('paths', 'validationOutput', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Extraction Settings */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">数据提取设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">批次大小:</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input input-small"
|
||||
value={settings.extraction.batchSize}
|
||||
onChange={(e) => updateSettings('extraction', 'batchSize', parseInt(e.target.value) || 100)}
|
||||
min="10"
|
||||
max="500"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.extraction.verbose}
|
||||
onChange={(e) => updateSettings('extraction', 'verbose', e.target.checked)}
|
||||
/>
|
||||
<span>启用详细日志</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.extraction.autoConvert}
|
||||
onChange={(e) => updateSettings('extraction', 'autoConvert', e.target.checked)}
|
||||
/>
|
||||
<span>自动转换 Excel 格式</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.extraction.mergeBatches}
|
||||
onChange={(e) => updateSettings('extraction', 'mergeBatches', e.target.checked)}
|
||||
/>
|
||||
<span>自动合并批次数据</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.extraction.enableDbPersistence}
|
||||
onChange={(e) => updateSettings('extraction', 'enableDbPersistence', e.target.checked)}
|
||||
/>
|
||||
<span>保存到数据库</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Validation Settings */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">物料校验设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">默认数据源:</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={settings.validation.dataSource}
|
||||
onChange={(e) => updateSettings('validation', 'dataSource', e.target.value as ValidationDataSource)}
|
||||
>
|
||||
<option value="database_full">database_full</option>
|
||||
<option value="database_filtered">database_filtered</option>
|
||||
<option value="excel_existing">excel_existing</option>
|
||||
<option value="excel_full">excel_full</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.validation.enableCrud}
|
||||
onChange={(e) => updateSettings('validation', 'enableCrud', e.target.checked)}
|
||||
/>
|
||||
<span>启用 CRUD 操作(管理待删除物料)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">批次大小:</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input input-small"
|
||||
value={settings.validation.batchSize}
|
||||
onChange={(e) => updateSettings('validation', 'batchSize', parseInt(e.target.value) || 2000)}
|
||||
min="100"
|
||||
max="2000"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">匹配模式:</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={settings.validation.matchMode}
|
||||
onChange={(e) => updateSettings('validation', 'matchMode', e.target.value as MatchMode)}
|
||||
>
|
||||
<option value="substring">模糊匹配 (substring)</option>
|
||||
<option value="exact">精确匹配 (exact)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">默认负责人:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.validation.defaultManager}
|
||||
onChange={(e) => updateSettings('validation', 'defaultManager', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* UI Settings */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">界面设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">字体:</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={settings.ui.fontFamily}
|
||||
onChange={(e) => updateSettings('ui', 'fontFamily', e.target.value)}
|
||||
>
|
||||
<option value="Microsoft YaHei UI">Microsoft YaHei UI</option>
|
||||
<option value="SimSun">SimSun (宋体)</option>
|
||||
<option value="KaiTi">KaiTi (楷体)</option>
|
||||
<option value="FangSong">FangSong (仿宋)</option>
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Segoe UI">Segoe UI</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">字号:</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input input-small"
|
||||
value={settings.ui.fontSize}
|
||||
onChange={(e) => updateSettings('ui', 'fontSize', parseInt(e.target.value) || 10)}
|
||||
min="8"
|
||||
max="24"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Production ID 输入框宽度 (字符):</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input input-small"
|
||||
value={settings.ui.productionIdInputWidth}
|
||||
onChange={(e) => updateSettings('ui', 'productionIdInputWidth', parseInt(e.target.value) || 20)}
|
||||
min="10"
|
||||
max="100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* User: ERP Credentials */}
|
||||
{isUserOnly && (
|
||||
<>
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">ERP 登录凭据</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">ERP 用户名:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.erp.username}
|
||||
onChange={(e) => updateSettings('erp', 'username', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">ERP 密码:</label>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
className="form-input"
|
||||
value={settings.erp.password}
|
||||
onChange={(e) => updateSettings('erp', 'password', e.target.value)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<label className="form-checkbox-label" style={{ margin: 0, whiteSpace: 'nowrap' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showPassword}
|
||||
onChange={(e) => setShowPassword(e.target.checked)}
|
||||
/>
|
||||
<span>显示密码</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<p className="form-hint">
|
||||
提示:此凭据为共享配置,修改后所有用户将使用新的 ERP 账号。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Paths Configuration (User) */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">路径设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">数据目录:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.paths.dataDir}
|
||||
onChange={(e) => updateSettings('paths', 'dataDir', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">默认输出文件:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.paths.defaultOutput}
|
||||
onChange={(e) => updateSettings('paths', 'defaultOutput', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">校验输出文件:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.paths.validationOutput}
|
||||
onChange={(e) => updateSettings('paths', 'validationOutput', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Execution Settings (User) */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">执行设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.execution.dryRun}
|
||||
onChange={(e) => updateSettings('execution', 'dryRun', e.target.checked)}
|
||||
/>
|
||||
<span>预览模式 (执行删除时不保存更改)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<p className="form-hint">
|
||||
提示:勾选后,执行删除操作时将只预览不实际保存,用于测试流程。
|
||||
</p>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.erp.headless}
|
||||
onChange={(e) => updateSettings('erp', 'headless', e.target.checked)}
|
||||
/>
|
||||
<span>无头模式 (不显示浏览器)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<p className="form-hint">
|
||||
提示:勾选后浏览器将在后台运行,不显示窗口。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Button Group */}
|
||||
<div className="settings-buttons">
|
||||
<button className="btn btn-primary" onClick={handleTestErpConnection}>
|
||||
测试 ERP 连接
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={handleTestDbConnection}>
|
||||
测试数据库连接
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={handleSaveSettings}
|
||||
disabled={!isModified}
|
||||
>
|
||||
保存设置
|
||||
</button>
|
||||
{userType === 'Admin' && (
|
||||
<button className="btn btn-warning" onClick={handleResetDefaults}>
|
||||
恢复默认
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Styles */}
|
||||
<style>{`
|
||||
.settings-page {
|
||||
padding: 24px;
|
||||
background: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 2px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.settings-header h1 {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user-type-badge {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
z-index: 10000;
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background: #fff1f0;
|
||||
border: 1px solid #ffa39e;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.settings-group-title {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
margin: 0 0 16px 0;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
min-width: 120px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.form-input.input-small {
|
||||
width: 100px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.checkbox-row {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.form-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-checkbox-label input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin: 4px 0 0 136px;
|
||||
}
|
||||
|
||||
.settings-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #40a9ff;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #52c41a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
background: #73d13d;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #faad14;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-warning:hover:not(:disabled) {
|
||||
background: #ffc53d;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsPage
|
||||
Reference in New Issue
Block a user