From 447f86e2b5a02eae9dbc9f2b0dabd160e81cb6a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Sun, 1 Feb 2026 14:53:00 +0800 Subject: [PATCH] Use tabs, redesign theme settings UI Replace the Accordion-based layout with a Tabs component and overhaul the Theme settings page UI. Rework the top header to a compact title/status area showing current theme, font and unsaved state; restyle action buttons and refresh icon. Convert font settings into a Card with improved FileInput flow (attempt delete before upload, better success/error toasts and page reload), and present theme previews and custom color editors as Cards per light/dark mode with updated ColorPicker handling. Update imports accordingly and apply various layout / class refinements. --- .../src/pages/dashboard/config/theme.tsx | 373 ++++++++++-------- 1 file changed, 202 insertions(+), 171 deletions(-) diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/config/theme.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/config/theme.tsx index 18dc8063..a1654127 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/config/theme.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/config/theme.tsx @@ -1,8 +1,8 @@ -import { Accordion, AccordionItem } from '@heroui/accordion'; import { Button } from '@heroui/button'; import { Card, CardBody, CardHeader } from '@heroui/card'; import { Select, SelectItem } from '@heroui/select'; import { Chip } from '@heroui/chip'; +import { Tab, Tabs } from '@heroui/tabs'; import { useRequest } from 'ahooks'; import clsx from 'clsx'; import { useEffect, useRef, useState, useMemo, useCallback } from 'react'; @@ -298,159 +298,180 @@ const ThemeConfigCard = () => { 主题配置 - NapCat WebUI {/* 顶部操作栏 */} -
-
-
-
- 当前主题: - - {savedThemeName || '加载中...'} - +
+
+
+

外观设置

+
+
+ + {savedThemeName || '加载中...'} +
+
+
+ + {savedFontModeDisplayName} +
+ {hasUnsavedChanges && ( + <> +
+
+
+ 待保存 +
+ + )}
-
- 字体: - - {savedFontModeDisplayName} - -
- {hasUnsavedChanges && ( - - 有未保存的更改 - - )}
-
+ +
+
-
- + - } + + + 字体设置 +
+ } > -
- ( - - )} - /> - {theme.fontMode === 'custom' && ( -
-
- 上传自定义字体(仅在选择"自定义字体"时生效) + + +
+
+

WebUI 字体

+

自定义界面显示的字体风格

+ ( + + )} + />
- {customFontExists && ( -
- 已上传自定义字体 -
- )} - { - try { - // 如果已存在自定义字体,先尝试删除 - if (customFontExists) { + + {theme.fontMode === 'custom' && ( +
+
+
自定义字体文件
+ {customFontExists && ( + }> + 已上传 + + )} +
+ + { + try { + if (customFontExists) { + try { + await FileManager.deleteWebUIFont(); + } catch (e) { + console.warn('Failed to delete existing font before upload:', e); + } + } + await FileManager.uploadWebUIFont(file); + toast.success('上传成功,即将刷新页面'); + setTimeout(() => window.location.reload(), 1000); + } catch (error) { + toast.error('上传失败: ' + (error as Error).message); + } + }} + onDelete={async () => { try { await FileManager.deleteWebUIFont(); - } catch (e) { - console.warn('Failed to delete existing font before upload:', e); - // 继续尝试上传,后端可能会覆盖或报错 + toast.success('删除成功,即将刷新页面'); + setTimeout(() => window.location.reload(), 1000); + } catch (error) { + toast.error('删除失败: ' + (error as Error).message); } - } - - await FileManager.uploadWebUIFont(file); - toast.success('上传成功,即将刷新页面'); - setTimeout(() => { - window.location.reload(); - }, 1000); - } catch (error) { - toast.error('上传失败: ' + (error as Error).message); - } - }} - onDelete={async () => { - try { - await FileManager.deleteWebUIFont(); - toast.success('删除成功,即将刷新页面'); - setTimeout(() => { - window.location.reload(); - }, 1000); - } catch (error) { - toast.error('删除失败: ' + (error as Error).message); - } - }} - /> + }} + /> +

+ 注意:上传新字体会覆盖旧字体文件,更改后需要刷新页面生效。 +

+
+ )}
- )} -
- + + + - } + + + 选择主题 +
+ } > -
+
{themes.map((t) => ( { /> ))}
- + - } + + + 自定义配色 +
+ } > -
+
{(['light', 'dark'] as const).map((mode) => ( -
-

- {mode === 'dark' ? : } - {mode === 'dark' ? '深色模式' : '浅色模式'} -

-
- {colorKeys.map((colorKey) => ( -
- { - const hslArray = value?.split(' ') ?? [0, 0, 0]; - const color = `hsl(${hslArray[0]}, ${hslArray[1]}, ${hslArray[2]})`; - return ( - { - // ColorPicker returns hsl(h, s%, l%) string - // We need to parse it and convert to "h s% l%" format for theme config - const match = hslString.match(/hsl\((\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?)%,\s*(\d+(?:\.\d+)?)%\)/); - if (match) { - onChange(`${match[1]} ${match[2]}% ${match[3]}%`); - } - }} - /> - ); - }} - /> - - {colorKey.replace('--heroui-', '')} - -
- ))} -
-
+ + +
+ {mode === 'dark' ? : } +

+ {mode === 'dark' ? '深色模式' : '浅色模式'} +

+
+

+ 调整{mode === 'dark' ? '深色' : '浅色'}主题下的颜色变量 +

+
+ +
+ {colorKeys.map((colorKey) => ( +
+ { + const hslArray = value?.split(' ') ?? [0, 0, 0]; + const color = `hsl(${hslArray[0]}, ${hslArray[1]}, ${hslArray[2]})`; + return ( + { + const match = hslString.match(/hsl\((\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?)%,\s*(\d+(?:\.\d+)?)%\)/); + if (match) { + onChange(`${match[1]} ${match[2]}% ${match[3]}%`); + } + }} + /> + ); + }} + /> +
+ + {colorKey.replace('--heroui-', '')} + + + Variable + +
+
+ ))} +
+
+
))}
- - + +
);