| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- 'use client';
- import { useState, useEffect } from 'react';
- import api from '@/lib/api';
- import { X, Mail, Loader2, Save, Lock, ArrowRight, ArrowLeft } from 'lucide-react';
- import { useLanguage } from '@/lib/i18n/LanguageContext';
- // 1. 引入通用消息弹窗
- import MessageModal from '@/components/common/MessageModal';
- interface BindEmailModalProps {
- isOpen: boolean;
- onClose: () => void;
- onSuccess: () => void;
- }
- export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmailModalProps) {
- const { t } = useLanguage();
- const [step, setStep] = useState<1 | 2>(1);
- const [loading, setLoading] = useState(false);
-
- const [email, setEmail] = useState('');
- const [code, setCode] = useState('');
-
- const [countdown, setCountdown] = useState(0);
- // 2. 消息弹窗状态
- const [msgModal, setMsgModal] = useState({
- isOpen: false,
- title: '',
- message: '',
- type: 'info' as 'info' | 'error' | 'success',
- onOk: null as (() => void) | null,
- });
- const showMessage = (msg: string, type: 'info' | 'error' | 'success' = 'info', onOk?: () => void) => {
- setMsgModal({
- isOpen: true,
- title: type === 'error' ? t('common.error') : t('common.notice'),
- message: msg,
- type,
- onOk: onOk || null,
- });
- };
- const handleCloseMsg = () => {
- const callback = msgModal.onOk;
- setMsgModal(prev => ({ ...prev, isOpen: false }));
- if (callback) callback();
- };
- useEffect(() => {
- let timer: NodeJS.Timeout;
- if (countdown > 0) {
- timer = setTimeout(() => setCountdown(c => c - 1), 1000);
- }
- return () => clearTimeout(timer);
- }, [countdown]);
- useEffect(() => {
- if (isOpen) {
- setStep(1);
- setCode('');
- }
- }, [isOpen]);
- const handleSendCode = async (e?: React.FormEvent) => {
- if (e) e.preventDefault();
- if (!email) return showMessage(t('bind_email.alert_input_email'), 'error');
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return showMessage(t('bind_email.alert_invalid_email'), 'error');
-
- setLoading(true);
- try {
- await api.post('/api/auth/send-bind-code', { email });
-
- showMessage(`${t('bind_email.alert_code_sent')} ${email}`, 'success');
- setStep(2);
- setCountdown(60);
- } catch (error: any) {
- console.error(error);
- const msg = error.response?.data?.message || t('common.unknown_error');
- showMessage(`${t('bind_email.alert_send_failed')}: ${msg}`, 'error');
- } finally {
- setLoading(false);
- }
- };
- const handleVerifyAndBind = async (e: React.FormEvent) => {
- e.preventDefault();
- if (!code) return showMessage(t('bind_email.alert_input_code'), 'error');
- if (code.length !== 6) return showMessage(t('bind_email.alert_code_length'), 'error');
- setLoading(true);
- try {
- const res = await api.post('/api/auth/bind-email', { email, code });
- const data = res.data.data || res.data;
- const newToken = data.token || data.access_token;
- const newUser = data.user;
- if (newToken) {
- localStorage.setItem('rsid', newToken);
- if (newUser) {
- localStorage.setItem('user_info', JSON.stringify(newUser));
- } else {
- const oldUser = JSON.parse(localStorage.getItem('user_info') || '{}');
- localStorage.setItem('user_info', JSON.stringify({ ...oldUser, email }));
- }
- window.dispatchEvent(new Event('storage'));
-
- // 绑定成功后提示,点击确认后关闭弹窗并执行 onSuccess
- showMessage(t('bind_email.success'), 'success', () => {
- onSuccess();
- onClose();
- });
- } else {
- throw new Error("Token missing");
- }
- } catch (error: any) {
- console.error(error);
- const msg = error.response?.data?.message || t('common.unknown_error');
- showMessage(`${t('bind_email.failed')}: ${msg}`, 'error');
- } finally {
- setLoading(false);
- }
- };
- if (!isOpen) return null;
- return (
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200">
- <div className="bg-white rounded-xl shadow-2xl w-full max-w-md overflow-hidden animate-in zoom-in-95 duration-200">
-
- {/* Header */}
- <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
- <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
- {step === 1 ? <Mail size={20} className="text-blue-600"/> : <Lock size={20} className="text-blue-600"/>}
- {step === 1 ? t('bind_email.title_step1') : t('bind_email.title_step2')}
- </h3>
- <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition p-1 hover:bg-gray-200 rounded-full">
- <X size={24} />
- </button>
- </div>
- <div className="p-6">
- {/* Progress Indicator */}
- <div className="flex gap-2 mb-6">
- <div className={`h-1.5 flex-1 rounded-full transition-colors duration-300 ${step >= 1 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
- <div className={`h-1.5 flex-1 rounded-full transition-colors duration-300 ${step >= 2 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
- </div>
- {step === 1 ? (
- <form onSubmit={handleSendCode} className="space-y-5">
- <p className="text-sm text-gray-500 leading-relaxed">
- {t('bind_email.desc_step1')}
- </p>
- <div>
- <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('bind_email.email_label')}</label>
- <input
- type="email" required
- className="w-full border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
- placeholder={t('bind_email.email_placeholder')}
- value={email}
- onChange={(e) => setEmail(e.target.value)}
- />
- </div>
- <button
- type="submit"
- disabled={loading}
- className="w-full bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 transition flex justify-center items-center gap-2 disabled:opacity-50 shadow-md shadow-blue-100"
- >
- {loading ? <Loader2 size={18} className="animate-spin" /> : <>{t('bind_email.send_btn')} <ArrowRight size={18} /></>}
- </button>
- </form>
- ) : (
- <form onSubmit={handleVerifyAndBind} className="space-y-5">
- <p className="text-sm text-gray-500">
- {t('bind_email.desc_step2_prefix')} <span className="font-bold text-gray-900">{email}</span>{t('bind_email.desc_step2_suffix')}
- </p>
-
- <div>
- <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('bind_email.code_label')}</label>
- <div className="flex gap-3">
- <input
- type="text" required
- className="flex-1 border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none font-mono text-center tracking-[0.25em] text-lg font-bold"
- placeholder="------"
- maxLength={6}
- value={code}
- onChange={(e) => setCode(e.target.value)}
- />
- <button
- type="button"
- disabled={countdown > 0 || loading}
- onClick={() => handleSendCode()}
- className="w-32 border border-slate-300 bg-gray-50 text-slate-600 rounded-lg text-xs font-medium hover:bg-white hover:border-blue-300 hover:text-blue-600 disabled:opacity-50 disabled:bg-slate-100 disabled:cursor-not-allowed transition"
- >
- {countdown > 0 ? `${countdown}${t('bind_email.resend_suffix')}` : t('bind_email.resend_btn')}
- </button>
- </div>
- </div>
- <div className="flex gap-3 pt-2">
- <button
- type="button"
- onClick={() => setStep(1)}
- className="px-4 py-2 text-slate-500 hover:text-slate-700 flex items-center gap-1 text-sm font-medium transition"
- >
- <ArrowLeft size={16} /> {t('bind_email.change_email_btn')}
- </button>
- <button
- type="submit"
- disabled={loading}
- className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 transition flex justify-center items-center gap-2 disabled:opacity-50 shadow-md shadow-blue-100"
- >
- {loading ? <Loader2 size={18} className="animate-spin" /> : <><Save size={18} /> {t('bind_email.confirm_btn')}</>}
- </button>
- </div>
- </form>
- )}
- </div>
- </div>
- {/* 3. 挂载消息弹窗 */}
- <MessageModal
- isOpen={msgModal.isOpen}
- title={msgModal.title}
- message={msgModal.message}
- type={msgModal.type}
- onClose={handleCloseMsg}
- />
- </div>
- );
- }
|