From 29b2d99a82a2923c248aae1a176ca8c8bc5aece2 Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Fri, 19 Dec 2025 13:56:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=96=B0=E5=A2=9E=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E7=9B=B8=E5=85=B3=20UI=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FontSizePicker: 字体大小选择器,支持实时预览 - ModelCardSelector: 模型卡片选择组件(Haiku/Sonnet/Opus) - UserMenu: 用户菜单弹出组件,支持主题切换和设置导航 --- src/components/ui/FontSizePicker.tsx | 138 ++++++++++++++++++ src/components/ui/ModelCardSelector.tsx | 108 ++++++++++++++ src/components/ui/UserMenu.tsx | 183 ++++++++++++++++++++++++ 3 files changed, 429 insertions(+) create mode 100644 src/components/ui/FontSizePicker.tsx create mode 100644 src/components/ui/ModelCardSelector.tsx create mode 100644 src/components/ui/UserMenu.tsx diff --git a/src/components/ui/FontSizePicker.tsx b/src/components/ui/FontSizePicker.tsx new file mode 100644 index 0000000..9bcad9d --- /dev/null +++ b/src/components/ui/FontSizePicker.tsx @@ -0,0 +1,138 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Minus, Plus } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface FontSizePickerProps { + value: number; + onChange: (size: number) => void; + min?: number; + max?: number; + disabled?: boolean; +} + +export function FontSizePicker({ + value, + onChange, + min = 12, + max = 20, + disabled = false, +}: FontSizePickerProps) { + const [previewVisible, setPreviewVisible] = useState(false); + const [localValue, setLocalValue] = useState(value); + + // 同步外部值 + useEffect(() => { + setLocalValue(value); + }, [value]); + + // 实时预览 - 修改 CSS 变量 + useEffect(() => { + document.documentElement.style.setProperty('--font-size-base', `${localValue}px`); + }, [localValue]); + + const handleDecrease = () => { + if (localValue > min && !disabled) { + const newValue = localValue - 1; + setLocalValue(newValue); + onChange(newValue); + } + }; + + const handleIncrease = () => { + if (localValue < max && !disabled) { + const newValue = localValue + 1; + setLocalValue(newValue); + onChange(newValue); + } + }; + + const togglePreview = () => { + setPreviewVisible(!previewVisible); + }; + + return ( +
+
+ {/* 减号按钮 */} + + + {/* 数字显示 */} +
+ + {localValue} + + px +
+ + {/* 加号按钮 */} + + + {/* Preview 按钮 */} + +
+ + {/* 预览区域 */} + {previewVisible && ( +
+

+ 这是一段预览文字,用于展示当前字体大小的效果。 +
+ The quick brown fox jumps over the lazy dog. +

+
+ )} +
+ ); +} diff --git a/src/components/ui/ModelCardSelector.tsx b/src/components/ui/ModelCardSelector.tsx new file mode 100644 index 0000000..a1c9bd2 --- /dev/null +++ b/src/components/ui/ModelCardSelector.tsx @@ -0,0 +1,108 @@ +'use client'; + +import { cn } from '@/lib/utils'; + +// 模型卡片配置 +const MODEL_CARDS = [ + { + id: 'haiku', + name: 'Haiku', + description: 'Fast & efficient', + modelIdPattern: 'haiku', + }, + { + id: 'sonnet', + name: 'Sonnet', + description: 'Balanced', + modelIdPattern: 'sonnet', + }, + { + id: 'opus', + name: 'Opus', + description: 'Most capable', + modelIdPattern: 'opus', + }, +]; + +interface ModelCardSelectorProps { + value: string; + onChange: (modelId: string) => void; + disabled?: boolean; + models?: { modelId: string; displayName: string }[]; +} + +export function ModelCardSelector({ + value, + onChange, + disabled = false, + models = [], +}: ModelCardSelectorProps) { + // 根据当前选中的模型ID判断选中的卡片 + const getSelectedCard = (modelId: string): string => { + const lowerModelId = modelId.toLowerCase(); + if (lowerModelId.includes('haiku')) return 'haiku'; + if (lowerModelId.includes('opus')) return 'opus'; + return 'sonnet'; // 默认 sonnet + }; + + // 根据卡片类型找到对应的实际模型ID + const findModelIdByCard = (cardId: string): string => { + const model = models.find((m) => + m.modelId.toLowerCase().includes(cardId) + ); + return model?.modelId || value; + }; + + const selectedCard = getSelectedCard(value); + + return ( +
+ {MODEL_CARDS.map((card) => { + const isSelected = selectedCard === card.id; + return ( + + ); + })} +
+ ); +} diff --git a/src/components/ui/UserMenu.tsx b/src/components/ui/UserMenu.tsx new file mode 100644 index 0000000..a3add28 --- /dev/null +++ b/src/components/ui/UserMenu.tsx @@ -0,0 +1,183 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { Moon, Sun, Settings, LogOut, ChevronUp, ChevronDown } from 'lucide-react'; +import { Avatar } from '@/components/ui/Avatar'; +import { useSettings } from '@/hooks/useSettings'; +import { cn } from '@/lib/utils'; +import type { User } from '@/types'; + +interface UserMenuProps { + user: User; +} + +export function UserMenu({ user }: UserMenuProps) { + const [isOpen, setIsOpen] = useState(false); + const menuRef = useRef(null); + const router = useRouter(); + const { settings, updateSettings } = useSettings(); + + // 点击外部关闭菜单 + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + // ESC 键关闭菜单 + useEffect(() => { + function handleEscKey(event: KeyboardEvent) { + if (event.key === 'Escape') { + setIsOpen(false); + } + } + + if (isOpen) { + document.addEventListener('keydown', handleEscKey); + } + + return () => { + document.removeEventListener('keydown', handleEscKey); + }; + }, [isOpen]); + + // 切换主题 + const handleToggleTheme = async () => { + const newTheme = settings.theme === 'dark' ? 'light' : 'dark'; + try { + await updateSettings({ theme: newTheme }); + // 可选:更新 document 的 class 来应用主题 + document.documentElement.setAttribute('data-theme', newTheme); + } catch (error) { + console.error('Failed to update theme:', error); + } + }; + + // 进入设置页面 + const handleGoToSettings = () => { + setIsOpen(false); + router.push('/settings'); + }; + + // 登出 + const handleLogout = () => { + setIsOpen(false); + // TODO: 实现实际的登出逻辑 + // 暂时跳转到首页 + console.log('Logging out...'); + router.push('/'); + }; + + const isDarkMode = settings.theme === 'dark'; + + return ( +
+ {/* 触发器 - 用户信息区域 */} + + + {/* 弹出菜单 - 向上弹出 */} + {isOpen && ( +
+ {/* 菜单项 */} +
+ {/* Dark mode 切换 */} + + + {/* Settings */} + + + {/* 分隔线 */} +
+ + {/* Log out */} + +
+
+ )} +
+ ); +}