diff --git a/napcat.webui/package.json b/napcat.webui/package.json index bdef9b0d..bd3c4239 100644 --- a/napcat.webui/package.json +++ b/napcat.webui/package.json @@ -13,6 +13,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@heroui/accordion": "^2.2.8", "@heroui/avatar": "2.2.7", "@heroui/breadcrumbs": "2.2.7", "@heroui/button": "2.2.10", @@ -64,6 +65,7 @@ "qrcode.react": "^4.2.0", "quill": "^2.0.3", "react": "^19.0.0", + "react-color": "^2.19.3", "react-dom": "^19.0.0", "react-dropzone": "^14.3.5", "react-error-boundary": "^5.0.0", diff --git a/napcat.webui/src/components/ColorPicker.tsx b/napcat.webui/src/components/ColorPicker.tsx new file mode 100644 index 00000000..eb9f423a --- /dev/null +++ b/napcat.webui/src/components/ColorPicker.tsx @@ -0,0 +1,36 @@ +import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover' +import React from 'react' +import { ColorResult, SketchPicker } from 'react-color' + +// 假定 heroui 提供的 Popover组件 + +interface ColorPickerProps { + color: string + onChange: (color: ColorResult) => void +} + +const ColorPicker: React.FC = ({ color, onChange }) => { + const handleChange = (colorResult: ColorResult) => { + onChange(colorResult) + } + + return ( + + +
+ + + + + + ) +} + +export default ColorPicker diff --git a/napcat.webui/src/components/sidebar/menus.tsx b/napcat.webui/src/components/sidebar/menus.tsx index 1cf1f645..79f6b08e 100644 --- a/napcat.webui/src/components/sidebar/menus.tsx +++ b/napcat.webui/src/components/sidebar/menus.tsx @@ -58,14 +58,13 @@ const renderItems = (items: MenuItem[], children = false) => { color="primary" endContent={ canOpen ? ( - // div实现箭头V效果
{ 'w-3 h-1.5 rounded-full ml-auto shadow-lg', isActive ? 'bg-primary-500 animate-spinner-ease-spin' - : 'bg-red-300 dark:bg-white' + : 'bg-primary-200 dark:bg-white' )} /> ) diff --git a/napcat.webui/src/const/themes.ts b/napcat.webui/src/const/themes.ts new file mode 100644 index 00000000..781ce351 --- /dev/null +++ b/napcat.webui/src/const/themes.ts @@ -0,0 +1,5 @@ +import nc_pink from './themes/nc_pink' + +const themes: ThemeInfo[] = [nc_pink] + +export default themes diff --git a/napcat.webui/src/const/themes/nc_pink.ts b/napcat.webui/src/const/themes/nc_pink.ts new file mode 100644 index 00000000..69fdec60 --- /dev/null +++ b/napcat.webui/src/const/themes/nc_pink.ts @@ -0,0 +1,258 @@ +const theme: ThemeConfig = { + dark: { + '--heroui-background': '0 0% 0%', + '--heroui-foreground-50': '240 5.88% 10%', + '--heroui-foreground-100': '240 3.7% 15.88%', + '--heroui-foreground-200': '240 5.26% 26.08%', + '--heroui-foreground-300': '240 5.2% 33.92%', + '--heroui-foreground-400': '240 3.83% 46.08%', + '--heroui-foreground-500': '240 5.03% 64.9%', + '--heroui-foreground-600': '240 4.88% 83.92%', + '--heroui-foreground-700': '240 5.88% 90%', + '--heroui-foreground-800': '240 4.76% 95.88%', + '--heroui-foreground-900': '0 0% 98.04%', + '--heroui-foreground': '210 5.56% 92.94%', + '--heroui-focus': '212.01999999999998 100% 46.67%', + '--heroui-overlay': '0 0% 0%', + '--heroui-divider': '0 0% 100%', + '--heroui-divider-opacity': '0.15', + '--heroui-content1': '240 5.88% 10%', + '--heroui-content1-foreground': '0 0% 98.04%', + '--heroui-content2': '240 3.7% 15.88%', + '--heroui-content2-foreground': '240 4.76% 95.88%', + '--heroui-content3': '240 5.26% 26.08%', + '--heroui-content3-foreground': '240 5.88% 90%', + '--heroui-content4': '240 5.2% 33.92%', + '--heroui-content4-foreground': '240 4.88% 83.92%', + '--heroui-default-50': '240 5.88% 10%', + '--heroui-default-100': '240 3.7% 15.88%', + '--heroui-default-200': '240 5.26% 26.08%', + '--heroui-default-300': '240 5.2% 33.92%', + '--heroui-default-400': '240 3.83% 46.08%', + '--heroui-default-500': '240 5.03% 64.9%', + '--heroui-default-600': '240 4.88% 83.92%', + '--heroui-default-700': '240 5.88% 90%', + '--heroui-default-800': '240 4.76% 95.88%', + '--heroui-default-900': '0 0% 98.04%', + '--heroui-default-foreground': '0 0% 100%', + '--heroui-default': '240 5.26% 26.08%', + '--heroui-danger-50': '301.89 82.61% 22.55%', + '--heroui-danger-100': '308.18 76.39% 28.24%', + '--heroui-danger-200': '313.85 70.65% 36.08%', + '--heroui-danger-300': '319.73 65.64% 44.51%', + '--heroui-danger-400': '325.82 69.62% 53.53%', + '--heroui-danger-500': '331.82 75% 65.49%', + '--heroui-danger-600': '337.84 83.46% 73.92%', + '--heroui-danger-700': '343.42 90.48% 83.53%', + '--heroui-danger-800': '350.53 90.48% 91.76%', + '--heroui-danger-900': '324 90.91% 95.69%', + '--heroui-danger-foreground': '0 0% 100%', + '--heroui-danger': '325.82 69.62% 53.53%', + '--heroui-primary-50': '340 84.91% 10.39%', + '--heroui-primary-100': '339.33 86.54% 20.39%', + '--heroui-primary-200': '339.11 85.99% 30.78%', + '--heroui-primary-300': '339 86.54% 40.78%', + '--heroui-primary-400': '339.2 90.36% 51.18%', + '--heroui-primary-500': '339 90% 60.78%', + '--heroui-primary-600': '339.11 90.6% 70.78%', + '--heroui-primary-700': '339.33 90% 80.39%', + '--heroui-primary-800': '340 91.84% 90.39%', + '--heroui-primary-900': '339.13 92% 95.1%', + '--heroui-primary-foreground': '0 0% 100%', + '--heroui-primary': '339.2 90.36% 51.18%', + '--heroui-secondary-50': '270 66.67% 9.41%', + '--heroui-secondary-100': '270 66.67% 18.82%', + '--heroui-secondary-200': '270 66.67% 28.24%', + '--heroui-secondary-300': '270 66.67% 37.65%', + '--heroui-secondary-400': '270 66.67% 47.06%', + '--heroui-secondary-500': '270 59.26% 57.65%', + '--heroui-secondary-600': '270 59.26% 68.24%', + '--heroui-secondary-700': '270 59.26% 78.82%', + '--heroui-secondary-800': '270 59.26% 89.41%', + '--heroui-secondary-900': '270 61.54% 94.9%', + '--heroui-secondary-foreground': '0 0% 100%', + '--heroui-secondary': '270 59.26% 57.65%', + '--heroui-success-50': '145.71 77.78% 8.82%', + '--heroui-success-100': '146.2 79.78% 17.45%', + '--heroui-success-200': '145.79 79.26% 26.47%', + '--heroui-success-300': '146.01 79.89% 35.1%', + '--heroui-success-400': '145.96 79.46% 43.92%', + '--heroui-success-500': '146.01 62.45% 55.1%', + '--heroui-success-600': '145.79 62.57% 66.47%', + '--heroui-success-700': '146.2 61.74% 77.45%', + '--heroui-success-800': '145.71 61.4% 88.82%', + '--heroui-success-900': '146.67 64.29% 94.51%', + '--heroui-success-foreground': '0 0% 0%', + '--heroui-success': '145.96 79.46% 43.92%', + '--heroui-warning-50': '37.14 75% 10.98%', + '--heroui-warning-100': '37.14 75% 21.96%', + '--heroui-warning-200': '36.96 73.96% 33.14%', + '--heroui-warning-300': '37.01 74.22% 44.12%', + '--heroui-warning-400': '37.03 91.27% 55.1%', + '--heroui-warning-500': '37.01 91.26% 64.12%', + '--heroui-warning-600': '36.96 91.24% 73.14%', + '--heroui-warning-700': '37.14 91.3% 81.96%', + '--heroui-warning-800': '37.14 91.3% 90.98%', + '--heroui-warning-900': '54.55 91.67% 95.29%', + '--heroui-warning-foreground': '0 0% 0%', + '--heroui-warning': '37.03 91.27% 55.1%', + '--heroui-code-background': '240 5.56% 7.06%', + '--heroui-strong': '190.14 94.67% 44.12%', + '--heroui-code-mdx': '190.14 94.67% 44.12%', + '--heroui-divider-weight': '1px', + '--heroui-disabled-opacity': '.5', + '--heroui-font-size-tiny': '0.75rem', + '--heroui-font-size-small': '0.875rem', + '--heroui-font-size-medium': '1rem', + '--heroui-font-size-large': '1.125rem', + '--heroui-line-height-tiny': '1rem', + '--heroui-line-height-small': '1.25rem', + '--heroui-line-height-medium': '1.5rem', + '--heroui-line-height-large': '1.75rem', + '--heroui-radius-small': '8px', + '--heroui-radius-medium': '12px', + '--heroui-radius-large': '14px', + '--heroui-border-width-small': '1px', + '--heroui-border-width-medium': '2px', + '--heroui-border-width-large': '3px', + '--heroui-box-shadow-small': + '0px 0px 5px 0px rgba(0, 0, 0, .05), 0px 2px 10px 0px rgba(0, 0, 0, .2), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)', + '--heroui-box-shadow-medium': + '0px 0px 15px 0px rgba(0, 0, 0, .06), 0px 2px 30px 0px rgba(0, 0, 0, .22), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)', + '--heroui-box-shadow-large': + '0px 0px 30px 0px rgba(0, 0, 0, .07), 0px 30px 60px 0px rgba(0, 0, 0, .26), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)', + '--heroui-hover-opacity': '.9' + }, + light: { + '--heroui-background': '0 0% 100%', + '--heroui-foreground-50': '240 5.88% 95%', + '--heroui-foreground-100': '240 3.7% 90%', + '--heroui-foreground-200': '240 5.26% 80%', + '--heroui-foreground-300': '240 5.2% 70%', + '--heroui-foreground-400': '240 3.83% 60%', + '--heroui-foreground-500': '240 5.03% 50%', + '--heroui-foreground-600': '240 4.88% 40%', + '--heroui-foreground-700': '240 5.88% 30%', + '--heroui-foreground-800': '240 4.76% 20%', + '--heroui-foreground-900': '0 0% 10%', + '--heroui-foreground': '210 5.56% 7.06%', + '--heroui-focus': '212.01999999999998 100% 53.33%', + '--heroui-overlay': '0 0% 100%', + '--heroui-divider': '0 0% 0%', + '--heroui-divider-opacity': '0.85', + '--heroui-content1': '240 5.88% 95%', + '--heroui-content1-foreground': '0 0% 10%', + '--heroui-content2': '240 3.7% 90%', + '--heroui-content2-foreground': '240 4.76% 20%', + '--heroui-content3': '240 5.26% 80%', + '--heroui-content3-foreground': '240 5.88% 30%', + '--heroui-content4': '240 5.2% 70%', + '--heroui-content4-foreground': '240 4.88% 40%', + '--heroui-default-50': '240 5.88% 95%', + '--heroui-default-100': '240 3.7% 90%', + '--heroui-default-200': '240 5.26% 80%', + '--heroui-default-300': '240 5.2% 70%', + '--heroui-default-400': '240 3.83% 60%', + '--heroui-default-500': '240 5.03% 50%', + '--heroui-default-600': '240 4.88% 40%', + '--heroui-default-700': '240 5.88% 30%', + '--heroui-default-800': '240 4.76% 20%', + '--heroui-default-900': '0 0% 10%', + '--heroui-default-foreground': '0 0% 0%', + '--heroui-default': '240 5.26% 80%', + '--heroui-danger-50': '324 90.91% 95.69%', + '--heroui-danger-100': '350.53 90.48% 91.76%', + '--heroui-danger-200': '343.42 90.48% 83.53%', + '--heroui-danger-300': '337.84 83.46% 73.92%', + '--heroui-danger-400': '331.82 75% 65.49%', + '--heroui-danger-500': '325.82 69.62% 53.53%', + '--heroui-danger-600': '319.73 65.64% 44.51%', + '--heroui-danger-700': '313.85 70.65% 36.08%', + '--heroui-danger-800': '308.18 76.39% 28.24%', + '--heroui-danger-900': '301.89 82.61% 22.55%', + '--heroui-danger-foreground': '0 0% 100%', + '--heroui-danger': '325.82 69.62% 53.53%', + '--heroui-primary-50': '339.13 92% 95.1%', + '--heroui-primary-100': '340 91.84% 90.39%', + '--heroui-primary-200': '339.33 90% 80.39%', + '--heroui-primary-300': '339.11 90.6% 70.78%', + '--heroui-primary-400': '339 90% 60.78%', + '--heroui-primary-500': '339.2 90.36% 51.18%', + '--heroui-primary-600': '339 86.54% 40.78%', + '--heroui-primary-700': '339.11 85.99% 30.78%', + '--heroui-primary-800': '339.33 86.54% 20.39%', + '--heroui-primary-900': '340 84.91% 10.39%', + '--heroui-primary-foreground': '0 0% 100%', + '--heroui-primary': '339.2 90.36% 51.18%', + '--heroui-secondary-50': '270 61.54% 94.9%', + '--heroui-secondary-100': '270 59.26% 89.41%', + '--heroui-secondary-200': '270 59.26% 78.82%', + '--heroui-secondary-300': '270 59.26% 68.24%', + '--heroui-secondary-400': '270 59.26% 57.65%', + '--heroui-secondary-500': '270 66.67% 47.06%', + '--heroui-secondary-600': '270 66.67% 37.65%', + '--heroui-secondary-700': '270 66.67% 28.24%', + '--heroui-secondary-800': '270 66.67% 18.82%', + '--heroui-secondary-900': '270 66.67% 9.41%', + '--heroui-secondary-foreground': '0 0% 100%', + '--heroui-secondary': '270 66.67% 47.06%', + '--heroui-success-50': '146.67 64.29% 94.51%', + '--heroui-success-100': '145.71 61.4% 88.82%', + '--heroui-success-200': '146.2 61.74% 77.45%', + '--heroui-success-300': '145.79 62.57% 66.47%', + '--heroui-success-400': '146.01 62.45% 55.1%', + '--heroui-success-500': '145.96 79.46% 43.92%', + '--heroui-success-600': '146.01 79.89% 35.1%', + '--heroui-success-700': '145.79 79.26% 26.47%', + '--heroui-success-800': '146.2 79.78% 17.45%', + '--heroui-success-900': '145.71 77.78% 8.82%', + '--heroui-success-foreground': '0 0% 0%', + '--heroui-success': '145.96 79.46% 43.92%', + '--heroui-warning-50': '54.55 91.67% 95.29%', + '--heroui-warning-100': '37.14 91.3% 90.98%', + '--heroui-warning-200': '37.14 91.3% 81.96%', + '--heroui-warning-300': '36.96 91.24% 73.14%', + '--heroui-warning-400': '37.01 91.26% 64.12%', + '--heroui-warning-500': '37.03 91.27% 55.1%', + '--heroui-warning-600': '37.01 74.22% 44.12%', + '--heroui-warning-700': '36.96 73.96% 33.14%', + '--heroui-warning-800': '37.14 75% 21.96%', + '--heroui-warning-900': '37.14 75% 10.98%', + '--heroui-warning-foreground': '0 0% 0%', + '--heroui-warning': '37.03 91.27% 55.1%', + '--heroui-code-background': '221.25 17.39% 18.04%', + '--heroui-strong': '316.95 100% 65.29%', + '--heroui-code-mdx': '316.95 100% 65.29%', + '--heroui-divider-weight': '1px', + '--heroui-disabled-opacity': '.5', + '--heroui-font-size-tiny': '0.75rem', + '--heroui-font-size-small': '0.875rem', + '--heroui-font-size-medium': '1rem', + '--heroui-font-size-large': '1.125rem', + '--heroui-line-height-tiny': '1rem', + '--heroui-line-height-small': '1.25rem', + '--heroui-line-height-medium': '1.5rem', + '--heroui-line-height-large': '1.75rem', + '--heroui-radius-small': '8px', + '--heroui-radius-medium': '12px', + '--heroui-radius-large': '14px', + '--heroui-border-width-small': '1px', + '--heroui-border-width-medium': '2px', + '--heroui-border-width-large': '3px', + '--heroui-box-shadow-small': + '0px 0px 5px 0px rgba(0, 0, 0, .02), 0px 2px 10px 0px rgba(0, 0, 0, .06), 0px 0px 1px 0px rgba(0, 0, 0, .3)', + '--heroui-box-shadow-medium': + '0px 0px 15px 0px rgba(0, 0, 0, .03), 0px 2px 30px 0px rgba(0, 0, 0, .08), 0px 0px 1px 0px rgba(0, 0, 0, .3)', + '--heroui-box-shadow-large': + '0px 0px 30px 0px rgba(0, 0, 0, .04), 0px 30px 60px 0px rgba(0, 0, 0, .12), 0px 0px 1px 0px rgba(0, 0, 0, .3)', + '--heroui-hover-opacity': '.8' + } +} +export default { + theme, + author: 'NapCat', + name: 'nc_pink', + bgColor: 'hsl(339.2,90.36%,51.18%)', + textColor: 'hsl(0,0%,100%)', + description: 'NapCat Pink Theme' +} satisfies ThemeInfo diff --git a/napcat.webui/src/controllers/qq_manager.ts b/napcat.webui/src/controllers/qq_manager.ts index 9375afc2..040eefb4 100644 --- a/napcat.webui/src/controllers/qq_manager.ts +++ b/napcat.webui/src/controllers/qq_manager.ts @@ -73,4 +73,17 @@ export default class QQManager { ) return data.data.data } + + public static async getQuickLoginQQ() { + const { data } = await serverRequest.post>( + '/QQLogin/GetQuickLoginQQ' + ) + return data.data + } + + public static async setQuickLoginQQ(uin: string) { + await serverRequest.post>('/QQLogin/SetQuickLoginQQ', { + uin + }) + } } diff --git a/napcat.webui/src/controllers/webui_manager.ts b/napcat.webui/src/controllers/webui_manager.ts index 35cb4ed4..ac472126 100644 --- a/napcat.webui/src/controllers/webui_manager.ts +++ b/napcat.webui/src/controllers/webui_manager.ts @@ -59,6 +59,20 @@ export default class WebUIManager { return data.data } + public static async getThemeConfig() { + const { data } = + await serverRequest.get>('/base/Theme') + return data.data + } + + public static async setThemeConfig(theme: ThemeConfig) { + const { data } = await serverRequest.post>( + '/base/SetTheme', + { theme } + ) + return data.data + } + public static async getLogList() { const { data } = await serverRequest.get>('/Log/GetLogList') diff --git a/napcat.webui/src/main.tsx b/napcat.webui/src/main.tsx index 0a549a81..0b9c4f9c 100644 --- a/napcat.webui/src/main.tsx +++ b/napcat.webui/src/main.tsx @@ -8,6 +8,7 @@ import '@/styles/globals.css' import key from './const/key' import WebUIManager from './controllers/webui_manager' +import { loadTheme } from './utils/theme' WebUIManager.checkWebUiLogined() @@ -22,6 +23,8 @@ if (theme && !theme.startsWith('"')) { localStorage.setItem(key.theme, JSON.stringify(theme)) } +loadTheme() + ReactDOM.createRoot(document.getElementById('root')!).render( // diff --git a/napcat.webui/src/pages/dashboard/config/index.tsx b/napcat.webui/src/pages/dashboard/config/index.tsx index b90558d3..067f6c4d 100644 --- a/napcat.webui/src/pages/dashboard/config/index.tsx +++ b/napcat.webui/src/pages/dashboard/config/index.tsx @@ -1,21 +1,36 @@ import { Card, CardBody } from '@heroui/card' import { Tab, Tabs } from '@heroui/tabs' +import clsx from 'clsx' import { useMediaQuery } from 'react-responsive' import { useNavigate, useSearchParams } from 'react-router-dom' import ChangePasswordCard from './change_password' +import LoginConfigCard from './login' import OneBotConfigCard from './onebot' +import ThemeConfigCard from './theme' import WebUIConfigCard from './webui' export interface ConfigPageProps { children?: React.ReactNode + size?: 'sm' | 'md' | 'lg' } -const ConfingPageItem: React.FC = ({ children }) => { +const ConfingPageItem: React.FC = ({ + children, + size = 'md' +}) => { return ( -
{children}
+
+ {children} +
) @@ -57,12 +72,22 @@ export default function ConfigPage() { - + + + + + + + + + + + ) diff --git a/napcat.webui/src/pages/dashboard/config/login.tsx b/napcat.webui/src/pages/dashboard/config/login.tsx new file mode 100644 index 00000000..0dc8e4a0 --- /dev/null +++ b/napcat.webui/src/pages/dashboard/config/login.tsx @@ -0,0 +1,89 @@ +import { Input } from '@heroui/input' +import { useRequest } from 'ahooks' +import { useEffect } from 'react' +import { Controller, useForm } from 'react-hook-form' +import toast from 'react-hot-toast' + +import SaveButtons from '@/components/button/save_buttons' +import PageLoading from '@/components/page_loading' + +import QQManager from '@/controllers/qq_manager' + +const LoginConfigCard = () => { + const { + data: quickLoginData, + loading: quickLoginLoading, + error: quickLoginError, + refreshAsync: refreshQuickLogin + } = useRequest(QQManager.getQuickLoginQQ) + const { + control, + handleSubmit: handleOnebotSubmit, + formState: { isSubmitting }, + setValue: setOnebotValue + } = useForm<{ + quickLoginQQ: string + }>({ + defaultValues: { + quickLoginQQ: '' + } + }) + + const reset = () => { + setOnebotValue('quickLoginQQ', quickLoginData ?? '') + } + + const onSubmit = handleOnebotSubmit(async (data) => { + try { + await QQManager.setQuickLoginQQ(data.quickLoginQQ) + toast.success('保存成功') + } catch (error) { + const msg = (error as Error).message + toast.error(`保存失败: ${msg}`) + } + }) + + const onRefresh = async () => { + try { + await refreshQuickLogin() + toast.success('刷新成功') + } catch (error) { + const msg = (error as Error).message + toast.error(`刷新失败: ${msg}`) + } + } + + useEffect(() => { + reset() + }, [quickLoginData]) + + if (quickLoginLoading) return + + return ( + <> + OneBot配置 - NapCat WebUI +
快速登录QQ
+ ( + + )} + /> + + + ) +} + +export default LoginConfigCard diff --git a/napcat.webui/src/pages/dashboard/config/onebot.tsx b/napcat.webui/src/pages/dashboard/config/onebot.tsx index 1155db0e..8311b617 100644 --- a/napcat.webui/src/pages/dashboard/config/onebot.tsx +++ b/napcat.webui/src/pages/dashboard/config/onebot.tsx @@ -30,9 +30,9 @@ const OneBotConfigCard = () => { setOnebotValue('parseMultMsg', config.parseMultMsg) } - const onSubmit = handleOnebotSubmit((data) => { + const onSubmit = handleOnebotSubmit(async (data) => { try { - saveConfigWithoutNetwork(data) + await saveConfigWithoutNetwork(data) toast.success('保存成功') } catch (error) { const msg = (error as Error).message diff --git a/napcat.webui/src/pages/dashboard/config/theme.tsx b/napcat.webui/src/pages/dashboard/config/theme.tsx new file mode 100644 index 00000000..099c78bf --- /dev/null +++ b/napcat.webui/src/pages/dashboard/config/theme.tsx @@ -0,0 +1,267 @@ +import { Accordion, AccordionItem } from '@heroui/accordion' +import { useRequest } from 'ahooks' +import { useEffect } from 'react' +import { Controller, useForm } from 'react-hook-form' +import toast from 'react-hot-toast' + +import themes from '@/const/themes' + +import ColorPicker from '@/components/ColorPicker' +import SaveButtons from '@/components/button/save_buttons' +import PageLoading from '@/components/page_loading' + +import { loadTheme } from '@/utils/theme' + +import WebUIManager from '@/controllers/webui_manager' + +// 将颜色 key 补全为 ThemeConfigItem 中定义的所有颜色相关属性 +const colorKeys = [ + '--heroui-background', + + '--heroui-foreground-50', + '--heroui-foreground-100', + '--heroui-foreground-200', + '--heroui-foreground-300', + '--heroui-foreground-400', + '--heroui-foreground-500', + '--heroui-foreground-600', + '--heroui-foreground-700', + '--heroui-foreground-800', + '--heroui-foreground-900', + '--heroui-foreground', + + '--heroui-content1', + '--heroui-content1-foreground', + '--heroui-content2', + '--heroui-content2-foreground', + '--heroui-content3', + '--heroui-content3-foreground', + '--heroui-content4', + '--heroui-content4-foreground', + + '--heroui-default-50', + '--heroui-default-100', + '--heroui-default-200', + '--heroui-default-300', + '--heroui-default-400', + '--heroui-default-500', + '--heroui-default-600', + '--heroui-default-700', + '--heroui-default-800', + '--heroui-default-900', + '--heroui-default-foreground', + '--heroui-default', + + '--heroui-danger-50', + '--heroui-danger-100', + '--heroui-danger-200', + '--heroui-danger-300', + '--heroui-danger-400', + '--heroui-danger-500', + '--heroui-danger-600', + '--heroui-danger-700', + '--heroui-danger-800', + '--heroui-danger-900', + '--heroui-danger-foreground', + '--heroui-danger', + + '--heroui-primary-50', + '--heroui-primary-100', + '--heroui-primary-200', + '--heroui-primary-300', + '--heroui-primary-400', + '--heroui-primary-500', + '--heroui-primary-600', + '--heroui-primary-700', + '--heroui-primary-800', + '--heroui-primary-900', + '--heroui-primary-foreground', + '--heroui-primary', + + '--heroui-secondary-50', + '--heroui-secondary-100', + '--heroui-secondary-200', + '--heroui-secondary-300', + '--heroui-secondary-400', + '--heroui-secondary-500', + '--heroui-secondary-600', + '--heroui-secondary-700', + '--heroui-secondary-800', + '--heroui-secondary-900', + '--heroui-secondary-foreground', + '--heroui-secondary', + + '--heroui-success-50', + '--heroui-success-100', + '--heroui-success-200', + '--heroui-success-300', + '--heroui-success-400', + '--heroui-success-500', + '--heroui-success-600', + '--heroui-success-700', + '--heroui-success-800', + '--heroui-success-900', + '--heroui-success-foreground', + '--heroui-success', + + '--heroui-warning-50', + '--heroui-warning-100', + '--heroui-warning-200', + '--heroui-warning-300', + '--heroui-warning-400', + '--heroui-warning-500', + '--heroui-warning-600', + '--heroui-warning-700', + '--heroui-warning-800', + '--heroui-warning-900', + '--heroui-warning-foreground', + '--heroui-warning', + + '--heroui-focus', + '--heroui-overlay', + '--heroui-divider', + '--heroui-code-background', + '--heroui-strong', + '--heroui-code-mdx' +] as const + +const ThemeConfigCard = () => { + const { data, loading, error, refreshAsync } = useRequest( + WebUIManager.getThemeConfig + ) + const { + control, + handleSubmit: handleOnebotSubmit, + formState: { isSubmitting }, + setValue: setOnebotValue + } = useForm<{ + theme: ThemeConfig + }>({ + defaultValues: { + theme: { + dark: {}, + light: {} + } + } + }) + + const reset = () => { + if (data) setOnebotValue('theme', data) + } + + const onSubmit = handleOnebotSubmit(async (data) => { + try { + await WebUIManager.setThemeConfig(data.theme) + toast.success('保存成功') + loadTheme() + } catch (error) { + const msg = (error as Error).message + toast.error(`保存失败: ${msg}`) + } + }) + + const onRefresh = async () => { + try { + await refreshAsync() + toast.success('刷新成功') + } catch (error) { + const msg = (error as Error).message + toast.error(`刷新失败: ${msg}`) + } + } + + useEffect(() => { + reset() + }, [data]) + + if (loading) return + + if (error) + return ( +
{error.message}
+ ) + + return ( + <> + 主题配置 - NapCat WebUI + + +
+ {themes.map((theme) => ( +
{ + setOnebotValue('theme', theme.theme) + onSubmit() + }} + > +
{theme.name}
+
{theme.author}
+
+ ))} +
+
+ + + {(['dark', 'light'] as const).map((mode) => ( +
+

{mode === 'dark' ? '暗色主题' : '亮色主题'}

+ {colorKeys.map((key) => ( +
+ + { + const hslArray = value?.split(' ') ?? [0, 0, 0] + const color = `hsl(${hslArray[0]}, ${hslArray[1]}, ${hslArray[2]})` + return ( + { + onChange( + `${result.hsl.h} ${result.hsl.s * 100}% ${result.hsl.l * 100}%` + ) + }} + /> + ) + }} + /> +
+ ))} +
+ ))} + + +
+
+ + ) +} + +export default ThemeConfigCard diff --git a/napcat.webui/src/types/server.d.ts b/napcat.webui/src/types/server.d.ts index 28096eba..59005c22 100644 --- a/napcat.webui/src/types/server.d.ts +++ b/napcat.webui/src/types/server.d.ts @@ -48,3 +48,136 @@ interface SystemStatus { } arch: string } + +interface ThemeConfigItem { + '--heroui-background': string + '--heroui-foreground-50': string + '--heroui-foreground-100': string + '--heroui-foreground-200': string + '--heroui-foreground-300': string + '--heroui-foreground-400': string + '--heroui-foreground-500': string + '--heroui-foreground-600': string + '--heroui-foreground-700': string + '--heroui-foreground-800': string + '--heroui-foreground-900': string + '--heroui-foreground': string + '--heroui-focus': string + '--heroui-overlay': string + '--heroui-divider': string + '--heroui-divider-opacity': string + '--heroui-content1': string + '--heroui-content1-foreground': string + '--heroui-content2': string + '--heroui-content2-foreground': string + '--heroui-content3': string + '--heroui-content3-foreground': string + '--heroui-content4': string + '--heroui-content4-foreground': string + '--heroui-default-50': string + '--heroui-default-100': string + '--heroui-default-200': string + '--heroui-default-300': string + '--heroui-default-400': string + '--heroui-default-500': string + '--heroui-default-600': string + '--heroui-default-700': string + '--heroui-default-800': string + '--heroui-default-900': string + '--heroui-default-foreground': string + '--heroui-default': string + // 新增 danger + '--heroui-danger-50': string + '--heroui-danger-100': string + '--heroui-danger-200': string + '--heroui-danger-300': string + '--heroui-danger-400': string + '--heroui-danger-500': string + '--heroui-danger-600': string + '--heroui-danger-700': string + '--heroui-danger-800': string + '--heroui-danger-900': string + '--heroui-danger-foreground': string + '--heroui-danger': string + // 新增 primary + '--heroui-primary-50': string + '--heroui-primary-100': string + '--heroui-primary-200': string + '--heroui-primary-300': string + '--heroui-primary-400': string + '--heroui-primary-500': string + '--heroui-primary-600': string + '--heroui-primary-700': string + '--heroui-primary-800': string + '--heroui-primary-900': string + '--heroui-primary-foreground': string + '--heroui-primary': string + // 新增 secondary + '--heroui-secondary-50': string + '--heroui-secondary-100': string + '--heroui-secondary-200': string + '--heroui-secondary-300': string + '--heroui-secondary-400': string + '--heroui-secondary-500': string + '--heroui-secondary-600': string + '--heroui-secondary-700': string + '--heroui-secondary-800': string + '--heroui-secondary-900': string + '--heroui-secondary-foreground': string + '--heroui-secondary': string + // 新增 success + '--heroui-success-50': string + '--heroui-success-100': string + '--heroui-success-200': string + '--heroui-success-300': string + '--heroui-success-400': string + '--heroui-success-500': string + '--heroui-success-600': string + '--heroui-success-700': string + '--heroui-success-800': string + '--heroui-success-900': string + '--heroui-success-foreground': string + '--heroui-success': string + // 新增 warning + '--heroui-warning-50': string + '--heroui-warning-100': string + '--heroui-warning-200': string + '--heroui-warning-300': string + '--heroui-warning-400': string + '--heroui-warning-500': string + '--heroui-warning-600': string + '--heroui-warning-700': string + '--heroui-warning-800': string + '--heroui-warning-900': string + '--heroui-warning-foreground': string + '--heroui-warning': string + // 其它配置 + '--heroui-code-background': string + '--heroui-strong': string + '--heroui-code-mdx': string + '--heroui-divider-weight': string + '--heroui-disabled-opacity': string + '--heroui-font-size-tiny': string + '--heroui-font-size-small': string + '--heroui-font-size-medium': string + '--heroui-font-size-large': string + '--heroui-line-height-tiny': string + '--heroui-line-height-small': string + '--heroui-line-height-medium': string + '--heroui-line-height-large': string + '--heroui-radius-small': string + '--heroui-radius-medium': string + '--heroui-radius-large': string + '--heroui-border-width-small': string + '--heroui-border-width-medium': string + '--heroui-border-width-large': string + '--heroui-box-shadow-small': string + '--heroui-box-shadow-medium': string + '--heroui-box-shadow-large': string + '--heroui-hover-opacity': string +} + +interface ThemeConfig { + dark: ThemeConfigItem + light: ThemeConfigItem +} diff --git a/napcat.webui/src/types/theme.d.ts b/napcat.webui/src/types/theme.d.ts new file mode 100644 index 00000000..75a98cb4 --- /dev/null +++ b/napcat.webui/src/types/theme.d.ts @@ -0,0 +1,8 @@ +interface ThemeInfo { + theme: ThemeConfig + name: string + bgColor: string + textColor: string + description?: string + author?: string +} diff --git a/napcat.webui/src/utils/theme.ts b/napcat.webui/src/utils/theme.ts new file mode 100644 index 00000000..dafe4516 --- /dev/null +++ b/napcat.webui/src/utils/theme.ts @@ -0,0 +1,15 @@ +import { request } from './request' + +const style = document.createElement('style') +document.head.appendChild(style) + +export function loadTheme() { + request('/files/theme.css?_t=' + Date.now()) + .then((res) => res.data) + .then((css) => { + style.innerHTML = css + }) + .catch(() => { + console.error('Failed to load theme.css') + }) +} diff --git a/napcat.webui/vite.config.ts b/napcat.webui/vite.config.ts index 043e0bfa..d9cf306c 100644 --- a/napcat.webui/vite.config.ts +++ b/napcat.webui/vite.config.ts @@ -34,7 +34,8 @@ export default defineConfig(({ mode }) => { ws: true, changeOrigin: true }, - '/api': backendDebugUrl + '/api': backendDebugUrl, + '/files': backendDebugUrl } }, build: { diff --git a/package.json b/package.json index 8473f8b7..09f6b7cb 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@types/multer": "^1.4.12", "@types/node": "^22.0.1", "@types/qrcode-terminal": "^0.12.2", + "@types/react-color": "^3.0.13", "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", diff --git a/src/webui/index.ts b/src/webui/index.ts index 5bde50e1..a4eae968 100644 --- a/src/webui/index.ts +++ b/src/webui/index.ts @@ -73,14 +73,32 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp // 如果是webui字体文件,挂载字体文件 app.use('/webui/fonts/AaCute.woff', async (_req, res, next) => { - const isFontExist = await WebUiConfigWrapper.CheckWebUIFontExist(); + const isFontExist = await WebUiConfig.CheckWebUIFontExist(); if (isFontExist) { - res.sendFile(WebUiConfigWrapper.GetWebUIFontPath()); + res.sendFile(WebUiConfig.GetWebUIFontPath()); } else { next(); } }); + // 如果是自定义色彩,构建一个css文件 + app.use('/files/theme.css', async (_req, res) => { + const colors = await WebUiConfig.GetTheme(); + + let css = ':root, .light, [data-theme="light"] {'; + for (const key in colors.light) { + css += `${key}: ${colors.light[key]};`; + } + css += '}'; + css += '.dark, [data-theme="dark"] {'; + for (const key in colors.dark) { + css += `${key}: ${colors.dark[key]};`; + } + css += '}'; + + res.send(css); + }); + // ------------中间件结束------------ // ------------挂载路由------------ diff --git a/src/webui/src/api/BaseInfo.ts b/src/webui/src/api/BaseInfo.ts index 12a5fd1f..a7d3c514 100644 --- a/src/webui/src/api/BaseInfo.ts +++ b/src/webui/src/api/BaseInfo.ts @@ -2,14 +2,25 @@ import { RequestHandler } from 'express'; import { WebUiDataRuntime } from '@webapi/helper/Data'; import { sendSuccess } from '@webapi/utils/response'; +import { WebUiConfig } from '@/webui'; export const PackageInfoHandler: RequestHandler = (_, res) => { const data = WebUiDataRuntime.getPackageJson(); sendSuccess(res, data); }; - export const QQVersionHandler: RequestHandler = (_, res) => { const data = WebUiDataRuntime.getQQVersion(); sendSuccess(res, data); }; + +export const GetThemeConfigHandler: RequestHandler = async (_, res) => { + const data = await WebUiConfig.GetTheme(); + sendSuccess(res, data); +}; + +export const SetThemeConfigHandler: RequestHandler = async (req, res) => { + const { theme } = req.body; + await WebUiConfig.UpdateTheme(theme); + sendSuccess(res, { message: '更新成功' }); +}; diff --git a/src/webui/src/api/File.ts b/src/webui/src/api/File.ts index 1bf5cf8c..db5e27b4 100644 --- a/src/webui/src/api/File.ts +++ b/src/webui/src/api/File.ts @@ -7,9 +7,9 @@ import os from 'os'; import compressing from 'compressing'; import { PassThrough } from 'stream'; import multer from 'multer'; -import { WebUiConfigWrapper } from '../helper/config'; import webUIFontUploader from '../uploader/webui_font'; import diskUploader from '../uploader/disk'; +import { WebUiConfig } from '@/webui'; const isWindows = os.platform() === 'win32'; @@ -384,8 +384,8 @@ export const UploadWebUIFontHandler: RequestHandler = async (req, res) => { // 删除WebUI字体文件处理方法 export const DeleteWebUIFontHandler: RequestHandler = async (_req, res) => { try { - const fontPath = WebUiConfigWrapper.GetWebUIFontPath(); - const exists = await WebUiConfigWrapper.CheckWebUIFontExist(); + const fontPath = WebUiConfig.GetWebUIFontPath(); + const exists = await WebUiConfig.CheckWebUIFontExist(); if (!exists) { return sendSuccess(res, true); diff --git a/src/webui/src/api/Log.ts b/src/webui/src/api/Log.ts index a531d275..4e66ee3e 100644 --- a/src/webui/src/api/Log.ts +++ b/src/webui/src/api/Log.ts @@ -1,8 +1,8 @@ import type { RequestHandler } from 'express'; import { sendError, sendSuccess } from '../utils/response'; -import { WebUiConfigWrapper } from '../helper/config'; import { logSubscription } from '@/common/log'; import { terminalManager } from '../terminal/terminal_manager'; +import { WebUiConfig } from '@/webui'; // 判断是否是 macos const isMacOS = process.platform === 'darwin'; // 日志记录 @@ -15,13 +15,13 @@ export const LogHandler: RequestHandler = async (req, res) => { if (filename.includes('..')) { return sendError(res, 'ID不合法'); } - const logContent = await WebUiConfigWrapper.GetLogContent(filename); + const logContent = await WebUiConfig.GetLogContent(filename); return sendSuccess(res, logContent); }; // 日志列表 export const LogListHandler: RequestHandler = async (_, res) => { - const logList = await WebUiConfigWrapper.GetLogsList(); + const logList = await WebUiConfig.GetLogsList(); return sendSuccess(res, logList); }; diff --git a/src/webui/src/api/QQLogin.ts b/src/webui/src/api/QQLogin.ts index 4f324d22..7da2000e 100644 --- a/src/webui/src/api/QQLogin.ts +++ b/src/webui/src/api/QQLogin.ts @@ -3,9 +3,10 @@ import { RequestHandler } from 'express'; import { WebUiDataRuntime } from '@webapi/helper/Data'; import { isEmpty } from '@webapi/utils/check'; import { sendError, sendSuccess } from '@webapi/utils/response'; +import { WebUiConfig } from '@/webui'; // 获取QQ登录二维码 -export const QQGetQRcodeHandler: RequestHandler = async (req, res) => { +export const QQGetQRcodeHandler: RequestHandler = async (_, res) => { // 判断是否已经登录 if (WebUiDataRuntime.getQQLoginStatus()) { // 已经登录 @@ -25,7 +26,7 @@ export const QQGetQRcodeHandler: RequestHandler = async (req, res) => { }; // 获取QQ登录状态 -export const QQCheckLoginStatusHandler: RequestHandler = async (req, res) => { +export const QQCheckLoginStatusHandler: RequestHandler = async (_, res) => { const data = { isLogin: WebUiDataRuntime.getQQLoginStatus(), qrcodeurl: WebUiDataRuntime.getQQLoginQrcodeURL(), @@ -74,3 +75,16 @@ export const getQQLoginInfoHandler: RequestHandler = async (_, res) => { const data = WebUiDataRuntime.getQQLoginInfo(); return sendSuccess(res, data); }; + +// 获取自动登录QQ账号 +export const getAutoLoginAccountHandler: RequestHandler = async (_, res) => { + const data = WebUiConfig.getAutoLoginAccount(); + return sendSuccess(res, data); +}; + +// 设置自动登录QQ账号 +export const setAutoLoginAccountHandler: RequestHandler = async (req, res) => { + const { uin } = req.body; + await WebUiConfig.UpdateAutoLoginAccount(uin); + return sendSuccess(res, null); +}; diff --git a/src/webui/src/helper/config.ts b/src/webui/src/helper/config.ts index b9ef685e..a21b3639 100644 --- a/src/webui/src/helper/config.ts +++ b/src/webui/src/helper/config.ts @@ -5,6 +5,9 @@ import fs, { constants } from 'node:fs/promises'; import { resolve } from 'node:path'; +import { deepMerge } from '../utils/object'; +import { themeType } from '../types/theme'; + // 限制尝试端口的次数,避免死循环 // 定义配置的类型 @@ -14,11 +17,11 @@ const WebUiConfigSchema = Type.Object({ token: Type.String({ default: 'napcat' }), loginRate: Type.Number({ default: 10 }), autoLoginAccount: Type.String({ default: '' }), + theme: themeType, }); export type WebUiConfigType = Static; - // 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件 export class WebUiConfigWrapper { WebUiConfigData: WebUiConfigType | undefined = undefined; @@ -29,7 +32,10 @@ export class WebUiConfigWrapper { } private async ensureConfigFileExists(configPath: string): Promise { - const configExists = await fs.access(configPath, constants.F_OK).then(() => true).catch(() => false); + const configExists = await fs + .access(configPath, constants.F_OK) + .then(() => true) + .catch(() => false); if (!configExists) { await fs.writeFile(configPath, JSON.stringify(this.validateAndApplyDefaults({}), null, 4)); } @@ -41,7 +47,10 @@ export class WebUiConfigWrapper { } private async writeConfig(configPath: string, config: WebUiConfigType): Promise { - const hasWritePermission = await fs.access(configPath, constants.W_OK).then(() => true).catch(() => false); + const hasWritePermission = await fs + .access(configPath, constants.W_OK) + .then(() => true) + .catch(() => false); if (hasWritePermission) { await fs.writeFile(configPath, JSON.stringify(config, null, 4)); } else { @@ -68,7 +77,8 @@ export class WebUiConfigWrapper { async UpdateWebUIConfig(newConfig: Partial): Promise { const configPath = resolve(webUiPathWrapper.configPath, './webui.json'); const currentConfig = await this.GetWebUIConfig(); - const updatedConfig = this.validateAndApplyDefaults({ ...currentConfig, ...newConfig }); + const mergedConfig = deepMerge({ ...currentConfig }, newConfig); + const updatedConfig = this.validateAndApplyDefaults(mergedConfig); await this.writeConfig(configPath, updatedConfig); this.WebUiConfigData = updatedConfig; } @@ -82,24 +92,32 @@ export class WebUiConfigWrapper { } // 获取日志文件夹路径 - public static async GetLogsPath(): Promise { + async GetLogsPath(): Promise { return resolve(webUiPathWrapper.logsPath); } // 获取日志列表 - public static async GetLogsList(): Promise { + async GetLogsList(): Promise { const logsPath = resolve(webUiPathWrapper.logsPath); - const logsExist = await fs.access(logsPath, constants.F_OK).then(() => true).catch(() => false); + const logsExist = await fs + .access(logsPath, constants.F_OK) + .then(() => true) + .catch(() => false); if (logsExist) { - return (await fs.readdir(logsPath)).filter(file => file.endsWith('.log')).map(file => file.replace('.log', '')); + return (await fs.readdir(logsPath)) + .filter((file) => file.endsWith('.log')) + .map((file) => file.replace('.log', '')); } return []; } // 获取指定日志文件内容 - public static async GetLogContent(filename: string): Promise { + async GetLogContent(filename: string): Promise { const logPath = resolve(webUiPathWrapper.logsPath, `${filename}.log`); - const logExists = await fs.access(logPath, constants.R_OK).then(() => true).catch(() => false); + const logExists = await fs + .access(logPath, constants.R_OK) + .then(() => true) + .catch(() => false); if (logExists) { return await fs.readFile(logPath, 'utf-8'); } @@ -107,27 +125,55 @@ export class WebUiConfigWrapper { } // 获取字体文件夹内的字体列表 - public static async GetFontList(): Promise { + async GetFontList(): Promise { const fontsPath = resolve(webUiPathWrapper.configPath, './fonts'); - const fontsExist = await fs.access(fontsPath, constants.F_OK).then(() => true).catch(() => false); + const fontsExist = await fs + .access(fontsPath, constants.F_OK) + .then(() => true) + .catch(() => false); if (fontsExist) { - return (await fs.readdir(fontsPath)).filter(file => file.endsWith('.ttf')); + return (await fs.readdir(fontsPath)).filter((file) => file.endsWith('.ttf')); } return []; } // 判断字体是否存在(webui.woff) - public static async CheckWebUIFontExist(): Promise { + async CheckWebUIFontExist(): Promise { const fontsPath = resolve(webUiPathWrapper.configPath, './fonts'); - return await fs.access(resolve(fontsPath, './webui.woff'), constants.F_OK).then(() => true).catch(() => false); + return await fs + .access(resolve(fontsPath, './webui.woff'), constants.F_OK) + .then(() => true) + .catch(() => false); } // 获取webui字体文件路径 - public static GetWebUIFontPath(): string { + GetWebUIFontPath(): string { return resolve(webUiPathWrapper.configPath, './fonts/webui.woff'); } - public getAutoLoginAccount(): string | undefined { + getAutoLoginAccount(): string | undefined { return this.WebUiConfigData?.autoLoginAccount; } -} \ No newline at end of file + + // 获取自动登录账号 + async GetAutoLoginAccount(): Promise { + return (await this.GetWebUIConfig()).autoLoginAccount; + } + + // 更新自动登录账号 + async UpdateAutoLoginAccount(uin: string): Promise { + await this.UpdateWebUIConfig({ autoLoginAccount: uin }); + } + + // 获取主题内容 + async GetTheme(): Promise { + const config = await this.GetWebUIConfig(); + + return config.theme; + } + + // 更新主题内容 + async UpdateTheme(theme: WebUiConfigType['theme']): Promise { + await this.UpdateWebUIConfig({ theme: theme }); + } +} diff --git a/src/webui/src/router/Base.ts b/src/webui/src/router/Base.ts index 0ec78532..f79975cf 100644 --- a/src/webui/src/router/Base.ts +++ b/src/webui/src/router/Base.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { PackageInfoHandler, QQVersionHandler } from '../api/BaseInfo'; +import { GetThemeConfigHandler, PackageInfoHandler, QQVersionHandler, SetThemeConfigHandler } from '../api/BaseInfo'; import { StatusRealTimeHandler } from '@webapi/api/Status'; import { GetProxyHandler } from '../api/Proxy'; @@ -9,4 +9,7 @@ router.get('/QQVersion', QQVersionHandler); router.get('/PackageInfo', PackageInfoHandler); router.get('/GetSysStatusRealTime', StatusRealTimeHandler); router.get('/proxy', GetProxyHandler); +router.get('/Theme', GetThemeConfigHandler); +router.post('/SetTheme', SetThemeConfigHandler); + export { router as BaseRouter }; diff --git a/src/webui/src/router/QQLogin.ts b/src/webui/src/router/QQLogin.ts index 5fd4ef18..aecb5b44 100644 --- a/src/webui/src/router/QQLogin.ts +++ b/src/webui/src/router/QQLogin.ts @@ -7,6 +7,8 @@ import { QQSetQuickLoginHandler, QQGetLoginListNewHandler, getQQLoginInfoHandler, + getAutoLoginAccountHandler, + setAutoLoginAccountHandler, } from '@webapi/api/QQLogin'; const router = Router(); @@ -22,5 +24,9 @@ router.post('/GetQQLoginQrcode', QQGetQRcodeHandler); router.post('/SetQuickLogin', QQSetQuickLoginHandler); // router:获取QQ登录信息 router.post('/GetQQLoginInfo', getQQLoginInfoHandler); +// router:获取快速登录QQ账号 +router.post('/GetQuickLoginQQ', getAutoLoginAccountHandler); +// router:设置自动登录QQ账号 +router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler); export { router as QQLoginRouter }; diff --git a/src/webui/src/types/theme.ts b/src/webui/src/types/theme.ts new file mode 100644 index 00000000..593eb96c --- /dev/null +++ b/src/webui/src/types/theme.ts @@ -0,0 +1,260 @@ +import { Type } from '@sinclair/typebox'; + +export const themeType = Type.Object( + { + dark: Type.Record(Type.String(), Type.String()), + light: Type.Record(Type.String(), Type.String()), + }, + { + default: { + dark: { + '--heroui-background': '0 0% 0%', + '--heroui-foreground-50': '240 5.88% 10%', + '--heroui-foreground-100': '240 3.7% 15.88%', + '--heroui-foreground-200': '240 5.26% 26.08%', + '--heroui-foreground-300': '240 5.2% 33.92%', + '--heroui-foreground-400': '240 3.83% 46.08%', + '--heroui-foreground-500': '240 5.03% 64.9%', + '--heroui-foreground-600': '240 4.88% 83.92%', + '--heroui-foreground-700': '240 5.88% 90%', + '--heroui-foreground-800': '240 4.76% 95.88%', + '--heroui-foreground-900': '0 0% 98.04%', + '--heroui-foreground': '210 5.56% 92.94%', + '--heroui-focus': '212.01999999999998 100% 46.67%', + '--heroui-overlay': '0 0% 0%', + '--heroui-divider': '0 0% 100%', + '--heroui-divider-opacity': '0.15', + '--heroui-content1': '240 5.88% 10%', + '--heroui-content1-foreground': '0 0% 98.04%', + '--heroui-content2': '240 3.7% 15.88%', + '--heroui-content2-foreground': '240 4.76% 95.88%', + '--heroui-content3': '240 5.26% 26.08%', + '--heroui-content3-foreground': '240 5.88% 90%', + '--heroui-content4': '240 5.2% 33.92%', + '--heroui-content4-foreground': '240 4.88% 83.92%', + '--heroui-default-50': '240 5.88% 10%', + '--heroui-default-100': '240 3.7% 15.88%', + '--heroui-default-200': '240 5.26% 26.08%', + '--heroui-default-300': '240 5.2% 33.92%', + '--heroui-default-400': '240 3.83% 46.08%', + '--heroui-default-500': '240 5.03% 64.9%', + '--heroui-default-600': '240 4.88% 83.92%', + '--heroui-default-700': '240 5.88% 90%', + '--heroui-default-800': '240 4.76% 95.88%', + '--heroui-default-900': '0 0% 98.04%', + '--heroui-default-foreground': '0 0% 100%', + '--heroui-default': '240 5.26% 26.08%', + '--heroui-danger-50': '301.89 82.61% 22.55%', + '--heroui-danger-100': '308.18 76.39% 28.24%', + '--heroui-danger-200': '313.85 70.65% 36.08%', + '--heroui-danger-300': '319.73 65.64% 44.51%', + '--heroui-danger-400': '325.82 69.62% 53.53%', + '--heroui-danger-500': '331.82 75% 65.49%', + '--heroui-danger-600': '337.84 83.46% 73.92%', + '--heroui-danger-700': '343.42 90.48% 83.53%', + '--heroui-danger-800': '350.53 90.48% 91.76%', + '--heroui-danger-900': '324 90.91% 95.69%', + '--heroui-danger-foreground': '0 0% 100%', + '--heroui-danger': '325.82 69.62% 53.53%', + '--heroui-primary-50': '340 84.91% 10.39%', + '--heroui-primary-100': '339.33 86.54% 20.39%', + '--heroui-primary-200': '339.11 85.99% 30.78%', + '--heroui-primary-300': '339 86.54% 40.78%', + '--heroui-primary-400': '339.2 90.36% 51.18%', + '--heroui-primary-500': '339 90% 60.78%', + '--heroui-primary-600': '339.11 90.6% 70.78%', + '--heroui-primary-700': '339.33 90% 80.39%', + '--heroui-primary-800': '340 91.84% 90.39%', + '--heroui-primary-900': '339.13 92% 95.1%', + '--heroui-primary-foreground': '0 0% 100%', + '--heroui-primary': '339.2 90.36% 51.18%', + '--heroui-secondary-50': '270 66.67% 9.41%', + '--heroui-secondary-100': '270 66.67% 18.82%', + '--heroui-secondary-200': '270 66.67% 28.24%', + '--heroui-secondary-300': '270 66.67% 37.65%', + '--heroui-secondary-400': '270 66.67% 47.06%', + '--heroui-secondary-500': '270 59.26% 57.65%', + '--heroui-secondary-600': '270 59.26% 68.24%', + '--heroui-secondary-700': '270 59.26% 78.82%', + '--heroui-secondary-800': '270 59.26% 89.41%', + '--heroui-secondary-900': '270 61.54% 94.9%', + '--heroui-secondary-foreground': '0 0% 100%', + '--heroui-secondary': '270 59.26% 57.65%', + '--heroui-success-50': '145.71 77.78% 8.82%', + '--heroui-success-100': '146.2 79.78% 17.45%', + '--heroui-success-200': '145.79 79.26% 26.47%', + '--heroui-success-300': '146.01 79.89% 35.1%', + '--heroui-success-400': '145.96 79.46% 43.92%', + '--heroui-success-500': '146.01 62.45% 55.1%', + '--heroui-success-600': '145.79 62.57% 66.47%', + '--heroui-success-700': '146.2 61.74% 77.45%', + '--heroui-success-800': '145.71 61.4% 88.82%', + '--heroui-success-900': '146.67 64.29% 94.51%', + '--heroui-success-foreground': '0 0% 0%', + '--heroui-success': '145.96 79.46% 43.92%', + '--heroui-warning-50': '37.14 75% 10.98%', + '--heroui-warning-100': '37.14 75% 21.96%', + '--heroui-warning-200': '36.96 73.96% 33.14%', + '--heroui-warning-300': '37.01 74.22% 44.12%', + '--heroui-warning-400': '37.03 91.27% 55.1%', + '--heroui-warning-500': '37.01 91.26% 64.12%', + '--heroui-warning-600': '36.96 91.24% 73.14%', + '--heroui-warning-700': '37.14 91.3% 81.96%', + '--heroui-warning-800': '37.14 91.3% 90.98%', + '--heroui-warning-900': '54.55 91.67% 95.29%', + '--heroui-warning-foreground': '0 0% 0%', + '--heroui-warning': '37.03 91.27% 55.1%', + '--heroui-code-background': '240 5.56% 7.06%', + '--heroui-strong': '190.14 94.67% 44.12%', + '--heroui-code-mdx': '190.14 94.67% 44.12%', + '--heroui-divider-weight': '1px', + '--heroui-disabled-opacity': '.5', + '--heroui-font-size-tiny': '0.75rem', + '--heroui-font-size-small': '0.875rem', + '--heroui-font-size-medium': '1rem', + '--heroui-font-size-large': '1.125rem', + '--heroui-line-height-tiny': '1rem', + '--heroui-line-height-small': '1.25rem', + '--heroui-line-height-medium': '1.5rem', + '--heroui-line-height-large': '1.75rem', + '--heroui-radius-small': '8px', + '--heroui-radius-medium': '12px', + '--heroui-radius-large': '14px', + '--heroui-border-width-small': '1px', + '--heroui-border-width-medium': '2px', + '--heroui-border-width-large': '3px', + '--heroui-box-shadow-small': + '0px 0px 5px 0px rgba(0, 0, 0, .05), 0px 2px 10px 0px rgba(0, 0, 0, .2), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)', + '--heroui-box-shadow-medium': + '0px 0px 15px 0px rgba(0, 0, 0, .06), 0px 2px 30px 0px rgba(0, 0, 0, .22), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)', + '--heroui-box-shadow-large': + '0px 0px 30px 0px rgba(0, 0, 0, .07), 0px 30px 60px 0px rgba(0, 0, 0, .26), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)', + '--heroui-hover-opacity': '.9', + }, + light: { + '--heroui-background': '0 0% 100%', + '--heroui-foreground-50': '240 5.88% 95%', + '--heroui-foreground-100': '240 3.7% 90%', + '--heroui-foreground-200': '240 5.26% 80%', + '--heroui-foreground-300': '240 5.2% 70%', + '--heroui-foreground-400': '240 3.83% 60%', + '--heroui-foreground-500': '240 5.03% 50%', + '--heroui-foreground-600': '240 4.88% 40%', + '--heroui-foreground-700': '240 5.88% 30%', + '--heroui-foreground-800': '240 4.76% 20%', + '--heroui-foreground-900': '0 0% 10%', + '--heroui-foreground': '210 5.56% 7.06%', + '--heroui-focus': '212.01999999999998 100% 53.33%', + '--heroui-overlay': '0 0% 100%', + '--heroui-divider': '0 0% 0%', + '--heroui-divider-opacity': '0.85', + '--heroui-content1': '240 5.88% 95%', + '--heroui-content1-foreground': '0 0% 10%', + '--heroui-content2': '240 3.7% 90%', + '--heroui-content2-foreground': '240 4.76% 20%', + '--heroui-content3': '240 5.26% 80%', + '--heroui-content3-foreground': '240 5.88% 30%', + '--heroui-content4': '240 5.2% 70%', + '--heroui-content4-foreground': '240 4.88% 40%', + '--heroui-default-50': '240 5.88% 95%', + '--heroui-default-100': '240 3.7% 90%', + '--heroui-default-200': '240 5.26% 80%', + '--heroui-default-300': '240 5.2% 70%', + '--heroui-default-400': '240 3.83% 60%', + '--heroui-default-500': '240 5.03% 50%', + '--heroui-default-600': '240 4.88% 40%', + '--heroui-default-700': '240 5.88% 30%', + '--heroui-default-800': '240 4.76% 20%', + '--heroui-default-900': '0 0% 10%', + '--heroui-default-foreground': '0 0% 0%', + '--heroui-default': '240 5.26% 80%', + '--heroui-danger-50': '324 90.91% 95.69%', + '--heroui-danger-100': '350.53 90.48% 91.76%', + '--heroui-danger-200': '343.42 90.48% 83.53%', + '--heroui-danger-300': '337.84 83.46% 73.92%', + '--heroui-danger-400': '331.82 75% 65.49%', + '--heroui-danger-500': '325.82 69.62% 53.53%', + '--heroui-danger-600': '319.73 65.64% 44.51%', + '--heroui-danger-700': '313.85 70.65% 36.08%', + '--heroui-danger-800': '308.18 76.39% 28.24%', + '--heroui-danger-900': '301.89 82.61% 22.55%', + '--heroui-danger-foreground': '0 0% 100%', + '--heroui-danger': '325.82 69.62% 53.53%', + '--heroui-primary-50': '339.13 92% 95.1%', + '--heroui-primary-100': '340 91.84% 90.39%', + '--heroui-primary-200': '339.33 90% 80.39%', + '--heroui-primary-300': '339.11 90.6% 70.78%', + '--heroui-primary-400': '339 90% 60.78%', + '--heroui-primary-500': '339.2 90.36% 51.18%', + '--heroui-primary-600': '339 86.54% 40.78%', + '--heroui-primary-700': '339.11 85.99% 30.78%', + '--heroui-primary-800': '339.33 86.54% 20.39%', + '--heroui-primary-900': '340 84.91% 10.39%', + '--heroui-primary-foreground': '0 0% 100%', + '--heroui-primary': '339.2 90.36% 51.18%', + '--heroui-secondary-50': '270 61.54% 94.9%', + '--heroui-secondary-100': '270 59.26% 89.41%', + '--heroui-secondary-200': '270 59.26% 78.82%', + '--heroui-secondary-300': '270 59.26% 68.24%', + '--heroui-secondary-400': '270 59.26% 57.65%', + '--heroui-secondary-500': '270 66.67% 47.06%', + '--heroui-secondary-600': '270 66.67% 37.65%', + '--heroui-secondary-700': '270 66.67% 28.24%', + '--heroui-secondary-800': '270 66.67% 18.82%', + '--heroui-secondary-900': '270 66.67% 9.41%', + '--heroui-secondary-foreground': '0 0% 100%', + '--heroui-secondary': '270 66.67% 47.06%', + '--heroui-success-50': '146.67 64.29% 94.51%', + '--heroui-success-100': '145.71 61.4% 88.82%', + '--heroui-success-200': '146.2 61.74% 77.45%', + '--heroui-success-300': '145.79 62.57% 66.47%', + '--heroui-success-400': '146.01 62.45% 55.1%', + '--heroui-success-500': '145.96 79.46% 43.92%', + '--heroui-success-600': '146.01 79.89% 35.1%', + '--heroui-success-700': '145.79 79.26% 26.47%', + '--heroui-success-800': '146.2 79.78% 17.45%', + '--heroui-success-900': '145.71 77.78% 8.82%', + '--heroui-success-foreground': '0 0% 0%', + '--heroui-success': '145.96 79.46% 43.92%', + '--heroui-warning-50': '54.55 91.67% 95.29%', + '--heroui-warning-100': '37.14 91.3% 90.98%', + '--heroui-warning-200': '37.14 91.3% 81.96%', + '--heroui-warning-300': '36.96 91.24% 73.14%', + '--heroui-warning-400': '37.01 91.26% 64.12%', + '--heroui-warning-500': '37.03 91.27% 55.1%', + '--heroui-warning-600': '37.01 74.22% 44.12%', + '--heroui-warning-700': '36.96 73.96% 33.14%', + '--heroui-warning-800': '37.14 75% 21.96%', + '--heroui-warning-900': '37.14 75% 10.98%', + '--heroui-warning-foreground': '0 0% 0%', + '--heroui-warning': '37.03 91.27% 55.1%', + '--heroui-code-background': '221.25 17.39% 18.04%', + '--heroui-strong': '316.95 100% 65.29%', + '--heroui-code-mdx': '316.95 100% 65.29%', + '--heroui-divider-weight': '1px', + '--heroui-disabled-opacity': '.5', + '--heroui-font-size-tiny': '0.75rem', + '--heroui-font-size-small': '0.875rem', + '--heroui-font-size-medium': '1rem', + '--heroui-font-size-large': '1.125rem', + '--heroui-line-height-tiny': '1rem', + '--heroui-line-height-small': '1.25rem', + '--heroui-line-height-medium': '1.5rem', + '--heroui-line-height-large': '1.75rem', + '--heroui-radius-small': '8px', + '--heroui-radius-medium': '12px', + '--heroui-radius-large': '14px', + '--heroui-border-width-small': '1px', + '--heroui-border-width-medium': '2px', + '--heroui-border-width-large': '3px', + '--heroui-box-shadow-small': + '0px 0px 5px 0px rgba(0, 0, 0, .02), 0px 2px 10px 0px rgba(0, 0, 0, .06), 0px 0px 1px 0px rgba(0, 0, 0, .3)', + '--heroui-box-shadow-medium': + '0px 0px 15px 0px rgba(0, 0, 0, .03), 0px 2px 30px 0px rgba(0, 0, 0, .08), 0px 0px 1px 0px rgba(0, 0, 0, .3)', + '--heroui-box-shadow-large': + '0px 0px 30px 0px rgba(0, 0, 0, .04), 0px 30px 60px 0px rgba(0, 0, 0, .12), 0px 0px 1px 0px rgba(0, 0, 0, .3)', + '--heroui-hover-opacity': '.8', + }, + }, + } +); diff --git a/src/webui/src/utils/object.ts b/src/webui/src/utils/object.ts new file mode 100644 index 00000000..e0fe8b55 --- /dev/null +++ b/src/webui/src/utils/object.ts @@ -0,0 +1,22 @@ +export function deepMerge>(target: T, source: Partial): T { + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + // 如果 source[key] 为 undefined,则跳过(保留 target[key]) + if (source[key] === undefined) { + continue; + } + if ( + target[key] !== undefined && + typeof target[key] === 'object' && + !Array.isArray(target[key]) && + typeof source[key] === 'object' && + !Array.isArray(source[key]) + ) { + target[key] = deepMerge({ ...target[key] }, source[key]!) as T[Extract]; + } else { + target[key] = source[key]! as T[Extract]; + } + } + } + return target; +}