|
@@ -1,18 +1,20 @@
|
|
|
/**
|
|
/**
|
|
|
- * 认证弹窗 - 登录 / 注册 / 重置密码三视图(前端交互占位,无后端)
|
|
|
|
|
|
|
+ * 认证弹窗 - 登录 / 注册 / 重置密码三视图,对接真实 API(演示环境默认 localhost:8081)
|
|
|
* 通过 ?auth=login|register|reset 查询参数打开;含 8 格验证码输入与 60 秒重发倒计时
|
|
* 通过 ?auth=login|register|reset 查询参数打开;含 8 格验证码输入与 60 秒重发倒计时
|
|
|
*/
|
|
*/
|
|
|
'use client';
|
|
'use client';
|
|
|
|
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
|
-import { useTranslations } from 'next-intl';
|
|
|
|
|
|
|
+import { useLocale, useTranslations } from 'next-intl';
|
|
|
|
|
|
|
|
type View = 'login' | 'register' | 'reset';
|
|
type View = 'login' | 'register' | 'reset';
|
|
|
-type ErrKind = 'email' | 'pwd' | 'code';
|
|
|
|
|
|
|
+type ErrKind = 'email' | 'pwd' | 'code' | 'server';
|
|
|
|
|
|
|
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
const CODE_RE = /^[A-Z0-9]{8}$/;
|
|
const CODE_RE = /^[A-Z0-9]{8}$/;
|
|
|
|
|
+/** 演示环境 API 地址(生产替换为真实网关域名) */
|
|
|
|
|
+const API_BASE = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:8081/v1';
|
|
|
|
|
|
|
|
/** 8 格验证码输入:自动跳格 + 转大写 + 仅 A-Z0-9 + 粘贴分发 */
|
|
/** 8 格验证码输入:自动跳格 + 转大写 + 仅 A-Z0-9 + 粘贴分发 */
|
|
|
function CodeInput({
|
|
function CodeInput({
|
|
@@ -77,6 +79,7 @@ function CodeInput({
|
|
|
|
|
|
|
|
export function AuthModal() {
|
|
export function AuthModal() {
|
|
|
const t = useTranslations('auth');
|
|
const t = useTranslations('auth');
|
|
|
|
|
+ const locale = useLocale();
|
|
|
const searchParams = useSearchParams();
|
|
const searchParams = useSearchParams();
|
|
|
const router = useRouter();
|
|
const router = useRouter();
|
|
|
const pathname = usePathname();
|
|
const pathname = usePathname();
|
|
@@ -92,6 +95,8 @@ export function AuthModal() {
|
|
|
const [confirmNew, setConfirmNew] = useState('');
|
|
const [confirmNew, setConfirmNew] = useState('');
|
|
|
const [code, setCode] = useState('');
|
|
const [code, setCode] = useState('');
|
|
|
const [error, setError] = useState<ErrKind | null>(null);
|
|
const [error, setError] = useState<ErrKind | null>(null);
|
|
|
|
|
+ const [serverMsg, setServerMsg] = useState('');
|
|
|
|
|
+ const [submitting, setSubmitting] = useState(false);
|
|
|
const [sentEmail, setSentEmail] = useState<string | null>(null);
|
|
const [sentEmail, setSentEmail] = useState<string | null>(null);
|
|
|
const [countdown, setCountdown] = useState(0);
|
|
const [countdown, setCountdown] = useState(0);
|
|
|
|
|
|
|
@@ -126,33 +131,103 @@ export function AuthModal() {
|
|
|
|
|
|
|
|
const close = () => router.replace(pathname, { scroll: false });
|
|
const close = () => router.replace(pathname, { scroll: false });
|
|
|
|
|
|
|
|
- const sendCode = () => {
|
|
|
|
|
|
|
+ const sendCode = async () => {
|
|
|
if (!EMAIL_RE.test(email)) {
|
|
if (!EMAIL_RE.test(email)) {
|
|
|
setError('email');
|
|
setError('email');
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
setError(null);
|
|
setError(null);
|
|
|
- setSentEmail(email);
|
|
|
|
|
- setCountdown(60);
|
|
|
|
|
|
|
+ setSubmitting(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const resp = await fetch(`${API_BASE}/auth/send-code`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ email, purpose: 'register' }),
|
|
|
|
|
+ });
|
|
|
|
|
+ const data = await resp.json().catch(() => ({}));
|
|
|
|
|
+ if (resp.ok) {
|
|
|
|
|
+ setSentEmail(email);
|
|
|
|
|
+ setCountdown(60);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setServerMsg(data.message || '验证码发送失败');
|
|
|
|
|
+ setError('server');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ setServerMsg('无法连接 API,请确认演示服务已启动(localhost:8081)');
|
|
|
|
|
+ setError('server');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setSubmitting(false);
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const submitLogin = () => {
|
|
|
|
|
- if (!EMAIL_RE.test(email)) setError('email');
|
|
|
|
|
- else setError(null);
|
|
|
|
|
|
|
+ const submitLogin = async () => {
|
|
|
|
|
+ if (!EMAIL_RE.test(email)) return setError('email');
|
|
|
|
|
+ setSubmitting(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const resp = await fetch(`${API_BASE}/auth/login`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ email, password }),
|
|
|
|
|
+ });
|
|
|
|
|
+ const data = await resp.json().catch(() => ({}));
|
|
|
|
|
+ if (resp.ok && data.accessToken) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ localStorage.setItem('dotouch_token', data.accessToken);
|
|
|
|
|
+ localStorage.setItem('dotouch_user', email);
|
|
|
|
|
+ } catch { /* 存储失败不影响跳转 */ }
|
|
|
|
|
+ setError(null);
|
|
|
|
|
+ close();
|
|
|
|
|
+ router.push(`/${locale}/console`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setServerMsg(data.message || '登录失败');
|
|
|
|
|
+ setError('server');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ setServerMsg('无法连接 API,请确认演示服务已启动(localhost:8081)');
|
|
|
|
|
+ setError('server');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setSubmitting(false);
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const submitRegister = () => {
|
|
|
|
|
|
|
+ const submitRegister = async () => {
|
|
|
if (!EMAIL_RE.test(email)) return setError('email');
|
|
if (!EMAIL_RE.test(email)) return setError('email');
|
|
|
if (password !== confirm) return setError('pwd');
|
|
if (password !== confirm) return setError('pwd');
|
|
|
if (!CODE_RE.test(code)) return setError('code');
|
|
if (!CODE_RE.test(code)) return setError('code');
|
|
|
- setError(null);
|
|
|
|
|
|
|
+ setSubmitting(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const resp = await fetch(`${API_BASE}/auth/register`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ email, password, code }),
|
|
|
|
|
+ });
|
|
|
|
|
+ const data = await resp.json().catch(() => ({}));
|
|
|
|
|
+ if (resp.ok) {
|
|
|
|
|
+ setError(null);
|
|
|
|
|
+ setView('login');
|
|
|
|
|
+ setSentEmail(email);
|
|
|
|
|
+ setEmail('');
|
|
|
|
|
+ setPassword('');
|
|
|
|
|
+ setConfirm('');
|
|
|
|
|
+ setCode('');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setServerMsg(data.message || '注册失败');
|
|
|
|
|
+ setError('server');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ setServerMsg('无法连接 API,请确认演示服务已启动(localhost:8081)');
|
|
|
|
|
+ setError('server');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setSubmitting(false);
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const submitReset = () => {
|
|
const submitReset = () => {
|
|
|
if (!EMAIL_RE.test(email)) return setError('email');
|
|
if (!EMAIL_RE.test(email)) return setError('email');
|
|
|
if (!CODE_RE.test(code)) return setError('code');
|
|
if (!CODE_RE.test(code)) return setError('code');
|
|
|
if (newPwd !== confirmNew) return setError('pwd');
|
|
if (newPwd !== confirmNew) return setError('pwd');
|
|
|
- setError(null);
|
|
|
|
|
|
|
+ setServerMsg('演示环境暂未开放自助重置,请联系管理员');
|
|
|
|
|
+ setError('server');
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const fieldClass =
|
|
const fieldClass =
|
|
@@ -181,7 +256,7 @@ export function AuthModal() {
|
|
|
|
|
|
|
|
const errorBox = error ? (
|
|
const errorBox = error ? (
|
|
|
<div className="mb-3 rounded-[9px] border border-[rgba(255,107,107,.35)] bg-[rgba(255,107,107,.1)] px-3 py-[9px] text-[12.5px] text-[#FF8A8A]">
|
|
<div className="mb-3 rounded-[9px] border border-[rgba(255,107,107,.35)] bg-[rgba(255,107,107,.1)] px-3 py-[9px] text-[12.5px] text-[#FF8A8A]">
|
|
|
- {t(`err_${error}`)}
|
|
|
|
|
|
|
+ {error === 'server' ? serverMsg : t(`err_${error}`)}
|
|
|
</div>
|
|
</div>
|
|
|
) : null;
|
|
) : null;
|
|
|
|
|
|