From 3cb54df62be546302a767619f5e4370499570986 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sun, 12 Apr 2026 18:05:37 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=8F=9C=E5=8D=95=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E4=B8=AA=E4=BA=BA=E4=B8=AD=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/watch-room/page.tsx | 10 +- src/components/AIChatPanel.tsx | 13 +- src/components/DeviceManagementPanel.tsx | 158 ++++++++ src/components/EmailSettingsPanel.tsx | 162 ++++++++ src/components/PersonalCenterPanel.tsx | 123 ++++++ src/components/UserMenu.tsx | 466 ++++++----------------- 6 files changed, 573 insertions(+), 359 deletions(-) create mode 100644 src/components/DeviceManagementPanel.tsx create mode 100644 src/components/EmailSettingsPanel.tsx create mode 100644 src/components/PersonalCenterPanel.tsx diff --git a/src/app/watch-room/page.tsx b/src/app/watch-room/page.tsx index 6daf4c2..6b54ba2 100644 --- a/src/app/watch-room/page.tsx +++ b/src/app/watch-room/page.tsx @@ -88,6 +88,8 @@ export default function WatchRoomPage() { }); }; + const getAvatarText = (name?: string) => (name?.trim().charAt(0).toUpperCase() || '用'); + // 加载房间列表 const loadRooms = async (showLoading = false) => { 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" >
-
- {member.name.charAt(0).toUpperCase()} +
+ {getAvatarText(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" >
-
- {member.name.charAt(0).toUpperCase()} +
+ {getAvatarText(member.name)}
{member.name} diff --git a/src/components/AIChatPanel.tsx b/src/components/AIChatPanel.tsx index 517dba0..fbbf21e 100644 --- a/src/components/AIChatPanel.tsx +++ b/src/components/AIChatPanel.tsx @@ -9,6 +9,7 @@ import { createPortal } from 'react-dom'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; +import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; import { VideoContext } from '@/lib/ai-orchestrator'; interface ChatMessage { @@ -51,6 +52,7 @@ export default function AIChatPanel({ const [input, setInput] = useState(''); const [isStreaming, setIsStreaming] = useState(false); const [isMobile, setIsMobile] = useState(false); + const [currentUsername, setCurrentUsername] = useState('用户'); const messagesEndRef = useRef(null); const inputRef = useRef(null); const prevStorageKeyRef = useRef(storageKey); @@ -74,6 +76,13 @@ export default function AIChatPanel({ scrollToBottom(); }, [messages]); + useEffect(() => { + const authInfo = getAuthInfoFromBrowserCookie(); + setCurrentUsername(authInfo?.username || '用户'); + }, []); + + const userAvatarText = currentUsername.trim().charAt(0).toUpperCase() || '用'; + // 从sessionStorage加载消息记录 useEffect(() => { if (typeof window === 'undefined') return; @@ -416,7 +425,7 @@ export default function AIChatPanel({ > {message.role === 'user' ? ( - U + {userAvatarText} ) : ( @@ -622,7 +631,7 @@ export default function AIChatPanel({ > {message.role === 'user' ? ( - U + {userAvatarText} ) : ( diff --git a/src/components/DeviceManagementPanel.tsx b/src/components/DeviceManagementPanel.tsx new file mode 100644 index 0000000..d5db4fd --- /dev/null +++ b/src/components/DeviceManagementPanel.tsx @@ -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( + <> +
e.preventDefault()} + onWheel={(e) => e.preventDefault()} + style={{ touchAction: 'none' }} + /> + +
+
e.stopPropagation()} + style={{ touchAction: 'auto' }} + > +
+

+ 设备管理 +

+ +
+ +
+ {devicesLoading ? ( +
+ {[1, 2, 3].map((i) => ( +
+
+
+ ))} +
+ 加载中... +
+
+ ) : devices.length === 0 ? ( +
+ +

暂无登录设备

+
+ ) : ( +
+ {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 ( +
+
+
+
+ + + {device.deviceInfo} + + {device.isCurrent && ( + + 当前设备 + + )} +
+
+
登录时间: {new Date(device.createdAt).toLocaleString('zh-CN')}
+
最后活跃: {new Date(device.lastUsed).toLocaleString('zh-CN')}
+
+
+ {!device.isCurrent && ( + + )} +
+
+ ); + })} +
+ )} +
+ +
+ +

+ 登出所有设备后需要重新登录 +

+
+
+
+ , + document.body + ); +} diff --git a/src/components/EmailSettingsPanel.tsx b/src/components/EmailSettingsPanel.tsx new file mode 100644 index 0000000..499d11a --- /dev/null +++ b/src/components/EmailSettingsPanel.tsx @@ -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( + <> +
e.preventDefault()} + onWheel={(e) => e.preventDefault()} + style={{ touchAction: 'none' }} + /> + +
+
e.stopPropagation()} + style={{ touchAction: 'auto' }} + > +
+

+ 邮件通知设置 +

+ +
+ + {emailSettingsLoading ? ( +
+
+
+
+
+
+
+
+
+
+
+
+ 加载中... +
+
+ ) : ( +
+
+ + 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' + /> +
+ +
+
+

+ 接收收藏更新通知 +

+

+ 当收藏的影片有更新时发送邮件通知 +

+
+ +
+ + + + {statusMessage ? ( +

+ {statusMessage} +

+ ) : null} +
+ )} + +
+

+ 💡 提示:需要管理员先在管理面板中配置邮件服务 +

+
+
+
+ , + document.body + ); +} diff --git a/src/components/PersonalCenterPanel.tsx b/src/components/PersonalCenterPanel.tsx new file mode 100644 index 0000000..70fe778 --- /dev/null +++ b/src/components/PersonalCenterPanel.tsx @@ -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( + <> +
{ + e.preventDefault(); + }} + onWheel={(e) => { + e.preventDefault(); + }} + style={{ touchAction: 'none' }} + /> + +
+
{ + e.stopPropagation(); + }} + style={{ touchAction: 'auto' }} + > +
+ +
+ {avatarText} +
+ {showRoleBadge && ( + + {roleText} + + )} +

+ {username} +

+
+ +
+ + + {showDeviceManagement && ( + + )} +
+
+
+ , + document.body + ); +} diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx index c52dda5..fc477e8 100644 --- a/src/components/UserMenu.tsx +++ b/src/components/UserMenu.tsx @@ -17,7 +17,6 @@ import { Home, KeyRound, LogOut, - Mail, MessageSquare, Monitor, MoveDown, @@ -46,6 +45,9 @@ import { UpdateStatus } from '@/lib/version_check'; import { FavoritesPanel } from './FavoritesPanel'; import { NotificationPanel } from './NotificationPanel'; import { OfflineDownloadPanel } from './OfflineDownloadPanel'; +import { DeviceManagementPanel } from './DeviceManagementPanel'; +import { EmailSettingsPanel } from './EmailSettingsPanel'; +import { PersonalCenterPanel } from './PersonalCenterPanel'; import { useVersionCheck } from './VersionCheckProvider'; import { VersionPanel } from './VersionPanel'; import { DownloadManagementPanel } from './DownloadManagementPanel'; @@ -59,6 +61,7 @@ export const UserMenu: React.FC = () => { const router = useRouter(); const { updateStatus, isChecking } = useVersionCheck(); const [isOpen, setIsOpen] = useState(false); + const [isProfileCenterOpen, setIsProfileCenterOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false); const [isSubscribeOpen, setIsSubscribeOpen] = useState(false); @@ -89,7 +92,7 @@ export const UserMenu: React.FC = () => { // Body 滚动锁定 - 使用 overflow 方式避免布局问题 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 html = document.documentElement; @@ -108,7 +111,7 @@ export const UserMenu: React.FC = () => { 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); @@ -149,6 +152,10 @@ export const UserMenu: React.FC = () => { const [emailNotifications, setEmailNotifications] = useState(false); const [emailSettingsLoading, setEmailSettingsLoading] = useState(false); const [emailSettingsSaving, setEmailSettingsSaving] = useState(false); + const [emailSettingsMessage, setEmailSettingsMessage] = useState(''); + const [emailSettingsMessageType, setEmailSettingsMessageType] = useState< + 'success' | 'error' | null + >(null); // 设备管理状态 const [devices, setDevices] = useState([]); @@ -618,6 +625,8 @@ export const UserMenu: React.FC = () => { // 加载邮件通知设置 const loadEmailSettings = async () => { setEmailSettingsLoading(true); + setEmailSettingsMessage(''); + setEmailSettingsMessageType(null); try { const response = await fetch('/api/user/email-settings'); if (response.ok) { @@ -635,6 +644,8 @@ export const UserMenu: React.FC = () => { // 保存邮件通知设置 const handleSaveEmailSettings = async () => { setEmailSettingsSaving(true); + setEmailSettingsMessage(''); + setEmailSettingsMessageType(null); try { const response = await fetch('/api/user/email-settings', { method: 'POST', @@ -645,32 +656,22 @@ export const UserMenu: React.FC = () => { }), }); - const messageEl = document.getElementById('email-settings-message'); if (response.ok) { - if (messageEl) { - messageEl.textContent = '保存成功!'; - messageEl.className = 'text-xs text-center text-green-600 dark:text-green-400'; - messageEl.classList.remove('hidden'); - setTimeout(() => { - messageEl.classList.add('hidden'); - }, 3000); - } + setEmailSettingsMessage('保存成功!'); + setEmailSettingsMessageType('success'); + setTimeout(() => { + setEmailSettingsMessage(''); + setEmailSettingsMessageType(null); + }, 3000); } else { const data = await response.json(); - if (messageEl) { - messageEl.textContent = data.error || '保存失败'; - messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400'; - messageEl.classList.remove('hidden'); - } + setEmailSettingsMessage(data.error || '保存失败'); + setEmailSettingsMessageType('error'); } } catch (error) { console.error('保存邮件设置失败:', error); - const messageEl = document.getElementById('email-settings-message'); - if (messageEl) { - messageEl.textContent = '保存失败,请重试'; - messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400'; - messageEl.classList.remove('hidden'); - } + setEmailSettingsMessage('保存失败,请重试'); + setEmailSettingsMessageType('error'); } finally { 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 = ( <> @@ -1446,62 +1465,35 @@ export const UserMenu: React.FC = () => { {/* 菜单面板 */}
{/* 用户信息区域 */} -
-
-
-
- - 当前用户 - - {/* 邮件设置图标按钮 */} - - {/* 设备管理图标按钮 */} - {storageType !== 'localstorage' && ( - + {currentRoleText} + )}
- - {getRoleText(authInfo?.role || 'user')} - -
-
-
- {authInfo?.username || 'default'} +
+ + {currentUsername} +
+ + +
- 数据存储: - {displayStorageType === 'localstorage' ? '本地' : displayStorageType} +
数据存储
+
+ {displayStorageType === 'localstorage' ? '本地' : displayStorageType} +
@@ -3353,277 +3345,6 @@ export const UserMenu: React.FC = () => { ); - // 邮件设置面板内容 - const emailSettingsPanel = ( - <> - {/* 背景遮罩 */} -
setIsEmailSettingsOpen(false)} - onTouchMove={(e) => { - e.preventDefault(); - }} - onWheel={(e) => { - e.preventDefault(); - }} - style={{ - touchAction: 'none', - }} - /> - - {/* 邮件设置面板 */} -
-
{ - e.stopPropagation(); - }} - style={{ - touchAction: 'auto', - }} - > - {/* 标题栏 */} -
-

- 邮件通知设置 -

- -
- - {/* 表单 */} - {emailSettingsLoading ? ( -
- {/* 加载骨架屏 */} -
-
-
-
-
-
-
-
-
-
-
- 加载中... -
-
- ) : ( -
-
- - 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' - /> -
- -
-
-

- 接收收藏更新通知 -

-

- 当收藏的影片有更新时发送邮件通知 -

-
- -
- - - -

-
- )} - - {/* 提示信息 */} -
-

- 💡 提示:需要管理员先在管理面板中配置邮件服务 -

-
-
-
- - ); - - // 设备管理面板内容 - const deviceManagementPanel = ( - <> - {/* 背景遮罩 */} -
setIsDeviceManagementOpen(false)} - onTouchMove={(e) => { - e.preventDefault(); - }} - onWheel={(e) => { - e.preventDefault(); - }} - style={{ - touchAction: 'none', - }} - /> - - {/* 设备管理面板 */} -
-
{ - e.stopPropagation(); - }} - style={{ - touchAction: 'auto', - }} - > - {/* 标题栏 */} -
-

- 设备管理 -

- -
- - {/* 设备列表 */} -
- {devicesLoading ? ( -
- {[1, 2, 3].map((i) => ( -
-
-
- ))} -
- 加载中... -
-
- ) : devices.length === 0 ? ( -
- -

暂无登录设备

-
- ) : ( -
- {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 ( -
-
-
-
- - - {device.deviceInfo} - - {device.isCurrent && ( - - 当前设备 - - )} -
-
-
登录时间: {new Date(device.createdAt).toLocaleString('zh-CN')}
-
最后活跃: {new Date(device.lastUsed).toLocaleString('zh-CN')}
-
-
- {!device.isCurrent && ( - - )} -
-
- ); - })} -
- )} -
- - {/* 底部操作 */} -
- -

- 登出所有设备后需要重新登录 -

-
-
-
- - ); - // 举报信息弹窗 const reportPanel = ( <> @@ -3932,6 +3653,28 @@ export const UserMenu: React.FC = () => { {/* 使用 Portal 将菜单面板渲染到 document.body */} {isOpen && mounted && createPortal(menuPanel, document.body)} + 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 */} {isSettingsOpen && mounted && createPortal(settingsPanel, document.body)} @@ -3993,15 +3736,32 @@ export const UserMenu: React.FC = () => { document.body )} - {/* 使用 Portal 将邮件设置面板渲染到 document.body */} - {isEmailSettingsOpen && - mounted && - createPortal(emailSettingsPanel, document.body)} + setIsEmailSettingsOpen(false)} + userEmail={userEmail} + onUserEmailChange={setUserEmail} + emailNotifications={emailNotifications} + onEmailNotificationsChange={setEmailNotifications} + emailSettingsLoading={emailSettingsLoading} + emailSettingsSaving={emailSettingsSaving} + onSave={handleSaveEmailSettings} + statusMessage={emailSettingsMessage} + statusType={emailSettingsMessageType} + /> - {/* 使用 Portal 将设备管理面板渲染到 document.body */} - {isDeviceManagementOpen && - mounted && - createPortal(deviceManagementPanel, document.body)} + setIsDeviceManagementOpen(false)} + devices={devices} + devicesLoading={devicesLoading} + revoking={revoking} + onRevokeDevice={handleRevokeDevice} + onRevokeAllDevices={handleRevokeAllDevices} + getDeviceIcon={getDeviceIcon} + /> {/* 使用 Portal 将生态应用面板渲染到 document.body */} {isEcoAppsOpen &&