Merge pull request #428 from a981008/main

feat(settings): add UI zoom slider to General Settings
This commit is contained in:
程序员阿江-Relakkes 2026-05-14 17:39:05 +08:00 committed by GitHub
commit fd1617522c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 61 additions and 3 deletions

View File

@ -28,6 +28,7 @@ function isChatTab(tab: Tab | undefined) {
export function AppShell() {
const fetchSettings = useSettingsStore((s) => s.fetchAll)
const uiZoom = useSettingsStore((s) => s.uiZoom)
const sidebarOpen = useUIStore((s) => s.sidebarOpen)
const toggleSidebar = useUIStore((s) => s.toggleSidebar)
const setSidebarOpen = useUIStore((s) => s.setSidebarOpen)
@ -194,7 +195,7 @@ export function AppShell() {
}
return (
<div className={`app-shell app-shell-viewport flex overflow-hidden bg-[var(--color-surface)]${isMobileShell ? ' app-shell--mobile' : ''}`}>
<div className={`app-shell app-shell-viewport flex overflow-hidden bg-[var(--color-surface)]${isMobileShell ? ' app-shell--mobile' : ''}`} style={{ zoom: uiZoom }}>
{isMobileShell && effectiveSidebarOpen ? (
<button
type="button"

View File

@ -583,7 +583,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
)}
{!isMobile && (
<div className={`border-t border-[var(--color-border)] p-3 ${expanded ? '' : 'flex justify-center'}`}>
<div className={`absolute bottom-0 left-0 right-0 border-t border-[var(--color-border)] p-3 ${expanded ? '' : 'flex justify-center'}`}>
<NavItem
active={activeTabId === SETTINGS_TAB_ID}
collapsed={!expanded}

View File

@ -767,6 +767,8 @@ export const en = {
'settings.general.webSearchBraveFreeHint': 'Create an account to generate a Search API key with free usage for testing.',
'settings.general.webSearchHint': 'Auto uses native Claude web search for Claude model names, then falls back to Tavily and Brave keys.',
'settings.general.webSearchSave': 'Save',
'settings.general.uiZoom': 'UI Zoom',
'settings.general.uiZoomDescription': 'Scale the entire interface for better visibility on high-DPI displays.',
// ─── Empty Session ──────────────────────────────────────
'empty.title': 'New session',

View File

@ -769,6 +769,8 @@ export const zh: Record<TranslationKey, string> = {
'settings.general.webSearchBraveFreeHint': '注册账号后可创建 Search API Key免费额度可用于测试。',
'settings.general.webSearchHint': '自动模式会对 Claude 模型名优先使用原生 WebSearch失败或非 Claude 模型时再使用 Tavily/Brave。',
'settings.general.webSearchSave': '保存',
'settings.general.uiZoom': '界面缩放',
'settings.general.uiZoomDescription': '缩放整个界面,以便在高 DPI 屏幕上获得更好的可见性。',
// ─── Empty Session ──────────────────────────────────────
'empty.title': '新建会话',

View File

@ -1,7 +1,7 @@
import { useState, useEffect, useMemo, useRef, type ReactNode } from 'react'
import QRCode from 'qrcode'
import { Copy, Eye, EyeOff, PowerOff, QrCode, RotateCw } from 'lucide-react'
import { useSettingsStore } from '../stores/settingsStore'
import { useSettingsStore, UI_ZOOM_MIN, UI_ZOOM_MAX, UI_ZOOM_STEP } from '../stores/settingsStore'
import { useProviderStore } from '../stores/providerStore'
import { useTranslation } from '../i18n'
import { Modal } from '../components/shared/Modal'
@ -1382,6 +1382,8 @@ function GeneralSettings() {
setWebSearch,
responseLanguage,
setResponseLanguage,
uiZoom,
setUiZoom,
} = useSettingsStore()
const t = useTranslation()
const [webSearchDraft, setWebSearchDraft] = useState(webSearch)
@ -1775,6 +1777,32 @@ function GeneralSettings() {
</div>
</div>
</div>
{/* UI Zoom */}
<div className="mt-8">
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.uiZoom')}</h2>
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">{t('settings.general.uiZoomDescription')}</p>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-end">
<span className="text-sm text-[var(--color-text-secondary)]">
{Math.round(uiZoom * 100)}%
</span>
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-[var(--color-text-tertiary)]">{Math.round(UI_ZOOM_MIN * 100)}%</span>
<input
type="range"
min={UI_ZOOM_MIN}
max={UI_ZOOM_MAX}
step={UI_ZOOM_STEP}
value={uiZoom}
onChange={(e) => setUiZoom(parseFloat(e.target.value))}
onMouseUp={(e) => e.currentTarget.blur()}
className="flex-1"
/>
<span className="text-xs text-[var(--color-text-tertiary)]">{Math.round(UI_ZOOM_MAX * 100)}%</span>
</div>
</div>
</div>
</div>
)

View File

@ -8,6 +8,10 @@ import type { Locale } from '../i18n'
import { useUIStore } from './uiStore'
const LOCALE_STORAGE_KEY = 'cc-haha-locale'
const UI_ZOOM_STORAGE_KEY = 'cc-haha-ui-zoom'
export const UI_ZOOM_MIN = 0.5
export const UI_ZOOM_MAX = 2.0
export const UI_ZOOM_STEP = 0.05
let desktopNotificationsSaveQueue: Promise<void> = Promise.resolve()
function getStoredLocale(): Locale {
@ -18,6 +22,17 @@ function getStoredLocale(): Locale {
return 'zh'
}
function getStoredUiZoom(): number {
try {
const stored = localStorage.getItem(UI_ZOOM_STORAGE_KEY)
if (stored === null) return 1.0
const parsed = parseFloat(stored)
if (isNaN(parsed)) return 1.0
return Math.min(UI_ZOOM_MAX, Math.max(UI_ZOOM_MIN, parsed))
} catch { /* localStorage unavailable */ }
return 1.0
}
type SettingsStore = {
permissionMode: PermissionMode
currentModel: ModelInfo | null
@ -33,6 +48,7 @@ type SettingsStore = {
h5Access: H5AccessSettings
h5AccessError: string | null
responseLanguage: string
uiZoom: number
isLoading: boolean
error: string | null
@ -55,6 +71,7 @@ type SettingsStore = {
publicBaseUrl?: string | null
}) => Promise<void>
setResponseLanguage: (language: string) => Promise<void>
setUiZoom: (zoom: number) => void
}
const DEFAULT_H5_ACCESS_SETTINGS: H5AccessSettings = {
@ -79,9 +96,16 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
h5Access: DEFAULT_H5_ACCESS_SETTINGS,
h5AccessError: null,
responseLanguage: '',
uiZoom: getStoredUiZoom(),
isLoading: false,
error: null,
setUiZoom: (zoom: number) => {
const clamped = Math.min(UI_ZOOM_MAX, Math.max(UI_ZOOM_MIN, zoom))
set({ uiZoom: clamped })
try { localStorage.setItem(UI_ZOOM_STORAGE_KEY, String(clamped)) } catch { /* noop */ }
},
fetchAll: async () => {
set({ isLoading: true, error: null })
try {

View File

@ -43,5 +43,6 @@ export type UserSettings = {
desktopNotificationsEnabled?: boolean
webSearch?: WebSearchSettings
language?: string
uiZoom?: number
[key: string]: unknown
}