Files
web-client/src/components/InstallPage.tsx
wdvipa 28040495c8 111
2026-02-09 16:33:52 +08:00

377 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from 'react'
import {
Card,
Form,
Input,
Button,
Typography,
Alert,
Space,
Steps,
Row,
Col,
message
} from 'antd'
import {
UserOutlined,
LockOutlined,
CheckCircleOutlined,
SettingOutlined,
SafetyOutlined
} from '@ant-design/icons'
import apiClient from '../services/apiClient'
const { Title, Text, Paragraph } = Typography
const { Step } = Steps
interface InstallPageProps {
onInstallComplete: () => void
}
/**
* 系统安装页面组件
* 用于首次运行时设置管理员账号和密码
*/
const InstallPage: React.FC<InstallPageProps> = ({ onInstallComplete }) => {
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
const [currentStep, setCurrentStep] = useState(0)
const [error, setError] = useState<string | null>(null)
const [lockFilePath, setLockFilePath] = useState<string>('')
const handleInstall = async (values: { username: string; password: string; confirmPassword: string }) => {
if (values.password !== values.confirmPassword) {
setError('两次输入的密码不一致')
return
}
setLoading(true)
setError(null)
try {
const result = await apiClient.post<any>('/api/auth/initialize', {
username: values.username,
password: values.password
})
if (result.success) {
setCurrentStep(2)
message.success('系统初始化成功!')
// 获取初始化信息以显示锁文件路径
try {
const checkResult = await apiClient.get<any>('/api/auth/check-initialization')
if (checkResult.success && checkResult.lockFilePath) {
setLockFilePath(checkResult.lockFilePath)
}
} catch (infoError) {
console.warn('获取初始化信息失败:', infoError)
}
// 延迟跳转到登录页面
setTimeout(() => {
onInstallComplete()
}, 3000)
} else {
setError(result.message || '初始化失败')
}
} catch (error: any) {
setError(error.message || '初始化失败,请稍后重试')
} finally {
setLoading(false)
}
}
const validateUsername = (_: any, value: string) => {
if (!value) {
return Promise.reject(new Error('请输入用户名'))
}
if (value.length < 3) {
return Promise.reject(new Error('用户名至少需要3个字符'))
}
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
return Promise.reject(new Error('用户名只能包含字母、数字、下划线和横线'))
}
return Promise.resolve()
}
const validatePassword = (_: any, value: string) => {
if (!value) {
return Promise.reject(new Error('请输入密码'))
}
if (value.length < 6) {
return Promise.reject(new Error('密码至少需要6个字符'))
}
return Promise.resolve()
}
const steps = [
{
title: '欢迎',
icon: <SettingOutlined />,
description: '系统初始化向导'
},
{
title: '设置账号',
icon: <UserOutlined />,
description: '创建管理员账号'
},
{
title: '完成',
icon: <CheckCircleOutlined />,
description: '初始化完成'
}
]
return (
<div style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px'
}}>
<Card
style={{
width: '100%',
maxWidth: '500px',
borderRadius: '16px',
boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
border: 'none'
}}
styles={{ body: { padding: '40px' } }}
>
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
<div style={{
fontSize: '48px',
marginBottom: '16px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
}}>
🚀
</div>
<Title level={2} style={{ margin: 0, color: '#1a1a1a' }}>
</Title>
<Text style={{ color: '#666', fontSize: '16px' }}>
</Text>
</div>
<Steps
current={currentStep}
style={{ marginBottom: '32px' }}
size="small"
>
{steps.map((step, index) => (
<Step
key={index}
title={step.title}
description={step.description}
icon={step.icon}
/>
))}
</Steps>
{currentStep === 0 && (
<div style={{ textAlign: 'center' }}>
<Alert
message="欢迎使用远程控制系统"
description={
<div>
<Paragraph style={{ marginBottom: '16px' }}>
</Paragraph>
<Paragraph style={{ marginBottom: '16px' }}>
</Paragraph>
<Space direction="vertical" style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<SafetyOutlined style={{ color: '#52c41a' }} />
<Text></Text>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<UserOutlined style={{ color: '#1890ff' }} />
<Text></Text>
</div>
</Space>
</div>
}
type="info"
showIcon
style={{ marginBottom: '24px', textAlign: 'left' }}
/>
<Button
type="primary"
size="large"
onClick={() => setCurrentStep(1)}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
borderRadius: '8px',
height: '48px',
fontSize: '16px'
}}
>
</Button>
</div>
)}
{currentStep === 1 && (
<Form
form={form}
layout="vertical"
onFinish={handleInstall}
size="large"
>
<Alert
message="创建管理员账号"
description="请设置您的管理员用户名和密码,建议使用强密码以确保系统安全。"
type="warning"
showIcon
style={{ marginBottom: '24px' }}
/>
{error && (
<Alert
message={error}
type="error"
closable
onClose={() => setError(null)}
style={{ marginBottom: '16px' }}
/>
)}
<Form.Item
name="username"
label="管理员用户名"
rules={[{ validator: validateUsername }]}
>
<Input
prefix={<UserOutlined />}
placeholder="请输入管理员用户名"
style={{ borderRadius: '8px' }}
/>
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[{ validator: validatePassword }]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请输入密码至少6个字符"
style={{ borderRadius: '8px' }}
/>
</Form.Item>
<Form.Item
name="confirmPassword"
label="确认密码"
rules={[
{ required: true, message: '请确认密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve()
}
return Promise.reject(new Error('两次输入的密码不一致'))
},
}),
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请再次输入密码"
style={{ borderRadius: '8px' }}
/>
</Form.Item>
<Row gutter={16} style={{ marginTop: '32px' }}>
<Col span={12}>
<Button
block
size="large"
onClick={() => setCurrentStep(0)}
style={{ borderRadius: '8px' }}
>
</Button>
</Col>
<Col span={12}>
<Button
type="primary"
htmlType="submit"
block
size="large"
loading={loading}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
borderRadius: '8px'
}}
>
{loading ? '初始化中...' : '完成设置'}
</Button>
</Col>
</Row>
</Form>
)}
{currentStep === 2 && (
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '64px', marginBottom: '24px' }}>
</div>
<Alert
message="初始化完成!"
description={
<div>
<Paragraph style={{ marginBottom: '16px' }}>
</Paragraph>
<Paragraph style={{ marginBottom: '16px' }}>
使
</Paragraph>
{lockFilePath && (
<div style={{
background: '#f6f6f6',
padding: '12px',
borderRadius: '6px',
marginTop: '16px',
textAlign: 'left'
}}>
<Text strong style={{ color: '#666' }}>💡 </Text>
<br />
<Text style={{ fontSize: '12px', color: '#666' }}>
</Text>
<br />
<Text code style={{ fontSize: '11px', wordBreak: 'break-all' }}>
{lockFilePath}
</Text>
<br />
<Text style={{ fontSize: '12px', color: '#666' }}>
</Text>
</div>
)}
</div>
}
type="success"
showIcon
style={{ textAlign: 'left' }}
/>
</div>
)}
</Card>
</div>
)
}
export default InstallPage