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
This commit is contained in:
223
src/main/ipc/auth-handler.ts
Normal file
223
src/main/ipc/auth-handler.ts
Normal file
@@ -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<string> => {
|
||||||
|
const os = await import('os')
|
||||||
|
return os.hostname()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Silent login by computer name
|
||||||
|
*/
|
||||||
|
ipcMain.handle(
|
||||||
|
'auth:silentLogin',
|
||||||
|
async (): Promise<SilentLoginResponse> => {
|
||||||
|
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<LoginResponse> => {
|
||||||
|
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<void> => {
|
||||||
|
sessionManager.logout()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user
|
||||||
|
*/
|
||||||
|
ipcMain.handle(
|
||||||
|
'auth:getCurrentUser',
|
||||||
|
async (): Promise<CurrentUserResponse> => {
|
||||||
|
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<UserInfo[]> => {
|
||||||
|
return await sessionManager.getAllUsers()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch user (admin only)
|
||||||
|
*/
|
||||||
|
ipcMain.handle(
|
||||||
|
'auth:switchUser',
|
||||||
|
async (_event, userInfo: UserInfo): Promise<UserSelectionResponse> => {
|
||||||
|
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<boolean> => {
|
||||||
|
return sessionManager.isAdmin()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { registerExtractorHandlers } from './extractor-handler'
|
|||||||
import { registerCleanerHandlers } from './cleaner-handler'
|
import { registerCleanerHandlers } from './cleaner-handler'
|
||||||
import { registerDatabaseHandlers } from './database-handler'
|
import { registerDatabaseHandlers } from './database-handler'
|
||||||
import { registerResolverHandlers } from './resolver-handler'
|
import { registerResolverHandlers } from './resolver-handler'
|
||||||
|
import { registerAuthHandlers } from './auth-handler'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register all IPC handlers
|
* Register all IPC handlers
|
||||||
@@ -18,4 +19,5 @@ export function registerIpcHandlers(): void {
|
|||||||
registerCleanerHandlers()
|
registerCleanerHandlers()
|
||||||
registerDatabaseHandlers()
|
registerDatabaseHandlers()
|
||||||
registerResolverHandlers()
|
registerResolverHandlers()
|
||||||
|
registerAuthHandlers()
|
||||||
}
|
}
|
||||||
|
|||||||
298
src/main/services/user/bip-users-dao.ts
Normal file
298
src/main/services/user/bip-users-dao.ts
Normal file
@@ -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<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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<UserInfo | null> {
|
||||||
|
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<UserInfo | null> {
|
||||||
|
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<UserInfo[]> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<void> {
|
||||||
|
if (this.mysqlService) {
|
||||||
|
await this.mysqlService.disconnect()
|
||||||
|
this.mysqlService = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
185
src/main/services/user/session-manager.ts
Normal file
185
src/main/services/user/session-manager.ts
Normal file
@@ -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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<UserInfo[]> {
|
||||||
|
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 []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/main/types/user.types.ts
Normal file
36
src/main/types/user.types.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
49
src/preload/index.d.ts
vendored
49
src/preload/index.d.ts
vendored
@@ -1,5 +1,13 @@
|
|||||||
import type { FileAPI, ExtractorAPI, CleanerAPI, DatabaseAPI } from '../main/types/ipc-api.types'
|
import type { FileAPI, ExtractorAPI, CleanerAPI, DatabaseAPI } from '../main/types/ipc-api.types'
|
||||||
import type { ResolverInput, ResolverResponse } from '../main/ipc/resolver-handler'
|
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
|
* Order number resolver API
|
||||||
@@ -21,6 +29,46 @@ export interface ResolverAPI {
|
|||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication API
|
||||||
|
*/
|
||||||
|
export interface AuthAPI {
|
||||||
|
/**
|
||||||
|
* Get computer name
|
||||||
|
*/
|
||||||
|
getComputerName: () => Promise<string>
|
||||||
|
/**
|
||||||
|
* Silent login by computer name
|
||||||
|
*/
|
||||||
|
silentLogin: () => Promise<SilentLoginResponse>
|
||||||
|
/**
|
||||||
|
* Login with username and password
|
||||||
|
* @param request - Login request with username and password
|
||||||
|
*/
|
||||||
|
login: (request: LoginRequest) => Promise<LoginResponse>
|
||||||
|
/**
|
||||||
|
* Logout
|
||||||
|
*/
|
||||||
|
logout: () => Promise<void>
|
||||||
|
/**
|
||||||
|
* Get current user
|
||||||
|
*/
|
||||||
|
getCurrentUser: () => Promise<CurrentUserResponse>
|
||||||
|
/**
|
||||||
|
* Get all users (for admin user selection)
|
||||||
|
*/
|
||||||
|
getAllUsers: () => Promise<UserInfo[]>
|
||||||
|
/**
|
||||||
|
* Switch user (admin only)
|
||||||
|
* @param userInfo - User info to switch to
|
||||||
|
*/
|
||||||
|
switchUser: (userInfo: UserInfo) => Promise<UserSelectionResponse>
|
||||||
|
/**
|
||||||
|
* Check if current user is admin
|
||||||
|
*/
|
||||||
|
isAdmin: () => Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electron: {
|
electron: {
|
||||||
@@ -44,6 +92,7 @@ declare global {
|
|||||||
cleaner: CleanerAPI
|
cleaner: CleanerAPI
|
||||||
database: DatabaseAPI
|
database: DatabaseAPI
|
||||||
resolver: ResolverAPI
|
resolver: ResolverAPI
|
||||||
|
auth: AuthAPI
|
||||||
}
|
}
|
||||||
api: unknown
|
api: unknown
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types'
|
|||||||
import type { ExtractorInput } from '../main/types/extractor.types'
|
import type { ExtractorInput } from '../main/types/extractor.types'
|
||||||
import type { CleanerInput } from '../main/types/cleaner.types'
|
import type { CleanerInput } from '../main/types/cleaner.types'
|
||||||
import type { ResolverInput } from '../main/ipc/resolver-handler'
|
import type { ResolverInput } from '../main/ipc/resolver-handler'
|
||||||
|
import type { LoginRequest } from '../main/ipc/auth-handler'
|
||||||
|
import type { UserInfo } from '../main/types/user.types'
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
const api = {
|
const api = {
|
||||||
@@ -32,6 +34,18 @@ const api = {
|
|||||||
validateFormat: (inputs: string[]) => ipcRenderer.invoke('resolver:validateFormat', inputs)
|
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 service
|
||||||
database: {
|
database: {
|
||||||
connectMySql: (config: MySqlConfig) => ipcRenderer.invoke('database:mysql:connect', config),
|
connectMySql: (config: MySqlConfig) => ipcRenderer.invoke('database:mysql:connect', config),
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Electron</title>
|
<title>ERP Auto Tool</title>
|
||||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
|
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -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 { ExtractorPage } from './pages/ExtractorPage'
|
||||||
import { CleanerPage } from './pages/CleanerPage'
|
import { CleanerPage } from './pages/CleanerPage'
|
||||||
import electronLogo from './assets/electron.svg'
|
|
||||||
|
|
||||||
type Page = 'home' | 'extractor' | 'cleaner'
|
type Page = 'home' | 'extractor' | 'cleaner'
|
||||||
|
|
||||||
|
interface CurrentUser {
|
||||||
|
username: string
|
||||||
|
userType: 'Admin' | 'User' | 'Guest'
|
||||||
|
}
|
||||||
|
|
||||||
function App(): React.JSX.Element {
|
function App(): React.JSX.Element {
|
||||||
|
// Authentication state
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||||
|
const [isAuthenticating, setIsAuthenticating] = useState(true)
|
||||||
|
const [currentUser, setCurrentUser] = useState<CurrentUser | null>(null)
|
||||||
|
const [computerName, setComputerName] = useState('')
|
||||||
|
|
||||||
|
// Dialog state
|
||||||
|
const [showLoginDialog, setShowLoginDialog] = useState(false)
|
||||||
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
|
|
||||||
|
// Navigation state
|
||||||
const [currentPage, setCurrentPage] = useState<Page>('home')
|
const [currentPage, setCurrentPage] = useState<Page>('home')
|
||||||
|
|
||||||
const renderPage = () => {
|
// Load error message from sessionStorage
|
||||||
switch (currentPage) {
|
const showError = (message: string) => {
|
||||||
case 'extractor':
|
setErrorMessage(message)
|
||||||
return <ExtractorPage />
|
setTimeout(() => setErrorMessage(''), 3000)
|
||||||
case 'cleaner':
|
}
|
||||||
return <CleanerPage />
|
|
||||||
default:
|
// Initialize authentication on mount
|
||||||
return (
|
useEffect(() => {
|
||||||
<>
|
console.log('=== App: Initializing auth... ===')
|
||||||
<img alt="logo" className="logo" src={electronLogo} />
|
initializeAuth()
|
||||||
<div className="creator">Powered by electron-vite</div>
|
}, [])
|
||||||
<div className="text">
|
|
||||||
Build an Electron app with <span className="react">React</span>
|
const initializeAuth = async () => {
|
||||||
and <span className="ts">TypeScript</span>
|
console.log('=== App: Starting initializeAuth ===')
|
||||||
</div>
|
try {
|
||||||
<p className="tip">
|
// Get computer name
|
||||||
Please try pressing <code>F12</code> to open the devTool
|
console.log('Getting computer name...')
|
||||||
</p>
|
const name = await window.electron.auth.getComputerName()
|
||||||
<div className="actions">
|
console.log('Computer name:', name)
|
||||||
<div className="action">
|
setComputerName(name)
|
||||||
<a
|
|
||||||
href="#"
|
// Try silent login
|
||||||
onClick={(e) => {
|
console.log('Trying silent login...')
|
||||||
e.preventDefault()
|
const result = await window.electron.auth.silentLogin()
|
||||||
setCurrentPage('extractor')
|
console.log('Silent login result:', result)
|
||||||
}}
|
|
||||||
>
|
if (result.success && result.userInfo) {
|
||||||
数据提取
|
console.log('Silent login success:', result.userInfo)
|
||||||
</a>
|
setCurrentUser({
|
||||||
</div>
|
username: result.userInfo.username,
|
||||||
<div className="action">
|
userType: result.userInfo.userType
|
||||||
<a
|
})
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
// Check if admin needs user selection
|
||||||
e.preventDefault()
|
if (result.requiresUserSelection) {
|
||||||
setCurrentPage('cleaner')
|
console.log('Admin user needs to select user - logging in anyway')
|
||||||
}}
|
// For now, just log in as the current user
|
||||||
>
|
setIsAuthenticated(true)
|
||||||
物料清理
|
} else {
|
||||||
</a>
|
console.log('Setting authenticated to true')
|
||||||
</div>
|
setIsAuthenticated(true)
|
||||||
<div className="action">
|
}
|
||||||
<a href="https://electron-vite.org/" target="_blank" rel="noreferrer">
|
} else {
|
||||||
Documentation
|
console.log('Silent login failed, showing login dialog')
|
||||||
</a>
|
// Silent login failed, show login dialog
|
||||||
</div>
|
setShowLoginDialog(true)
|
||||||
</div>
|
}
|
||||||
<Versions />
|
} 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<boolean> => {
|
||||||
|
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 (
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show login dialog if not authenticated
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
console.log('Render: not authenticated, showLoginDialog:', showLoginDialog, 'computerName:', computerName)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LoginDialog
|
||||||
|
isOpen={showLoginDialog}
|
||||||
|
computerName={computerName}
|
||||||
|
onLogin={handleLogin}
|
||||||
|
onCancel={handleLoginCancel}
|
||||||
|
onError={showError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="error-toast">{errorMessage}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* If showLoginDialog is false but not authenticated, show a message */}
|
||||||
|
{!showLoginDialog && (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
minHeight: '100vh',
|
||||||
|
backgroundColor: '#f5f7fa'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
padding: '40px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: '8px',
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
|
<p style={{ color: '#52c41a', fontSize: '18px', fontWeight: 600 }}>
|
||||||
|
欢迎,{currentUser?.username}!
|
||||||
|
</p>
|
||||||
|
<p style={{ color: '#666', marginTop: '16px' }}>正在加载主界面...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.error-toast {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #fff1f0;
|
||||||
|
border: 1px solid #ffa39e;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 10000;
|
||||||
|
animation: slideDown 0.3s ease-out;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show main content when authenticated
|
||||||
|
console.log('Render: authenticated, currentUser:', currentUser, 'currentPage:', currentPage)
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div style={{
|
||||||
{currentPage !== 'home' && (
|
minHeight: '100vh',
|
||||||
<nav className="nav">
|
backgroundColor: '#f5f7fa'
|
||||||
<button className="nav-btn" onClick={() => setCurrentPage('home')}>
|
}}>
|
||||||
← 返回主页
|
{/* Header with user info and logout */}
|
||||||
|
<header style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '12px 24px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
marginBottom: '16px'
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<span style={{
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#666'
|
||||||
|
}}>
|
||||||
|
欢迎,{currentUser?.username} ({currentUser?.userType})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={handleLogout} style={{
|
||||||
|
padding: '8px 16px',
|
||||||
|
backgroundColor: '#ff4d4f',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '14px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}>
|
||||||
|
退出登录
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</div>
|
||||||
)}
|
</header>
|
||||||
{renderPage()}
|
|
||||||
<style>{`
|
{/* Main content */}
|
||||||
.app {
|
<div style={{ padding: '24px' }}>
|
||||||
min-height: 100vh;
|
{currentPage === 'home' && (
|
||||||
background: #f5f7fa;
|
<div>
|
||||||
}
|
<h1 style={{ fontSize: '24px', color: '#333', marginBottom: '24px' }}>主页面</h1>
|
||||||
.nav {
|
<div style={{
|
||||||
background: #fff;
|
display: 'grid',
|
||||||
padding: 12px 24px;
|
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
gap: '16px'
|
||||||
}
|
}}>
|
||||||
.nav-btn {
|
<div onClick={() => setCurrentPage('extractor')} style={{
|
||||||
background: #1890ff;
|
padding: '24px',
|
||||||
color: #fff;
|
backgroundColor: '#fff',
|
||||||
border: none;
|
borderRadius: '8px',
|
||||||
padding: 8px 16px;
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
border-radius: 6px;
|
cursor: 'pointer',
|
||||||
cursor: pointer;
|
textAlign: 'center'
|
||||||
font-size: 14px;
|
}}>
|
||||||
transition: background 0.3s;
|
<h3 style={{ margin: '0 0 8px 0', color: '#1890ff' }}>数据提取</h3>
|
||||||
}
|
<p style={{ margin: 0, color: '#666', fontSize: '14px' }}>提取 ERP 数据</p>
|
||||||
.nav-btn:hover {
|
</div>
|
||||||
background: #40a9ff;
|
<div onClick={() => setCurrentPage('cleaner')} style={{
|
||||||
}
|
padding: '24px',
|
||||||
`}</style>
|
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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentPage === 'extractor' && <ExtractorPage />}
|
||||||
|
{currentPage === 'cleaner' && <CleanerPage />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,18 @@
|
|||||||
@import './base.css';
|
@import './base.css';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: block;
|
||||||
align-items: center;
|
margin: 0;
|
||||||
justify-content: center;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
background-image: url('./wavy-lines.svg');
|
background-image: url('./wavy-lines.svg');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
user-select: none;
|
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 {
|
#root {
|
||||||
display: flex;
|
width: 100%;
|
||||||
align-items: center;
|
min-height: 100vh;
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 80px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
|||||||
267
src/renderer/src/components/LoginDialog.tsx
Normal file
267
src/renderer/src/components/LoginDialog.tsx
Normal file
@@ -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<boolean>
|
||||||
|
onCancel: () => void
|
||||||
|
onError: (message: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoginDialog: React.FC<LoginDialogProps> = ({
|
||||||
|
isOpen,
|
||||||
|
computerName,
|
||||||
|
onLogin,
|
||||||
|
onCancel,
|
||||||
|
onError
|
||||||
|
}) => {
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [isLoggingIn, setIsLoggingIn] = useState(false)
|
||||||
|
const usernameInputRef = useRef<HTMLInputElement>(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 (
|
||||||
|
<div className="login-overlay" onKeyDown={handleKeyDown}>
|
||||||
|
<div className="login-dialog">
|
||||||
|
<div className="login-header">
|
||||||
|
<h2 className="login-title">请登录</h2>
|
||||||
|
<p className="computer-name">当前计算机:{computerName}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="login-body">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">用户名:</label>
|
||||||
|
<input
|
||||||
|
ref={usernameInputRef}
|
||||||
|
type="text"
|
||||||
|
className="form-input"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
disabled={isLoggingIn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">密码:</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-input"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="请输入密码"
|
||||||
|
disabled={isLoggingIn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="login-footer">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleLogin}
|
||||||
|
disabled={isLoggingIn}
|
||||||
|
>
|
||||||
|
{isLoggingIn ? '登录中...' : '登录'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={isLoggingIn}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="login-version">
|
||||||
|
v1.0
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.login-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-dialog {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 400px;
|
||||||
|
padding: 24px;
|
||||||
|
animation: slideDown 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.computer-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-body {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s, box-shadow 0.3s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #1890ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:disabled {
|
||||||
|
background: #f5f5f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-version {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginDialog
|
||||||
290
src/renderer/src/components/UserSelectionDialog.tsx
Normal file
290
src/renderer/src/components/UserSelectionDialog.tsx
Normal file
@@ -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<UserSelectionDialogProps> = ({
|
||||||
|
isOpen,
|
||||||
|
users,
|
||||||
|
currentUsername,
|
||||||
|
onSelectUser,
|
||||||
|
onCancel
|
||||||
|
}) => {
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState<number | null>(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 (
|
||||||
|
<div className="user-selection-overlay">
|
||||||
|
<div className="user-selection-dialog">
|
||||||
|
<div className="user-selection-header">
|
||||||
|
<h2 className="user-selection-title">选择用户</h2>
|
||||||
|
<p className="user-selection-hint">当前登录:{currentUsername}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="user-selection-body">
|
||||||
|
<div className="user-list">
|
||||||
|
{users.map((user) => (
|
||||||
|
<div
|
||||||
|
key={user.id}
|
||||||
|
className={`user-item ${selectedUserId === user.id ? 'selected' : ''}`}
|
||||||
|
onClick={() => setSelectedUserId(user.id)}
|
||||||
|
onDoubleClick={() => handleDoubleClick(user)}
|
||||||
|
>
|
||||||
|
<div className="user-item-content">
|
||||||
|
<div className="user-item-row">
|
||||||
|
<span className="user-name">{user.username}</span>
|
||||||
|
<span className={`user-type user-type-${user.userType.toLowerCase()}`}>
|
||||||
|
{user.userType}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{user.createTime && (
|
||||||
|
<div className="user-item-row">
|
||||||
|
<span className="user-create-time">
|
||||||
|
创建于:{new Date(user.createTime).toLocaleString('zh-CN')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="user-selection-footer">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={selectedUserId === null}
|
||||||
|
>
|
||||||
|
确认
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="user-selection-hint-footer">
|
||||||
|
双击用户可直接选择
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.user-selection-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-selection-dialog {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 450px;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-selection-header {
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-selection-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-selection-hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-selection-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px 24px;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
background: #f6ffed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item.selected {
|
||||||
|
border-color: #1890ff;
|
||||||
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-type {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-type-admin {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-type-user {
|
||||||
|
background: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-type-guest {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-create-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-selection-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-selection-hint-footer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserSelectionDialog
|
||||||
Reference in New Issue
Block a user