mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
fix
This commit is contained in:
@@ -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",
|
||||||
|
40
napcat.webui/src/components/ColorPicker.tsx
Normal file
40
napcat.webui/src/components/ColorPicker.tsx
Normal 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
|
@@ -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>
|
||||||
)
|
)
|
||||||
|
@@ -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 (
|
||||||
|
106
napcat.webui/src/pages/dashboard/config/theme.tsx
Normal file
106
napcat.webui/src/pages/dashboard/config/theme.tsx
Normal 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
|
@@ -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",
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新主题内容
|
// 更新主题内容
|
||||||
|
Reference in New Issue
Block a user