diff --git a/src/main/index.ts b/src/main/index.ts index 06e06b4..9b6edfc 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2,6 +2,7 @@ import { app, shell, BrowserWindow, ipcMain } from 'electron' import { join } from 'path' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' +import { registerIpcHandlers } from './ipc' function createWindow(): void { // Create the browser window. @@ -49,6 +50,9 @@ app.whenReady().then(() => { optimizer.watchWindowShortcuts(window) }) + // Register IPC handlers + registerIpcHandlers() + // IPC test ipcMain.on('ping', () => console.log('pong')) diff --git a/src/main/ipc/cleaner-handler.ts b/src/main/ipc/cleaner-handler.ts new file mode 100644 index 0000000..a410144 --- /dev/null +++ b/src/main/ipc/cleaner-handler.ts @@ -0,0 +1,49 @@ +import { ipcMain } from 'electron' +import { ErpAuthService } from '../services/erp/erp-auth' +import { CleanerService } from '../services/erp/cleaner' +import type { CleanerInput, CleanerResult } from '../types/cleaner.types' + +/** + * Register IPC handlers for cleaner service + */ +export function registerCleanerHandlers(): void { + ipcMain.handle( + 'cleaner:run', + async ( + _event, + input: CleanerInput + ): Promise<{ success: boolean; data?: CleanerResult; error?: string }> => { + let authService: ErpAuthService | null = null + + try { + // Create auth service and login + authService = new ErpAuthService({ + url: process.env.ERP_URL || '', + username: process.env.ERP_USERNAME || '', + password: process.env.ERP_PASSWORD || '', + headless: true + }) + + await authService.login() + + // Create cleaner service and run cleaning + const cleaner = new CleanerService(authService) + const result = await cleaner.clean(input) + + return { success: true, data: result } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + return { success: false, error: message } + } finally { + // Clean up: close browser + if (authService) { + try { + await authService.close() + } catch { + // Ignore cleanup errors + } + } + } + } + ) +} diff --git a/src/main/ipc/database-handler.ts b/src/main/ipc/database-handler.ts new file mode 100644 index 0000000..f0e592f --- /dev/null +++ b/src/main/ipc/database-handler.ts @@ -0,0 +1,181 @@ +import { ipcMain } from 'electron' +import { MySqlService } from '../services/database/mysql' +import { SqlServerService } from '../services/database/sql-server' +import type { + MySqlConfig, + MySqlQueryResult, + SqlServerConfig, + SqlServerQueryResult +} from '../types/ipc-api.types' + +// Store MySQL service instances per window/connection +const mysqlServices = new Map() + +// Store SQL Server service instances per window/connection +const sqlServerServices = new Map() + +/** + * Get or create MySQL service for a connection ID + */ +function getMySqlService(connectionId: string): MySqlService | undefined { + return mysqlServices.get(connectionId) +} + +/** + * Set MySQL service for a connection ID + */ +function setMySqlService(connectionId: string, service: MySqlService): void { + mysqlServices.set(connectionId, service) +} + +/** + * Delete MySQL service for a connection ID + */ +function deleteMySqlService(connectionId: string): void { + mysqlServices.delete(connectionId) +} + +/** + * Get or create SQL Server service for a connection ID + */ +function getSqlServerService(connectionId: string): SqlServerService | undefined { + return sqlServerServices.get(connectionId) +} + +/** + * Set SQL Server service for a connection ID + */ +function setSqlServerService(connectionId: string, service: SqlServerService): void { + sqlServerServices.set(connectionId, service) +} + +/** + * Delete SQL Server service for a connection ID + */ +function deleteSqlServerService(connectionId: string): void { + sqlServerServices.delete(connectionId) +} + +/** + * Register IPC handlers for database operations + */ +export function registerDatabaseHandlers(): void { + // Connect to MySQL + ipcMain.handle('database:mysql:connect', async (event, config: MySqlConfig): Promise => { + try { + // Use window ID as connection identifier + const windowId = (event.sender as any).id.toString() + const service = new MySqlService(config) + await service.connect() + setMySqlService(windowId, service) + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to connect to MySQL' + throw new Error(message) + } + }) + + // Disconnect from MySQL + ipcMain.handle('database:mysql:disconnect', async (event): Promise => { + try { + const windowId = (event.sender as any).id.toString() + const service = getMySqlService(windowId) + if (service) { + await service.disconnect() + deleteMySqlService(windowId) + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to disconnect from MySQL' + throw new Error(message) + } + }) + + // Check if MySQL is connected + ipcMain.handle('database:mysql:isConnected', async (event): Promise => { + const windowId = (event.sender as any).id.toString() + const service = getMySqlService(windowId) + return service ? service.isConnected() : false + }) + + // Execute MySQL query + ipcMain.handle( + 'database:mysql:query', + async (event, sql: string, params?: any[]): Promise => { + try { + const windowId = (event.sender as any).id.toString() + const service = getMySqlService(windowId) + + if (!service) { + throw new Error('Not connected to MySQL. Call connect() first.') + } + + return await service.query(sql, params) + } catch (error) { + const message = error instanceof Error ? error.message : 'MySQL query failed' + throw new Error(message) + } + } + ) + + // Connect to SQL Server + ipcMain.handle( + 'database:sqlserver:connect', + async (event, config: SqlServerConfig): Promise => { + try { + const windowId = (event.sender as any).id.toString() + const service = new SqlServerService(config) + await service.connect() + setSqlServerService(windowId, service) + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to connect to SQL Server' + throw new Error(message) + } + } + ) + + // Disconnect from SQL Server + ipcMain.handle('database:sqlserver:disconnect', async (event): Promise => { + try { + const windowId = (event.sender as any).id.toString() + const service = getSqlServerService(windowId) + if (service) { + await service.disconnect() + deleteSqlServerService(windowId) + } + } catch (error) { + const message = + error instanceof Error ? error.message : 'Failed to disconnect from SQL Server' + throw new Error(message) + } + }) + + // Check if SQL Server is connected + ipcMain.handle('database:sqlserver:isConnected', async (event): Promise => { + const windowId = (event.sender as any).id.toString() + const service = getSqlServerService(windowId) + return service ? service.isConnected() : false + }) + + // Execute SQL Server query + ipcMain.handle( + 'database:sqlserver:query', + async ( + event, + sqlString: string, + params?: Record + ): Promise => { + try { + const windowId = (event.sender as any).id.toString() + const service = getSqlServerService(windowId) + + if (!service) { + throw new Error('Not connected to SQL Server. Call connect() first.') + } + + return await service.query(sqlString, params) + } catch (error) { + const message = error instanceof Error ? error.message : 'SQL Server query failed' + throw new Error(message) + } + } + ) +} diff --git a/src/main/ipc/extractor-handler.ts b/src/main/ipc/extractor-handler.ts new file mode 100644 index 0000000..ec6a2ff --- /dev/null +++ b/src/main/ipc/extractor-handler.ts @@ -0,0 +1,49 @@ +import { ipcMain } from 'electron' +import { ErpAuthService } from '../services/erp/erp-auth' +import { ExtractorService } from '../services/erp/extractor' +import type { ExtractorInput, ExtractorResult } from '../types/extractor.types' + +/** + * Register IPC handlers for extractor service + */ +export function registerExtractorHandlers(): void { + ipcMain.handle( + 'extractor:run', + async ( + _event, + input: ExtractorInput + ): Promise<{ success: boolean; data?: ExtractorResult; error?: string }> => { + let authService: ErpAuthService | null = null + + try { + // Create auth service and login + authService = new ErpAuthService({ + url: process.env.ERP_URL || '', + username: process.env.ERP_USERNAME || '', + password: process.env.ERP_PASSWORD || '', + headless: true + }) + + await authService.login() + + // Create extractor service and run extraction + const extractor = new ExtractorService(authService) + const result = await extractor.extract(input) + + return { success: true, data: result } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + return { success: false, error: message } + } finally { + // Clean up: close browser + if (authService) { + try { + await authService.close() + } catch { + // Ignore cleanup errors + } + } + } + } + ) +} diff --git a/src/main/ipc/file-handler.ts b/src/main/ipc/file-handler.ts new file mode 100644 index 0000000..4bf9d2a --- /dev/null +++ b/src/main/ipc/file-handler.ts @@ -0,0 +1,55 @@ +import { ipcMain } from 'electron' +import * as fs from 'fs/promises' +import * as path from 'path' + +/** + * Register IPC handlers for file operations + */ +export function registerFileHandlers(): void { + // Read file content + ipcMain.handle('file:read', async (_event, filePath: string): Promise => { + try { + return await fs.readFile(filePath, 'utf-8') + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to read file' + throw new Error(message) + } + }) + + // Write content to file + ipcMain.handle('file:write', async (_event, filePath: string, content: string): Promise => { + try { + // Ensure directory exists + const dir = path.dirname(filePath) + await fs.mkdir(dir, { recursive: true }) + await fs.writeFile(filePath, content, 'utf-8') + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to write file' + throw new Error(message) + } + }) + + // Check if file exists + ipcMain.handle('file:exists', async (_event, filePath: string): Promise => { + try { + await fs.access(filePath) + return true + } catch { + return false + } + }) + + // List files in directory + ipcMain.handle('file:list', async (_event, dirPath: string): Promise => { + try { + const entries = await fs.readdir(dirPath, { withFileTypes: true }) + return entries + .filter((entry) => entry.isFile()) + .map((entry) => entry.name) + .sort() + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to list directory' + throw new Error(message) + } + }) +} diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts new file mode 100644 index 0000000..00f4315 --- /dev/null +++ b/src/main/ipc/index.ts @@ -0,0 +1,19 @@ +/** + * IPC Handler registration + * Centralized registration for all IPC handlers + */ + +import { registerFileHandlers } from './file-handler' +import { registerExtractorHandlers } from './extractor-handler' +import { registerCleanerHandlers } from './cleaner-handler' +import { registerDatabaseHandlers } from './database-handler' + +/** + * Register all IPC handlers + */ +export function registerIpcHandlers(): void { + registerFileHandlers() + registerExtractorHandlers() + registerCleanerHandlers() + registerDatabaseHandlers() +} diff --git a/src/main/services/database/mysql.ts b/src/main/services/database/mysql.ts new file mode 100644 index 0000000..ba2bed4 --- /dev/null +++ b/src/main/services/database/mysql.ts @@ -0,0 +1,123 @@ +import mysql from 'mysql2/promise' + +export interface MySqlConfig { + host: string + port: number + user: string + password: string + database: string +} + +export interface MySqlQueryResult { + rows: Record[] + columns: string[] + rowCount: number +} + +export class MySqlService { + private connection: mysql.Connection | null = null + private config: MySqlConfig + + constructor(config: MySqlConfig) { + this.config = config + } + + /** + * Connect to MySQL database + */ + async connect(): Promise { + if (this.connection) { + throw new Error('Already connected to MySQL') + } + + try { + this.connection = await mysql.createConnection({ + host: this.config.host, + port: this.config.port, + user: this.config.user, + password: this.config.password, + database: this.config.database + }) + + // Test connection + await this.connection.ping() + } catch (error) { + throw new Error(`Failed to connect to MySQL: ${(error as Error).message}`) + } + } + + /** + * Disconnect from MySQL database + */ + async disconnect(): Promise { + if (!this.connection) { + return + } + + try { + await this.connection.end() + this.connection = null + } catch (error) { + throw new Error(`Failed to disconnect from MySQL: ${(error as Error).message}`) + } + } + + /** + * Check if connected to MySQL database + */ + isConnected(): boolean { + return this.connection !== null + } + + /** + * Execute a query and return results + */ + async query(sql: string, params?: any[]): Promise { + if (!this.connection) { + throw new Error('Not connected to MySQL. Call connect() first.') + } + + try { + const [rows, 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 + + // Type assertion for rows - mysql2 returns different types based on query + const typedRows = Array.isArray(rows) ? (rows as Record[]) : [] + + return { + rows: typedRows, + columns, + rowCount + } + } catch (error) { + throw new Error(`MySQL query failed: ${(error as Error).message}`) + } + } + + /** + * Execute multiple queries in a transaction + */ + async transaction(queries: { sql: string; params?: any[] }[]): Promise { + if (!this.connection) { + throw new Error('Not connected to MySQL. Call connect() first.') + } + + try { + await this.connection.beginTransaction() + + for (const { sql, params } of queries) { + await this.connection.execute(sql, params) + } + + await this.connection.commit() + } catch (error) { + if (this.connection) { + await this.connection.rollback() + } + throw new Error(`MySQL transaction failed: ${(error as Error).message}`) + } + } +} diff --git a/src/main/services/database/sql-server.ts b/src/main/services/database/sql-server.ts new file mode 100644 index 0000000..2222a21 --- /dev/null +++ b/src/main/services/database/sql-server.ts @@ -0,0 +1,184 @@ +import sql from 'mssql' + +export interface SqlServerConfig { + server: string + port: number + user: string + password: string + database: string + options?: { + encrypt?: boolean + trustServerCertificate?: boolean + } +} + +export interface SqlServerQueryResult { + rows: Record[] + columns: string[] + rowCount: number +} + +export class SqlServerService { + private pool: sql.ConnectionPool | null = null + private config: SqlServerConfig + + constructor(config: SqlServerConfig) { + this.config = config + } + + /** + * Connect to SQL Server database + */ + async connect(): Promise { + if (this.pool) { + throw new Error('Already connected to SQL Server') + } + + try { + const poolConfig: sql.config = { + server: this.config.server, + port: this.config.port, + user: this.config.user, + password: this.config.password, + database: this.config.database, + options: { + encrypt: this.config.options?.encrypt ?? true, + trustServerCertificate: this.config.options?.trustServerCertificate ?? false + } + } + + this.pool = new sql.ConnectionPool(poolConfig) + await this.pool.connect() + } catch (error) { + throw new Error(`Failed to connect to SQL Server: ${(error as Error).message}`) + } + } + + /** + * Disconnect from SQL Server database + */ + async disconnect(): Promise { + if (!this.pool) { + return + } + + try { + await this.pool.close() + this.pool = null + } catch (error) { + throw new Error(`Failed to disconnect from SQL Server: ${(error as Error).message}`) + } + } + + /** + * Check if connected to SQL Server database + */ + isConnected(): boolean { + return this.pool !== null && this.pool.connected + } + + /** + * Execute a query and return results + */ + async query(sqlString: string, params?: Record): Promise { + if (!this.pool) { + throw new Error('Not connected to SQL Server. Call connect() first.') + } + + try { + const request = this.pool.request() + + // Add parameters if provided + if (params) { + for (const [key, value] of Object.entries(params)) { + request.input(key, value) + } + } + + const result = await request.query(sqlString) + + // Convert recordset to array of objects + const columns = result.recordset.columns?.map((col) => col.name) || [] + const rows = result.recordset as Record[] + + return { + rows, + columns, + rowCount: result.rowsAffected?.[0] || rows.length + } + } catch (error) { + throw new Error(`SQL Server query failed: ${(error as Error).message}`) + } + } + + /** + * Execute a prepared statement with parameters + */ + async queryWithParams( + sqlString: string, + params: Record + ): Promise { + if (!this.pool) { + throw new Error('Not connected to SQL Server. Call connect() first.') + } + + try { + const request = this.pool.request() + + // Add parameters with explicit types + for (const [key, { value, type }] of Object.entries(params)) { + if (type) { + request.input(key, type, value) + } else { + request.input(key, value) + } + } + + const result = await request.query(sqlString) + + // Convert recordset to array of objects + const columns = result.recordset.columns?.map((col) => col.name) || [] + const rows = result.recordset as Record[] + + return { + rows, + columns, + rowCount: result.rowsAffected?.[0] || rows.length + } + } catch (error) { + throw new Error(`SQL Server query failed: ${(error as Error).message}`) + } + } + + /** + * Execute multiple queries in a transaction + */ + async transaction(queries: { sql: string; params?: Record }[]): Promise { + if (!this.pool) { + throw new Error('Not connected to SQL Server. Call connect() first.') + } + + const transaction = new sql.Transaction(this.pool) + + try { + await transaction.begin() + + for (const { sql: sqlString, params } of queries) { + const request = new sql.Request(transaction) + + if (params) { + for (const [key, value] of Object.entries(params)) { + request.input(key, value) + } + } + + await request.query(sqlString) + } + + await transaction.commit() + } catch (error) { + await transaction.rollback() + throw new Error(`SQL Server transaction failed: ${(error as Error).message}`) + } + } +} diff --git a/src/main/types/ipc-api.types.ts b/src/main/types/ipc-api.types.ts new file mode 100644 index 0000000..4af8f5d --- /dev/null +++ b/src/main/types/ipc-api.types.ts @@ -0,0 +1,156 @@ +/** + * IPC API type definitions for renderer process + * These types define the API exposed to the renderer process via contextBridge + */ + +import type { ExtractorInput, ExtractorResult } from './extractor.types' +import type { CleanerInput, CleanerResult } from './cleaner.types' + +/** + * MySQL connection configuration + */ +export interface MySqlConfig { + host: string + port: number + user: string + password: string + database: string +} + +/** + * MySQL query result + */ +export interface MySqlQueryResult { + rows: Record[] + columns: string[] + rowCount: number +} + +/** + * SQL Server connection configuration + */ +export interface SqlServerConfig { + server: string + port: number + user: string + password: string + database: string + options?: { + encrypt?: boolean + trustServerCertificate?: boolean + } +} + +/** + * SQL Server query result + */ +export interface SqlServerQueryResult { + rows: Record[] + columns: string[] + rowCount: number +} + +/** + * File operation APIs + */ +export interface FileAPI { + /** + * Read file content as text + * @param filePath - Path to the file + */ + readFile: (filePath: string) => Promise + + /** + * Write content to file + * @param filePath - Path to the file + * @param content - Content to write + */ + writeFile: (filePath: string, content: string) => Promise + + /** + * Check if file exists + * @param filePath - Path to the file + */ + fileExists: (filePath: string) => Promise + + /** + * Get list of files in directory + * @param dirPath - Directory path + */ + listFiles: (dirPath: string) => Promise +} + +/** + * Extractor service APIs + */ +export interface ExtractorAPI { + /** + * Run ERP data extractor + * @param input - Extractor input parameters + */ + runExtractor: ( + input: ExtractorInput + ) => Promise<{ success: boolean; data?: ExtractorResult; error?: string }> +} + +/** + * Cleaner service APIs + */ +export interface CleanerAPI { + /** + * Run ERP cleaner service + * @param input - Cleaner input parameters + */ + runCleaner: (input: CleanerInput) => Promise +} + +/** + * Database service APIs + */ +export interface DatabaseAPI { + /** + * Connect to MySQL database + * @param config - MySQL connection config + */ + connectMySql: (config: MySqlConfig) => Promise + + /** + * Disconnect from MySQL database + */ + disconnectMySql: () => Promise + + /** + * Check if MySQL is connected + */ + isMySqlConnected: () => Promise + + /** + * Execute MySQL query + * @param sql - SQL query + * @param params - Query parameters + */ + queryMySql: (sql: string, params?: unknown[]) => Promise + + /** + * Connect to SQL Server database + * @param config - SQL Server connection config + */ + connectSqlServer: (config: SqlServerConfig) => Promise + + /** + * Disconnect from SQL Server database + */ + disconnectSqlServer: () => Promise + + /** + * Check if SQL Server is connected + */ + isSqlServerConnected: () => Promise + + /** + * Execute SQL Server query + * @param sql - SQL query + * @param params - Query parameters + */ + querySqlServer: (sql: string, params?: Record) => Promise +} diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index a153669..7346335 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -1,8 +1,28 @@ -import { ElectronAPI } from '@electron-toolkit/preload' +import type { FileAPI, ExtractorAPI, CleanerAPI, DatabaseAPI } from '../main/types/ipc-api.types' declare global { interface Window { - electron: ElectronAPI + electron: { + ipcRenderer: { + send: (channel: string, ...args: unknown[]) => void + on: (channel: string, func: (...args: unknown[]) => void) => void + once: (channel: string, func: (...args: unknown[]) => void) => void + removeListener: (channel: string, func: (...args: unknown[]) => void) => void + removeAllListeners: (channel: string) => void + invoke: (channel: string, ...args: unknown[]) => Promise + } + process: { + versions: { + electron: string + chrome: string + node: string + } + } + file: FileAPI + extractor: ExtractorAPI + cleaner: CleanerAPI + database: DatabaseAPI + } api: unknown } } diff --git a/src/preload/index.ts b/src/preload/index.ts index 2d18524..48d7a45 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,22 +1,61 @@ -import { contextBridge } from 'electron' +import { contextBridge, ipcRenderer } from 'electron' import { electronAPI } from '@electron-toolkit/preload' +import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types' +import type { ExtractorInput } from '../main/types/extractor.types' +import type { CleanerInput } from '../main/types/cleaner.types' // Custom APIs for renderer -const api = {} +const api = { + // File operations + file: { + readFile: (filePath: string) => ipcRenderer.invoke('file:read', filePath), + writeFile: (filePath: string, content: string) => + ipcRenderer.invoke('file:write', filePath, content), + fileExists: (filePath: string) => ipcRenderer.invoke('file:exists', filePath), + listFiles: (dirPath: string) => ipcRenderer.invoke('file:list', dirPath) + }, + + // Extractor service + extractor: { + runExtractor: (input: ExtractorInput) => ipcRenderer.invoke('extractor:run', input) + }, + + // Cleaner service + cleaner: { + runCleaner: (input: CleanerInput) => ipcRenderer.invoke('cleaner:run', input) + }, + + // Database service + database: { + connectMySql: (config: MySqlConfig) => ipcRenderer.invoke('database:mysql:connect', config), + disconnectMySql: () => ipcRenderer.invoke('database:mysql:disconnect'), + isMySqlConnected: () => ipcRenderer.invoke('database:mysql:isConnected'), + queryMySql: (sql: string, params?: any[]) => + ipcRenderer.invoke('database:mysql:query', sql, params), + + // SQL Server + connectSqlServer: (config: SqlServerConfig) => + ipcRenderer.invoke('database:sqlserver:connect', config), + disconnectSqlServer: () => ipcRenderer.invoke('database:sqlserver:disconnect'), + isSqlServerConnected: () => ipcRenderer.invoke('database:sqlserver:isConnected'), + querySqlServer: (sql: string, params?: Record) => + ipcRenderer.invoke('database:sqlserver:query', sql, params) + } +} as const // Use `contextBridge` APIs to expose Electron APIs to // renderer only if context isolation is enabled, otherwise // just add to the DOM global. if (process.contextIsolated) { try { - contextBridge.exposeInMainWorld('electron', electronAPI) + contextBridge.exposeInMainWorld('electron', { ...electronAPI, ...api }) contextBridge.exposeInMainWorld('api', api) } catch (error) { console.error(error) } } else { // @ts-ignore (define in dts) - window.electron = electronAPI + window.electron = { ...electronAPI, ...api } // @ts-ignore (define in dts) window.api = api } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 73de51c..6ae441b 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,34 +1,88 @@ +import { useState } from 'react' import Versions from './components/Versions' +import { ExtractorPage } from './pages/ExtractorPage' import electronLogo from './assets/electron.svg' +type Page = 'home' | 'extractor' | 'cleaner' + function App(): React.JSX.Element { - const ipcHandle = (): void => window.electron.ipcRenderer.send('ping') + const [currentPage, setCurrentPage] = useState('home') + + const renderPage = () => { + switch (currentPage) { + case 'extractor': + return + default: + return ( + <> + logo +
Powered by electron-vite
+
+ Build an Electron app with React +  and TypeScript +
+

+ Please try pressing F12 to open the devTool +

+ + + + ) + } + } return ( - <> - logo -
Powered by electron-vite
-
- Build an Electron app with React -  and TypeScript -
-

- Please try pressing F12 to open the devTool -

- - - +
+ {currentPage !== 'home' && ( + + )} + {renderPage()} + +
) } diff --git a/src/renderer/src/components/OrderNumberInput.tsx b/src/renderer/src/components/OrderNumberInput.tsx new file mode 100644 index 0000000..ceabc8e --- /dev/null +++ b/src/renderer/src/components/OrderNumberInput.tsx @@ -0,0 +1,92 @@ +import React, { useState, useEffect } from 'react' + +interface OrderNumberInputProps { + value: string + onChange: (value: string) => void + placeholder?: string + label?: string +} + +/** + * OrderNumberInput - A textarea component for entering line-separated order numbers + * Displays a count of valid order numbers entered + */ +export const OrderNumberInput: React.FC = ({ + value, + onChange, + placeholder = '请输入订单号,每行一个', + label = '订单号列表' +}) => { + const [count, setCount] = useState(0) + + useEffect(() => { + // Count non-empty lines + const lines = value.split('\n').filter((line) => line.trim().length > 0) + setCount(lines.length) + }, [value]) + + const handleChange = (e: React.ChangeEvent) => { + onChange(e.target.value) + } + + return ( +
+
+ + {count} 个订单号 +
+