import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react'; import { NavLink, Outlet, useNavigate } from 'react-router-dom'; import { FolderOpen, Key, LayoutDashboard, LogOut, Mail, Send, Settings, Shield, Smartphone, X, Menu, } from 'lucide-react'; import { AnimatePresence, motion } from 'motion/react'; import { useAuth } from '@/src/auth/AuthProvider'; import { apiBinaryUploadRequest, apiDownload, apiRequest, apiUploadRequest } from '@/src/lib/api'; import { createSession, readStoredSession, saveStoredSession } from '@/src/lib/session'; import type { AuthResponse, InitiateUploadResponse, UserProfile } from '@/src/lib/types'; import { cn } from '@/src/lib/utils'; import { Button } from '@/src/components/ui/button'; import { Input } from '@/src/components/ui/input'; import { buildAccountDraft, getRoleLabel, shouldLoadAvatarWithAuth } from '@/src/components/layout/account-utils'; import { UploadProgressPanel } from '@/src/components/layout/UploadProgressPanel'; const NAV_ITEMS = [ { name: '总览', path: '/overview', icon: LayoutDashboard }, { name: '网盘', path: '/files', icon: FolderOpen }, { name: '快传', path: '/transfer', icon: Send }, ] as const; type ActiveModal = 'security' | 'settings' | null; export function getVisibleNavItems(isAdmin: boolean) { // 底部导航栏容量有限,后台页面可通过顶部头像菜单或者折叠菜单进入 return NAV_ITEMS; } interface LayoutProps { children?: ReactNode; } export function MobileLayout({ children }: LayoutProps = {}) { const navigate = useNavigate(); const { isAdmin, logout, refreshProfile, user } = useAuth(); const navItems = getVisibleNavItems(isAdmin); const fileInputRef = useRef(null); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [activeModal, setActiveModal] = useState(null); const [avatarPreviewUrl, setAvatarPreviewUrl] = useState(null); const [selectedAvatarFile, setSelectedAvatarFile] = useState(null); const [avatarSourceUrl, setAvatarSourceUrl] = useState(user?.avatarUrl ?? null); const [profileDraft, setProfileDraft] = useState(() => buildAccountDraft( user ?? { id: 0, username: '', email: '', createdAt: '', }, ), ); // States related to modales and profile editing (Same as original) const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [profileMessage, setProfileMessage] = useState(''); const [passwordMessage, setPasswordMessage] = useState(''); const [profileError, setProfileError] = useState(''); const [passwordError, setPasswordError] = useState(''); const [profileSubmitting, setProfileSubmitting] = useState(false); const [passwordSubmitting, setPasswordSubmitting] = useState(false); useEffect(() => { if (!user) return; setProfileDraft(buildAccountDraft(user)); }, [user]); useEffect(() => { if (!avatarPreviewUrl) return undefined; return () => URL.revokeObjectURL(avatarPreviewUrl); }, [avatarPreviewUrl]); useEffect(() => { let active = true; let objectUrl: string | null = null; async function syncAvatar() { if (!user?.avatarUrl) { if (active) setAvatarSourceUrl(null); return; } if (!shouldLoadAvatarWithAuth(user.avatarUrl)) { if (active) setAvatarSourceUrl(user.avatarUrl); return; } try { const response = await apiDownload(user.avatarUrl); const blob = await response.blob(); objectUrl = URL.createObjectURL(blob); if (active) setAvatarSourceUrl(objectUrl); } catch { if (active) setAvatarSourceUrl(null); } } void syncAvatar(); return () => { active = false; if (objectUrl) URL.revokeObjectURL(objectUrl); }; }, [user?.avatarUrl]); const displayName = useMemo(() => user?.displayName || user?.username || '账户', [user]); const email = user?.email || '暂无邮箱'; const phoneNumber = user?.phoneNumber || '未设置手机号'; const roleLabel = getRoleLabel(user?.role); const avatarFallback = (displayName || 'Y').charAt(0).toUpperCase(); const displayedAvatarUrl = avatarPreviewUrl || avatarSourceUrl; const handleLogout = () => { logout(); navigate('/login'); }; const handleAvatarClick = () => fileInputRef.current?.click(); const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; setSelectedAvatarFile(file); setAvatarPreviewUrl((current) => { if (current) URL.revokeObjectURL(current); return URL.createObjectURL(file); }); }; const handleProfileDraftChange = (field: keyof typeof profileDraft, value: string) => { setProfileDraft((current) => ({ ...current, [field]: value })); }; const closeModal = () => { setActiveModal(null); setProfileMessage(''); setProfileError(''); setPasswordMessage(''); setPasswordError(''); }; const persistSessionUser = (nextProfile: UserProfile) => { const currentSession = readStoredSession(); if (!currentSession) return; saveStoredSession({ ...currentSession, user: nextProfile }); }; const uploadAvatar = async (file: File) => { const initiated = await apiRequest('/user/avatar/upload/initiate', { method: 'POST', body: { filename: file.name, contentType: file.type || 'image/png', size: file.size }, }); if (initiated.direct) { try { await apiBinaryUploadRequest(initiated.uploadUrl, { method: initiated.method, headers: initiated.headers, body: file }); } catch { const formData = new FormData(); formData.append('file', file); await apiUploadRequest(`/user/avatar/upload?storageName=${encodeURIComponent(initiated.storageName)}`, { body: formData }); } } else { const formData = new FormData(); formData.append('file', file); await apiUploadRequest(initiated.uploadUrl, { body: formData, method: initiated.method === 'PUT' ? 'PUT' : 'POST', headers: initiated.headers }); } const nextProfile = await apiRequest('/user/avatar/upload/complete', { method: 'POST', body: { filename: file.name, contentType: file.type || 'image/png', size: file.size, storageName: initiated.storageName }, }); persistSessionUser(nextProfile); return nextProfile; }; const handleSaveProfile = async () => { setProfileSubmitting(true); setProfileMessage(''); setProfileError(''); try { if (selectedAvatarFile) await uploadAvatar(selectedAvatarFile); const nextProfile = await apiRequest('/user/profile', { method: 'PUT', body: { displayName: profileDraft.displayName.trim(), email: profileDraft.email.trim(), phoneNumber: profileDraft.phoneNumber.trim(), bio: profileDraft.bio, preferredLanguage: profileDraft.preferredLanguage, }, }); persistSessionUser(nextProfile); await refreshProfile(); setSelectedAvatarFile(null); setAvatarPreviewUrl((current) => { if (current) URL.revokeObjectURL(current); return null; }); setProfileMessage('资料已保存'); } catch (error) { setProfileError(error instanceof Error ? error.message : '保存失败'); } finally { setProfileSubmitting(false); } }; const handleChangePassword = async () => { setPasswordMessage(''); setPasswordError(''); if (newPassword !== confirmPassword) { setPasswordError('密码不一致'); return; } setPasswordSubmitting(true); try { const auth = await apiRequest('/user/password', { method: 'POST', body: { currentPassword, newPassword }, }); const currentSession = readStoredSession(); if (currentSession) { saveStoredSession({ ...currentSession, ...createSession(auth), user: auth.user }); } setCurrentPassword(''); setNewPassword(''); setConfirmPassword(''); setPasswordMessage('密码已更新'); } catch (error) { setPasswordError(error instanceof Error ? error.message : '修改失败'); } finally { setPasswordSubmitting(false); } }; return (
{/* Background Animated Blobs */}
{/* Top App Bar */}
Y
优立云盘
{isDropdownOpen && ( <> setIsDropdownOpen(false)} />
{displayedAvatarUrl ? :
{avatarFallback}
}
{displayName}
{email}
{roleLabel}
{isAdmin && ( )}
)} {/* Main Content Area */}
{children ?? }
{/* Upload Panel (Floating above bottom bar) */}
{/* Bottom Navigation Bar */} {/* Support Modals (Settings & Security) */} {activeModal === 'security' && (
安全中心
{/* Similar to original but vertically stacked without hover constraints */}
修改密码
setCurrentPassword(e.target.value)} className="bg-black/20" /> setNewPassword(e.target.value)} className="bg-black/20" /> setConfirmPassword(e.target.value)} className="bg-black/20" /> {passwordError &&

{passwordError}

} {passwordMessage &&

{passwordMessage}

}
手机绑定
{phoneNumber}
)} {activeModal === 'settings' && (
账户设置
{displayedAvatarUrl ? :
{avatarFallback}
}
更换
{displayName}
{roleLabel}
handleProfileDraftChange('displayName', e.target.value)} className="bg-black/20 mt-1" />
handleProfileDraftChange('email', e.target.value)} className="bg-black/20 mt-1" />
handleProfileDraftChange('phoneNumber', e.target.value)} className="bg-black/20 mt-1" />