用户菜单新增个人中心
This commit is contained in:
@@ -88,6 +88,8 @@ export default function WatchRoomPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAvatarText = (name?: string) => (name?.trim().charAt(0).toUpperCase() || '用');
|
||||||
|
|
||||||
// 加载房间列表
|
// 加载房间列表
|
||||||
const loadRooms = async (showLoading = false) => {
|
const loadRooms = async (showLoading = false) => {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
@@ -477,8 +479,8 @@ export default function WatchRoomPage() {
|
|||||||
className="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg p-3"
|
className="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg p-3"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-blue-400 to-purple-500 flex items-center justify-center text-white font-bold">
|
<div className="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center text-white font-bold">
|
||||||
{member.name.charAt(0).toUpperCase()}
|
{getAvatarText(member.name)}
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
{member.name}
|
{member.name}
|
||||||
@@ -673,8 +675,8 @@ export default function WatchRoomPage() {
|
|||||||
className="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg p-3"
|
className="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg p-3"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-green-400 to-teal-500 flex items-center justify-center text-white font-bold">
|
<div className="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center text-white font-bold">
|
||||||
{member.name.charAt(0).toUpperCase()}
|
{getAvatarText(member.name)}
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
{member.name}
|
{member.name}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { createPortal } from 'react-dom';
|
|||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
|
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
||||||
import { VideoContext } from '@/lib/ai-orchestrator';
|
import { VideoContext } from '@/lib/ai-orchestrator';
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
@@ -51,6 +52,7 @@ export default function AIChatPanel({
|
|||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [isStreaming, setIsStreaming] = useState(false);
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
const [currentUsername, setCurrentUsername] = useState('用户');
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const prevStorageKeyRef = useRef<string>(storageKey);
|
const prevStorageKeyRef = useRef<string>(storageKey);
|
||||||
@@ -74,6 +76,13 @@ export default function AIChatPanel({
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const authInfo = getAuthInfoFromBrowserCookie();
|
||||||
|
setCurrentUsername(authInfo?.username || '用户');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const userAvatarText = currentUsername.trim().charAt(0).toUpperCase() || '用';
|
||||||
|
|
||||||
// 从sessionStorage加载消息记录
|
// 从sessionStorage加载消息记录
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
@@ -416,7 +425,7 @@ export default function AIChatPanel({
|
|||||||
>
|
>
|
||||||
{message.role === 'user' ? (
|
{message.role === 'user' ? (
|
||||||
<span className='text-xs font-semibold text-white'>
|
<span className='text-xs font-semibold text-white'>
|
||||||
U
|
{userAvatarText}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<Bot size={16} className='text-white' />
|
<Bot size={16} className='text-white' />
|
||||||
@@ -622,7 +631,7 @@ export default function AIChatPanel({
|
|||||||
>
|
>
|
||||||
{message.role === 'user' ? (
|
{message.role === 'user' ? (
|
||||||
<span className='text-xs font-semibold text-white'>
|
<span className='text-xs font-semibold text-white'>
|
||||||
U
|
{userAvatarText}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<Bot size={16} className='text-white' />
|
<Bot size={16} className='text-white' />
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { LucideIcon, Monitor, X } from 'lucide-react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
interface DeviceItem {
|
||||||
|
tokenId: string;
|
||||||
|
deviceInfo: string;
|
||||||
|
isCurrent: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
lastUsed: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeviceManagementPanelProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
mounted: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
devices: DeviceItem[];
|
||||||
|
devicesLoading: boolean;
|
||||||
|
revoking: string | null;
|
||||||
|
onRevokeDevice: (tokenId: string) => void;
|
||||||
|
onRevokeAllDevices: () => void;
|
||||||
|
getDeviceIcon: (deviceInfo: string) => LucideIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceManagementPanel({
|
||||||
|
isOpen,
|
||||||
|
mounted,
|
||||||
|
onClose,
|
||||||
|
devices,
|
||||||
|
devicesLoading,
|
||||||
|
revoking,
|
||||||
|
onRevokeDevice,
|
||||||
|
onRevokeAllDevices,
|
||||||
|
getDeviceIcon,
|
||||||
|
}: DeviceManagementPanelProps) {
|
||||||
|
if (!isOpen || !mounted) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className='fixed inset-0 bg-black/50 backdrop-blur-sm z-[1000]'
|
||||||
|
onClick={onClose}
|
||||||
|
onTouchMove={(e) => e.preventDefault()}
|
||||||
|
onWheel={(e) => e.preventDefault()}
|
||||||
|
style={{ touchAction: 'none' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'>
|
||||||
|
<div
|
||||||
|
className='h-full max-h-[80vh] flex flex-col'
|
||||||
|
data-panel-content
|
||||||
|
onTouchMove={(e) => e.stopPropagation()}
|
||||||
|
style={{ touchAction: 'auto' }}
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700'>
|
||||||
|
<h3 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
|
||||||
|
设备管理
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className='w-8 h-8 p-1 rounded-full flex items-center justify-center text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors'
|
||||||
|
aria-label='Close'
|
||||||
|
>
|
||||||
|
<X className='w-full h-full' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex-1 overflow-y-auto p-6'>
|
||||||
|
{devicesLoading ? (
|
||||||
|
<div className='space-y-3'>
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div key={i} className='animate-pulse'>
|
||||||
|
<div className='h-20 bg-gray-200 dark:bg-gray-700 rounded-lg'></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className='text-center text-sm text-gray-500 dark:text-gray-400 mt-4'>
|
||||||
|
加载中...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : devices.length === 0 ? (
|
||||||
|
<div className='text-center py-8'>
|
||||||
|
<Monitor className='w-12 h-12 mx-auto text-gray-400 dark:text-gray-500 mb-3' />
|
||||||
|
<p className='text-sm text-gray-500 dark:text-gray-400'>暂无登录设备</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='space-y-3'>
|
||||||
|
{devices
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.isCurrent && !b.isCurrent) return -1;
|
||||||
|
if (!a.isCurrent && b.isCurrent) return 1;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
.map((device) => {
|
||||||
|
const DeviceIcon = getDeviceIcon(device.deviceInfo);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={device.tokenId}
|
||||||
|
className={`p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border ${
|
||||||
|
device.isCurrent
|
||||||
|
? 'border-yellow-400 dark:border-yellow-500'
|
||||||
|
: 'border-gray-200 dark:border-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className='flex items-start justify-between'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<div className='flex items-center gap-2 mb-2'>
|
||||||
|
<DeviceIcon className='w-4 h-4 text-gray-600 dark:text-gray-400' />
|
||||||
|
<span className='text-sm font-medium text-gray-900 dark:text-gray-100'>
|
||||||
|
{device.deviceInfo}
|
||||||
|
</span>
|
||||||
|
{device.isCurrent && (
|
||||||
|
<span className='px-2 py-0.5 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded-full'>
|
||||||
|
当前设备
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='space-y-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||||
|
<div>登录时间: {new Date(device.createdAt).toLocaleString('zh-CN')}</div>
|
||||||
|
<div>最后活跃: {new Date(device.lastUsed).toLocaleString('zh-CN')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!device.isCurrent && (
|
||||||
|
<button
|
||||||
|
onClick={() => onRevokeDevice(device.tokenId)}
|
||||||
|
disabled={revoking === device.tokenId}
|
||||||
|
className='ml-3 px-3 py-1.5 text-xs font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 border border-red-200 hover:border-red-300 dark:border-red-800 dark:hover:border-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed'
|
||||||
|
>
|
||||||
|
{revoking === device.tokenId ? '撤销中...' : '撤销'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='p-6 border-t border-gray-200 dark:border-gray-700 space-y-3'>
|
||||||
|
<button
|
||||||
|
onClick={onRevokeAllDevices}
|
||||||
|
disabled={devices.length === 0}
|
||||||
|
className='w-full px-4 py-2.5 bg-red-500 hover:bg-red-600 disabled:bg-red-400 dark:bg-red-600 dark:hover:bg-red-700 dark:disabled:bg-red-500 text-white text-sm font-medium rounded-lg transition-colors disabled:cursor-not-allowed'
|
||||||
|
>
|
||||||
|
登出所有设备
|
||||||
|
</button>
|
||||||
|
<p className='text-xs text-gray-500 dark:text-gray-400 text-center'>
|
||||||
|
登出所有设备后需要重新登录
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
interface EmailSettingsPanelProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
mounted: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
userEmail: string;
|
||||||
|
onUserEmailChange: (value: string) => void;
|
||||||
|
emailNotifications: boolean;
|
||||||
|
onEmailNotificationsChange: (value: boolean) => void;
|
||||||
|
emailSettingsLoading: boolean;
|
||||||
|
emailSettingsSaving: boolean;
|
||||||
|
onSave: () => void;
|
||||||
|
statusMessage?: string;
|
||||||
|
statusType?: 'success' | 'error' | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmailSettingsPanel({
|
||||||
|
isOpen,
|
||||||
|
mounted,
|
||||||
|
onClose,
|
||||||
|
userEmail,
|
||||||
|
onUserEmailChange,
|
||||||
|
emailNotifications,
|
||||||
|
onEmailNotificationsChange,
|
||||||
|
emailSettingsLoading,
|
||||||
|
emailSettingsSaving,
|
||||||
|
onSave,
|
||||||
|
statusMessage,
|
||||||
|
statusType,
|
||||||
|
}: EmailSettingsPanelProps) {
|
||||||
|
if (!isOpen || !mounted) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className='fixed inset-0 bg-black/50 backdrop-blur-sm z-[1000]'
|
||||||
|
onClick={onClose}
|
||||||
|
onTouchMove={(e) => e.preventDefault()}
|
||||||
|
onWheel={(e) => e.preventDefault()}
|
||||||
|
style={{ touchAction: 'none' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'>
|
||||||
|
<div
|
||||||
|
className='h-full p-6'
|
||||||
|
data-panel-content
|
||||||
|
onTouchMove={(e) => e.stopPropagation()}
|
||||||
|
style={{ touchAction: 'auto' }}
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-between mb-6'>
|
||||||
|
<h3 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
|
||||||
|
邮件通知设置
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className='w-8 h-8 p-1 rounded-full flex items-center justify-center text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors'
|
||||||
|
aria-label='Close'
|
||||||
|
>
|
||||||
|
<X className='w-full h-full' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{emailSettingsLoading ? (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<div className='animate-pulse'>
|
||||||
|
<div className='h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2'></div>
|
||||||
|
<div className='h-10 bg-gray-200 dark:bg-gray-700 rounded'></div>
|
||||||
|
</div>
|
||||||
|
<div className='animate-pulse'>
|
||||||
|
<div className='h-20 bg-gray-200 dark:bg-gray-700 rounded'></div>
|
||||||
|
</div>
|
||||||
|
<div className='animate-pulse'>
|
||||||
|
<div className='h-10 bg-gray-200 dark:bg-gray-700 rounded'></div>
|
||||||
|
</div>
|
||||||
|
<div className='text-center text-sm text-gray-500 dark:text-gray-400'>
|
||||||
|
加载中...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<div>
|
||||||
|
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||||
|
邮箱地址
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='email'
|
||||||
|
value={userEmail}
|
||||||
|
onChange={(e) => onUserEmailChange(e.target.value)}
|
||||||
|
placeholder='输入您的邮箱地址'
|
||||||
|
disabled={emailSettingsSaving}
|
||||||
|
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg'>
|
||||||
|
<div>
|
||||||
|
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||||
|
接收收藏更新通知
|
||||||
|
</h4>
|
||||||
|
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||||
|
当收藏的影片有更新时发送邮件通知
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => onEmailNotificationsChange(!emailNotifications)}
|
||||||
|
disabled={emailSettingsSaving}
|
||||||
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||||
|
emailNotifications ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||||
|
emailNotifications ? 'translate-x-6' : 'translate-x-1'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onSave}
|
||||||
|
disabled={emailSettingsSaving}
|
||||||
|
className='w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 dark:disabled:bg-blue-500 text-white text-sm font-medium rounded-lg transition-colors disabled:cursor-not-allowed flex items-center justify-center gap-2'
|
||||||
|
>
|
||||||
|
{emailSettingsSaving ? (
|
||||||
|
<>
|
||||||
|
<div className='w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin'></div>
|
||||||
|
<span>保存中...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'保存设置'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{statusMessage ? (
|
||||||
|
<p
|
||||||
|
className={`text-xs text-center ${
|
||||||
|
statusType === 'success'
|
||||||
|
? 'text-green-600 dark:text-green-400'
|
||||||
|
: 'text-red-600 dark:text-red-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{statusMessage}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='mt-6 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg'>
|
||||||
|
<p className='text-xs text-blue-800 dark:text-blue-200'>
|
||||||
|
💡 提示:需要管理员先在管理面板中配置邮件服务
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Mail, Monitor, X } from 'lucide-react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
interface PersonalCenterPanelProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
mounted: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
username: string;
|
||||||
|
roleText: string;
|
||||||
|
showRoleBadge: boolean;
|
||||||
|
avatarText: string;
|
||||||
|
roleBadgeClassName: string;
|
||||||
|
showDeviceManagement: boolean;
|
||||||
|
onOpenEmailSettings: () => void;
|
||||||
|
onOpenDeviceManagement: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PersonalCenterPanel({
|
||||||
|
isOpen,
|
||||||
|
mounted,
|
||||||
|
onClose,
|
||||||
|
username,
|
||||||
|
roleText,
|
||||||
|
showRoleBadge,
|
||||||
|
avatarText,
|
||||||
|
roleBadgeClassName,
|
||||||
|
showDeviceManagement,
|
||||||
|
onOpenEmailSettings,
|
||||||
|
onOpenDeviceManagement,
|
||||||
|
}: PersonalCenterPanelProps) {
|
||||||
|
if (!isOpen || !mounted) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className='fixed inset-0 bg-black/50 backdrop-blur-sm z-[1000]'
|
||||||
|
onClick={onClose}
|
||||||
|
onTouchMove={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
onWheel={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
style={{ touchAction: 'none' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='fixed top-1/2 left-1/2 z-[1001] w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-2xl bg-white shadow-xl dark:bg-gray-900'>
|
||||||
|
<div
|
||||||
|
className='p-6'
|
||||||
|
data-panel-content
|
||||||
|
onTouchMove={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
style={{ touchAction: 'auto' }}
|
||||||
|
>
|
||||||
|
<div className='relative mb-6 flex flex-col items-center text-center'>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className='absolute right-0 top-0 flex h-8 w-8 items-center justify-center rounded-full text-gray-500 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
aria-label='Close'
|
||||||
|
>
|
||||||
|
<X className='w-5 h-5' />
|
||||||
|
</button>
|
||||||
|
<div className='mb-3 flex h-20 w-20 items-center justify-center rounded-full bg-blue-500 text-3xl font-semibold text-white shadow-md'>
|
||||||
|
{avatarText}
|
||||||
|
</div>
|
||||||
|
{showRoleBadge && (
|
||||||
|
<span
|
||||||
|
className={`mb-2 inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${roleBadgeClassName}`}
|
||||||
|
>
|
||||||
|
{roleText}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<h3 className='text-xl font-bold text-gray-900 dark:text-gray-100'>
|
||||||
|
{username}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='space-y-3'>
|
||||||
|
<button
|
||||||
|
onClick={onOpenEmailSettings}
|
||||||
|
className='flex w-full items-center gap-3 rounded-2xl border border-gray-200 bg-gray-50 px-4 py-4 text-left transition-colors hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-750'
|
||||||
|
>
|
||||||
|
<div className='flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-300'>
|
||||||
|
<Mail className='w-6 h-6' />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className='text-base font-semibold text-gray-900 dark:text-gray-100'>
|
||||||
|
邮件通知设置
|
||||||
|
</div>
|
||||||
|
<div className='mt-1 text-sm text-gray-500 dark:text-gray-400'>
|
||||||
|
管理接收收藏更新通知的邮箱和开关
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showDeviceManagement && (
|
||||||
|
<button
|
||||||
|
onClick={onOpenDeviceManagement}
|
||||||
|
className='flex w-full items-center gap-3 rounded-2xl border border-gray-200 bg-gray-50 px-4 py-4 text-left transition-colors hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-750'
|
||||||
|
>
|
||||||
|
<div className='flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-300'>
|
||||||
|
<Monitor className='w-6 h-6' />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className='text-base font-semibold text-gray-900 dark:text-gray-100'>
|
||||||
|
设备管理
|
||||||
|
</div>
|
||||||
|
<div className='mt-1 text-sm text-gray-500 dark:text-gray-400'>
|
||||||
|
查看并管理当前账号的登录设备
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
+113
-353
@@ -17,7 +17,6 @@ import {
|
|||||||
Home,
|
Home,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
LogOut,
|
LogOut,
|
||||||
Mail,
|
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Monitor,
|
Monitor,
|
||||||
MoveDown,
|
MoveDown,
|
||||||
@@ -46,6 +45,9 @@ import { UpdateStatus } from '@/lib/version_check';
|
|||||||
import { FavoritesPanel } from './FavoritesPanel';
|
import { FavoritesPanel } from './FavoritesPanel';
|
||||||
import { NotificationPanel } from './NotificationPanel';
|
import { NotificationPanel } from './NotificationPanel';
|
||||||
import { OfflineDownloadPanel } from './OfflineDownloadPanel';
|
import { OfflineDownloadPanel } from './OfflineDownloadPanel';
|
||||||
|
import { DeviceManagementPanel } from './DeviceManagementPanel';
|
||||||
|
import { EmailSettingsPanel } from './EmailSettingsPanel';
|
||||||
|
import { PersonalCenterPanel } from './PersonalCenterPanel';
|
||||||
import { useVersionCheck } from './VersionCheckProvider';
|
import { useVersionCheck } from './VersionCheckProvider';
|
||||||
import { VersionPanel } from './VersionPanel';
|
import { VersionPanel } from './VersionPanel';
|
||||||
import { DownloadManagementPanel } from './DownloadManagementPanel';
|
import { DownloadManagementPanel } from './DownloadManagementPanel';
|
||||||
@@ -59,6 +61,7 @@ export const UserMenu: React.FC = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { updateStatus, isChecking } = useVersionCheck();
|
const { updateStatus, isChecking } = useVersionCheck();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isProfileCenterOpen, setIsProfileCenterOpen] = useState(false);
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
|
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
|
||||||
const [isSubscribeOpen, setIsSubscribeOpen] = useState(false);
|
const [isSubscribeOpen, setIsSubscribeOpen] = useState(false);
|
||||||
@@ -89,7 +92,7 @@ export const UserMenu: React.FC = () => {
|
|||||||
|
|
||||||
// Body 滚动锁定 - 使用 overflow 方式避免布局问题
|
// Body 滚动锁定 - 使用 overflow 方式避免布局问题
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSettingsOpen || isChangePasswordOpen || isSubscribeOpen || isOfflineDownloadPanelOpen || isEmailSettingsOpen || isDeviceManagementOpen || isEcoAppsOpen || isReportOpen || isDownloadManagementOpen) {
|
if (isProfileCenterOpen || isSettingsOpen || isChangePasswordOpen || isSubscribeOpen || isOfflineDownloadPanelOpen || isEmailSettingsOpen || isDeviceManagementOpen || isEcoAppsOpen || isReportOpen || isDownloadManagementOpen) {
|
||||||
const body = document.body;
|
const body = document.body;
|
||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
|
|
||||||
@@ -108,7 +111,7 @@ export const UserMenu: React.FC = () => {
|
|||||||
html.style.overflow = originalHtmlOverflow;
|
html.style.overflow = originalHtmlOverflow;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [isSettingsOpen, isChangePasswordOpen, isSubscribeOpen, isOfflineDownloadPanelOpen, isEmailSettingsOpen, isDeviceManagementOpen, isEcoAppsOpen]);
|
}, [isProfileCenterOpen, isSettingsOpen, isChangePasswordOpen, isSubscribeOpen, isOfflineDownloadPanelOpen, isEmailSettingsOpen, isDeviceManagementOpen, isEcoAppsOpen, isReportOpen, isDownloadManagementOpen]);
|
||||||
|
|
||||||
// 设置相关状态
|
// 设置相关状态
|
||||||
const [defaultAggregateSearch, setDefaultAggregateSearch] = useState(true);
|
const [defaultAggregateSearch, setDefaultAggregateSearch] = useState(true);
|
||||||
@@ -149,6 +152,10 @@ export const UserMenu: React.FC = () => {
|
|||||||
const [emailNotifications, setEmailNotifications] = useState(false);
|
const [emailNotifications, setEmailNotifications] = useState(false);
|
||||||
const [emailSettingsLoading, setEmailSettingsLoading] = useState(false);
|
const [emailSettingsLoading, setEmailSettingsLoading] = useState(false);
|
||||||
const [emailSettingsSaving, setEmailSettingsSaving] = useState(false);
|
const [emailSettingsSaving, setEmailSettingsSaving] = useState(false);
|
||||||
|
const [emailSettingsMessage, setEmailSettingsMessage] = useState('');
|
||||||
|
const [emailSettingsMessageType, setEmailSettingsMessageType] = useState<
|
||||||
|
'success' | 'error' | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
// 设备管理状态
|
// 设备管理状态
|
||||||
const [devices, setDevices] = useState<any[]>([]);
|
const [devices, setDevices] = useState<any[]>([]);
|
||||||
@@ -618,6 +625,8 @@ export const UserMenu: React.FC = () => {
|
|||||||
// 加载邮件通知设置
|
// 加载邮件通知设置
|
||||||
const loadEmailSettings = async () => {
|
const loadEmailSettings = async () => {
|
||||||
setEmailSettingsLoading(true);
|
setEmailSettingsLoading(true);
|
||||||
|
setEmailSettingsMessage('');
|
||||||
|
setEmailSettingsMessageType(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/user/email-settings');
|
const response = await fetch('/api/user/email-settings');
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -635,6 +644,8 @@ export const UserMenu: React.FC = () => {
|
|||||||
// 保存邮件通知设置
|
// 保存邮件通知设置
|
||||||
const handleSaveEmailSettings = async () => {
|
const handleSaveEmailSettings = async () => {
|
||||||
setEmailSettingsSaving(true);
|
setEmailSettingsSaving(true);
|
||||||
|
setEmailSettingsMessage('');
|
||||||
|
setEmailSettingsMessageType(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/user/email-settings', {
|
const response = await fetch('/api/user/email-settings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -645,32 +656,22 @@ export const UserMenu: React.FC = () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const messageEl = document.getElementById('email-settings-message');
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
if (messageEl) {
|
setEmailSettingsMessage('保存成功!');
|
||||||
messageEl.textContent = '保存成功!';
|
setEmailSettingsMessageType('success');
|
||||||
messageEl.className = 'text-xs text-center text-green-600 dark:text-green-400';
|
setTimeout(() => {
|
||||||
messageEl.classList.remove('hidden');
|
setEmailSettingsMessage('');
|
||||||
setTimeout(() => {
|
setEmailSettingsMessageType(null);
|
||||||
messageEl.classList.add('hidden');
|
}, 3000);
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (messageEl) {
|
setEmailSettingsMessage(data.error || '保存失败');
|
||||||
messageEl.textContent = data.error || '保存失败';
|
setEmailSettingsMessageType('error');
|
||||||
messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400';
|
|
||||||
messageEl.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存邮件设置失败:', error);
|
console.error('保存邮件设置失败:', error);
|
||||||
const messageEl = document.getElementById('email-settings-message');
|
setEmailSettingsMessage('保存失败,请重试');
|
||||||
if (messageEl) {
|
setEmailSettingsMessageType('error');
|
||||||
messageEl.textContent = '保存失败,请重试';
|
|
||||||
messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400';
|
|
||||||
messageEl.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setEmailSettingsSaving(false);
|
setEmailSettingsSaving(false);
|
||||||
}
|
}
|
||||||
@@ -1434,6 +1435,24 @@ export const UserMenu: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentUsername = authInfo?.username || 'default';
|
||||||
|
const currentRole = authInfo?.role || 'user';
|
||||||
|
const currentRoleText = getRoleText(currentRole);
|
||||||
|
const shouldShowRoleBadge = currentRole !== 'user';
|
||||||
|
const avatarText = currentUsername.trim().charAt(0).toUpperCase() || 'D';
|
||||||
|
|
||||||
|
const roleBadgeClassName =
|
||||||
|
currentRole === 'owner'
|
||||||
|
? 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
|
||||||
|
: currentRole === 'admin'
|
||||||
|
? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'
|
||||||
|
: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
|
||||||
|
|
||||||
|
const handleOpenProfileCenter = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setIsProfileCenterOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
// 菜单面板内容
|
// 菜单面板内容
|
||||||
const menuPanel = (
|
const menuPanel = (
|
||||||
<>
|
<>
|
||||||
@@ -1446,62 +1465,35 @@ export const UserMenu: React.FC = () => {
|
|||||||
{/* 菜单面板 */}
|
{/* 菜单面板 */}
|
||||||
<div className='fixed top-14 right-4 w-56 bg-white dark:bg-gray-900 rounded-lg shadow-xl z-[1001] border border-gray-200/50 dark:border-gray-700/50 overflow-hidden select-none'>
|
<div className='fixed top-14 right-4 w-56 bg-white dark:bg-gray-900 rounded-lg shadow-xl z-[1001] border border-gray-200/50 dark:border-gray-700/50 overflow-hidden select-none'>
|
||||||
{/* 用户信息区域 */}
|
{/* 用户信息区域 */}
|
||||||
<div className='px-3 py-2.5 border-b border-gray-200 dark:border-gray-700 bg-gradient-to-r from-gray-50 to-gray-100/50 dark:from-gray-800 dark:to-gray-800/50'>
|
<div className='px-3 py-1 border-b border-gray-200 dark:border-gray-700 bg-gradient-to-r from-gray-50 to-gray-100/50 dark:from-gray-800 dark:to-gray-800/50'>
|
||||||
<div className='space-y-1'>
|
<div className='flex items-start justify-between gap-3'>
|
||||||
<div className='flex items-center justify-between'>
|
<button
|
||||||
<div className='flex items-center gap-0.5'>
|
onClick={handleOpenProfileCenter}
|
||||||
<span className='text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
className='flex items-center gap-3 rounded-xl px-2 py-1 text-left hover:bg-white/70 dark:hover:bg-gray-700/40 transition-colors'
|
||||||
当前用户
|
>
|
||||||
</span>
|
<div className='relative flex h-11 w-11 items-center justify-center rounded-full bg-blue-500 text-lg font-semibold text-white shadow-sm'>
|
||||||
{/* 邮件设置图标按钮 */}
|
<span>{avatarText}</span>
|
||||||
<button
|
{shouldShowRoleBadge && (
|
||||||
onClick={(e) => {
|
<span
|
||||||
e.stopPropagation();
|
className={`absolute left-1/2 top-[calc(100%-6px)] z-10 -translate-x-1/2 inline-flex min-w-[26px] items-center justify-center whitespace-nowrap rounded-full px-1.5 py-[2px] text-[8px] leading-none font-medium shadow-sm ${roleBadgeClassName}`}
|
||||||
setIsOpen(false);
|
|
||||||
setIsEmailSettingsOpen(true);
|
|
||||||
// 懒加载:打开面板时才请求邮件设置
|
|
||||||
loadEmailSettings();
|
|
||||||
}}
|
|
||||||
className='p-0.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors'
|
|
||||||
title='邮件通知设置'
|
|
||||||
>
|
|
||||||
<Mail className='w-3 h-3 text-gray-500 dark:text-gray-400' />
|
|
||||||
</button>
|
|
||||||
{/* 设备管理图标按钮 */}
|
|
||||||
{storageType !== 'localstorage' && (
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsOpen(false);
|
|
||||||
setIsDeviceManagementOpen(true);
|
|
||||||
// 懒加载:打开面板时才请求设备列表
|
|
||||||
loadDevices();
|
|
||||||
}}
|
|
||||||
className='p-0.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors'
|
|
||||||
title='设备管理'
|
|
||||||
>
|
>
|
||||||
<Monitor className='w-3 h-3 text-gray-500 dark:text-gray-400' />
|
{currentRoleText}
|
||||||
</button>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<div className='min-w-0'>
|
||||||
className={`inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium ${(authInfo?.role || 'user') === 'owner'
|
<span className='block max-w-[84px] truncate text-sm font-semibold text-gray-900 dark:text-gray-100 leading-none'>
|
||||||
? 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
|
{currentUsername}
|
||||||
: (authInfo?.role || 'user') === 'admin'
|
</span>
|
||||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'
|
|
||||||
: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{getRoleText(authInfo?.role || 'user')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<div className='font-semibold text-gray-900 dark:text-gray-100 text-sm truncate'>
|
|
||||||
{authInfo?.username || 'default'}
|
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className='pt-1 text-right'>
|
||||||
<div className='text-[10px] text-gray-400 dark:text-gray-500'>
|
<div className='text-[10px] text-gray-400 dark:text-gray-500'>
|
||||||
数据存储:
|
<div>数据存储</div>
|
||||||
{displayStorageType === 'localstorage' ? '本地' : displayStorageType}
|
<div className='mt-0.5'>
|
||||||
|
{displayStorageType === 'localstorage' ? '本地' : displayStorageType}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3353,277 +3345,6 @@ export const UserMenu: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 邮件设置面板内容
|
|
||||||
const emailSettingsPanel = (
|
|
||||||
<>
|
|
||||||
{/* 背景遮罩 */}
|
|
||||||
<div
|
|
||||||
className='fixed inset-0 bg-black/50 backdrop-blur-sm z-[1000]'
|
|
||||||
onClick={() => setIsEmailSettingsOpen(false)}
|
|
||||||
onTouchMove={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
onWheel={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
touchAction: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 邮件设置面板 */}
|
|
||||||
<div
|
|
||||||
className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className='h-full p-6'
|
|
||||||
data-panel-content
|
|
||||||
onTouchMove={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
touchAction: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* 标题栏 */}
|
|
||||||
<div className='flex items-center justify-between mb-6'>
|
|
||||||
<h3 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
|
|
||||||
邮件通知设置
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsEmailSettingsOpen(false)}
|
|
||||||
className='w-8 h-8 p-1 rounded-full flex items-center justify-center text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors'
|
|
||||||
aria-label='Close'
|
|
||||||
>
|
|
||||||
<X className='w-full h-full' />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 表单 */}
|
|
||||||
{emailSettingsLoading ? (
|
|
||||||
<div className='space-y-4'>
|
|
||||||
{/* 加载骨架屏 */}
|
|
||||||
<div className='animate-pulse'>
|
|
||||||
<div className='h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2'></div>
|
|
||||||
<div className='h-10 bg-gray-200 dark:bg-gray-700 rounded'></div>
|
|
||||||
</div>
|
|
||||||
<div className='animate-pulse'>
|
|
||||||
<div className='h-20 bg-gray-200 dark:bg-gray-700 rounded'></div>
|
|
||||||
</div>
|
|
||||||
<div className='animate-pulse'>
|
|
||||||
<div className='h-10 bg-gray-200 dark:bg-gray-700 rounded'></div>
|
|
||||||
</div>
|
|
||||||
<div className='text-center text-sm text-gray-500 dark:text-gray-400'>
|
|
||||||
加载中...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='space-y-4'>
|
|
||||||
<div>
|
|
||||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
|
||||||
邮箱地址
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type='email'
|
|
||||||
value={userEmail}
|
|
||||||
onChange={(e) => setUserEmail(e.target.value)}
|
|
||||||
placeholder='输入您的邮箱地址'
|
|
||||||
disabled={emailSettingsSaving}
|
|
||||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg'>
|
|
||||||
<div>
|
|
||||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
|
||||||
接收收藏更新通知
|
|
||||||
</h4>
|
|
||||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
|
||||||
当收藏的影片有更新时发送邮件通知
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setEmailNotifications(!emailNotifications)}
|
|
||||||
disabled={emailSettingsSaving}
|
|
||||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
|
|
||||||
emailNotifications ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
||||||
emailNotifications ? 'translate-x-6' : 'translate-x-1'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleSaveEmailSettings}
|
|
||||||
disabled={emailSettingsSaving}
|
|
||||||
className='w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 dark:disabled:bg-blue-500 text-white text-sm font-medium rounded-lg transition-colors disabled:cursor-not-allowed flex items-center justify-center gap-2'
|
|
||||||
>
|
|
||||||
{emailSettingsSaving ? (
|
|
||||||
<>
|
|
||||||
<div className='w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin'></div>
|
|
||||||
<span>保存中...</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'保存设置'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<p id='email-settings-message' className='text-xs text-center hidden'></p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 提示信息 */}
|
|
||||||
<div className='mt-6 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg'>
|
|
||||||
<p className='text-xs text-blue-800 dark:text-blue-200'>
|
|
||||||
💡 提示:需要管理员先在管理面板中配置邮件服务
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 设备管理面板内容
|
|
||||||
const deviceManagementPanel = (
|
|
||||||
<>
|
|
||||||
{/* 背景遮罩 */}
|
|
||||||
<div
|
|
||||||
className='fixed inset-0 bg-black/50 backdrop-blur-sm z-[1000]'
|
|
||||||
onClick={() => setIsDeviceManagementOpen(false)}
|
|
||||||
onTouchMove={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
onWheel={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
touchAction: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 设备管理面板 */}
|
|
||||||
<div
|
|
||||||
className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className='h-full max-h-[80vh] flex flex-col'
|
|
||||||
data-panel-content
|
|
||||||
onTouchMove={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
touchAction: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* 标题栏 */}
|
|
||||||
<div className='flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700'>
|
|
||||||
<h3 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
|
|
||||||
设备管理
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsDeviceManagementOpen(false)}
|
|
||||||
className='w-8 h-8 p-1 rounded-full flex items-center justify-center text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors'
|
|
||||||
aria-label='Close'
|
|
||||||
>
|
|
||||||
<X className='w-full h-full' />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 设备列表 */}
|
|
||||||
<div className='flex-1 overflow-y-auto p-6'>
|
|
||||||
{devicesLoading ? (
|
|
||||||
<div className='space-y-3'>
|
|
||||||
{[1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className='animate-pulse'>
|
|
||||||
<div className='h-20 bg-gray-200 dark:bg-gray-700 rounded-lg'></div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className='text-center text-sm text-gray-500 dark:text-gray-400 mt-4'>
|
|
||||||
加载中...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : devices.length === 0 ? (
|
|
||||||
<div className='text-center py-8'>
|
|
||||||
<Monitor className='w-12 h-12 mx-auto text-gray-400 dark:text-gray-500 mb-3' />
|
|
||||||
<p className='text-sm text-gray-500 dark:text-gray-400'>暂无登录设备</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='space-y-3'>
|
|
||||||
{devices
|
|
||||||
.sort((a, b) => {
|
|
||||||
// 当前设备置顶
|
|
||||||
if (a.isCurrent && !b.isCurrent) return -1;
|
|
||||||
if (!a.isCurrent && b.isCurrent) return 1;
|
|
||||||
return 0;
|
|
||||||
})
|
|
||||||
.map((device) => {
|
|
||||||
const DeviceIcon = getDeviceIcon(device.deviceInfo);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={device.tokenId}
|
|
||||||
className={`p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border ${
|
|
||||||
device.isCurrent
|
|
||||||
? 'border-yellow-400 dark:border-yellow-500'
|
|
||||||
: 'border-gray-200 dark:border-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className='flex items-start justify-between'>
|
|
||||||
<div className='flex-1'>
|
|
||||||
<div className='flex items-center gap-2 mb-2'>
|
|
||||||
<DeviceIcon className='w-4 h-4 text-gray-600 dark:text-gray-400' />
|
|
||||||
<span className='text-sm font-medium text-gray-900 dark:text-gray-100'>
|
|
||||||
{device.deviceInfo}
|
|
||||||
</span>
|
|
||||||
{device.isCurrent && (
|
|
||||||
<span className='px-2 py-0.5 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded-full'>
|
|
||||||
当前设备
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className='space-y-1 text-xs text-gray-500 dark:text-gray-400'>
|
|
||||||
<div>登录时间: {new Date(device.createdAt).toLocaleString('zh-CN')}</div>
|
|
||||||
<div>最后活跃: {new Date(device.lastUsed).toLocaleString('zh-CN')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!device.isCurrent && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleRevokeDevice(device.tokenId)}
|
|
||||||
disabled={revoking === device.tokenId}
|
|
||||||
className='ml-3 px-3 py-1.5 text-xs font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 border border-red-200 hover:border-red-300 dark:border-red-800 dark:hover:border-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed'
|
|
||||||
>
|
|
||||||
{revoking === device.tokenId ? '撤销中...' : '撤销'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 底部操作 */}
|
|
||||||
<div className='p-6 border-t border-gray-200 dark:border-gray-700 space-y-3'>
|
|
||||||
<button
|
|
||||||
onClick={handleRevokeAllDevices}
|
|
||||||
disabled={devices.length === 0}
|
|
||||||
className='w-full px-4 py-2.5 bg-red-500 hover:bg-red-600 disabled:bg-red-400 dark:bg-red-600 dark:hover:bg-red-700 dark:disabled:bg-red-500 text-white text-sm font-medium rounded-lg transition-colors disabled:cursor-not-allowed'
|
|
||||||
>
|
|
||||||
登出所有设备
|
|
||||||
</button>
|
|
||||||
<p className='text-xs text-gray-500 dark:text-gray-400 text-center'>
|
|
||||||
登出所有设备后需要重新登录
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 举报信息弹窗
|
// 举报信息弹窗
|
||||||
const reportPanel = (
|
const reportPanel = (
|
||||||
<>
|
<>
|
||||||
@@ -3932,6 +3653,28 @@ export const UserMenu: React.FC = () => {
|
|||||||
{/* 使用 Portal 将菜单面板渲染到 document.body */}
|
{/* 使用 Portal 将菜单面板渲染到 document.body */}
|
||||||
{isOpen && mounted && createPortal(menuPanel, document.body)}
|
{isOpen && mounted && createPortal(menuPanel, document.body)}
|
||||||
|
|
||||||
|
<PersonalCenterPanel
|
||||||
|
isOpen={isProfileCenterOpen}
|
||||||
|
mounted={mounted}
|
||||||
|
onClose={() => setIsProfileCenterOpen(false)}
|
||||||
|
username={currentUsername}
|
||||||
|
roleText={currentRoleText}
|
||||||
|
showRoleBadge={shouldShowRoleBadge}
|
||||||
|
avatarText={avatarText}
|
||||||
|
roleBadgeClassName={roleBadgeClassName}
|
||||||
|
showDeviceManagement={storageType !== 'localstorage'}
|
||||||
|
onOpenEmailSettings={() => {
|
||||||
|
setIsProfileCenterOpen(false);
|
||||||
|
setIsEmailSettingsOpen(true);
|
||||||
|
loadEmailSettings();
|
||||||
|
}}
|
||||||
|
onOpenDeviceManagement={() => {
|
||||||
|
setIsProfileCenterOpen(false);
|
||||||
|
setIsDeviceManagementOpen(true);
|
||||||
|
loadDevices();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 使用 Portal 将设置面板渲染到 document.body */}
|
{/* 使用 Portal 将设置面板渲染到 document.body */}
|
||||||
{isSettingsOpen && mounted && createPortal(settingsPanel, document.body)}
|
{isSettingsOpen && mounted && createPortal(settingsPanel, document.body)}
|
||||||
|
|
||||||
@@ -3993,15 +3736,32 @@ export const UserMenu: React.FC = () => {
|
|||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 使用 Portal 将邮件设置面板渲染到 document.body */}
|
<EmailSettingsPanel
|
||||||
{isEmailSettingsOpen &&
|
isOpen={isEmailSettingsOpen}
|
||||||
mounted &&
|
mounted={mounted}
|
||||||
createPortal(emailSettingsPanel, document.body)}
|
onClose={() => setIsEmailSettingsOpen(false)}
|
||||||
|
userEmail={userEmail}
|
||||||
|
onUserEmailChange={setUserEmail}
|
||||||
|
emailNotifications={emailNotifications}
|
||||||
|
onEmailNotificationsChange={setEmailNotifications}
|
||||||
|
emailSettingsLoading={emailSettingsLoading}
|
||||||
|
emailSettingsSaving={emailSettingsSaving}
|
||||||
|
onSave={handleSaveEmailSettings}
|
||||||
|
statusMessage={emailSettingsMessage}
|
||||||
|
statusType={emailSettingsMessageType}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 使用 Portal 将设备管理面板渲染到 document.body */}
|
<DeviceManagementPanel
|
||||||
{isDeviceManagementOpen &&
|
isOpen={isDeviceManagementOpen}
|
||||||
mounted &&
|
mounted={mounted}
|
||||||
createPortal(deviceManagementPanel, document.body)}
|
onClose={() => setIsDeviceManagementOpen(false)}
|
||||||
|
devices={devices}
|
||||||
|
devicesLoading={devicesLoading}
|
||||||
|
revoking={revoking}
|
||||||
|
onRevokeDevice={handleRevokeDevice}
|
||||||
|
onRevokeAllDevices={handleRevokeAllDevices}
|
||||||
|
getDeviceIcon={getDeviceIcon}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 使用 Portal 将生态应用面板渲染到 document.body */}
|
{/* 使用 Portal 将生态应用面板渲染到 document.body */}
|
||||||
{isEcoAppsOpen &&
|
{isEcoAppsOpen &&
|
||||||
|
|||||||
Reference in New Issue
Block a user