mars f20988b90c feat: 完成所有页面的演示模式实现
- 更新 DashboardLayout 组件,统一使用演示模式布局
- 实现仪表盘页面的完整演示数据和功能
- 完成用户管理页面的演示模式,包含搜索、过滤、分页等功能
- 实现通话记录页面的演示数据和录音播放功能
- 完成翻译员管理页面的演示模式
- 实现订单管理页面的完整功能
- 完成发票管理页面的演示数据
- 更新文档管理页面
- 添加 utils.ts 工具函数库
- 完善 API 路由和数据库结构
- 修复各种 TypeScript 类型错误
- 统一界面风格和用户体验
2025-06-30 19:42:43 +08:00

208 lines
7.8 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 { useRouter } from 'next/router';
import Head from 'next/head';
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
const LoginPage = () => {
const router = useRouter();
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [isRedirecting, setIsRedirecting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// 防止重复提交
if (loading || isRedirecting) {
return;
}
setLoading(true);
setError('');
try {
const response = await fetch('/api/auth/admin-login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const data = await response.json();
if (response.ok && data.success) {
// 设置重定向状态,防止重复提交
setIsRedirecting(true);
// 存储用户信息和令牌
localStorage.setItem('user', JSON.stringify(data.user));
localStorage.setItem('access_token', data.token);
// 使用 window.location 进行重定向,避免 Next.js 路由问题
window.location.href = '/dashboard';
} else {
setError(data.error || '登录失败');
setLoading(false);
}
} catch (error) {
console.error('登录错误:', error);
setError('网络错误,请稍后重试');
setLoading(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
// 预设账号快速填充
const fillDemoAccount = (email: string, password: string) => {
if (loading || isRedirecting) return;
setFormData({ email, password });
setError('');
};
// 如果正在重定向,显示加载状态
if (isRedirecting) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
<div className="mt-4 text-lg text-gray-600">...</div>
</div>
</div>
);
}
return (
<>
<Head>
<title> - </title>
</Head>
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
</p>
</div>
{/* 预设账号提示 */}
<div className="bg-blue-50 border border-blue-200 rounded-md p-4">
<h3 className="text-sm font-medium text-blue-800 mb-2"></h3>
<div className="space-y-2 text-xs text-blue-700">
<div className="flex justify-between items-center">
<span>admin@example.com / admin123</span>
<button
type="button"
onClick={() => fillDemoAccount('admin@example.com', 'admin123')}
className="text-blue-600 hover:text-blue-800 underline disabled:opacity-50 disabled:cursor-not-allowed"
disabled={loading || isRedirecting}
>
使
</button>
</div>
</div>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
disabled={loading || isRedirecting}
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm disabled:bg-gray-100"
placeholder="邮箱地址"
value={formData.email}
onChange={handleInputChange}
/>
</div>
<div className="relative">
<label htmlFor="password" className="sr-only">
</label>
<input
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
required
disabled={loading || isRedirecting}
className="appearance-none rounded-none relative block w-full px-3 py-2 pr-10 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm disabled:bg-gray-100"
placeholder="密码"
value={formData.password}
onChange={handleInputChange}
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
disabled={loading || isRedirecting}
>
{showPassword ? (
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
) : (
<EyeIcon className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
</div>
<div>
<button
type="submit"
disabled={loading || isRedirecting}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<span className="flex items-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
...
</span>
) : '登录'}
</button>
</div>
<div className="text-center">
<p className="text-sm text-gray-600">
</p>
</div>
</form>
</div>
</div>
</>
);
};
export default LoginPage;