Explorar o código

feat: 主题切换改为下拉框选择,深蓝护眼图标更换为Eye

- 主题切换由循环点击改为点击弹出下拉框,可直接选择目标主题
- 下拉框展示4种主题,当前主题带勾选标记
- 修复跟随系统图标与切换项目图标相同的问题,改用SunMoon
- 深蓝护眼模式图标从Monitor更换为Eye,更贴合护眼语义
- 更新中英文国际化主题标签
Mochocyang hai 3 meses
pai
achega
d941d303a1

+ 22 - 0
GenxinLOG/20260628-1630-更新日志.md

@@ -0,0 +1,22 @@
+# 青幕AI写作 更新日志
+
+## v2.2.25 - 2026-06-28
+
+### 界面优化
+
+**主题切换改为下拉框选择**
+- 主题切换由循环点击按钮改为点击弹出下拉框,用户可直接选择目标主题
+- 下拉框展示全部4种主题:浅色模式、深色模式、深蓝护眼、跟随系统,当前主题带勾选标记
+- 修复"跟随系统"图标与"切换项目"图标相同的问题,跟随系统图标更换为 SunMoon 图标
+- 深蓝护眼模式图标从 Monitor(显示器)更换为 Eye(眼睛),更贴合护眼语义
+
+### 问题修复
+
+**故事框架页面白屏修复**
+- 修复故事框架页面显示 "Cannot read properties of null (reading 'useMemo')" 错误的问题
+- 根因:@dnd-kit/sortable 包版本被错误设置为 ^10.0.0
+- 修复:将 @dnd-kit/sortable 版本降级为正确的 ^8.0.0
+
+**AI会话输入框高度调整修复**
+- 修复输入框在修复边界溢出后无法拉高的异常
+- 添加 ResizeContext 缓存初始DOM位置,基于位移delta预测新位置计算maxHeight

+ 5 - 5
package-lock.json

@@ -10,7 +10,7 @@
       "dependencies": {
         "@base-ui/react": "^1.3.0",
         "@dnd-kit/core": "^6.3.1",
-        "@dnd-kit/sortable": "^10.0.0",
+        "@dnd-kit/sortable": "^8.0.0",
         "@dnd-kit/utilities": "^3.2.2",
         "@fontsource-variable/geist": "^5.2.8",
         "@milkdown/kit": "^7.20.0",
@@ -1225,16 +1225,16 @@
       }
     },
     "node_modules/@dnd-kit/sortable": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
-      "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz",
+      "integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==",
       "license": "MIT",
       "dependencies": {
         "@dnd-kit/utilities": "^3.2.2",
         "tslib": "^2.0.0"
       },
       "peerDependencies": {
-        "@dnd-kit/core": "^6.3.0",
+        "@dnd-kit/core": "^6.1.0",
         "react": ">=16.8.0"
       }
     },

+ 2 - 2
package.json

@@ -1,7 +1,7 @@
 {
   "name": "llm-wiki",
   "private": true,
-  "version": "2.2.24",
+  "version": "2.2.25",
   "type": "module",
   "scripts": {
     "dev": "vite",
@@ -19,7 +19,7 @@
   "dependencies": {
     "@base-ui/react": "^1.3.0",
     "@dnd-kit/core": "^6.3.1",
-    "@dnd-kit/sortable": "^10.0.0",
+    "@dnd-kit/sortable": "^8.0.0",
     "@dnd-kit/utilities": "^3.2.2",
     "@fontsource-variable/geist": "^5.2.8",
     "@milkdown/kit": "^7.20.0",

+ 1 - 1
src-tauri/tauri.conf.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://schema.tauri.app/config/2",
   "productName": "QMaiWrite",
-  "version": "2.2.24",
+  "version": "2.2.25",
   "identifier": "com.qingmuai.writer",
   "build": {
     "beforeDevCommand": "npm run dev",

+ 86 - 29
src/components/layout/icon-sidebar.tsx

@@ -1,6 +1,8 @@
+import { useState, useRef, useEffect } from "react"
 import {
-  FileText, FolderOpen, Search, Network, Brain, Settings, ArrowLeftRight, Sun, Moon, Monitor, Trash2, Sparkles, LayoutDashboard, BookOpen, Drama,
+  FileText, FolderOpen, Search, Network, Brain, Settings, ArrowLeftRight, Sun, Moon, Eye, SunMoon, Check, Trash2, Sparkles, LayoutDashboard, BookOpen, Drama,
 } from "lucide-react"
+import { createPortal } from "react-dom"
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
 import { useWikiStore } from "@/stores/wiki-store"
 import { useReviewStore } from "@/stores/review-store"
@@ -8,7 +10,7 @@ import { useTranslation } from "react-i18next"
 import logoImg from "@/assets/QM-LOGO.png"
 import type { WikiState } from "@/stores/wiki-store"
 import { saveTheme } from "@/lib/project-store"
-import { applyTheme } from "@/lib/theme-utils"
+import { applyTheme, type ThemeMode } from "@/lib/theme-utils"
 
 type NavView = WikiState["activeView"]
 
@@ -35,6 +37,13 @@ interface IconSidebarProps {
   onSwitchProject: () => void
 }
 
+const THEME_OPTIONS: { value: ThemeMode; icon: typeof Sun; labelKey: string }[] = [
+  { value: "light", icon: Sun, labelKey: "theme.light" },
+  { value: "dark", icon: Moon, labelKey: "theme.dark" },
+  { value: "deep-blue", icon: Eye, labelKey: "theme.deepBlue" },
+  { value: "system", icon: SunMoon, labelKey: "theme.system" },
+]
+
 export function IconSidebar({ onToggleSidebar, onOpenSidebar, onSwitchProject }: IconSidebarProps) {
   const { t } = useTranslation()
   const activeView = useWikiStore((s) => s.activeView)
@@ -46,36 +55,49 @@ export function IconSidebar({ onToggleSidebar, onOpenSidebar, onSwitchProject }:
   const setTheme = useWikiStore((s) => s.setTheme)
   const pendingCount = useReviewStore((s) => s.items.filter((i) => !i.resolved).length)
 
-  const handleCycleTheme = () => {
-    const themes: ("light" | "dark" | "deep-blue" | "system")[] = ["system", "light", "dark", "deep-blue"]
-    const currentIndex = themes.indexOf(theme)
-    const nextIndex = (currentIndex + 1) % themes.length
-    const nextTheme = themes[nextIndex]
-    
+  // 主题下拉框状态
+  const [themeMenuOpen, setThemeMenuOpen] = useState(false)
+  const themeTriggerRef = useRef<HTMLButtonElement>(null)
+  const [themeMenuStyle, setThemeMenuStyle] = useState<{ left: number; top: number } | null>(null)
+
+  const getThemeIcon = () => {
+    const option = THEME_OPTIONS.find((o) => o.value === theme)
+    const Icon = option?.icon ?? Sun
+    return <Icon className="h-5 w-5" />
+  }
+
+  const handleSelectTheme = (nextTheme: ThemeMode) => {
     setTheme(nextTheme)
     saveTheme(nextTheme)
     applyTheme(nextTheme)
+    setThemeMenuOpen(false)
   }
 
-  const getThemeIcon = () => {
-    switch (theme) {
-      case "light": return <Sun className="h-5 w-5" />
-      case "dark": return <Moon className="h-5 w-5" />
-      case "deep-blue": return <Monitor className="h-5 w-5" />
-      case "system": return <ArrowLeftRight className="h-5 w-5" />
-      default: return <Sun className="h-5 w-5" />
+  useEffect(() => {
+    if (!themeMenuOpen) {
+      setThemeMenuStyle(null)
+      return
     }
-  }
-
-  const getThemeTooltip = () => {
-    switch (theme) {
-      case "system": return t("theme.toLight")
-      case "light": return t("theme.toDark")
-      case "dark": return t("theme.toDeepBlue")
-      case "deep-blue": return t("theme.toSystem")
-      default: return t("theme.switch")
+    const updatePosition = () => {
+      const rect = themeTriggerRef.current?.getBoundingClientRect()
+      if (!rect) return
+      const menuWidth = 180
+      const menuHeight = THEME_OPTIONS.length * 36 + 8
+      const left = Math.min(rect.right + 6, window.innerWidth - menuWidth - 4)
+      let top: number
+      const availableAbove = rect.top
+      const availableBelow = window.innerHeight - rect.bottom
+      if (availableAbove >= menuHeight + 6 || availableAbove >= availableBelow) {
+        top = Math.max(4, rect.bottom - menuHeight)
+      } else {
+        top = rect.top
+      }
+      setThemeMenuStyle({ left, top })
     }
-  }
+    updatePosition()
+    window.addEventListener("resize", updatePosition)
+    return () => window.removeEventListener("resize", updatePosition)
+  }, [themeMenuOpen])
 
   const handleNavClick = (view: NavView) => {
     setSearchPanelOpen(false)
@@ -177,18 +199,53 @@ export function IconSidebar({ onToggleSidebar, onOpenSidebar, onSwitchProject }:
         </div>
         {/* Bottom: daemon status + theme toggle + settings + switch project */}
         <div className="flex flex-col items-center gap-1 pb-1">
-          {/* Theme toggle */}
+          {/* Theme selector dropdown */}
           <Tooltip>
             <TooltipTrigger
-              onClick={handleCycleTheme}
-              className="relative flex h-10 w-10 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent/50 hover:text-accent-foreground"
+              ref={themeTriggerRef}
+              onClick={() => setThemeMenuOpen(!themeMenuOpen)}
+              className={`relative flex h-10 w-10 items-center justify-center rounded-md transition-colors hover:bg-accent/50 hover:text-accent-foreground ${
+                themeMenuOpen ? "bg-accent/50 text-accent-foreground" : "text-muted-foreground"
+              }`}
             >
               {getThemeIcon()}
             </TooltipTrigger>
             <TooltipContent side="right">
-              {getThemeTooltip()}
+              {t("theme.switch")}
             </TooltipContent>
           </Tooltip>
+          {themeMenuOpen && themeMenuStyle && createPortal(
+            <>
+              <div
+                className="fixed inset-0 z-40"
+                onClick={() => setThemeMenuOpen(false)}
+              />
+              <div
+                className="fixed z-50 rounded-md border bg-popover p-1 shadow-md"
+                style={{ left: themeMenuStyle.left, top: themeMenuStyle.top, width: 180 }}
+              >
+                {THEME_OPTIONS.map((option) => {
+                  const Icon = option.icon
+                  const isActive = theme === option.value
+                  return (
+                    <button
+                      key={option.value}
+                      type="button"
+                      onClick={() => handleSelectTheme(option.value)}
+                      className={`flex w-full items-center gap-2 rounded-sm px-2.5 py-1.5 text-left text-sm transition-colors hover:bg-accent ${
+                        isActive ? "text-foreground" : "text-muted-foreground"
+                      }`}
+                    >
+                      <Icon className="h-4 w-4 shrink-0" />
+                      <span className="flex-1">{t(option.labelKey)}</span>
+                      <Check className={`h-4 w-4 shrink-0 ${isActive ? "opacity-100" : "opacity-0"}`} />
+                    </button>
+                  )
+                })}
+              </div>
+            </>,
+            document.body,
+          )}
           <Tooltip>
             <TooltipTrigger
               onClick={() => {

+ 4 - 0
src/i18n/en.json

@@ -229,6 +229,10 @@
   },
   "theme": {
     "switch": "Switch theme",
+    "light": "Light",
+    "dark": "Dark",
+    "deepBlue": "Deep Blue",
+    "system": "System",
     "toDark": "Switch to dark mode",
     "toDeepBlue": "Switch to deep blue eye-care mode",
     "toLight": "Switch to light mode",

+ 4 - 0
src/i18n/zh.json

@@ -121,6 +121,10 @@
   },
   "theme": {
     "switch": "切换主题",
+    "light": "浅色模式",
+    "dark": "深色模式",
+    "deepBlue": "深蓝护眼",
+    "system": "跟随系统",
     "toDark": "切换到深色模式",
     "toDeepBlue": "切换到深蓝护眼模式",
     "toLight": "切换到浅色模式",