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 */}
+
+
+
+ )}
+
+ );
+}