feat: implement settings interface with user permission control
New Features: - Add SettingsPage component with Admin/User view differentiation - Create ConfigManager service for .env file management - Add IPC handlers for settings CRUD operations - Implement connection testing for ERP and database - Add settings navigation to main app Files Created: - src/main/types/settings.types.ts - Type definitions - src/main/services/config/config-manager.ts - Config service - src/main/ipc/settings-handler.ts - IPC handlers - src/renderer/src/pages/SettingsPage.tsx - React UI component Files Modified: - src/main/ipc/index.ts - Register settings handlers - src/preload/index.ts - Expose settings API - src/preload/index.d.ts - Add SettingsAPI type - src/renderer/src/App.tsx - Add settings navigation Co-Authored-By: Claude (qwen3.5-plus) <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,9 @@ import LoginDialog from './components/LoginDialog'
|
||||
import UserSelectionDialog, { type UserInfo as SelectedUserInfo } from './components/UserSelectionDialog'
|
||||
import { ExtractorPage } from './pages/ExtractorPage'
|
||||
import { CleanerPage } from './pages/CleanerPage'
|
||||
import SettingsPage from './pages/SettingsPage'
|
||||
|
||||
type Page = 'home' | 'extractor' | 'cleaner'
|
||||
type Page = 'home' | 'extractor' | 'cleaner' | 'settings'
|
||||
|
||||
interface CurrentUser {
|
||||
username: string
|
||||
@@ -354,6 +355,21 @@ function App(): React.JSX.Element {
|
||||
>
|
||||
物料清理
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('settings')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: currentPage === 'settings' ? '#1890ff' : '#f5f5f5',
|
||||
color: currentPage === 'settings' ? '#fff' : '#666',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s'
|
||||
}}
|
||||
>
|
||||
系统设置
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div>
|
||||
@@ -379,13 +395,14 @@ function App(): React.JSX.Element {
|
||||
<div>
|
||||
<h1 style={{ fontSize: '24px', color: '#333', marginBottom: '24px' }}>主页面</h1>
|
||||
<p style={{ color: '#666', fontSize: '14px' }}>
|
||||
请使用顶部导航栏切换到 数据提取 或 物料清理 页面
|
||||
请使用顶部导航栏切换到 数据提取、物料清理 或 系统设置 页面
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentPage === 'extractor' && <ExtractorPage />}
|
||||
{currentPage === 'cleaner' && <CleanerPage />}
|
||||
{currentPage === 'settings' && <SettingsPage />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -24,13 +24,7 @@ interface ValidationResult {
|
||||
*/
|
||||
export const CleanerPage: React.FC = () => {
|
||||
// State with sessionStorage persistence
|
||||
const [orderNumbers, setOrderNumbers] = useState(() => {
|
||||
return sessionStorage.getItem('cleaner_orderNumbers') || ''
|
||||
})
|
||||
const [materialCodes, setMaterialCodes] = useState(() => {
|
||||
return sessionStorage.getItem('cleaner_materialCodes') || ''
|
||||
})
|
||||
const [dryRun, setDryRun] = useState(() => {
|
||||
const [dryRun] = useState(() => {
|
||||
const saved = sessionStorage.getItem('cleaner_dryRun')
|
||||
return saved ? saved === 'true' : true
|
||||
})
|
||||
@@ -92,13 +86,6 @@ export const CleanerPage: React.FC = () => {
|
||||
results = results.filter(r => !hiddenItems.has(r.materialCode))
|
||||
return results
|
||||
}, [validationResults, isAdmin, currentUsername, managers, selectedManagers, hiddenItems])
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem('cleaner_orderNumbers', orderNumbers)
|
||||
}, [orderNumbers])
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem('cleaner_materialCodes', materialCodes)
|
||||
}, [materialCodes])
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem('cleaner_dryRun', dryRun.toString())
|
||||
@@ -121,11 +108,11 @@ export const CleanerPage: React.FC = () => {
|
||||
const admin = await window.electron.auth.isAdmin()
|
||||
const user = await window.electron.auth.getCurrentUser()
|
||||
setIsAdmin(admin)
|
||||
if (user) {
|
||||
setCurrentUsername(user.username)
|
||||
if (user && user.userInfo) {
|
||||
setCurrentUsername(user.userInfo.username)
|
||||
if (!admin) {
|
||||
// Non-admin users can only see their own data
|
||||
setSelectedManagers(new Set([user.username]))
|
||||
setSelectedManagers(new Set([user.userInfo.username]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,64 +332,8 @@ export const CleanerPage: React.FC = () => {
|
||||
}
|
||||
}, [selectedItems, validationResults, loadManagers])
|
||||
|
||||
// Handle export results
|
||||
const handleExportResults = useCallback(async () => {
|
||||
if (validationResults.length === 0) {
|
||||
setError('没有数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Prepare data for export
|
||||
const data = validationResults.map(result => ({
|
||||
'选择': selectedItems.has(result.materialCode) ? '是' : '否',
|
||||
'材料名称': result.materialName,
|
||||
'材料代码': result.materialCode,
|
||||
'规格': result.specification,
|
||||
'型号': result.model,
|
||||
'负责人': result.managerName || ''
|
||||
}))
|
||||
|
||||
// Create CSV content
|
||||
const headers = ['选择', '材料名称', '材料代码', '规格', '型号', '负责人']
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...data.map(row =>
|
||||
headers.map(header => {
|
||||
const value = row[header as keyof typeof row]
|
||||
// Escape quotes and wrap in quotes if contains comma
|
||||
const escaped = String(value).replace(/"/g, '""')
|
||||
return escaped.includes(',') ? `"${escaped}"` : escaped
|
||||
}).join(',')
|
||||
)
|
||||
].join('\n')
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
|
||||
const link = document.createElement('a')
|
||||
const url = URL.createObjectURL(blob)
|
||||
link.setAttribute('href', url)
|
||||
link.setAttribute('download', `物料状态校验结果_${new Date().getTime()}.csv`)
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '导出失败')
|
||||
}
|
||||
}, [validationResults, selectedItems])
|
||||
|
||||
// Handle execute deletion
|
||||
// Handle execute deletion (清理)
|
||||
const handleExecuteDeletion = useCallback(async () => {
|
||||
if (!orderNumbers.trim()) {
|
||||
setError('请输入至少一个订单号')
|
||||
return
|
||||
}
|
||||
if (!materialCodes.trim()) {
|
||||
setError('请输入至少一个物料代码')
|
||||
return
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
const confirmed = window.confirm('警告:正式执行将删除 ERP 系统中的物料数据,是否继续?')
|
||||
if (!confirmed) return
|
||||
@@ -414,15 +345,35 @@ export const CleanerPage: React.FC = () => {
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const orderNumberList = orderNumbers
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
// Get cleaner data from backend (order numbers from shared Production IDs + material codes from DB)
|
||||
const cleanerDataResult = await window.electron.validation.getCleanerData()
|
||||
|
||||
const materialCodeList = materialCodes
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
if (!cleanerDataResult.success) {
|
||||
setError(cleanerDataResult.error || '获取清理数据失败')
|
||||
setIsRunning(false)
|
||||
return
|
||||
}
|
||||
|
||||
const orderNumberList = cleanerDataResult.orderNumbers || []
|
||||
const materialCodeList = cleanerDataResult.materialCodes || []
|
||||
|
||||
if (orderNumberList.length === 0) {
|
||||
setError('没有订单号数据。请先到数据提取页面输入 Production ID。')
|
||||
setIsRunning(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (materialCodeList.length === 0) {
|
||||
setError('没有物料代码数据。请确认已在物料清理界面勾选要删除的物料。')
|
||||
setIsRunning(false)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[CleanerPage] Starting cleaner with:', {
|
||||
orderNumbers: orderNumberList.length,
|
||||
materialCodes: materialCodeList.length,
|
||||
dryRun
|
||||
})
|
||||
|
||||
const response = await window.electron.cleaner.runCleaner({
|
||||
orderNumbers: orderNumberList,
|
||||
@@ -441,20 +392,7 @@ export const CleanerPage: React.FC = () => {
|
||||
setIsRunning(false)
|
||||
setProgress(null)
|
||||
}
|
||||
}, [orderNumbers, materialCodes, dryRun])
|
||||
|
||||
// Handle clean (alias for execute deletion)
|
||||
const handleClean = useCallback(handleExecuteDeletion, [handleExecuteDeletion])
|
||||
|
||||
// Handle reset
|
||||
const handleReset = useCallback(() => {
|
||||
setOrderNumbers('')
|
||||
setMaterialCodes('')
|
||||
setDryRun(true)
|
||||
setResult(null)
|
||||
setError(null)
|
||||
setProgress(null)
|
||||
}, [])
|
||||
}, [dryRun])
|
||||
|
||||
// Handle hide checked items (for user mode)
|
||||
const handleHideChecked = useCallback(() => {
|
||||
@@ -559,13 +497,6 @@ export const CleanerPage: React.FC = () => {
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-warning"
|
||||
onClick={handleConfirmDeletion}
|
||||
disabled={selectedItems.size === 0 || isRunning}
|
||||
>
|
||||
确认删除
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{validationStats && (
|
||||
@@ -575,26 +506,6 @@ export const CleanerPage: React.FC = () => {
|
||||
<span>已标记删除:{validationStats.markedCount}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Additional action buttons */}
|
||||
<div className="button-group" style={{ marginTop: '12px' }}>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={handleExportResults}
|
||||
disabled={validationResults.length === 0}
|
||||
>
|
||||
导出结果
|
||||
</button>
|
||||
{isAdmin && (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleExecuteDeletion}
|
||||
disabled={!orderNumbers.trim() || !materialCodes.trim() || isRunning}
|
||||
>
|
||||
{isRunning ? '清理中...' : '执行删除'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Manager Filter (Admin only) */}
|
||||
@@ -631,25 +542,44 @@ export const CleanerPage: React.FC = () => {
|
||||
<div className="validation-results-section">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<h2 style={{ margin: 0 }}>校验结果</h2>
|
||||
{/* User mode buttons: Hide Checked / Show All */}
|
||||
{!isAdmin && (
|
||||
{/* Action buttons: left group (Hide Checked, Show All, Confirm Delete) + right button (Execute Delete in red) */}
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
{!isAdmin && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={handleHideChecked}
|
||||
disabled={selectedItems.size === 0}
|
||||
>
|
||||
隐藏勾选
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={handleShowAll}
|
||||
disabled={hiddenItems.size === 0}
|
||||
>
|
||||
显示全部
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={handleHideChecked}
|
||||
disabled={selectedItems.size === 0}
|
||||
className="btn btn-warning"
|
||||
onClick={handleConfirmDeletion}
|
||||
disabled={selectedItems.size === 0 || isRunning}
|
||||
>
|
||||
隐藏勾选
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={handleShowAll}
|
||||
disabled={hiddenItems.size === 0}
|
||||
>
|
||||
显示全部
|
||||
确认删除
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={handleExecuteDeletion}
|
||||
disabled={isRunning || validationResults.length === 0}
|
||||
style={{ marginLeft: '16px' }}
|
||||
>
|
||||
开始清理
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table className="validation-results-table">
|
||||
<thead>
|
||||
@@ -684,57 +614,6 @@ export const CleanerPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Simple Input Section */}
|
||||
<div className="input-section">
|
||||
<h2>订单号和物料代码输入</h2>
|
||||
<div className="input-row">
|
||||
<div className="input-group">
|
||||
<label>订单号列表</label>
|
||||
<textarea
|
||||
value={orderNumbers}
|
||||
onChange={(e) => setOrderNumbers(e.target.value)}
|
||||
placeholder="请输入订单号,每行一个"
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input-group">
|
||||
<label>物料代码列表</label>
|
||||
<textarea
|
||||
value={materialCodes}
|
||||
onChange={(e) => setMaterialCodes(e.target.value)}
|
||||
placeholder="请输入物料代码,每行一个"
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dry-run-option">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={dryRun}
|
||||
onChange={(e) => setDryRun(e.target.checked)}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
<span>干运行模式(不实际删除,仅预览)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="button-group">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleClean}
|
||||
disabled={isRunning || !orderNumbers.trim() || !materialCodes.trim()}
|
||||
>
|
||||
{isRunning ? '清理中...' : '开始清理'}
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={handleReset} disabled={isRunning}>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Section */}
|
||||
{progress && (
|
||||
<div className="progress-section">
|
||||
@@ -848,12 +727,12 @@ export const CleanerPage: React.FC = () => {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
}
|
||||
.validation-section, .manager-filter-section, .input-section {
|
||||
.validation-section, .manager-filter-section {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
.validation-section h2, .manager-filter-section h3, .input-section h2 {
|
||||
.validation-section h2, .manager-filter-section h3 {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
@@ -911,6 +790,13 @@ export const CleanerPage: React.FC = () => {
|
||||
.btn-warning:hover:not(:disabled) {
|
||||
background: #ffc069;
|
||||
}
|
||||
.btn-danger {
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #ff7875;
|
||||
}
|
||||
.validation-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
@@ -971,46 +857,6 @@ export const CleanerPage: React.FC = () => {
|
||||
.validation-results-table tr:hover {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
.input-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.input-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.input-group textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
.input-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #52c41a;
|
||||
box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.2);
|
||||
}
|
||||
.dry-run-option {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.dry-run-option label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.progress-section {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
|
||||
975
src/renderer/src/pages/SettingsPage.tsx
Normal file
975
src/renderer/src/pages/SettingsPage.tsx
Normal file
@@ -0,0 +1,975 @@
|
||||
/**
|
||||
* Settings Page
|
||||
*
|
||||
* System settings configuration page with user type-based access control
|
||||
* Admin users see all settings, User users see limited settings
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import type {
|
||||
SettingsData,
|
||||
UserType,
|
||||
DatabaseType,
|
||||
MatchMode,
|
||||
ValidationDataSource
|
||||
} from '../../../main/types/settings.types'
|
||||
|
||||
// Default settings
|
||||
const DEFAULT_SETTINGS: SettingsData = {
|
||||
erp: {
|
||||
url: 'https://68.11.34.30:8082/',
|
||||
username: '',
|
||||
password: '',
|
||||
headless: true,
|
||||
ignoreHttpsErrors: true,
|
||||
autoCloseBrowser: true
|
||||
},
|
||||
database: {
|
||||
dbType: 'mysql',
|
||||
server: '',
|
||||
mysqlHost: '192.168.31.83',
|
||||
mysqlPort: 3306,
|
||||
database: 'BLD_DB',
|
||||
username: 'remote_user',
|
||||
password: ''
|
||||
},
|
||||
paths: {
|
||||
dataDir: 'D:/python/playwrite/data/',
|
||||
defaultOutput: '离散备料计划维护_合并.xlsx',
|
||||
validationOutput: '物料状态校验结果.xlsx'
|
||||
},
|
||||
extraction: {
|
||||
batchSize: 100,
|
||||
verbose: true,
|
||||
autoConvert: true,
|
||||
mergeBatches: true,
|
||||
enableDbPersistence: true
|
||||
},
|
||||
validation: {
|
||||
dataSource: 'database_full',
|
||||
batchSize: 2000,
|
||||
matchMode: 'substring',
|
||||
enableCrud: false,
|
||||
defaultManager: ''
|
||||
},
|
||||
ui: {
|
||||
fontFamily: 'Microsoft YaHei UI',
|
||||
fontSize: 10,
|
||||
productionIdInputWidth: 20
|
||||
},
|
||||
execution: {
|
||||
dryRun: false
|
||||
}
|
||||
}
|
||||
|
||||
const SettingsPage: React.FC = () => {
|
||||
// User type state
|
||||
const [userType, setUserType] = useState<UserType>('Guest')
|
||||
|
||||
// Settings state
|
||||
const [settings, setSettings] = useState<SettingsData>(DEFAULT_SETTINGS)
|
||||
|
||||
// UI state
|
||||
const [isModified, setIsModified] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
||||
|
||||
// Password visibility state for User ERP credentials
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
// Load settings on mount
|
||||
useEffect(() => {
|
||||
loadSettings()
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Load user type and settings from main process
|
||||
*/
|
||||
const loadSettings = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// Get user type
|
||||
const type = await window.electron.settings.getUserType()
|
||||
setUserType(type)
|
||||
|
||||
// Get settings (filtered by user type)
|
||||
const loadedSettings = await window.electron.settings.getSettings()
|
||||
setSettings(loadedSettings)
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error)
|
||||
showMessage('加载设置失败', 'error')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a message to user
|
||||
*/
|
||||
const showMessage = (text: string, type: 'success' | 'error') => {
|
||||
setMessage({ type, text })
|
||||
setTimeout(() => setMessage(null), 3000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update settings and mark as modified
|
||||
*/
|
||||
const updateSettings = (
|
||||
section: keyof SettingsData,
|
||||
key: string,
|
||||
value: unknown
|
||||
) => {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section],
|
||||
[key]: value
|
||||
}
|
||||
}))
|
||||
setIsModified(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test ERP connection
|
||||
*/
|
||||
const handleTestErpConnection = async () => {
|
||||
try {
|
||||
const result = await window.electron.settings.testErpConnection()
|
||||
if (result.success) {
|
||||
showMessage(result.message || 'ERP 连接测试成功!', 'success')
|
||||
} else {
|
||||
showMessage(result.message || 'ERP 连接测试失败', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('ERP connection test error:', error)
|
||||
showMessage('ERP 连接测试失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test database connection
|
||||
*/
|
||||
const handleTestDbConnection = async () => {
|
||||
try {
|
||||
const result = await window.electron.settings.testDbConnection()
|
||||
if (result.success) {
|
||||
showMessage(result.message || '数据库连接测试成功!', 'success')
|
||||
} else {
|
||||
showMessage(result.message || '数据库连接测试失败', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Database connection test error:', error)
|
||||
showMessage('数据库连接测试失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings
|
||||
*/
|
||||
const handleSaveSettings = async () => {
|
||||
try {
|
||||
const result = await window.electron.settings.saveSettings(settings)
|
||||
if (result.success) {
|
||||
showMessage('设置已保存', 'success')
|
||||
setIsModified(false)
|
||||
} else {
|
||||
showMessage(result.error || '保存设置失败', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save settings error:', error)
|
||||
showMessage('保存设置失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to default settings (Admin only)
|
||||
*/
|
||||
const handleResetDefaults = async () => {
|
||||
if (!window.confirm('确定要恢复默认设置吗?这将覆盖 .env 文件中的所有配置。')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await window.electron.settings.resetDefaults()
|
||||
if (result.success) {
|
||||
showMessage('已恢复默认设置', 'success')
|
||||
const loadedSettings = await window.electron.settings.getSettings()
|
||||
setSettings(loadedSettings)
|
||||
setIsModified(false)
|
||||
} else {
|
||||
showMessage(result.error || '恢复默认设置失败', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Reset defaults error:', error)
|
||||
showMessage('恢复默认设置失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<div className="loading-content">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
<style>{`
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
.loading-content {
|
||||
text-align: center;
|
||||
}
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #e8e8e8;
|
||||
border-top-color: #1890ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const isUserOnly = userType === 'User'
|
||||
|
||||
return (
|
||||
<div className="settings-page">
|
||||
{/* Header */}
|
||||
<div className="settings-header">
|
||||
<h1>系统设置</h1>
|
||||
<span className="user-type-badge">
|
||||
当前用户类型:{userType === 'Admin' ? '管理员' : userType === 'User' ? '普通用户' : '访客'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Message toast */}
|
||||
{message && (
|
||||
<div className={`toast toast-${message.type}`}>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings content */}
|
||||
<div className="settings-content">
|
||||
{/* Admin: ERP System Configuration */}
|
||||
{userType === 'Admin' && (
|
||||
<>
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">ERP 系统配置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">ERP URL:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.erp.url}
|
||||
onChange={(e) => updateSettings('erp', 'url', e.target.value)}
|
||||
placeholder="https://68.11.34.30:8082/"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">用户名:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.erp.username}
|
||||
onChange={(e) => updateSettings('erp', 'username', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">密码:</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-input"
|
||||
value={settings.erp.password}
|
||||
onChange={(e) => updateSettings('erp', 'password', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Database Configuration */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">数据库配置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">数据库类型:</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={settings.database.dbType}
|
||||
onChange={(e) => updateSettings('database', 'dbType', e.target.value as DatabaseType)}
|
||||
>
|
||||
<option value="sqlserver">SQL Server</option>
|
||||
<option value="mysql">MySQL</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{settings.database.dbType === 'mysql' ? (
|
||||
<>
|
||||
<div className="form-row">
|
||||
<label className="form-label">MySQL 主机:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.database.mysqlHost}
|
||||
onChange={(e) => updateSettings('database', 'mysqlHost', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">MySQL 端口:</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
value={settings.database.mysqlPort}
|
||||
onChange={(e) => updateSettings('database', 'mysqlPort', parseInt(e.target.value) || 3306)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="form-row">
|
||||
<label className="form-label">SQL Server 服务器:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.database.server}
|
||||
onChange={(e) => updateSettings('database', 'server', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-label">数据库名:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.database.database}
|
||||
onChange={(e) => updateSettings('database', 'database', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">数据库用户名:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.database.username}
|
||||
onChange={(e) => updateSettings('database', 'username', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">数据库密码:</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-input"
|
||||
value={settings.database.password}
|
||||
onChange={(e) => updateSettings('database', 'password', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Browser Settings */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">浏览器设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.erp.headless}
|
||||
onChange={(e) => updateSettings('erp', 'headless', e.target.checked)}
|
||||
/>
|
||||
<span>无头模式 (不显示浏览器)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.erp.ignoreHttpsErrors}
|
||||
onChange={(e) => updateSettings('erp', 'ignoreHttpsErrors', e.target.checked)}
|
||||
/>
|
||||
<span>忽略 HTTPS 错误</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.erp.autoCloseBrowser}
|
||||
onChange={(e) => updateSettings('erp', 'autoCloseBrowser', e.target.checked)}
|
||||
/>
|
||||
<span>操作完成后自动关闭浏览器</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Paths Configuration */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">路径设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">数据目录:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.paths.dataDir}
|
||||
onChange={(e) => updateSettings('paths', 'dataDir', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">默认输出文件:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.paths.defaultOutput}
|
||||
onChange={(e) => updateSettings('paths', 'defaultOutput', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">校验输出文件:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.paths.validationOutput}
|
||||
onChange={(e) => updateSettings('paths', 'validationOutput', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Extraction Settings */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">数据提取设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">批次大小:</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input input-small"
|
||||
value={settings.extraction.batchSize}
|
||||
onChange={(e) => updateSettings('extraction', 'batchSize', parseInt(e.target.value) || 100)}
|
||||
min="10"
|
||||
max="500"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.extraction.verbose}
|
||||
onChange={(e) => updateSettings('extraction', 'verbose', e.target.checked)}
|
||||
/>
|
||||
<span>启用详细日志</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.extraction.autoConvert}
|
||||
onChange={(e) => updateSettings('extraction', 'autoConvert', e.target.checked)}
|
||||
/>
|
||||
<span>自动转换 Excel 格式</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.extraction.mergeBatches}
|
||||
onChange={(e) => updateSettings('extraction', 'mergeBatches', e.target.checked)}
|
||||
/>
|
||||
<span>自动合并批次数据</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.extraction.enableDbPersistence}
|
||||
onChange={(e) => updateSettings('extraction', 'enableDbPersistence', e.target.checked)}
|
||||
/>
|
||||
<span>保存到数据库</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Validation Settings */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">物料校验设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">默认数据源:</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={settings.validation.dataSource}
|
||||
onChange={(e) => updateSettings('validation', 'dataSource', e.target.value as ValidationDataSource)}
|
||||
>
|
||||
<option value="database_full">database_full</option>
|
||||
<option value="database_filtered">database_filtered</option>
|
||||
<option value="excel_existing">excel_existing</option>
|
||||
<option value="excel_full">excel_full</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.validation.enableCrud}
|
||||
onChange={(e) => updateSettings('validation', 'enableCrud', e.target.checked)}
|
||||
/>
|
||||
<span>启用 CRUD 操作(管理待删除物料)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">批次大小:</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input input-small"
|
||||
value={settings.validation.batchSize}
|
||||
onChange={(e) => updateSettings('validation', 'batchSize', parseInt(e.target.value) || 2000)}
|
||||
min="100"
|
||||
max="2000"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">匹配模式:</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={settings.validation.matchMode}
|
||||
onChange={(e) => updateSettings('validation', 'matchMode', e.target.value as MatchMode)}
|
||||
>
|
||||
<option value="substring">模糊匹配 (substring)</option>
|
||||
<option value="exact">精确匹配 (exact)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">默认负责人:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.validation.defaultManager}
|
||||
onChange={(e) => updateSettings('validation', 'defaultManager', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* UI Settings */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">界面设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">字体:</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={settings.ui.fontFamily}
|
||||
onChange={(e) => updateSettings('ui', 'fontFamily', e.target.value)}
|
||||
>
|
||||
<option value="Microsoft YaHei UI">Microsoft YaHei UI</option>
|
||||
<option value="SimSun">SimSun (宋体)</option>
|
||||
<option value="KaiTi">KaiTi (楷体)</option>
|
||||
<option value="FangSong">FangSong (仿宋)</option>
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Segoe UI">Segoe UI</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">字号:</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input input-small"
|
||||
value={settings.ui.fontSize}
|
||||
onChange={(e) => updateSettings('ui', 'fontSize', parseInt(e.target.value) || 10)}
|
||||
min="8"
|
||||
max="24"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Production ID 输入框宽度 (字符):</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input input-small"
|
||||
value={settings.ui.productionIdInputWidth}
|
||||
onChange={(e) => updateSettings('ui', 'productionIdInputWidth', parseInt(e.target.value) || 20)}
|
||||
min="10"
|
||||
max="100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* User: ERP Credentials */}
|
||||
{isUserOnly && (
|
||||
<>
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">ERP 登录凭据</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">ERP 用户名:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.erp.username}
|
||||
onChange={(e) => updateSettings('erp', 'username', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">ERP 密码:</label>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
className="form-input"
|
||||
value={settings.erp.password}
|
||||
onChange={(e) => updateSettings('erp', 'password', e.target.value)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<label className="form-checkbox-label" style={{ margin: 0, whiteSpace: 'nowrap' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showPassword}
|
||||
onChange={(e) => setShowPassword(e.target.checked)}
|
||||
/>
|
||||
<span>显示密码</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<p className="form-hint">
|
||||
提示:此凭据为共享配置,修改后所有用户将使用新的 ERP 账号。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Paths Configuration (User) */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">路径设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row">
|
||||
<label className="form-label">数据目录:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.paths.dataDir}
|
||||
onChange={(e) => updateSettings('paths', 'dataDir', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">默认输出文件:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.paths.defaultOutput}
|
||||
onChange={(e) => updateSettings('paths', 'defaultOutput', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">校验输出文件:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={settings.paths.validationOutput}
|
||||
onChange={(e) => updateSettings('paths', 'validationOutput', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Execution Settings (User) */}
|
||||
<div className="settings-group">
|
||||
<h2 className="settings-group-title">执行设置</h2>
|
||||
<div className="settings-form">
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.execution.dryRun}
|
||||
onChange={(e) => updateSettings('execution', 'dryRun', e.target.checked)}
|
||||
/>
|
||||
<span>预览模式 (执行删除时不保存更改)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<p className="form-hint">
|
||||
提示:勾选后,执行删除操作时将只预览不实际保存,用于测试流程。
|
||||
</p>
|
||||
</div>
|
||||
<div className="form-row checkbox-row">
|
||||
<label className="form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.erp.headless}
|
||||
onChange={(e) => updateSettings('erp', 'headless', e.target.checked)}
|
||||
/>
|
||||
<span>无头模式 (不显示浏览器)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<p className="form-hint">
|
||||
提示:勾选后浏览器将在后台运行,不显示窗口。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Button Group */}
|
||||
<div className="settings-buttons">
|
||||
<button className="btn btn-primary" onClick={handleTestErpConnection}>
|
||||
测试 ERP 连接
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={handleTestDbConnection}>
|
||||
测试数据库连接
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={handleSaveSettings}
|
||||
disabled={!isModified}
|
||||
>
|
||||
保存设置
|
||||
</button>
|
||||
{userType === 'Admin' && (
|
||||
<button className="btn btn-warning" onClick={handleResetDefaults}>
|
||||
恢复默认
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Styles */}
|
||||
<style>{`
|
||||
.settings-page {
|
||||
padding: 24px;
|
||||
background: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 2px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.settings-header h1 {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user-type-badge {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
z-index: 10000;
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background: #fff1f0;
|
||||
border: 1px solid #ffa39e;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.settings-group-title {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
margin: 0 0 16px 0;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
min-width: 120px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.form-input.input-small {
|
||||
width: 100px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.checkbox-row {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.form-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-checkbox-label input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin: 4px 0 0 136px;
|
||||
}
|
||||
|
||||
.settings-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #40a9ff;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #52c41a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
background: #73d13d;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #faad14;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-warning:hover:not(:disabled) {
|
||||
background: #ffc53d;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsPage
|
||||
Reference in New Issue
Block a user