Files
agent/apps/web_v1/src/pages/login.tsx

117 lines
4.3 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 { useEffect, useState } from 'react'
import { Button, Card, Form, Input, Space, Typography } from '@arco-design/web-react'
import {
IconCheckCircleFill,
IconCloseCircleFill,
IconLaunch,
IconLoading,
} from '@arco-design/web-react/icon'
const { Title, Text } = Typography
type ConnectionStatus = 'checking' | 'online' | 'offline'
export function LoginPage({ onLogin }: { onLogin: (input: { server: string; email: string; password: string }) => void }) {
const [server, setServer] = useState('http://localhost:9150')
const [email, setEmail] = useState('demo@senlin.ai')
const [password, setPassword] = useState('password123')
const [status, setStatus] = useState<ConnectionStatus>('checking')
useEffect(() => {
const controller = new AbortController()
const timer = window.setTimeout(() => {
void checkServerStatus(server, controller.signal, setStatus)
}, 300)
return () => {
controller.abort()
window.clearTimeout(timer)
}
}, [server])
return (
<section className="login-screen">
<Card className="login-card" bordered>
<div className="login-brand-panel">
<div className="brand-lockup large">
<img className="brand-icon" src="/senlinai-icon.svg" alt="" />
<Title heading={2}>AI</Title>
</div>
<Text className="login-slogan"> · </Text>
<Text type="secondary"> · · AI </Text>
</div>
<div className="login-form-panel">
<Title heading={4}>AI</Title>
<Text type="secondary"></Text>
<Form layout="vertical" className="login-form">
<Form.Item label="服务器地址IP 或域名优先)">
<Input value={server} onChange={setServer} placeholder="例如https://10.0.0.15:8080" suffix={<IconLaunch />} />
<ConnectionCard status={status} onCheck={() => checkServerStatus(server, undefined, setStatus)} />
</Form.Item>
<Form.Item label="账号">
<Input value={email} onChange={setEmail} placeholder="请输入用户名或邮箱" />
</Form.Item>
<Form.Item label="密码">
<Input.Password value={password} onChange={setPassword} placeholder="请输入密码" />
</Form.Item>
<div className="login-row">
<Space size={8}>
<span className="check-dot" />
<Text></Text>
</Space>
<Button type="text" size="mini">?</Button>
</div>
<Button type="primary" long size="large" onClick={() => onLogin({ server, email, password })}>
</Button>
</Form>
</div>
</Card>
</section>
)
}
function ConnectionCard({ status, onCheck }: { status: ConnectionStatus; onCheck: () => void }) {
const icon = status === 'checking' ? <IconLoading /> : status === 'online' ? <IconCheckCircleFill /> : <IconCloseCircleFill />
const title = status === 'checking' ? '正在检查' : status === 'online' ? '连接正常' : '连接异常'
return (
<div className={`connection-card ${status}`}>
{icon}
<div>
<Text className="connection-title">{title}</Text>
</div>
<Button type="text" size="mini" loading={status === 'checking'} onClick={onCheck}>
</Button>
</div>
)
}
async function checkServerStatus(
server: string,
signal: AbortSignal | undefined,
setStatus: (status: ConnectionStatus) => void,
) {
const baseUrl = normalizeBaseUrl(server)
setStatus('checking')
try {
const response = await fetch(`${baseUrl}/api/status`, { signal })
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const payload = await response.json() as { timestamp_ms?: number }
if (typeof payload.timestamp_ms !== 'number') throw new Error('invalid status payload')
setStatus('online')
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') return
setStatus('offline')
}
}
function normalizeBaseUrl(value: string) {
const trimmed = value.trim()
return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed
}