This commit is contained in:
bietiaop
2025-02-08 22:43:53 +08:00
parent f8c396b1fe
commit 2e013ed4f5
7 changed files with 444 additions and 278 deletions

View File

@@ -64,6 +64,7 @@
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"quill": "^2.0.3", "quill": "^2.0.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-color": "^2.19.3",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-dropzone": "^14.3.5", "react-dropzone": "^14.3.5",
"react-error-boundary": "^5.0.0", "react-error-boundary": "^5.0.0",

View File

@@ -0,0 +1,40 @@
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: string) => void
}
const ColorPicker: React.FC<ColorPickerProps> = ({ color, onChange }) => {
const handleChange = (colorResult: ColorResult) => {
const hsl = colorResult.hsl
const color = `${hsl.h} ${hsl.s}% ${hsl.l}%`
onChange(color)
}
return (
<Popover>
<PopoverTrigger>
<div
style={{
background: color,
width: 36,
height: 14,
borderRadius: 2,
cursor: 'pointer',
border: '1px solid #ddd'
}}
/>
</PopoverTrigger>
<PopoverContent>
<SketchPicker color={color} onChange={handleChange} />
</PopoverContent>
</Popover>
)
}
export default ColorPicker

View File

@@ -6,6 +6,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom'
import ChangePasswordCard from './change_password' import ChangePasswordCard from './change_password'
import LoginConfigCard from './login' import LoginConfigCard from './login'
import OneBotConfigCard from './onebot' import OneBotConfigCard from './onebot'
import ThemeConfigCard from './theme'
import WebUIConfigCard from './webui' import WebUIConfigCard from './webui'
export interface ConfigPageProps { export interface ConfigPageProps {
@@ -58,7 +59,6 @@ export default function ConfigPage() {
<WebUIConfigCard /> <WebUIConfigCard />
</ConfingPageItem> </ConfingPageItem>
</Tab> </Tab>
<Tab title="登录配置" key="login"> <Tab title="登录配置" key="login">
<ConfingPageItem> <ConfingPageItem>
<LoginConfigCard /> <LoginConfigCard />
@@ -69,6 +69,12 @@ export default function ConfigPage() {
<ChangePasswordCard /> <ChangePasswordCard />
</ConfingPageItem> </ConfingPageItem>
</Tab> </Tab>
<Tab title="主题配置" key="theme">
<ConfingPageItem>
<ThemeConfigCard />
</ConfingPageItem>
</Tab>
</Tabs> </Tabs>
</section> </section>
) )

View File

@@ -47,11 +47,11 @@ const LoginConfigCard = () => {
} }
}) })
const onRefresh = async (shotTip = true) => { const onRefresh = async () => {
try { try {
setLoading(true) setLoading(true)
await refreshQuickLogin() await refreshQuickLogin()
if (shotTip) toast.success('刷新成功') toast.success('刷新成功')
} catch (error) { } catch (error) {
const msg = (error as Error).message const msg = (error as Error).message
toast.error(`刷新失败: ${msg}`) toast.error(`刷新失败: ${msg}`)
@@ -64,10 +64,6 @@ const LoginConfigCard = () => {
reset() reset()
}, [quickLoginData]) }, [quickLoginData])
useEffect(() => {
onRefresh(false)
}, [])
if (loading) return <PageLoading loading={true} /> if (loading) return <PageLoading loading={true} />
return ( return (

View File

@@ -0,0 +1,106 @@
import { useRequest } from 'ahooks'
import { useEffect } from 'react'
import { Controller, useForm } from 'react-hook-form'
import toast from 'react-hot-toast'
import ColorPicker from '@/components/ColorPicker'
import SaveButtons from '@/components/button/save_buttons'
import PageLoading from '@/components/page_loading'
import WebUIManager from '@/controllers/webui_manager'
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((data) => {
try {
WebUIManager.setThemeConfig(data.theme)
toast.success('保存成功')
} 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}`)
}
}
if (loading) return <PageLoading loading={true} />
if (error)
return (
<div className="py-24 text-danger-500 text-center">{error.message}</div>
)
const colorKeys = [
'--heroui-background',
'--heroui-primary',
'--heroui-danger'
] as const
return (
<>
<title> - NapCat WebUI</title>
<div className="flex-shrink-0 w-full"></div>
{(['dark', 'light'] as const).map((mode) => (
<div key={mode}>
<h3>{mode === 'dark' ? '暗色主题' : '亮色主题'}</h3>
{colorKeys.map((key) => (
<div
key={key}
style={{ display: 'flex', alignItems: 'center', marginBottom: 8 }}
>
<label style={{ width: 150 }}>{key}</label>
<Controller
control={control}
name={`theme.${mode}.${key}`}
render={({ field: { value, onChange } }) => {
console.log(value)
const hslArray = value?.split(' ') ?? [0, 0, 0]
const color = `hsl(${hslArray[0]}, ${hslArray[1]}, ${hslArray[2]})`
return <ColorPicker color={color} onChange={onChange} />
}}
/>
</div>
))}
</div>
))}
<SaveButtons
onSubmit={onSubmit}
reset={reset}
isSubmitting={isSubmitting}
refresh={onRefresh}
/>
</>
)
}
export default ThemeConfigCard

View File

@@ -33,6 +33,7 @@
"@types/multer": "^1.4.12", "@types/multer": "^1.4.12",
"@types/node": "^22.0.1", "@types/node": "^22.0.1",
"@types/qrcode-terminal": "^0.12.2", "@types/qrcode-terminal": "^0.12.2",
"@types/react-color": "^3.0.13",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0", "@typescript-eslint/parser": "^8.3.0",

View File

@@ -14,8 +14,10 @@ const WebUiConfigSchema = Type.Object({
token: Type.String({ default: 'napcat' }), token: Type.String({ default: 'napcat' }),
loginRate: Type.Number({ default: 10 }), loginRate: Type.Number({ default: 10 }),
autoLoginAccount: Type.String({ default: '' }), autoLoginAccount: Type.String({ default: '' }),
theme: Type.Object({ theme: Type.Object(
dark: Type.Object({ {
dark: Type.Object(
{
'--heroui-background': Type.String({ default: '0 0% 0%' }), '--heroui-background': Type.String({ default: '0 0% 0%' }),
'--heroui-foreground-50': Type.String({ default: '240 5.88% 10%' }), '--heroui-foreground-50': Type.String({ default: '240 5.88% 10%' }),
'--heroui-foreground-100': Type.String({ default: '240 3.7% 15.88%' }), '--heroui-foreground-100': Type.String({ default: '240 3.7% 15.88%' }),
@@ -148,8 +150,11 @@ const WebUiConfigSchema = Type.Object({
'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)', '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': Type.String({ default: '.9' }), '--heroui-hover-opacity': Type.String({ default: '.9' }),
}, { default: {} }), },
light: Type.Object({ { default: {} }
),
light: Type.Object(
{
'--heroui-background': Type.String({ default: '0 0% 100%' }), '--heroui-background': Type.String({ default: '0 0% 100%' }),
'--heroui-foreground-50': Type.String({ default: '240 5.88% 95%' }), '--heroui-foreground-50': Type.String({ default: '240 5.88% 95%' }),
'--heroui-foreground-100': Type.String({ default: '240 3.7% 90%' }), '--heroui-foreground-100': Type.String({ default: '240 3.7% 90%' }),
@@ -282,8 +287,12 @@ const WebUiConfigSchema = Type.Object({
'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)', '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': Type.String({ default: '.8' }), '--heroui-hover-opacity': Type.String({ default: '.8' }),
}, { default: {} }), },
}, { default: {} }), { default: {} }
),
},
{ default: {} }
),
}); });
export type WebUiConfigType = Static<typeof WebUiConfigSchema>; export type WebUiConfigType = Static<typeof WebUiConfigSchema>;
@@ -432,7 +441,14 @@ export class WebUiConfigWrapper {
// 获取主题内容 // 获取主题内容
async GetTheme(): Promise<WebUiConfigType['theme']> { async GetTheme(): Promise<WebUiConfigType['theme']> {
return (await this.GetWebUIConfig()).theme; const config = await this.GetWebUIConfig();
if (!config.theme || Object.keys(config.theme).length === 0) {
const defaultConfig = this.validateAndApplyDefaults({});
config.theme = defaultConfig.theme;
// 更新配置文件中的 theme 字段
await this.UpdateWebUIConfig({ theme: config.theme });
}
return config.theme;
} }
// 更新主题内容 // 更新主题内容