Files
kefu_cloud/web/src/pages/Login.tsx
T
2026-08-31 16:37:13 +08:00

245 lines
11 KiB
TypeScript

import { useState } from 'react'
import { Form, Input, Button, message } from 'antd'
import {
MessageOutlined, UserOutlined, LockOutlined,
SafetyCertificateOutlined, CrownOutlined, ArrowRightOutlined,
CheckCircleFilled, CustomerServiceOutlined,
} from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '@/stores/auth'
type DemoRole = {
key: string
title: string
username: string
pass: string
icon: typeof CrownOutlined
}
const demoRoles: DemoRole[] = [
{ key: 'tenant', title: '租户管理', username: 'kefu_admin', pass: 'kefu_admin123', icon: CrownOutlined },
{ key: 'admin', title: '平台管理', username: 'platform_admin', pass: 'kefu_admin123', icon: SafetyCertificateOutlined },
{ key: 'agent', title: '坐席人员', username: 'agent1', pass: 'kefu_admin123', icon: CustomerServiceOutlined },
]
const Login = () => {
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
const [selectedRole, setSelectedRole] = useState<string>('tenant')
// 记住密码状态持久化
const [remember, setRemember] = useState<boolean>(() => {
return localStorage.getItem('kefu_remember_me') === 'true'
})
const [username, setUsername] = useState<string>(() => {
const isRemembered = localStorage.getItem('kefu_remember_me') === 'true'
if (isRemembered) {
return localStorage.getItem('kefu_saved_username') || 'kefu_admin'
}
return 'kefu_admin'
})
const [password, setPassword] = useState<string>(() => {
const isRemembered = localStorage.getItem('kefu_remember_me') === 'true'
if (isRemembered) {
return localStorage.getItem('kefu_saved_password') || 'kefu_admin123'
}
return 'kefu_admin123'
})
const { login } = useAuth()
const navigate = useNavigate()
const onFinish = async (values: { username?: string; password?: string }) => {
setLoading(true)
const u = (values.username || username).trim()
const p = (values.password || password).trim()
try {
const user = await login(u, p)
if (remember) {
localStorage.setItem('kefu_remember_me', 'true')
localStorage.setItem('kefu_saved_username', u)
localStorage.setItem('kefu_saved_password', p)
} else {
localStorage.removeItem('kefu_remember_me')
localStorage.removeItem('kefu_saved_username')
localStorage.removeItem('kefu_saved_password')
}
message.success('登录成功')
navigate(user.role === 'platform_admin' ? '/admin/dashboard' : '/agent/dashboard', { replace: true })
} catch {
message.error('用户名或密码错误')
} finally {
setLoading(false)
}
}
const selectDemoRole = (role: DemoRole) => {
setSelectedRole(role.key)
setUsername(role.username)
setPassword(role.pass)
form.setFieldsValue({ username: role.username, password: role.pass })
}
return (
<div className="min-h-screen w-full relative flex flex-col justify-between bg-slate-950 p-4 sm:p-6 overflow-y-auto select-none">
{/* 科技感深邃渐变背景 */}
<div className="fixed inset-0 bg-gradient-to-br from-slate-950 via-[#0a0f1d] to-[#0d1527] pointer-events-none" />
<div className="fixed top-[-10%] left-[-10%] w-[550px] h-[550px] rounded-full bg-blue-600/15 blur-[140px] pointer-events-none" />
<div className="fixed bottom-[-10%] right-[-10%] w-[650px] h-[650px] rounded-full bg-indigo-600/15 blur-[160px] pointer-events-none" />
<div className="fixed top-[40%] right-[20%] w-[350px] h-[350px] rounded-full bg-cyan-500/10 blur-[120px] pointer-events-none" />
{/* 顶栏品牌信息 */}
<header className="relative z-10 w-full max-w-4xl mx-auto flex items-center justify-between py-3">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-xl bg-gradient-to-tr from-blue-600 to-indigo-500 flex items-center justify-center text-white shadow-md shadow-blue-500/30 border border-white/20">
<MessageOutlined className="text-base" />
</div>
<span className="text-base font-bold text-white tracking-tight">Kefu Cloud</span>
</div>
<div className="flex items-center gap-2">
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-slate-900/80 text-slate-300 border border-slate-700/60 backdrop-blur-md">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
系统服务正常
</span>
</div>
</header>
{/* 核心登录卡片 */}
<main className="relative z-10 flex-1 flex items-center justify-center py-6">
<div className="w-full max-w-[420px]">
<div className="relative rounded-3xl bg-slate-900/90 border border-slate-800 shadow-2xl p-7 sm:p-8 backdrop-blur-xl">
{/* 顶端亮色发光描边 */}
<div className="absolute inset-x-0 -top-px h-px bg-gradient-to-r from-transparent via-blue-500/50 to-transparent" />
{/* 卡片头部 */}
<div className="text-center mb-6">
<div className="inline-flex items-center justify-center w-13 h-13 rounded-2xl bg-gradient-to-tr from-blue-600 to-indigo-500 text-white shadow-lg shadow-blue-500/25 mb-3 border border-white/20">
<MessageOutlined className="text-xl" />
</div>
<h1 className="text-xl font-bold text-white tracking-tight m-0">登录客服云平台</h1>
<p className="text-xs text-slate-400 mt-1 m-0">全渠道多租户智能客服与运营管理</p>
</div>
{/* 顶部一键填入演示账号切换 Tab */}
<div className="mb-6">
<div className="flex items-center justify-between text-[11px] text-slate-400 mb-2 font-medium">
<span>快捷填入演示账号:</span>
<span className="text-slate-500">点击自动填入</span>
</div>
<div className="grid grid-cols-3 gap-1.5 p-1 rounded-xl bg-slate-950/80 border border-slate-800/80">
{demoRoles.map(role => {
const Icon = role.icon
const active = selectedRole === role.key
return (
<button
key={role.key}
type="button"
onClick={() => selectDemoRole(role)}
className={`flex flex-col items-center justify-center py-2 px-1 rounded-lg text-xs font-medium border-0 cursor-pointer transition-all ${
active
? 'bg-blue-600 text-white shadow-sm font-semibold'
: 'bg-transparent text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
}`}
>
<Icon className="text-xs mb-1" />
<span className="text-[11px] leading-none">{role.title}</span>
</button>
)
})}
</div>
</div>
{/* 登录表单 */}
<Form form={form} layout="vertical" onFinish={onFinish} autoComplete="off" size="large">
<Form.Item
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
className="!mb-4"
>
<div className="space-y-1.5">
<label className="block text-xs font-semibold text-slate-300">账号 / 用户名</label>
<Input
value={username}
onChange={e => {
setUsername(e.target.value)
form.setFieldsValue({ username: e.target.value })
}}
prefix={<UserOutlined className="text-slate-400 mr-1.5 text-sm" />}
placeholder="请输入登录账号"
className="login-dark-input !h-11 !rounded-xl !text-slate-100 placeholder:!text-slate-500 transition-all text-sm"
/>
</div>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请输入密码' }]}
className="!mb-4"
>
<div className="space-y-1.5">
<label className="block text-xs font-semibold text-slate-300">密码</label>
<Input.Password
value={password}
onChange={e => {
setPassword(e.target.value)
form.setFieldsValue({ password: e.target.value })
}}
prefix={<LockOutlined className="text-slate-400 mr-1.5 text-sm" />}
placeholder="请输入登录密码"
className="login-dark-input !h-11 !rounded-xl !text-slate-100 placeholder:!text-slate-500 transition-all text-sm"
/>
</div>
</Form.Item>
{/* 记住密码勾选栏 */}
<div className="flex items-center justify-between mb-5">
<label className="flex items-center gap-2 cursor-pointer text-xs text-slate-300 hover:text-white select-none transition-colors">
<input
type="checkbox"
checked={remember}
onChange={e => setRemember(e.target.checked)}
className="w-4 h-4 rounded bg-slate-800 border-slate-700 text-blue-600 focus:ring-blue-500/20 cursor-pointer accent-blue-600"
/>
<span className="font-medium">记住密码</span>
</label>
<span className="text-[11px] text-slate-500 hover:text-slate-400 cursor-pointer">
忘记密码请联系管理员
</span>
</div>
<Form.Item className="!mb-2">
<Button
type="primary"
htmlType="submit"
block
loading={loading}
className="!h-11 !rounded-xl !bg-gradient-to-r !from-blue-600 !to-indigo-600 hover:!from-blue-500 hover:!to-indigo-500 !border-0 !text-sm !font-semibold shadow-lg shadow-blue-600/30 transition-all duration-200 cursor-pointer flex items-center justify-center gap-2"
>
<span>立即登录</span>
<ArrowRightOutlined className="text-xs" />
</Button>
</Form.Item>
</Form>
</div>
</div>
</main>
{/* 底部功能特性与版权 */}
<footer className="relative z-10 w-full max-w-4xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-2 text-xs text-slate-500 py-3 border-t border-slate-800/80">
<div className="flex items-center gap-4 text-[11px]">
<span className="inline-flex items-center gap-1"><CheckCircleFilled className="text-emerald-500 text-xs" /> 多租户数据隔离</span>
<span className="inline-flex items-center gap-1"><CheckCircleFilled className="text-emerald-500 text-xs" /> WebSocket 实时长连</span>
<span className="inline-flex items-center gap-1"><CheckCircleFilled className="text-emerald-500 text-xs" /> 全渠道客服集成</span>
</div>
<div className="text-[11px] text-slate-500">
&copy; {new Date().getFullYear()} Kefu Cloud. All Rights Reserved.
</div>
</footer>
</div>
)
}
export default Login