Files
AIclinicalresearch/frontend-v2/src/pages/LoginPage.tsx
HaHafeng 1b53ab9d52 feat(aia): Complete AIA V2.0 with universal streaming capabilities
Major Changes:
- Add StreamingService with OpenAI Compatible format
- Upgrade Chat component V2 with Ant Design X integration
- Implement AIA module with 12 intelligent agents
- Update API routes to unified /api/v1 prefix
- Update system documentation

Backend (~1300 lines):
- common/streaming: OpenAI Compatible adapter
- modules/aia: 12 agents, conversation service, streaming integration
- Update route versions (RVW, PKB to v1)

Frontend (~3500 lines):
- modules/aia: AgentHub + ChatWorkspace (100% prototype restoration)
- shared/Chat: AIStreamChat, ThinkingBlock, useAIStream Hook
- Update API endpoints to v1

Documentation:
- AIA module status guide
- Universal capabilities catalog
- System overview updates
- All module documentation sync

Tested: Stream response verified, authentication working
Status: AIA V2.0 core completed (85%)
2026-01-14 19:15:01 +08:00

374 lines
11 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.
/**
* 登录页面
*
* 支持两种登录方式:
* 1. 手机号 + 密码
* 2. 手机号 + 验证码
*
* 路由:
* - /login - 通用登录(个人用户)
* - /t/{tenantCode}/login - 租户专属登录(机构用户)
*/
import { useState, useEffect } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { Form, Input, Button, Tabs, message, Card, Space, Typography, Alert, Modal } from 'antd';
import { PhoneOutlined, LockOutlined, SafetyOutlined, UserOutlined } from '@ant-design/icons';
import { useAuth } from '../framework/auth';
import type { ChangePasswordRequest } from '../framework/auth';
const { Title, Text, Paragraph } = Typography;
const { TabPane } = Tabs;
// 租户配置类型
interface TenantConfig {
name: string;
logo?: string;
primaryColor: string;
systemName: string;
}
// 默认配置
const DEFAULT_CONFIG: TenantConfig = {
name: 'AI临床研究平台',
primaryColor: '#1890ff',
systemName: 'AI临床研究平台',
};
export default function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const { tenantCode } = useParams<{ tenantCode?: string }>();
const {
loginWithPassword,
loginWithCode,
sendVerificationCode,
isLoading,
error,
user,
changePassword,
} = useAuth();
const [form] = Form.useForm();
const [activeTab, setActiveTab] = useState<'password' | 'code'>('password');
const [countdown, setCountdown] = useState(0);
const [tenantConfig, setTenantConfig] = useState<TenantConfig>(DEFAULT_CONFIG);
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [passwordForm] = Form.useForm();
// 获取租户配置
useEffect(() => {
if (tenantCode) {
// TODO: 从API获取租户配置
fetch(`/api/v1/public/tenant-config/${tenantCode}`)
.then(res => res.json())
.then(data => {
if (data.success && data.data) {
setTenantConfig(data.data);
}
})
.catch(() => {
// 使用默认配置
});
}
}, [tenantCode]);
// 验证码倒计时
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
return () => clearTimeout(timer);
}
}, [countdown]);
// 登录成功后检查是否需要修改密码
useEffect(() => {
if (user && user.isDefaultPassword) {
setShowPasswordModal(true);
} else if (user) {
// 登录成功,跳转
const from = (location.state as any)?.from?.pathname || '/';
navigate(from, { replace: true });
}
}, [user, navigate, location]);
// 发送验证码
const handleSendCode = async () => {
try {
const phone = form.getFieldValue('phone');
if (!phone) {
message.error('请输入手机号');
return;
}
if (!/^1[3-9]\d{9}$/.test(phone)) {
message.error('请输入正确的手机号');
return;
}
await sendVerificationCode(phone, 'LOGIN');
message.success('验证码已发送');
setCountdown(60);
} catch (err) {
message.error(err instanceof Error ? err.message : '发送失败');
}
};
// 提交登录
const handleSubmit = async (values: any) => {
try {
if (activeTab === 'password') {
await loginWithPassword(values.phone, values.password);
} else {
await loginWithCode(values.phone, values.code);
}
message.success('登录成功');
} catch (err) {
message.error(err instanceof Error ? err.message : '登录失败');
}
};
// 修改密码
const handleChangePassword = async (values: ChangePasswordRequest) => {
try {
await changePassword(values);
message.success('密码修改成功');
setShowPasswordModal(false);
// 跳转到首页
const from = (location.state as any)?.from?.pathname || '/';
navigate(from, { replace: true });
} catch (err) {
message.error(err instanceof Error ? err.message : '修改密码失败');
}
};
// 跳过修改密码
const handleSkipPassword = () => {
setShowPasswordModal(false);
const from = (location.state as any)?.from?.pathname || '/';
navigate(from, { replace: true });
};
return (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: `linear-gradient(135deg, ${tenantConfig.primaryColor}15 0%, ${tenantConfig.primaryColor}05 100%)`,
padding: '20px',
}}>
<Card
style={{
width: '100%',
maxWidth: 420,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.08)',
borderRadius: 16,
}}
bodyStyle={{ padding: '40px 32px' }}
>
{/* Logo和标题 */}
<div style={{ textAlign: 'center', marginBottom: 32 }}>
{tenantConfig.logo ? (
<img
src={tenantConfig.logo}
alt={tenantConfig.name}
style={{ height: 48, marginBottom: 16 }}
/>
) : (
<div style={{
width: 64,
height: 64,
borderRadius: 16,
background: `linear-gradient(135deg, ${tenantConfig.primaryColor} 0%, ${tenantConfig.primaryColor}dd 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 16px',
}}>
<UserOutlined style={{ fontSize: 32, color: '#fff' }} />
</div>
)}
<Title level={3} style={{ margin: 0, marginBottom: 8 }}>
{tenantConfig.systemName}
</Title>
{tenantCode && (
<Text type="secondary">{tenantConfig.name}</Text>
)}
</div>
{/* 错误提示 */}
{error && (
<Alert
message={error}
type="error"
showIcon
style={{ marginBottom: 24 }}
closable
/>
)}
{/* 登录表单 */}
<Form
form={form}
onFinish={handleSubmit}
layout="vertical"
size="large"
>
<Tabs
activeKey={activeTab}
onChange={(key) => setActiveTab(key as 'password' | 'code')}
centered
>
<TabPane tab="密码登录" key="password" />
<TabPane tab="验证码登录" key="code" />
</Tabs>
<Form.Item
name="phone"
rules={[
{ required: true, message: '请输入手机号' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' },
]}
>
<Input
prefix={<PhoneOutlined />}
placeholder="手机号"
maxLength={11}
/>
</Form.Item>
{activeTab === 'password' ? (
<Form.Item
name="password"
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码至少6位' },
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="密码"
/>
</Form.Item>
) : (
<Form.Item
name="code"
rules={[
{ required: true, message: '请输入验证码' },
{ len: 6, message: '验证码为6位数字' },
]}
>
<Space.Compact style={{ width: '100%' }}>
<Input
prefix={<SafetyOutlined />}
placeholder="验证码"
maxLength={6}
style={{ flex: 1 }}
/>
<Button
onClick={handleSendCode}
disabled={countdown > 0}
style={{ width: 120 }}
>
{countdown > 0 ? `${countdown}s后重发` : '获取验证码'}
</Button>
</Space.Compact>
</Form.Item>
)}
<Form.Item style={{ marginBottom: 16 }}>
<Button
type="primary"
htmlType="submit"
block
loading={isLoading}
style={{
height: 44,
borderRadius: 8,
background: tenantConfig.primaryColor,
}}
>
</Button>
</Form.Item>
</Form>
{/* 底部信息 */}
<div style={{ textAlign: 'center' }}>
<Text type="secondary" style={{ fontSize: 12 }}>
© 2026 · AI临床研究平台
</Text>
</div>
</Card>
{/* 修改默认密码弹窗 */}
<Modal
title="修改默认密码"
open={showPasswordModal}
footer={null}
closable={false}
maskClosable={false}
>
<Paragraph type="warning">
使
</Paragraph>
<Form
form={passwordForm}
onFinish={handleChangePassword}
layout="vertical"
>
<Form.Item
name="newPassword"
label="新密码"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码至少6位' },
]}
>
<Input.Password placeholder="请输入新密码" />
</Form.Item>
<Form.Item
name="confirmPassword"
label="确认密码"
dependencies={['newPassword']}
rules={[
{ required: true, message: '请确认新密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('newPassword') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次输入的密码不一致'));
},
}),
]}
>
<Input.Password placeholder="请再次输入新密码" />
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={handleSkipPassword}>
</Button>
<Button type="primary" htmlType="submit" loading={isLoading}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
}