Files
web-client/src/components/InstallPage.tsx

377 lines
12 KiB
TypeScript
Raw Normal View History

2026-02-09 16:33:52 +08:00
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