| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896 |
- import React, { useState, useEffect, useRef } from 'react';
- import { useNavigate, useLocation } from 'react-router-dom';
- import { Copy, LogOut, Edit2, Mail, Phone, User, Camera, AlertCircle, CheckCircle2, Shield, Check } from 'lucide-react';
- import { authService, UserInfo } from '../services/authService';
- import { userApi, UpdateUserRequest } from '../services/userApi';
- import { ossApi } from '../services/ossApi';
- import { Loader2 } from '../icons/commonIcons';
- import { validateIDCard, validateRealName } from '../utils/idCardValidator';
- import { encryptVerificationData } from '../utils/rsaEncryption';
- import { copyToClipboard } from '../utils/clipboard';
- interface EditModalProps {
- isOpen: boolean;
- onClose: () => void;
- title: string;
- field: keyof UpdateUserRequest;
- currentValue: string;
- onSave: (value: string) => Promise<void>;
- placeholder?: string;
- type?: string;
- }
- const EditModal: React.FC<EditModalProps> = ({
- isOpen, onClose, title, currentValue, onSave, placeholder, type = 'text'
- }) => {
- const [value, setValue] = useState(currentValue);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string | null>(null);
- useEffect(() => { setValue(currentValue); setError(null); }, [currentValue, isOpen]);
- const handleSave = async () => {
- if (value.trim() === currentValue) { onClose(); return; }
- setLoading(true); setError(null);
- try { await onSave(value.trim()); onClose(); }
- catch (err) { setError(err instanceof Error ? err.message : '保存失败'); }
- finally { setLoading(false); }
- };
- if (!isOpen) return null;
- return (
- <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
- <div className="bg-white rounded-2xl shadow-2xl w-full max-w-md p-6">
- <h3 className="text-lg font-bold text-gray-900 mb-4">{title}</h3>
- {error && (
- <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2">
- <AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
- <p className="text-sm text-red-600">{error}</p>
- </div>
- )}
- <div className="mb-6">
- <input type={type} value={value} onChange={e => setValue(e.target.value)}
- placeholder={placeholder}
- className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
- disabled={loading} />
- </div>
- <div className="flex gap-3">
- <button onClick={onClose} disabled={loading}
- className="flex-1 py-2 px-4 bg-gray-100 text-gray-700 rounded-lg font-medium hover:bg-gray-200 transition-colors disabled:opacity-50">取消</button>
- <button onClick={handleSave} disabled={loading || value.trim() === currentValue}
- className="flex-1 py-2 px-4 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2">
- {loading ? <><Loader2 className="w-4 h-4 animate-spin" /><span>保存中...</span></> : <span>保存</span>}
- </button>
- </div>
- </div>
- </div>
- );
- };
- // 绑定/换绑邮箱弹窗(带验证码)
- interface BindEmailModalProps {
- isOpen: boolean;
- onClose: () => void;
- onSaved: (email: string) => void;
- }
- const BindEmailModal: React.FC<BindEmailModalProps> = ({ isOpen, onClose, onSaved }) => {
- const [email, setEmail] = useState('');
- const [code, setCode] = useState('');
- const [sending, setSending] = useState(false);
- const [countdown, setCountdown] = useState(0);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState('');
- useEffect(() => { if (!isOpen) { setEmail(''); setCode(''); setError(''); setCountdown(0); } }, [isOpen]);
- useEffect(() => {
- if (countdown <= 0) return;
- const t = setTimeout(() => setCountdown(c => c - 1), 1000);
- return () => clearTimeout(t);
- }, [countdown]);
- const handleSend = async () => {
- if (!email) { setError('请输入邮箱'); return; }
- const emailRe = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
- if (!emailRe.test(email)) { setError('邮箱格式不正确'); return; }
- setSending(true); setError('');
- try { await userApi.sendEmailCode(email, 'register'); setCountdown(60); }
- catch (e: any) { setError(e.message || '发送失败'); }
- finally { setSending(false); }
- };
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- if (!email || !code) { setError('请填写邮箱和验证码'); return; }
- setLoading(true); setError('');
- try {
- await userApi.verifyEmailCode(email, code);
- const updatedUser = await userApi.updateCurrentUser({ email });
- onSaved(updatedUser.email || email);
- onClose();
- } catch (e: any) { setError(e.message || '操作失败'); }
- finally { setLoading(false); }
- };
- if (!isOpen) return null;
- return (
- <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
- <div className="bg-white rounded-2xl shadow-2xl w-full max-w-md p-6">
- <h3 className="text-lg font-bold text-gray-900 mb-4">绑定邮箱</h3>
- <form onSubmit={handleSubmit} className="space-y-4">
- {error && (
- <div className="p-3 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2">
- <AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
- <p className="text-sm text-red-600">{error}</p>
- </div>
- )}
- <div>
- <label className="block text-sm font-bold text-gray-700 mb-1">邮箱</label>
- <input type="email" value={email} onChange={e => setEmail(e.target.value)}
- placeholder="请输入邮箱地址" autoComplete="off"
- className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500" required />
- </div>
- <div>
- <label className="block text-sm font-bold text-gray-700 mb-1">验证码</label>
- <div className="flex gap-2">
- <input type="text" value={code} onChange={e => setCode(e.target.value)} maxLength={6}
- placeholder="请输入验证码" autoComplete="one-time-code"
- className="flex-1 px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500" required />
- <button type="button" onClick={handleSend} disabled={sending || countdown > 0}
- className="px-4 py-3 bg-blue-600 text-white rounded-xl text-sm font-bold hover:bg-blue-700 disabled:opacity-50 whitespace-nowrap">
- {sending ? '发送中...' : countdown > 0 ? `${countdown}s` : '获取验证码'}
- </button>
- </div>
- </div>
- <div className="flex gap-3 pt-2">
- <button type="button" onClick={onClose}
- className="flex-1 py-2.5 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200">取消</button>
- <button type="submit" disabled={loading}
- className="flex-1 py-2.5 bg-blue-600 text-white rounded-xl font-bold hover:bg-blue-700 disabled:opacity-50 flex items-center justify-center gap-2">
- {loading ? <><Loader2 className="w-4 h-4 animate-spin" />保存中...</> : '确认绑定'}
- </button>
- </div>
- </form>
- </div>
- </div>
- );
- };
- // 绑定/换绑手机号弹窗(带验证码)
- interface BindPhoneModalProps {
- isOpen: boolean;
- onClose: () => void;
- onSaved: (phone: string) => void;
- }
- const BindPhoneModal: React.FC<BindPhoneModalProps> = ({ isOpen, onClose, onSaved }) => {
- const [phone, setPhone] = useState('');
- const [code, setCode] = useState('');
- const [sending, setSending] = useState(false);
- const [countdown, setCountdown] = useState(0);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState('');
- useEffect(() => { if (!isOpen) { setPhone(''); setCode(''); setError(''); setCountdown(0); } }, [isOpen]);
- useEffect(() => {
- if (countdown <= 0) return;
- const t = setTimeout(() => setCountdown(c => c - 1), 1000);
- return () => clearTimeout(t);
- }, [countdown]);
- const handleSend = async () => {
- if (!phone || phone.length !== 11) { setError('请输入正确的手机号'); return; }
- setSending(true); setError('');
- try { await userApi.sendSmsCode(phone, 'register'); setCountdown(60); }
- catch (e: any) { setError(e.message || '发送失败'); }
- finally { setSending(false); }
- };
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- if (!phone || !code) { setError('请填写手机号和验证码'); return; }
- setLoading(true); setError('');
- try {
- // 先验证验证码
- await userApi.verifySmsCode(phone, code);
- // 再更新手机号,用服务器返回的掩码值
- const updatedUser = await userApi.updateCurrentUser({ phone });
- onSaved(updatedUser.phone || phone);
- onClose();
- } catch (e: any) { setError(e.message || '操作失败'); }
- finally { setLoading(false); }
- };
- if (!isOpen) return null;
- return (
- <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
- <div className="bg-white rounded-2xl shadow-2xl w-full max-w-md p-6">
- <h3 className="text-lg font-bold text-gray-900 mb-4">绑定手机号</h3>
- <form onSubmit={handleSubmit} className="space-y-4">
- {error && (
- <div className="p-3 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2">
- <AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
- <p className="text-sm text-red-600">{error}</p>
- </div>
- )}
- <div>
- <label className="block text-sm font-bold text-gray-700 mb-1">手机号</label>
- <input type="tel" value={phone} onChange={e => setPhone(e.target.value)} maxLength={11}
- placeholder="请输入手机号" autoComplete="off"
- className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500" required />
- </div>
- <div>
- <label className="block text-sm font-bold text-gray-700 mb-1">验证码</label>
- <div className="flex gap-2">
- <input type="text" value={code} onChange={e => setCode(e.target.value)} maxLength={6}
- placeholder="请输入验证码" autoComplete="one-time-code"
- className="flex-1 px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500" required />
- <button type="button" onClick={handleSend} disabled={sending || countdown > 0}
- className="px-4 py-3 bg-blue-600 text-white rounded-xl text-sm font-bold hover:bg-blue-700 disabled:opacity-50 whitespace-nowrap">
- {sending ? '发送中...' : countdown > 0 ? `${countdown}s` : '获取验证码'}
- </button>
- </div>
- </div>
- <div className="flex gap-3 pt-2">
- <button type="button" onClick={onClose}
- className="flex-1 py-2.5 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200">取消</button>
- <button type="submit" disabled={loading}
- className="flex-1 py-2.5 bg-blue-600 text-white rounded-xl font-bold hover:bg-blue-700 disabled:opacity-50 flex items-center justify-center gap-2">
- {loading ? <><Loader2 className="w-4 h-4 animate-spin" />保存中...</> : '确认绑定'}
- </button>
- </div>
- </form>
- </div>
- </div>
- );
- };
- const Profile: React.FC = () => {
- const navigate = useNavigate();
- const location = useLocation();
- const [copied, setCopied] = useState(false);
- const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
- const [editModal, setEditModal] = useState<{
- isOpen: boolean;
- field: keyof UpdateUserRequest;
- title: string;
- placeholder?: string;
- type?: string;
- }>({
- isOpen: false,
- field: 'nickname',
- title: '',
- });
- const [bindPhoneOpen, setBindPhoneOpen] = useState(false);
- const [bindEmailOpen, setBindEmailOpen] = useState(false);
- const [successMessage, setSuccessMessage] = useState<string | null>(null);
- const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
- const [avatarUploading, setAvatarUploading] = useState(false);
- const [verifyModalOpen, setVerifyModalOpen] = useState(false);
- const [verifyData, setVerifyData] = useState({ real_name: '', id_card: '' });
- const [verifyLoading, setVerifyLoading] = useState(false);
- const [verifyError, setVerifyError] = useState<string | null>(null);
- const avatarInputRef = useRef<HTMLInputElement>(null);
- useEffect(() => {
- loadUserInfo();
- }, []);
- // 检查URL参数,如果有 openVerify=true,自动打开认证弹窗
- useEffect(() => {
- const searchParams = new URLSearchParams(location.search);
- if (searchParams.get('openVerify') === 'true' && userInfo && userInfo.isVerified !== 'verified') {
- // 延迟一下,等页面加载完成
- setTimeout(() => {
- handleOpenVerifyModal();
- }, 500);
- }
- }, [location.search, userInfo]);
- const loadUserInfo = async () => {
- try {
- setLoading(true);
- setError(null);
-
- // 检查是否已登录
- if (!authService.isAuthenticated()) {
- navigate('/login');
- return;
- }
- // 从本地存储获取用户信息
- const localUserInfo = authService.getUserInfo();
- if (localUserInfo) {
- setUserInfo(localUserInfo);
- }
- // 从服务器获取最新用户信息
- const currentUser = await userApi.getCurrentUser();
- const updatedUserInfo = {
- id: currentUser.id,
- nickname: currentUser.nickname,
- phone: currentUser.phone || undefined,
- email: currentUser.email || undefined,
- avatar: currentUser.avatar || undefined,
- registrationDate: currentUser.registration_date,
- realName: currentUser.real_name || undefined,
- isVerified: currentUser.is_verified || 'unverified',
- verifiedAt: currentUser.verified_at || undefined,
- };
-
- setUserInfo(updatedUserInfo);
- // 更新本地存储
- authService.setToken(authService.getToken()!, updatedUserInfo);
- } catch (err) {
- setError(err instanceof Error ? err.message : '加载用户信息失败');
- // 如果是认证错误,跳转到登录页
- if (err instanceof Error && err.message.includes('未授权')) {
- navigate('/login');
- }
- } finally {
- setLoading(false);
- }
- };
- const handleEdit = (field: keyof UpdateUserRequest, title: string, placeholder?: string, type?: string) => {
- setEditModal({
- isOpen: true,
- field,
- title,
- placeholder,
- type,
- });
- };
- const handleSave = async (value: string) => {
- if (!userInfo) return;
- const updateData: UpdateUserRequest = {
- [editModal.field]: value,
- };
- const updatedUser = await userApi.updateCurrentUser(updateData);
-
- // 用服务器返回的值(已掩码)更新本地状态
- // phone/email 取服务器返回的掩码值,其他字段取本地输入值
- const serverValue = (updatedUser as Record<string, unknown>)[editModal.field];
- const updatedUserInfo = {
- ...userInfo,
- [editModal.field]: (serverValue !== undefined && serverValue !== null) ? serverValue : value,
- };
- setUserInfo(updatedUserInfo);
-
- // 更新本地存储
- authService.setToken(authService.getToken()!, updatedUserInfo);
-
- // 显示成功消息
- setSuccessMessage('信息更新成功');
- setTimeout(() => setSuccessMessage(null), 3000);
- };
- const handleLogout = async () => {
- console.log('[Profile] handleLogout called');
- authService.clearToken();
- navigate('/login');
- };
- const handleApplyDelete = () => {
- if (!userInfo) return;
- setDeleteConfirmOpen(true);
- };
- const handleConfirmDelete = async () => {
- if (!userInfo) return;
- setDeleteConfirmOpen(false);
- try {
- setLoading(true);
- await userApi.deleteCurrentUser();
- // 清理本地登录状态并跳转到首页
- authService.clearStorage();
- setUserInfo(null);
- setSuccessMessage('申请注销成功,账户已删除。');
- setTimeout(() => {
- setSuccessMessage(null);
- navigate('/', { replace: true });
- }, 1500);
- } catch (err) {
- setError(err instanceof Error ? err.message : '注销失败,请重试');
- } finally {
- setLoading(false);
- }
- };
- const handleCancelDelete = () => {
- setDeleteConfirmOpen(false);
- };
- const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
- const file = e.target.files?.[0];
- if (!file || !userInfo) return;
- // 验证文件类型
- if (!file.type.startsWith('image/')) {
- setError('请选择图片文件');
- return;
- }
- // 验证文件大小(最大5MB)
- if (file.size > 5 * 1024 * 1024) {
- setError('图片大小不能超过5MB');
- return;
- }
- setAvatarUploading(true);
- setError(null);
- try {
- // 上传到OSS
- const response = await ossApi.uploadFile(file, 'avatars');
- if (response.code === 200) {
- // 更新用户头像
- await userApi.updateCurrentUser({ avatar: response.data.url });
-
- // 更新本地状态
- const updatedUserInfo = { ...userInfo, avatar: response.data.url };
- setUserInfo(updatedUserInfo);
- authService.setToken(authService.getToken()!, updatedUserInfo);
-
- setSuccessMessage('头像更新成功');
- setTimeout(() => setSuccessMessage(null), 3000);
- } else {
- setError('上传失败,请重试');
- }
- } catch (err) {
- setError(err instanceof Error ? err.message : '上传失败');
- } finally {
- setAvatarUploading(false);
- // 清空input,允许重复选择同一文件
- if (avatarInputRef.current) {
- avatarInputRef.current.value = '';
- }
- }
- };
- const handleCopyId = () => {
- if (!userInfo?.id) return;
- copyToClipboard(userInfo.id);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- };
- const handleOpenVerifyModal = () => {
- setVerifyModalOpen(true);
- setVerifyError(null);
- setVerifyData({ real_name: '', id_card: '' });
- };
- const handleSubmitVerification = async () => {
- if (!verifyData.real_name.trim() || !verifyData.id_card.trim()) {
- setVerifyError('请填写完整信息');
- return;
- }
- // 验证真实姓名
- const nameValidation = validateRealName(verifyData.real_name);
- if (!nameValidation.valid) {
- setVerifyError(nameValidation.error);
- return;
- }
- // 验证身份证号
- const idCardValidation = validateIDCard(verifyData.id_card);
- if (!idCardValidation.valid) {
- setVerifyError(idCardValidation.error);
- return;
- }
- setVerifyLoading(true);
- setVerifyError(null);
- try {
- // 获取RSA公钥
- const { public_key } = await userApi.getRSAPublicKey();
-
- // 使用RSA加密数据
- const encryptedData = encryptVerificationData(
- verifyData.real_name.trim(),
- verifyData.id_card.trim().toUpperCase(),
- public_key
- );
-
- // 提交加密数据
- const updatedUser = await userApi.submitVerification(encryptedData);
-
- // 更新本地状态
- const updatedUserInfo = {
- ...userInfo!,
- realName: updatedUser.real_name || undefined,
- isVerified: updatedUser.is_verified || 'verified',
- verifiedAt: updatedUser.verified_at || undefined,
- };
- setUserInfo(updatedUserInfo);
- authService.setToken(authService.getToken()!, updatedUserInfo);
-
- setVerifyModalOpen(false);
- setSuccessMessage('实名认证提交成功');
- setTimeout(() => setSuccessMessage(null), 3000);
-
- // 清空表单
- setVerifyData({ real_name: '', id_card: '' });
- } catch (err) {
- setVerifyError(err instanceof Error ? err.message : '认证失败,请重试');
- } finally {
- setVerifyLoading(false);
- }
- };
- if (loading) {
- return (
- <div className="min-h-screen bg-gray-50 flex items-center justify-center">
- <div className="flex items-center gap-3">
- <Loader2 className="w-6 h-6 text-blue-600 animate-spin" />
- <span className="text-gray-600">加载用户信息中...</span>
- </div>
- </div>
- );
- }
- if (error) {
- return (
- <div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
- <div className="bg-white rounded-2xl shadow-lg p-8 max-w-md w-full text-center">
- <AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
- <h2 className="text-lg font-bold text-gray-900 mb-2">加载失败</h2>
- <p className="text-sm text-gray-600 mb-6">{error}</p>
- <button
- onClick={loadUserInfo}
- className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
- >
- 重试
- </button>
- </div>
- </div>
- );
- }
- if (!userInfo) {
- return null;
- }
- return (
- <div className="min-h-screen bg-gray-50 relative">
- {/* 成功消息 */}
- {successMessage && (
- <div className="fixed top-4 right-4 z-50 bg-green-50 border border-green-200 rounded-lg p-4 flex items-center gap-2 shadow-lg">
- <CheckCircle2 className="w-5 h-5 text-green-500" />
- <span className="text-sm text-green-700 font-medium">{successMessage}</span>
- </div>
- )}
- {/* 主要内容 */}
- <div className="flex flex-col items-center justify-start min-h-screen pt-8 pb-12 px-4">
- {/* 欢迎信息 */}
- <div className="text-center mb-6">
- <h1 className="text-2xl font-bold text-gray-900 tracking-tight mb-2">欢迎, {userInfo.nickname}</h1>
- <p className="text-sm text-gray-500">
- 管理自己的信息、隐私和安全,让我们更好地为您服务。
- </p>
- </div>
- {/* 用户信息卡片 */}
- <div className="w-full max-w-sm sm:max-w-xl md:max-w-2xl bg-white rounded-2xl border border-gray-100 p-4 sm:p-6 space-y-4 sm:space-y-6">
- {/* 账号 ID */}
- <div className="flex items-center justify-between py-3 border-b border-gray-200">
- <span className="text-sm text-gray-700 flex-shrink-0">账号 ID</span>
- <div className="flex items-center gap-2 min-w-0 ml-2">
- <span className="text-sm text-gray-900 font-mono truncate max-w-[120px] sm:max-w-none">{userInfo.id}</span>
- <button
- onClick={handleCopyId}
- className="p-1.5 hover:bg-gray-200 rounded transition-colors"
- title="复制"
- >
- <Copy className="w-4 h-4 text-gray-600" />
- </button>
- {copied && (
- <span className="text-xs text-green-600">已复制</span>
- )}
- </div>
- </div>
- {/* 头像 */}
- <div className="flex items-center justify-between py-3 border-b border-gray-200">
- <span className="text-sm text-gray-700">头像</span>
- <div className="flex items-center gap-4">
- {userInfo.avatar ? (
- <img
- src={userInfo.avatar}
- alt="头像"
- className="w-12 h-12 rounded-full object-cover"
- />
- ) : (
- <div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center text-white text-lg font-bold">
- {userInfo.nickname.charAt(0).toUpperCase()}
- </div>
- )}
- <input
- ref={avatarInputRef}
- type="file"
- accept="image/*"
- onChange={handleAvatarUpload}
- className="hidden"
- />
- <button
- onClick={() => avatarInputRef.current?.click()}
- disabled={avatarUploading}
- className="text-blue-600 hover:text-blue-700 text-sm font-bold transition-colors flex items-center gap-1 disabled:opacity-50"
- >
- {avatarUploading ? (
- <>
- <Loader2 className="w-4 h-4 animate-spin" />
- 上传中...
- </>
- ) : (
- <>
- <Camera className="w-4 h-4" />
- 更换
- </>
- )}
- </button>
- </div>
- </div>
- {/* 昵称 */}
- <div className="flex items-center justify-between py-3 border-b border-gray-200">
- <span className="text-sm text-gray-700">昵称</span>
- <div className="flex items-center gap-2">
- <span className="text-sm text-gray-900">{userInfo.nickname}</span>
- <button
- onClick={() => handleEdit('nickname', '修改昵称', '请输入新昵称')}
- className="text-blue-600 hover:text-blue-700 text-sm font-bold transition-colors flex items-center gap-1"
- >
- <Edit2 className="w-3 h-3" />
- 修改
- </button>
- </div>
- </div>
- {/* 手机 */}
- <div className="flex items-center justify-between py-3 border-b border-gray-200">
- <span className="text-sm text-gray-700">手机</span>
- <div className="flex items-center gap-2">
- <span className="text-sm text-gray-900">
- {userInfo.phone || '未绑定'}
- </span>
- <button
- onClick={() => setBindPhoneOpen(true)}
- className="text-blue-600 hover:text-blue-700 text-sm font-bold transition-colors flex items-center gap-1"
- >
- <Phone className="w-3 h-3" />
- {userInfo.phone ? '换绑' : '绑定'}
- </button>
- </div>
- </div>
- {/* 邮箱 */}
- <div className="flex items-center justify-between py-3 border-b border-gray-200">
- <span className="text-sm text-gray-700">邮箱</span>
- <div className="flex items-center gap-2">
- <span className="text-sm text-gray-900">
- {userInfo.email || '未绑定'}
- </span>
- <button
- onClick={() => setBindEmailOpen(true)}
- className="text-blue-600 hover:text-blue-700 text-sm font-bold transition-colors flex items-center gap-1"
- >
- <Mail className="w-3 h-3" />
- {userInfo.email ? '换绑' : '绑定'}
- </button>
- </div>
- </div>
- {/* API密钥 */}
- <div className="flex items-center justify-between py-3 border-b border-gray-200">
- <span className="text-sm text-gray-700">API密钥</span>
- <div className="flex items-center gap-2">
- <span className="text-sm text-gray-900 font-mono">
- {userInfo.apikey ? `${userInfo.apikey.substring(0, 8)}...${userInfo.apikey.substring(userInfo.apikey.length - 8)}` : '未设置'}
- </span>
- <button
- onClick={() => handleEdit('apikey', userInfo.apikey ? '更新API密钥' : '设置API密钥', '请输入API密钥')}
- className="text-blue-600 hover:text-blue-700 text-sm font-bold transition-colors"
- >
- {userInfo.apikey ? '更新' : '设置'}
- </button>
- </div>
- </div>
- {/* 注册时间 */}
- <div className="flex items-center justify-between py-3 border-b border-gray-200">
- <span className="text-sm text-gray-700">注册时间</span>
- <span className="text-sm text-gray-900">{userInfo.registrationDate}</span>
- </div>
- {/* 注销账号 */}
- <div className="flex items-center justify-between py-3">
- <span className="text-sm text-gray-700">注销账号</span>
- <button onClick={handleApplyDelete} className="text-red-600 hover:text-red-700 text-sm font-bold transition-colors">
- 申请注销
- </button>
- </div>
- </div>
- {/* 退出登录 */}
- <div className="mt-8">
- <button
- onClick={handleLogout}
- className="flex items-center gap-2 text-blue-600 hover:text-blue-700 text-sm font-bold transition-colors"
- >
- <LogOut className="w-4 h-4" />
- <span>退出登录</span>
- </button>
- </div>
- </div>
- {/* 顶部确认弹窗(参考图片2样式) */}
- {deleteConfirmOpen && (
- <div className="fixed top-6 left-1/2 -translate-x-1/2 z-50 w-[calc(100%-2rem)] sm:w-[560px] max-w-full p-4">
- <div className="bg-white border border-red-200 rounded-lg shadow-lg p-4">
- <div className="flex items-start gap-4">
- <div className="flex-shrink-0 mt-1">
- <AlertCircle className="w-6 h-6 text-red-500" />
- </div>
- <div className="flex-1">
- <div className="text-sm text-gray-900 mb-2">确认申请注销账号吗?</div>
- <div className="text-xs text-gray-500">账号将被永久删除,此操作不可撤销。</div>
- </div>
- <div className="flex items-center gap-2 ml-4">
- <button
- onClick={handleConfirmDelete}
- className="bg-black text-white px-4 py-2 rounded-md text-sm hover:opacity-95"
- >
- 确定
- </button>
- <button
- onClick={handleCancelDelete}
- className="bg-white border border-gray-200 text-gray-700 px-3 py-2 rounded-md text-sm hover:bg-gray-50"
- >
- 取消
- </button>
- </div>
- </div>
- </div>
- </div>
- )}
- {/* 编辑模态框 */}
- <EditModal
- isOpen={editModal.isOpen}
- onClose={() => setEditModal({ ...editModal, isOpen: false })}
- title={editModal.title}
- field={editModal.field}
- currentValue={userInfo[editModal.field as keyof UserInfo] as string || ''}
- onSave={handleSave}
- placeholder={editModal.placeholder}
- type={editModal.type}
- />
- {/* 实名认证模态框 */}
- {verifyModalOpen && (
- <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
- <div className="bg-white rounded-2xl shadow-2xl w-full max-w-md p-6">
- <div className="flex items-center gap-3 mb-4">
- <div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
- <Shield className="w-5 h-5 text-blue-600" />
- </div>
- <h3 className="text-lg font-bold text-gray-900">实名认证</h3>
- </div>
-
- <p className="text-sm text-gray-600 mb-6">
- 为了保障您的账号安全,请填写真实姓名和身份证号进行实名认证。
- </p>
- {verifyError && (
- <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2">
- <AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
- <p className="text-sm text-red-600">{verifyError}</p>
- </div>
- )}
- <div className="space-y-4 mb-6">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 真实姓名
- </label>
- <input
- type="text"
- value={verifyData.real_name}
- onChange={(e) => setVerifyData({ ...verifyData, real_name: e.target.value })}
- placeholder="请输入真实姓名"
- className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
- disabled={verifyLoading}
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 身份证号
- </label>
- <input
- type="text"
- value={verifyData.id_card}
- onChange={(e) => setVerifyData({ ...verifyData, id_card: e.target.value })}
- placeholder="请输入身份证号"
- maxLength={18}
- className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
- disabled={verifyLoading}
- />
- </div>
- </div>
- <div className="bg-amber-50 border border-amber-200 rounded-lg p-3 mb-6">
- <p className="text-xs text-amber-800">
- <strong>温馨提示:</strong>您的个人信息将被严格保密,仅用于身份验证,不会用于其他用途。
- </p>
- </div>
- <div className="flex gap-3">
- <button
- onClick={() => setVerifyModalOpen(false)}
- disabled={verifyLoading}
- className="flex-1 py-2 px-4 bg-gray-100 text-gray-700 rounded-lg font-medium hover:bg-gray-200 transition-colors disabled:opacity-50"
- >
- 取消
- </button>
- <button
- onClick={handleSubmitVerification}
- disabled={verifyLoading || !verifyData.real_name.trim() || !verifyData.id_card.trim()}
- className="flex-1 py-2 px-4 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
- >
- {verifyLoading ? (
- <>
- <Loader2 className="w-4 h-4 animate-spin" />
- <span>提交中...</span>
- </>
- ) : (
- <span>提交认证</span>
- )}
- </button>
- </div>
- </div>
- </div>
- )}
- {/* 绑定/换绑手机号弹窗 */}
- <BindPhoneModal
- isOpen={bindPhoneOpen}
- onClose={() => setBindPhoneOpen(false)}
- onSaved={(phone) => {
- const updated = { ...userInfo, phone };
- setUserInfo(updated);
- authService.setToken(authService.getToken()!, updated);
- setSuccessMessage('手机号绑定成功');
- setTimeout(() => setSuccessMessage(null), 3000);
- }}
- />
- {/* 绑定/换绑邮箱弹窗 */}
- <BindEmailModal
- isOpen={bindEmailOpen}
- onClose={() => setBindEmailOpen(false)}
- onSaved={(email) => {
- const updated = { ...userInfo, email };
- setUserInfo(updated);
- authService.setToken(authService.getToken()!, updated);
- setSuccessMessage('邮箱绑定成功');
- setTimeout(() => setSuccessMessage(null), 3000);
- }}
- />
- </div>
- );
- };
- export default Profile;
|