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:
@@ -2,11 +2,11 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Electron</title>
|
||||
<title>ERP Auto Tool</title>
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
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>
|
||||
|
||||
|
||||
@@ -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<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 renderPage = () => {
|
||||
switch (currentPage) {
|
||||
case 'extractor':
|
||||
return <ExtractorPage />
|
||||
case 'cleaner':
|
||||
return <CleanerPage />
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<img alt="logo" className="logo" src={electronLogo} />
|
||||
<div className="creator">Powered by electron-vite</div>
|
||||
<div className="text">
|
||||
Build an Electron app with <span className="react">React</span>
|
||||
and <span className="ts">TypeScript</span>
|
||||
</div>
|
||||
<p className="tip">
|
||||
Please try pressing <code>F12</code> to open the devTool
|
||||
</p>
|
||||
<div className="actions">
|
||||
<div className="action">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentPage('extractor')
|
||||
}}
|
||||
>
|
||||
数据提取
|
||||
</a>
|
||||
</div>
|
||||
<div className="action">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentPage('cleaner')
|
||||
}}
|
||||
>
|
||||
物料清理
|
||||
</a>
|
||||
</div>
|
||||
<div className="action">
|
||||
<a href="https://electron-vite.org/" target="_blank" rel="noreferrer">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Versions />
|
||||
</>
|
||||
)
|
||||
// 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<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 (
|
||||
<div className="app">
|
||||
{currentPage !== 'home' && (
|
||||
<nav className="nav">
|
||||
<button className="nav-btn" onClick={() => setCurrentPage('home')}>
|
||||
← 返回主页
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
backgroundColor: '#f5f7fa'
|
||||
}}>
|
||||
{/* 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>
|
||||
</nav>
|
||||
)}
|
||||
{renderPage()}
|
||||
<style>{`
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
.nav {
|
||||
background: #fff;
|
||||
padding: 12px 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.nav-btn {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.nav-btn:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main content */}
|
||||
<div style={{ padding: '24px' }}>
|
||||
{currentPage === 'home' && (
|
||||
<div>
|
||||
<h1 style={{ fontSize: '24px', color: '#333', marginBottom: '24px' }}>主页面</h1>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: '16px'
|
||||
}}>
|
||||
<div onClick={() => setCurrentPage('extractor')} style={{
|
||||
padding: '24px',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<h3 style={{ margin: '0 0 8px 0', color: '#1890ff' }}>数据提取</h3>
|
||||
<p style={{ margin: 0, color: '#666', fontSize: '14px' }}>提取 ERP 数据</p>
|
||||
</div>
|
||||
<div onClick={() => setCurrentPage('cleaner')} style={{
|
||||
padding: '24px',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<h3 style={{ margin: '0 0 8px 0', color: '#52c41a' }}>物料清理</h3>
|
||||
<p style={{ margin: 0, color: '#666', fontSize: '14px' }}>清理 ERP 物料</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentPage === 'extractor' && <ExtractorPage />}
|
||||
{currentPage === 'cleaner' && <CleanerPage />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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