From 829851e3cacede3ced989746d7b15e31457b90e8 Mon Sep 17 00:00:00 2001 From: Misaka Date: Sun, 1 Mar 2026 17:46:15 +0800 Subject: [PATCH] feat: implement user authentication system - Add SessionManager for managing user sessions (singleton pattern) - Add BIPUsersDAO for database authentication - Add LoginDialog component for username/password login - Add UserSelectionDialog component for admin user selection - Support silent login by computer name - Implement main page with navigation to Extractor and Cleaner Database: - Table: dbo_BIPUsers - Fields: UserName, Password, UserType, ComputerNmae UI Flow: 1. Silent login on startup via computer name 2. Show login dialog if silent login fails 3. Display main page with user info and navigation 4. Support logout and re-login --- src/main/ipc/auth-handler.ts | 223 ++++++++++ src/main/ipc/index.ts | 2 + src/main/services/user/bip-users-dao.ts | 298 ++++++++++++++ src/main/services/user/session-manager.ts | 185 +++++++++ src/main/types/user.types.ts | 36 ++ src/preload/index.d.ts | 49 +++ src/preload/index.ts | 14 + src/renderer/index.html | 4 +- src/renderer/src/App.tsx | 384 ++++++++++++++---- src/renderer/src/assets/main.css | 31 +- src/renderer/src/components/LoginDialog.tsx | 267 ++++++++++++ .../src/components/UserSelectionDialog.tsx | 290 +++++++++++++ 12 files changed, 1671 insertions(+), 112 deletions(-) create mode 100644 src/main/ipc/auth-handler.ts create mode 100644 src/main/services/user/bip-users-dao.ts create mode 100644 src/main/services/user/session-manager.ts create mode 100644 src/main/types/user.types.ts create mode 100644 src/renderer/src/components/LoginDialog.tsx create mode 100644 src/renderer/src/components/UserSelectionDialog.tsx diff --git a/src/main/ipc/auth-handler.ts b/src/main/ipc/auth-handler.ts new file mode 100644 index 0000000..e5fea4b --- /dev/null +++ b/src/main/ipc/auth-handler.ts @@ -0,0 +1,223 @@ +/** + * IPC handlers for User Authentication + * + * Provides APIs for the renderer process to: + * - Login with username and password + * - Silent login by computer name + * - Logout + * - Get current user info + * - Get all users (for admin user selection) + * - Switch user (admin only) + */ + +import { ipcMain } from 'electron' +import { SessionManager } from '../services/user/session-manager' +import type { UserInfo } from '../types/user.types' + +/** + * Login request + */ +export interface LoginRequest { + username: string + password: string +} + +/** + * Login response + */ +export interface LoginResponse { + success: boolean + userInfo?: UserInfo + error?: string +} + +/** + * Silent login response + */ +export interface SilentLoginResponse { + success: boolean + userInfo?: UserInfo + requiresUserSelection?: boolean // True if admin needs to select a user + error?: string +} + +/** + * User selection response + */ +export interface UserSelectionResponse { + success: boolean + userInfo?: UserInfo + error?: string +} + +/** + * Current user response + */ +export interface CurrentUserResponse { + isAuthenticated: boolean + userInfo?: UserInfo +} + +/** + * Register IPC handlers for user authentication + */ +export function registerAuthHandlers(): void { + const sessionManager = SessionManager.getInstance() + + /** + * Get computer name + */ + ipcMain.handle('auth:getComputerName', async (): Promise => { + const os = await import('os') + return os.hostname() + }) + + /** + * Silent login by computer name + */ + ipcMain.handle( + 'auth:silentLogin', + async (): Promise => { + try { + const success = await sessionManager.loginByComputerName() + const userInfo = sessionManager.getUserInfo() + + if (success && userInfo) { + // Check if admin needs user selection + const requiresUserSelection = userInfo.userType === 'Admin' + + return { + success: true, + userInfo, + requiresUserSelection + } + } + + return { + success: false, + requiresUserSelection: false + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + return { + success: false, + error: `无感登录失败:${message}` + } + } + } + ) + + /** + * Login with username and password + */ + ipcMain.handle( + 'auth:login', + async (_event, request: LoginRequest): Promise => { + try { + const { username, password } = request + + if (!username || !password) { + return { + success: false, + error: '请输入用户名和密码' + } + } + + const success = await sessionManager.login(username, password) + const userInfo = sessionManager.getUserInfo() + + if (success && userInfo) { + return { + success: true, + userInfo + } + } + + return { + success: false, + error: '用户名或密码错误' + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + return { + success: false, + error: `登录失败:${message}` + } + } + } + ) + + /** + * Logout + */ + ipcMain.handle('auth:logout', async (): Promise => { + sessionManager.logout() + }) + + /** + * Get current user + */ + ipcMain.handle( + 'auth:getCurrentUser', + async (): Promise => { + const isAuthenticated = sessionManager.isAuthenticated() + const userInfo = sessionManager.getUserInfo() + + return { + isAuthenticated, + userInfo: userInfo ?? undefined + } + } + ) + + /** + * Get all users (for admin user selection) + */ + ipcMain.handle( + 'auth:getAllUsers', + async (): Promise => { + return await sessionManager.getAllUsers() + } + ) + + /** + * Switch user (admin only) + */ + ipcMain.handle( + 'auth:switchUser', + async (_event, userInfo: UserInfo): Promise => { + try { + const success = sessionManager.switchUser(userInfo) + + if (success) { + const newUser = sessionManager.getUserInfo() + return { + success: true, + userInfo: newUser ?? undefined + } + } + + return { + success: false, + error: '用户切换失败' + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + return { + success: false, + error: `用户切换失败:${message}` + } + } + } + ) + + /** + * Check if current user is admin + */ + ipcMain.handle( + 'auth:isAdmin', + async (): Promise => { + return sessionManager.isAdmin() + } + ) +} diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index c0b5a4f..8f03495 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -8,6 +8,7 @@ import { registerExtractorHandlers } from './extractor-handler' import { registerCleanerHandlers } from './cleaner-handler' import { registerDatabaseHandlers } from './database-handler' import { registerResolverHandlers } from './resolver-handler' +import { registerAuthHandlers } from './auth-handler' /** * Register all IPC handlers @@ -18,4 +19,5 @@ export function registerIpcHandlers(): void { registerCleanerHandlers() registerDatabaseHandlers() registerResolverHandlers() + registerAuthHandlers() } diff --git a/src/main/services/user/bip-users-dao.ts b/src/main/services/user/bip-users-dao.ts new file mode 100644 index 0000000..1a281e7 --- /dev/null +++ b/src/main/services/user/bip-users-dao.ts @@ -0,0 +1,298 @@ +/** + * BIPUsers DAO - Data access object for user authentication and management + * + * Mirrors the Python BIPUsersDAO functionality: + * - Authenticate users by username and password + * - Authenticate by computer name (silent login) + * - Get all users for admin user selection + * - Create, update, delete users + */ + +import { MySqlService } from '../database/mysql' +import type { UserInfo } from '../../types/user.types' + +/** + * Database configuration for BIPUsers table + */ +export const BIP_USERS_CONFIG = { + /** Table name in SQL Server: [dbo].[BIPUsers] */ + TABLE_NAME_SQLSERVER: '[dbo].[BIPUsers]', + /** Table name in MySQL: dbo_BIPUsers */ + TABLE_NAME_MYSQL: 'dbo_BIPUsers', + /** Column names */ + COLUMNS: { + ID: 'ID', + USERNAME: 'UserName', + USER_TYPE: 'UserType', + PASSWORD: 'Password', + COMPUTER_NAME: 'ComputerNmae', // Note: typo in database schema + CREATE_TIME: 'CreateTime' + } +} as const + +/** + * BIPUsers DAO Class + */ +export class BIPUsersDAO { + private mysqlService: MySqlService | null = null + + /** + * Get MySQL service instance + */ + private async getMySqlService(): Promise { + 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 + } + + /** + * Authenticate a user with username and password + * @param username - The username to authenticate + * @param password - The password to verify + * @returns User info if authentication successful, null otherwise + */ + async authenticate(username: string, password: string): Promise { + try { + const mysqlService = await this.getMySqlService() + + const sql = ` + SELECT ID, UserName, UserType + FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} + WHERE UserName = ? AND Password = ? + ` + + const result = await mysqlService.query(sql, [username, password]) + + if (result.rows.length > 0) { + const row = result.rows[0] + return { + id: row.ID as number, + username: row.UserName as string, + userType: row.UserType as 'Admin' | 'User' | 'Guest' + } + } + return null + } catch (error) { + console.error('[BIPUsersDAO] Authenticate error:', error) + return null + } + } + + /** + * Authenticate a user using computer name (silent login) + * @param computerName - The computer name to authenticate + * @returns User info if authentication successful, null otherwise + */ + async authenticateByComputerName(computerName: string): Promise { + try { + const mysqlService = await this.getMySqlService() + + const sql = ` + SELECT ID, UserName, UserType + FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} + WHERE ComputerNmae = ? + ` + + const result = await mysqlService.query(sql, [computerName]) + + if (result.rows.length > 0) { + const row = result.rows[0] + return { + id: row.ID as number, + username: row.UserName as string, + userType: row.UserType as 'Admin' | 'User' | 'Guest' + } + } + return null + } catch (error) { + console.error('[BIPUsersDAO] Authenticate by computer name error:', error) + return null + } + } + + /** + * Get all users from the database + * @returns List of user information + */ + async getAllUsers(): Promise { + try { + const mysqlService = await this.getMySqlService() + + const sql = ` + SELECT ID, UserName, UserType, CreateTime + FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} + ORDER BY UserName + ` + + const result = await mysqlService.query(sql) + + return result.rows.map(row => ({ + id: row.ID as number, + username: row.UserName as string, + userType: row.UserType as 'Admin' | 'User' | 'Guest', + createTime: row.CreateTime as Date | undefined + })) + } catch (error) { + console.error('[BIPUsersDAO] Get all users error:', error) + return [] + } + } + + /** + * Create a new user + * @param username - The username (must be unique) + * @param password - The password + * @param userType - User type ('Admin', 'User', or 'Guest') + * @param computerName - Optional computer name for silent login + * @returns True if successful + */ + async createUser( + username: string, + password: string, + userType: string, + computerName: string = '' + ): Promise { + try { + const mysqlService = await this.getMySqlService() + + let sql: string + let params: any[] + + if (computerName) { + sql = ` + INSERT INTO ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} + (UserName, Password, UserType, ComputerNmae) + VALUES (?, ?, ?, ?) + ` + params = [username, password, userType, computerName] + } else { + sql = ` + INSERT INTO ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} + (UserName, Password, UserType) + VALUES (?, ?, ?) + ` + params = [username, password, userType] + } + + await mysqlService.query(sql, params) + return true + } catch (error) { + console.error('[BIPUsersDAO] Create user error:', error) + return false + } + } + + /** + * Update a user's type + * @param username - The username to update + * @param userType - New user type + * @returns True if successful + */ + async updateUserType(username: string, userType: string): Promise { + try { + const mysqlService = await this.getMySqlService() + + const sql = ` + UPDATE ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} + SET UserType = ? + WHERE UserName = ? + ` + + await mysqlService.query(sql, [userType, username]) + return true + } catch (error) { + console.error('[BIPUsersDAO] Update user type error:', error) + return false + } + } + + /** + * Update a user's password + * @param username - The username to update + * @param newPassword - The new password + * @returns True if successful + */ + async updatePassword(username: string, newPassword: string): Promise { + try { + const mysqlService = await this.getMySqlService() + + const sql = ` + UPDATE ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} + SET Password = ? + WHERE UserName = ? + ` + + await mysqlService.query(sql, [newPassword, username]) + return true + } catch (error) { + console.error('[BIPUsersDAO] Update password error:', error) + return false + } + } + + /** + * Delete a user + * @param username - The username to delete + * @returns True if successful + */ + async deleteUser(username: string): Promise { + try { + const mysqlService = await this.getMySqlService() + + const sql = ` + DELETE FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} + WHERE UserName = ? + ` + + await mysqlService.query(sql, [username]) + return true + } catch (error) { + console.error('[BIPUsersDAO] Delete user error:', error) + return false + } + } + + /** + * Check if a username already exists + * @param username - The username to check + * @returns True if username exists + */ + async userExists(username: string): Promise { + try { + const mysqlService = await this.getMySqlService() + + const sql = ` + SELECT COUNT(*) as count + FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} + WHERE UserName = ? + ` + + const result = await mysqlService.query(sql, [username]) + return result.rows.length > 0 && (result.rows[0].count as number) > 0 + } catch (error) { + console.error('[BIPUsersDAO] User exists error:', error) + return false + } + } + + /** + * Disconnect from database + */ + async disconnect(): Promise { + if (this.mysqlService) { + await this.mysqlService.disconnect() + this.mysqlService = null + } + } +} diff --git a/src/main/services/user/session-manager.ts b/src/main/services/user/session-manager.ts new file mode 100644 index 0000000..c29102c --- /dev/null +++ b/src/main/services/user/session-manager.ts @@ -0,0 +1,185 @@ +/** + * User Session Manager - Singleton pattern for managing authenticated user session + * + * Mimics the Python SessionManager functionality: + * - Singleton pattern to maintain user state throughout application lifecycle + * - Support for username/password authentication + * - Support for silent login by computer name + * - Admin user can switch to other users + */ + +import type { UserInfo } from '../../types/user.types' + +/** + * Session Manager Class + */ +export class SessionManager { + private static instance: SessionManager | null = null + private currentUser: UserInfo | null = null + private originalAdminUser: UserInfo | null = null + private initialized: boolean = false + + private constructor() { + if (this.initialized) { + return + } + this.initialized = true + } + + /** + * Get the singleton instance + */ + public static getInstance(): SessionManager { + if (SessionManager.instance === null) { + SessionManager.instance = new SessionManager() + } + return SessionManager.instance + } + + /** + * Authenticate and login a user + * @param username - The username to authenticate + * @param password - The password to verify + * @returns True if login successful, false otherwise + */ + public async login(username: string, password: string): Promise { + try { + const { BIPUsersDAO } = await import('./bip-users-dao') + const dao = new BIPUsersDAO() + const userInfo = await dao.authenticate(username, password) + + if (userInfo) { + this.currentUser = { + id: userInfo.id, + username: userInfo.username, + userType: userInfo.userType + } + return true + } + return false + } catch (error) { + console.error('[SessionManager] Login error:', error) + return false + } + } + + /** + * Attempt silent login using computer name + * @returns True if login successful, false otherwise + */ + public async loginByComputerName(): Promise { + try { + const { BIPUsersDAO } = await import('./bip-users-dao') + const dao = new BIPUsersDAO() + const computerName = require('os').hostname() + const userInfo = await dao.authenticateByComputerName(computerName) + + if (userInfo) { + this.currentUser = { + id: userInfo.id, + username: userInfo.username, + userType: userInfo.userType + } + return true + } + return false + } catch (error) { + console.error('[SessionManager] Silent login error:', error) + return false + } + } + + /** + * Logout the current user and clear session + */ + public logout(): void { + this.currentUser = null + this.originalAdminUser = null + } + + /** + * Check if a user is currently authenticated + */ + public isAuthenticated(): boolean { + return this.currentUser !== null + } + + /** + * Check if the current user is an admin + */ + public isAdmin(): boolean { + return this.currentUser?.userType === 'Admin' + } + + /** + * Check if the current user is a guest + */ + public isGuest(): boolean { + return this.currentUser?.userType === 'Guest' + } + + /** + * Get the current username + */ + public getUsername(): string | null { + return this.currentUser?.username ?? null + } + + /** + * Get the current user type + */ + public getUserType(): string | null { + return this.currentUser?.userType ?? null + } + + /** + * Get all current user information + */ + public getUserInfo(): UserInfo | null { + return this.currentUser + } + + /** + * Switch to a different user (Admin only feature) + * @param userInfo - User info to switch to + * @returns True if switch successful + */ + public switchUser(userInfo: UserInfo): boolean { + if (!this.currentUser) { + return false + } + + // Store original admin user for reference + if (!this.originalAdminUser) { + this.originalAdminUser = { ...this.currentUser } + } + + this.currentUser = { + id: userInfo.id, + username: userInfo.username, + userType: userInfo.userType + } + return true + } + + /** + * Get the original Admin user before any user switch + */ + public getOriginalAdmin(): UserInfo | null { + return this.originalAdminUser + } + + /** + * Get all users from database (for Admin user selection) + */ + public async getAllUsers(): Promise { + try { + const { BIPUsersDAO } = await import('./bip-users-dao') + const dao = new BIPUsersDAO() + return await dao.getAllUsers() + } catch (error) { + console.error('[SessionManager] Get all users error:', error) + return [] + } + } +} diff --git a/src/main/types/user.types.ts b/src/main/types/user.types.ts new file mode 100644 index 0000000..23cb6fa --- /dev/null +++ b/src/main/types/user.types.ts @@ -0,0 +1,36 @@ +/** + * User types and interfaces + */ + +/** + * User type enumeration + */ +export type UserType = 'Admin' | 'User' | 'Guest' + +/** + * User information interface + */ +export interface UserInfo { + /** User ID */ + id: number + /** Username */ + username: string + /** User type */ + userType: UserType + /** Create time */ + createTime?: Date +} + +/** + * User session interface + */ +export interface UserSession { + /** Current authenticated user */ + user: UserInfo | null + /** Session token (optional for future use) */ + token?: string + /** Login timestamp */ + loginTime: Date + /** Whether session is active */ + isActive: boolean +} diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 1187e05..14d6452 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -1,5 +1,13 @@ import type { FileAPI, ExtractorAPI, CleanerAPI, DatabaseAPI } from '../main/types/ipc-api.types' import type { ResolverInput, ResolverResponse } from '../main/ipc/resolver-handler' +import type { UserInfo } from '../main/types/user.types' +import type { + LoginRequest, + LoginResponse, + SilentLoginResponse, + UserSelectionResponse, + CurrentUserResponse +} from '../main/ipc/auth-handler' /** * Order number resolver API @@ -21,6 +29,46 @@ export interface ResolverAPI { }> } +/** + * Authentication API + */ +export interface AuthAPI { + /** + * Get computer name + */ + getComputerName: () => Promise + /** + * Silent login by computer name + */ + silentLogin: () => Promise + /** + * Login with username and password + * @param request - Login request with username and password + */ + login: (request: LoginRequest) => Promise + /** + * Logout + */ + logout: () => Promise + /** + * Get current user + */ + getCurrentUser: () => Promise + /** + * Get all users (for admin user selection) + */ + getAllUsers: () => Promise + /** + * Switch user (admin only) + * @param userInfo - User info to switch to + */ + switchUser: (userInfo: UserInfo) => Promise + /** + * Check if current user is admin + */ + isAdmin: () => Promise +} + declare global { interface Window { electron: { @@ -44,6 +92,7 @@ declare global { cleaner: CleanerAPI database: DatabaseAPI resolver: ResolverAPI + auth: AuthAPI } api: unknown } diff --git a/src/preload/index.ts b/src/preload/index.ts index b28628c..6dd1451 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -4,6 +4,8 @@ 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' import type { ResolverInput } from '../main/ipc/resolver-handler' +import type { LoginRequest } from '../main/ipc/auth-handler' +import type { UserInfo } from '../main/types/user.types' // Custom APIs for renderer const api = { @@ -32,6 +34,18 @@ const api = { validateFormat: (inputs: string[]) => ipcRenderer.invoke('resolver:validateFormat', inputs) }, + // Authentication service + auth: { + getComputerName: () => ipcRenderer.invoke('auth:getComputerName'), + silentLogin: () => ipcRenderer.invoke('auth:silentLogin'), + login: (request: LoginRequest) => ipcRenderer.invoke('auth:login', request), + logout: () => ipcRenderer.invoke('auth:logout'), + getCurrentUser: () => ipcRenderer.invoke('auth:getCurrentUser'), + getAllUsers: () => ipcRenderer.invoke('auth:getAllUsers'), + switchUser: (userInfo: UserInfo) => ipcRenderer.invoke('auth:switchUser', userInfo), + isAdmin: () => ipcRenderer.invoke('auth:isAdmin') + }, + // Database service database: { connectMySql: (config: MySqlConfig) => ipcRenderer.invoke('database:mysql:connect', config), diff --git a/src/renderer/index.html b/src/renderer/index.html index e198e05..31f4207 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -2,11 +2,11 @@ - Electron + ERP Auto Tool diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 4ddd417..faa545e 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,101 +1,315 @@ -import { useState } from 'react' -import Versions from './components/Versions' +/** + * ERP App - Main application with authentication + * + * Mimics the Python ERPApp functionality: + * - Silent login by computer name on startup + * - Show login dialog if silent login fails + * - Show user selection dialog for Admin users + * - Display main content after successful authentication + */ + +import React, { useState, useEffect } from 'react' +import LoginDialog from './components/LoginDialog' import { ExtractorPage } from './pages/ExtractorPage' import { CleanerPage } from './pages/CleanerPage' -import electronLogo from './assets/electron.svg' type Page = 'home' | 'extractor' | 'cleaner' +interface CurrentUser { + username: string + userType: 'Admin' | 'User' | 'Guest' +} + function App(): React.JSX.Element { + // Authentication state + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isAuthenticating, setIsAuthenticating] = useState(true) + const [currentUser, setCurrentUser] = useState(null) + const [computerName, setComputerName] = useState('') + + // Dialog state + const [showLoginDialog, setShowLoginDialog] = useState(false) + const [errorMessage, setErrorMessage] = useState('') + + // Navigation state const [currentPage, setCurrentPage] = useState('home') - const renderPage = () => { - switch (currentPage) { - case 'extractor': - return - case 'cleaner': - return - default: - return ( - <> - logo -
Powered by electron-vite
-
- Build an Electron app with React -  and TypeScript -
-

- Please try pressing F12 to open the devTool -

- - - - ) + // Load error message from sessionStorage + const showError = (message: string) => { + setErrorMessage(message) + setTimeout(() => setErrorMessage(''), 3000) + } + + // Initialize authentication on mount + useEffect(() => { + console.log('=== App: Initializing auth... ===') + initializeAuth() + }, []) + + const initializeAuth = async () => { + console.log('=== App: Starting initializeAuth ===') + try { + // Get computer name + console.log('Getting computer name...') + const name = await window.electron.auth.getComputerName() + console.log('Computer name:', name) + setComputerName(name) + + // Try silent login + console.log('Trying silent login...') + const result = await window.electron.auth.silentLogin() + console.log('Silent login result:', result) + + if (result.success && result.userInfo) { + console.log('Silent login success:', result.userInfo) + setCurrentUser({ + username: result.userInfo.username, + userType: result.userInfo.userType + }) + + // Check if admin needs user selection + if (result.requiresUserSelection) { + console.log('Admin user needs to select user - logging in anyway') + // For now, just log in as the current user + setIsAuthenticated(true) + } else { + console.log('Setting authenticated to true') + setIsAuthenticated(true) + } + } else { + console.log('Silent login failed, showing login dialog') + // Silent login failed, show login dialog + setShowLoginDialog(true) + } + } catch (error) { + console.error('Auth initialization error:', error) + setShowLoginDialog(true) + } finally { + console.log('Setting isAuthenticating to false') + setIsAuthenticating(false) + } + console.log('=== App: Auth initialization complete ===') + } + + // Handle login dialog submit + const handleLogin = async (username: string, password: string): Promise => { + try { + const result = await window.electron.auth.login({ username, password }) + + if (result.success && result.userInfo) { + setCurrentUser({ + username: result.userInfo.username, + userType: result.userInfo.userType + }) + + // Check if admin needs user selection + if (result.userInfo.userType === 'Admin') { + setShowLoginDialog(false) + // TODO: Show user selection dialog + console.log('Admin user needs to select user') + } else { + setIsAuthenticated(true) + setShowLoginDialog(false) + } + return true + } + return false + } catch (error) { + console.error('Login error:', error) + return false } } + // Handle login dialog cancel + const handleLoginCancel = () => { + // User cancelled, keep showing dialog or exit + setShowLoginDialog(false) + } + + // Handle logout + const handleLogout = async () => { + await window.electron.auth.logout() + setIsAuthenticated(false) + setCurrentUser(null) + setShowLoginDialog(true) + } + + // Show loading state during authentication + if (isAuthenticating) { + return ( +
+
+
+

认证中...

+
+ +
+ ) + } + + // Show login dialog if not authenticated + if (!isAuthenticated) { + console.log('Render: not authenticated, showLoginDialog:', showLoginDialog, 'computerName:', computerName) + return ( + <> + + + {errorMessage && ( +
{errorMessage}
+ )} + + {/* If showLoginDialog is false but not authenticated, show a message */} + {!showLoginDialog && ( +
+
+

+ 欢迎,{currentUser?.username}! +

+

正在加载主界面...

+
+
+ )} + + + + ) + } + + // Show main content when authenticated + console.log('Render: authenticated, currentUser:', currentUser, 'currentPage:', currentPage) return ( -
- {currentPage !== 'home' && ( - - )} - {renderPage()} - +
+ + + {/* Main content */} +
+ {currentPage === 'home' && ( +
+

主页面

+
+
setCurrentPage('extractor')} style={{ + padding: '24px', + backgroundColor: '#fff', + borderRadius: '8px', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', + cursor: 'pointer', + textAlign: 'center' + }}> +

数据提取

+

提取 ERP 数据

+
+
setCurrentPage('cleaner')} style={{ + padding: '24px', + backgroundColor: '#fff', + borderRadius: '8px', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', + cursor: 'pointer', + textAlign: 'center' + }}> +

物料清理

+

清理 ERP 物料

+
+
+
+ )} + + {currentPage === 'extractor' && } + {currentPage === 'cleaner' && } +
) } diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 0179fc4..c8cfc9f 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -1,37 +1,18 @@ @import './base.css'; body { - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; + display: block; + margin: 0; + padding: 0; + overflow: auto; background-image: url('./wavy-lines.svg'); background-size: cover; user-select: none; } -code { - font-weight: 600; - padding: 3px 5px; - border-radius: 2px; - background-color: var(--color-background-mute); - font-family: - ui-monospace, - SFMono-Regular, - SF Mono, - Menlo, - Consolas, - Liberation Mono, - monospace; - font-size: 85%; -} - #root { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - margin-bottom: 80px; + width: 100%; + min-height: 100vh; } .logo { diff --git a/src/renderer/src/components/LoginDialog.tsx b/src/renderer/src/components/LoginDialog.tsx new file mode 100644 index 0000000..173c256 --- /dev/null +++ b/src/renderer/src/components/LoginDialog.tsx @@ -0,0 +1,267 @@ +/** + * Login Dialog - Modal dialog for user authentication + * + * Mimics the Python LoginDialog functionality: + * - Modal dialog for username/password input + * - Display computer name + * - Enter key to submit + */ + +import React, { useState, useEffect, useRef } from 'react' + +interface LoginDialogProps { + isOpen: boolean + computerName: string + onLogin: (username: string, password: string) => Promise + onCancel: () => void + onError: (message: string) => void +} + +export const LoginDialog: React.FC = ({ + isOpen, + computerName, + onLogin, + onCancel, + onError +}) => { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [isLoggingIn, setIsLoggingIn] = useState(false) + const usernameInputRef = useRef(null) + + // Focus on username input when dialog opens + useEffect(() => { + if (isOpen && usernameInputRef.current) { + usernameInputRef.current.focus() + } + }, [isOpen]) + + const handleLogin = async () => { + if (!username.trim()) { + onError('请输入用户名') + usernameInputRef.current?.focus() + return + } + + if (!password.trim()) { + onError('请输入密码') + return + } + + setIsLoggingIn(true) + const success = await onLogin(username.trim(), password.trim()) + setIsLoggingIn(false) + + if (!success) { + onError('用户名或密码错误') + setPassword('') + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleLogin() + } else if (e.key === 'Escape') { + onCancel() + } + } + + if (!isOpen) return null + + return ( +
+
+
+

请登录

+

当前计算机:{computerName}

+
+ +
+
+ + setUsername(e.target.value)} + placeholder="请输入用户名" + disabled={isLoggingIn} + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="请输入密码" + disabled={isLoggingIn} + /> +
+
+ +
+ + +
+ +
+ v1.0 +
+
+ + +
+ ) +} + +export default LoginDialog diff --git a/src/renderer/src/components/UserSelectionDialog.tsx b/src/renderer/src/components/UserSelectionDialog.tsx new file mode 100644 index 0000000..f14e7d9 --- /dev/null +++ b/src/renderer/src/components/UserSelectionDialog.tsx @@ -0,0 +1,290 @@ +/** + * User Selection Dialog - For Admin user to select which user to operate as + * + * Mimics the Python UserSelectionDialog functionality: + * - Display list of all users + * - Allow admin to select a user + * - Return selected user info + */ + +import React, { useState, useEffect } from 'react' + +export interface UserInfo { + id: number + username: string + userType: 'Admin' | 'User' | 'Guest' + createTime?: Date +} + +interface UserSelectionDialogProps { + isOpen: boolean + users: UserInfo[] + currentUsername: string + onSelectUser: (user: UserInfo) => void + onCancel: () => void +} + +export const UserSelectionDialog: React.FC = ({ + isOpen, + users, + currentUsername, + onSelectUser, + onCancel +}) => { + const [selectedUserId, setSelectedUserId] = useState(null) + + // Reset selection when dialog opens + useEffect(() => { + if (isOpen) { + setSelectedUserId(null) + } + }, [isOpen]) + + const handleConfirm = () => { + if (selectedUserId === null) { + return + } + + const selectedUser = users.find(u => u.id === selectedUserId) + if (selectedUser) { + onSelectUser(selectedUser) + } + } + + const handleDoubleClick = (user: UserInfo) => { + onSelectUser(user) + } + + if (!isOpen) return null + + return ( +
+
+
+

选择用户

+

当前登录:{currentUsername}

+
+ +
+
+ {users.map((user) => ( +
setSelectedUserId(user.id)} + onDoubleClick={() => handleDoubleClick(user)} + > +
+
+ {user.username} + + {user.userType} + +
+ {user.createTime && ( +
+ + 创建于:{new Date(user.createTime).toLocaleString('zh-CN')} + +
+ )} +
+
+ ))} +
+
+ +
+ + +
+ +
+ 双击用户可直接选择 +
+
+ + +
+ ) +} + +export default UserSelectionDialog