119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
'use client';
|
||
|
||
import * as Dialog from '@radix-ui/react-dialog';
|
||
import { useRouter } from 'next/navigation';
|
||
import { useState } from 'react';
|
||
import { useAuthStore } from '@/stores/auth-store';
|
||
import { useAuthUiStore } from '@/stores/auth-ui-store';
|
||
|
||
export function LoginModal() {
|
||
const open = useAuthUiStore((s) => s.loginModalOpen);
|
||
const close = useAuthUiStore((s) => s.closeLoginModal);
|
||
const hint = useAuthUiStore((s) => s.loginHint);
|
||
const login = useAuthStore((s) => s.login);
|
||
const router = useRouter();
|
||
|
||
const [user, setUser] = useState('');
|
||
const [pass, setPass] = useState('');
|
||
const [tenant, setTenant] = useState('');
|
||
const [err, setErr] = useState<string | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
async function onSubmit(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
setErr(null);
|
||
setLoading(true);
|
||
try {
|
||
await login(user, pass, tenant || undefined);
|
||
close();
|
||
setUser('');
|
||
setPass('');
|
||
setTenant('');
|
||
router.refresh();
|
||
} catch (ex) {
|
||
setErr(ex instanceof Error ? ex.message : String(ex));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Dialog.Root
|
||
open={open}
|
||
onOpenChange={(next) => {
|
||
if (!next) {
|
||
close();
|
||
}
|
||
}}
|
||
>
|
||
<Dialog.Portal>
|
||
<Dialog.Overlay className="fixed inset-0 z-280 bg-black/40" />
|
||
<Dialog.Content
|
||
className="fixed left-1/2 top-1/2 z-280 w-full max-w-sm -translate-x-1/2 -translate-y-1/2 rounded-lg border border-neutral-200 bg-white p-5 shadow-xl outline-none focus:outline-none"
|
||
onPointerDownOutside={(e) => e.preventDefault()}
|
||
>
|
||
<Dialog.Title id="relogin-title" className="text-lg font-medium text-neutral-900">
|
||
重新登录
|
||
</Dialog.Title>
|
||
{hint ? (
|
||
<Dialog.Description className="mt-1 text-sm text-neutral-600">{hint}</Dialog.Description>
|
||
) : (
|
||
<Dialog.Description className="sr-only">请输入用户名与密码以继续操作。</Dialog.Description>
|
||
)}
|
||
<form onSubmit={onSubmit} className="mt-4 space-y-3">
|
||
<label className="block text-sm">
|
||
<span className="text-neutral-600">用户名</span>
|
||
<input
|
||
className="mt-1 w-full rounded border border-neutral-300 px-2 py-1"
|
||
value={user}
|
||
onChange={(e) => setUser(e.target.value)}
|
||
autoComplete="username"
|
||
required
|
||
/>
|
||
</label>
|
||
<label className="block text-sm">
|
||
<span className="text-neutral-600">密码</span>
|
||
<input
|
||
type="password"
|
||
className="mt-1 w-full rounded border border-neutral-300 px-2 py-1"
|
||
value={pass}
|
||
onChange={(e) => setPass(e.target.value)}
|
||
autoComplete="current-password"
|
||
required
|
||
/>
|
||
</label>
|
||
<label className="block text-sm">
|
||
<span className="text-neutral-600">租户 ID(可空)</span>
|
||
<input
|
||
className="mt-1 w-full rounded border border-neutral-300 px-2 py-1"
|
||
value={tenant}
|
||
onChange={(e) => setTenant(e.target.value)}
|
||
/>
|
||
</label>
|
||
{err ? <p className="text-sm text-red-600">{err}</p> : null}
|
||
<div className="flex justify-end gap-2 pt-2">
|
||
<button
|
||
type="button"
|
||
className="rounded border border-neutral-300 px-3 py-1.5 text-sm"
|
||
onClick={() => {
|
||
close();
|
||
setErr(null);
|
||
}}
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
className="rounded bg-neutral-900 px-3 py-1.5 text-sm text-white disabled:opacity-50"
|
||
>
|
||
{loading ? '提交中…' : '登录'}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</Dialog.Content>
|
||
</Dialog.Portal>
|
||
</Dialog.Root>
|
||
);
|
||
}
|