feat: 新增启动参数设置页面,区分 desktop 可编辑与 server 只读
This commit is contained in:
106
frontend/src/__tests__/hooks/useSettings.test.tsx
Normal file
106
frontend/src/__tests__/hooks/useSettings.test.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
import React from 'react'
|
||||
import { MessagePlugin } from 'tdesign-react'
|
||||
import { useStartupSettings, useSaveStartupSettings } from '@/hooks/useSettings'
|
||||
import type { StartupSettings } from '@/types'
|
||||
|
||||
vi.mock('tdesign-react', () => ({
|
||||
MessagePlugin: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const mockDesktopSettings: StartupSettings = {
|
||||
mode: 'desktop',
|
||||
editable: true,
|
||||
configPath: '/home/user/.nex/config.yaml',
|
||||
restartRequired: true,
|
||||
config: {
|
||||
server: { port: 9826, readTimeout: '30s', writeTimeout: '30s' },
|
||||
database: {
|
||||
driver: 'sqlite',
|
||||
path: '/home/user/.nex/config.db',
|
||||
host: '',
|
||||
port: 3306,
|
||||
user: '',
|
||||
password: '',
|
||||
dbname: 'nex',
|
||||
maxIdleConns: 10,
|
||||
maxOpenConns: 100,
|
||||
connMaxLifetime: '1h',
|
||||
},
|
||||
log: { level: 'info', path: '/home/user/.nex/log', maxSize: 100, maxBackups: 10, maxAge: 30, compress: true },
|
||||
},
|
||||
}
|
||||
|
||||
const handlers = [
|
||||
http.get('/api/settings/startup', () => {
|
||||
return HttpResponse.json(mockDesktopSettings)
|
||||
}),
|
||||
http.put('/api/settings/startup', async ({ request }) => {
|
||||
const body = (await request.json()) as Record<string, unknown>
|
||||
return HttpResponse.json({
|
||||
...mockDesktopSettings,
|
||||
config: (body as Record<string, unknown>).config,
|
||||
})
|
||||
}),
|
||||
]
|
||||
|
||||
const server = setupServer(...handlers)
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }))
|
||||
afterEach(() => server.resetHandlers())
|
||||
afterAll(() => server.close())
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useStartupSettings', () => {
|
||||
it('fetches startup settings', async () => {
|
||||
const { result } = renderHook(() => useStartupSettings(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(result.current.data?.mode).toBe('desktop')
|
||||
expect(result.current.data?.editable).toBe(true)
|
||||
expect(result.current.data?.configPath).toBe('/home/user/.nex/config.yaml')
|
||||
expect(result.current.data?.restartRequired).toBe(true)
|
||||
expect(result.current.data?.config.server.port).toBe(9826)
|
||||
expect(result.current.data?.config.database.driver).toBe('sqlite')
|
||||
expect(result.current.data?.config.log.level).toBe('info')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useSaveStartupSettings', () => {
|
||||
it('saves settings and shows success message for desktop', async () => {
|
||||
const { result } = renderHook(() => useSaveStartupSettings(), { wrapper: createWrapper() })
|
||||
|
||||
result.current.mutate({ config: mockDesktopSettings.config })
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||
expect(MessagePlugin.success).toHaveBeenCalledWith(
|
||||
'配置已保存到配置文件。当前运行中的服务仍使用启动时配置,重启 Desktop 后生效'
|
||||
)
|
||||
})
|
||||
|
||||
it('shows error message on failure', async () => {
|
||||
server.use(
|
||||
http.put('/api/settings/startup', () => {
|
||||
return HttpResponse.json({ error: '保存失败' }, { status: 500 })
|
||||
})
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useSaveStartupSettings(), { wrapper: createWrapper() })
|
||||
|
||||
result.current.mutate({ config: mockDesktopSettings.config })
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true))
|
||||
expect(MessagePlugin.error).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
169
frontend/src/__tests__/pages/Settings.test.tsx
Normal file
169
frontend/src/__tests__/pages/Settings.test.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
import React from 'react'
|
||||
import { MemoryRouter } from 'react-router'
|
||||
import { MessagePlugin } from 'tdesign-react'
|
||||
import SettingsPage from '@/pages/Settings'
|
||||
import type { StartupSettings } from '@/types'
|
||||
|
||||
vi.mock('tdesign-react', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('tdesign-react')>()
|
||||
return {
|
||||
...actual,
|
||||
MessagePlugin: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const mockDesktopSettings: StartupSettings = {
|
||||
mode: 'desktop',
|
||||
editable: true,
|
||||
configPath: '/home/user/.nex/config.yaml',
|
||||
restartRequired: true,
|
||||
config: {
|
||||
server: {
|
||||
port: 9826,
|
||||
readTimeout: '30s',
|
||||
writeTimeout: '30s',
|
||||
},
|
||||
database: {
|
||||
driver: 'sqlite',
|
||||
path: '/home/user/.nex/config.db',
|
||||
host: '',
|
||||
port: 3306,
|
||||
user: '',
|
||||
password: '',
|
||||
dbname: 'nex',
|
||||
maxIdleConns: 10,
|
||||
maxOpenConns: 100,
|
||||
connMaxLifetime: '1h',
|
||||
},
|
||||
log: {
|
||||
level: 'info',
|
||||
path: '/home/user/.nex/log',
|
||||
maxSize: 100,
|
||||
maxBackups: 10,
|
||||
maxAge: 30,
|
||||
compress: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const mockServerSettings: StartupSettings = {
|
||||
...mockDesktopSettings,
|
||||
mode: 'server',
|
||||
editable: false,
|
||||
restartRequired: false,
|
||||
configPath: '/etc/nex/config.yaml',
|
||||
}
|
||||
|
||||
const desktopHandlers = [
|
||||
http.get('/api/settings/startup', () => HttpResponse.json(mockDesktopSettings)),
|
||||
http.put('/api/settings/startup', async ({ request }) => {
|
||||
const body = (await request.json()) as Record<string, unknown>
|
||||
return HttpResponse.json({ ...mockDesktopSettings, config: (body as Record<string, unknown>).config })
|
||||
}),
|
||||
]
|
||||
|
||||
const serverHandlers = [http.get('/api/settings/startup', () => HttpResponse.json(mockServerSettings))]
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<MemoryRouter>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</MemoryRouter>
|
||||
)
|
||||
}
|
||||
|
||||
describe('SettingsPage', () => {
|
||||
it('renders startup settings card', async () => {
|
||||
const mswServer = setupServer(...desktopHandlers)
|
||||
mswServer.listen({ onUnhandledRequest: 'bypass' })
|
||||
|
||||
render(<SettingsPage />, { wrapper: createWrapper() })
|
||||
|
||||
expect(await screen.findByText('服务配置')).toBeInTheDocument()
|
||||
expect(screen.getByText('启动参数设置')).toBeInTheDocument()
|
||||
|
||||
mswServer.close()
|
||||
})
|
||||
})
|
||||
|
||||
describe('StartupSettingsCard - Desktop mode', () => {
|
||||
const mswServer = setupServer(...desktopHandlers)
|
||||
|
||||
beforeAll(() => mswServer.listen({ onUnhandledRequest: 'bypass' }))
|
||||
afterEach(() => mswServer.resetHandlers())
|
||||
afterAll(() => mswServer.close())
|
||||
|
||||
it('shows editable form with save button in desktop mode', async () => {
|
||||
render(<SettingsPage />, { wrapper: createWrapper() })
|
||||
|
||||
expect(
|
||||
await screen.findByText('Desktop 模式下此页面编辑的是启动配置文件,保存后重启 Desktop 生效')
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: '保存' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows form fields for server, database, and log', async () => {
|
||||
render(<SettingsPage />, { wrapper: createWrapper() })
|
||||
|
||||
expect(await screen.findByText('服务配置')).toBeInTheDocument()
|
||||
expect(screen.getByText('数据库配置')).toBeInTheDocument()
|
||||
expect(screen.getByText('日志配置')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows success message on save', async () => {
|
||||
render(<SettingsPage />, { wrapper: createWrapper() })
|
||||
|
||||
expect(await screen.findByText('服务配置')).toBeInTheDocument()
|
||||
|
||||
const saveButton = screen.getByRole('button', { name: '保存' })
|
||||
await userEvent.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(MessagePlugin.success).toHaveBeenCalledWith(
|
||||
'配置已保存到配置文件。当前运行中的服务仍使用启动时配置,重启 Desktop 后生效'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('StartupSettingsCard - Server mode', () => {
|
||||
const mswServer = setupServer(...serverHandlers)
|
||||
|
||||
beforeAll(() => mswServer.listen({ onUnhandledRequest: 'bypass' }))
|
||||
afterEach(() => mswServer.resetHandlers())
|
||||
afterAll(() => mswServer.close())
|
||||
|
||||
it('shows read-only form with server-only warning', async () => {
|
||||
render(<SettingsPage />, { wrapper: createWrapper() })
|
||||
|
||||
expect(await screen.findByText('Server 模式下启动参数仅支持查看,不支持从前端编辑')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: '保存' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
function waitFor(fn: () => void, opts?: { timeout?: number }) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const start = Date.now()
|
||||
const interval = setInterval(() => {
|
||||
try {
|
||||
fn()
|
||||
clearInterval(interval)
|
||||
resolve()
|
||||
} catch {
|
||||
if (Date.now() - start > (opts?.timeout ?? 3000)) {
|
||||
clearInterval(interval)
|
||||
reject(new Error('waitFor timeout'))
|
||||
}
|
||||
}
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
10
frontend/src/api/settings.ts
Normal file
10
frontend/src/api/settings.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { StartupSettings, SaveStartupSettingsInput } from '@/types'
|
||||
import { request } from './client'
|
||||
|
||||
export async function getStartupSettings(): Promise<StartupSettings> {
|
||||
return request<StartupSettings>('GET', '/api/settings/startup')
|
||||
}
|
||||
|
||||
export async function saveStartupSettings(input: SaveStartupSettingsInput): Promise<StartupSettings> {
|
||||
return request<StartupSettings>('PUT', '/api/settings/startup', input)
|
||||
}
|
||||
33
frontend/src/hooks/useSettings.ts
Normal file
33
frontend/src/hooks/useSettings.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { MessagePlugin } from 'tdesign-react'
|
||||
import * as api from '@/api/settings'
|
||||
import type { SaveStartupSettingsInput, ApiError } from '@/types'
|
||||
|
||||
export const settingsKeys = {
|
||||
startup: ['settings', 'startup'] as const,
|
||||
}
|
||||
|
||||
export function useStartupSettings() {
|
||||
return useQuery({
|
||||
queryKey: settingsKeys.startup,
|
||||
queryFn: api.getStartupSettings,
|
||||
staleTime: 0,
|
||||
})
|
||||
}
|
||||
|
||||
export function useSaveStartupSettings() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (input: SaveStartupSettingsInput) => api.saveStartupSettings(input),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: settingsKeys.startup })
|
||||
if (data.mode === 'desktop') {
|
||||
MessagePlugin.success('配置已保存到配置文件。当前运行中的服务仍使用启动时配置,重启 Desktop 后生效')
|
||||
}
|
||||
},
|
||||
onError: (error: ApiError) => {
|
||||
MessagePlugin.error(error.message)
|
||||
},
|
||||
})
|
||||
}
|
||||
275
frontend/src/pages/Settings/StartupSettingsCard.tsx
Normal file
275
frontend/src/pages/Settings/StartupSettingsCard.tsx
Normal file
@@ -0,0 +1,275 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Form, Input, InputNumber, Select, Switch, Button, Alert, Divider, Space, Loading } from 'tdesign-react'
|
||||
import { useStartupSettings, useSaveStartupSettings } from '@/hooks/useSettings'
|
||||
import type { StartupConfig } from '@/types'
|
||||
import type { SubmitContext } from 'tdesign-react/es/form/type'
|
||||
|
||||
const DURATION_PLACEHOLDERS = {
|
||||
readTimeout: '例如 30s',
|
||||
writeTimeout: '例如 30s',
|
||||
connMaxLifetime: '例如 1h',
|
||||
}
|
||||
|
||||
function flattenConfig(c: StartupConfig): Record<string, unknown> {
|
||||
return {
|
||||
'server.port': c.server.port,
|
||||
'server.readTimeout': c.server.readTimeout,
|
||||
'server.writeTimeout': c.server.writeTimeout,
|
||||
'database.driver': c.database.driver,
|
||||
'database.path': c.database.path,
|
||||
'database.host': c.database.host,
|
||||
'database.port': c.database.port,
|
||||
'database.user': c.database.user,
|
||||
'database.password': c.database.password,
|
||||
'database.dbname': c.database.dbname,
|
||||
'database.maxIdleConns': c.database.maxIdleConns,
|
||||
'database.maxOpenConns': c.database.maxOpenConns,
|
||||
'database.connMaxLifetime': c.database.connMaxLifetime,
|
||||
'log.level': c.log.level,
|
||||
'log.path': c.log.path,
|
||||
'log.maxSize': c.log.maxSize,
|
||||
'log.maxBackups': c.log.maxBackups,
|
||||
'log.maxAge': c.log.maxAge,
|
||||
'log.compress': c.log.compress,
|
||||
}
|
||||
}
|
||||
|
||||
export function StartupSettingsCard() {
|
||||
const { data: settings, isLoading, isError } = useStartupSettings()
|
||||
const saveMutation = useSaveStartupSettings()
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const isDesktop = settings?.mode === 'desktop'
|
||||
const editable = settings?.editable ?? false
|
||||
|
||||
const [driver, setDriver] = useState<string>(settings?.config.database.driver ?? 'sqlite')
|
||||
|
||||
useEffect(() => {
|
||||
if (settings?.config && form) {
|
||||
form.setFieldsValue(flattenConfig(settings.config))
|
||||
}
|
||||
}, [form, settings?.config])
|
||||
|
||||
const handleDriverChange = (changedValues: Record<string, unknown>) => {
|
||||
if ('database.driver' in changedValues) {
|
||||
setDriver(changedValues['database.driver'] as string)
|
||||
}
|
||||
}
|
||||
|
||||
const isSqlite = driver === 'sqlite'
|
||||
const isMysql = driver === 'mysql'
|
||||
|
||||
const handleSubmit = (context: SubmitContext) => {
|
||||
if (context.validateResult !== true || !form) return
|
||||
const values = form.getFieldsValue(true) as Record<string, unknown>
|
||||
const config: StartupConfig = {
|
||||
server: {
|
||||
port: values['server.port'] as number,
|
||||
readTimeout: values['server.readTimeout'] as string,
|
||||
writeTimeout: values['server.writeTimeout'] as string,
|
||||
},
|
||||
database: {
|
||||
driver: values['database.driver'] as string,
|
||||
path: values['database.path'] as string,
|
||||
host: values['database.host'] as string,
|
||||
port: values['database.port'] as number,
|
||||
user: values['database.user'] as string,
|
||||
password: values['database.password'] as string,
|
||||
dbname: values['database.dbname'] as string,
|
||||
maxIdleConns: values['database.maxIdleConns'] as number,
|
||||
maxOpenConns: values['database.maxOpenConns'] as number,
|
||||
connMaxLifetime: values['database.connMaxLifetime'] as string,
|
||||
},
|
||||
log: {
|
||||
level: values['log.level'] as string,
|
||||
path: values['log.path'] as string,
|
||||
maxSize: values['log.maxSize'] as number,
|
||||
maxBackups: values['log.maxBackups'] as number,
|
||||
maxAge: values['log.maxAge'] as number,
|
||||
compress: values['log.compress'] as boolean,
|
||||
},
|
||||
}
|
||||
saveMutation.mutate({ config })
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card title='启动参数设置'>
|
||||
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
||||
<Loading text='加载中...' />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (isError || !settings) {
|
||||
return (
|
||||
<Card title='启动参数设置'>
|
||||
<Alert theme='error' message='加载启动参数失败,请刷新页面重试' />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title='启动参数设置'>
|
||||
{isDesktop && (
|
||||
<Alert
|
||||
theme='info'
|
||||
message='Desktop 模式下此页面编辑的是启动配置文件,保存后重启 Desktop 生效'
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
{!editable && (
|
||||
<Alert
|
||||
theme='warning'
|
||||
message='Server 模式下启动参数仅支持查看,不支持从前端编辑'
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout='vertical'
|
||||
labelWidth={140}
|
||||
initialData={flattenConfig(settings.config)}
|
||||
onSubmit={handleSubmit}
|
||||
onValuesChange={handleDriverChange}
|
||||
disabled={!editable}
|
||||
>
|
||||
<div style={{ fontSize: 16, fontWeight: 600, marginBottom: 12 }}>服务配置</div>
|
||||
<Form.FormItem label='端口' name='server.port' rules={[{ required: true, message: '请输入端口' }]}>
|
||||
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
|
||||
</Form.FormItem>
|
||||
<Form.FormItem label='读超时' name='server.readTimeout' rules={[{ required: true, message: '请输入读超时' }]}>
|
||||
<Input placeholder={DURATION_PLACEHOLDERS.readTimeout} />
|
||||
</Form.FormItem>
|
||||
<Form.FormItem label='写超时' name='server.writeTimeout' rules={[{ required: true, message: '请输入写超时' }]}>
|
||||
<Input placeholder={DURATION_PLACEHOLDERS.writeTimeout} />
|
||||
</Form.FormItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div style={{ fontSize: 16, fontWeight: 600, marginBottom: 12 }}>数据库配置</div>
|
||||
<Form.FormItem label='驱动' name='database.driver' rules={[{ required: true, message: '请选择数据库驱动' }]}>
|
||||
<Select>
|
||||
<Select.Option value='sqlite'>SQLite</Select.Option>
|
||||
<Select.Option value='mysql'>MySQL</Select.Option>
|
||||
</Select>
|
||||
</Form.FormItem>
|
||||
|
||||
{isSqlite && (
|
||||
<Form.FormItem
|
||||
label='数据库路径'
|
||||
name='database.path'
|
||||
rules={[{ required: true, message: '请输入数据库路径' }]}
|
||||
>
|
||||
<Input placeholder='例如 ~/.nex/config.db' />
|
||||
</Form.FormItem>
|
||||
)}
|
||||
|
||||
{isMysql && (
|
||||
<>
|
||||
<Form.FormItem
|
||||
label='主机地址'
|
||||
name='database.host'
|
||||
rules={[{ required: true, message: '请输入主机地址' }]}
|
||||
>
|
||||
<Input placeholder='例如 localhost' />
|
||||
</Form.FormItem>
|
||||
<Form.FormItem label='端口' name='database.port' rules={[{ required: true, message: '请输入端口' }]}>
|
||||
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
|
||||
</Form.FormItem>
|
||||
<Form.FormItem label='用户名' name='database.user' rules={[{ required: true, message: '请输入用户名' }]}>
|
||||
<Input placeholder='例如 root' />
|
||||
</Form.FormItem>
|
||||
<Form.FormItem label='密码' name='database.password'>
|
||||
<Input placeholder='MySQL 密码' />
|
||||
</Form.FormItem>
|
||||
<Form.FormItem
|
||||
label='数据库名'
|
||||
name='database.dbname'
|
||||
rules={[{ required: true, message: '请输入数据库名' }]}
|
||||
>
|
||||
<Input placeholder='例如 nex' />
|
||||
</Form.FormItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.FormItem
|
||||
label='最大空闲连接数'
|
||||
name='database.maxIdleConns'
|
||||
rules={[{ required: true, message: '请输入最大空闲连接数' }]}
|
||||
>
|
||||
<InputNumber min={1} style={{ width: '100%' }} />
|
||||
</Form.FormItem>
|
||||
<Form.FormItem
|
||||
label='最大打开连接数'
|
||||
name='database.maxOpenConns'
|
||||
rules={[{ required: true, message: '请输入最大打开连接数' }]}
|
||||
>
|
||||
<InputNumber min={1} style={{ width: '100%' }} />
|
||||
</Form.FormItem>
|
||||
<Form.FormItem
|
||||
label='连接最大生命周期'
|
||||
name='database.connMaxLifetime'
|
||||
rules={[{ required: true, message: '请输入连接最大生命周期' }]}
|
||||
>
|
||||
<Input placeholder={DURATION_PLACEHOLDERS.connMaxLifetime} />
|
||||
</Form.FormItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div style={{ fontSize: 16, fontWeight: 600, marginBottom: 12 }}>日志配置</div>
|
||||
<Form.FormItem label='日志级别' name='log.level' rules={[{ required: true, message: '请选择日志级别' }]}>
|
||||
<Select>
|
||||
<Select.Option value='debug'>debug</Select.Option>
|
||||
<Select.Option value='info'>info</Select.Option>
|
||||
<Select.Option value='warn'>warn</Select.Option>
|
||||
<Select.Option value='error'>error</Select.Option>
|
||||
</Select>
|
||||
</Form.FormItem>
|
||||
<Form.FormItem label='日志路径' name='log.path' rules={[{ required: true, message: '请输入日志路径' }]}>
|
||||
<Input placeholder='例如 ~/.nex/log' />
|
||||
</Form.FormItem>
|
||||
<Form.FormItem
|
||||
label='单文件最大大小'
|
||||
name='log.maxSize'
|
||||
rules={[{ required: true, message: '请输入最大大小' }]}
|
||||
>
|
||||
<InputNumber min={1} suffix=' MB' style={{ width: '100%' }} />
|
||||
</Form.FormItem>
|
||||
<Form.FormItem
|
||||
label='最大备份数'
|
||||
name='log.maxBackups'
|
||||
rules={[{ required: true, message: '请输入最大备份数' }]}
|
||||
>
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.FormItem>
|
||||
<Form.FormItem
|
||||
label='最大保留天数'
|
||||
name='log.maxAge'
|
||||
rules={[{ required: true, message: '请输入最大保留天数' }]}
|
||||
>
|
||||
<InputNumber min={0} suffix=' 天' style={{ width: '100%' }} />
|
||||
</Form.FormItem>
|
||||
<Form.FormItem label='压缩旧日志' name='log.compress'>
|
||||
<Switch />
|
||||
</Form.FormItem>
|
||||
|
||||
{editable && (
|
||||
<Space style={{ marginTop: 16 }}>
|
||||
<Button
|
||||
theme='primary'
|
||||
loading={saveMutation.isPending}
|
||||
onClick={() => {
|
||||
form?.submit()
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</Form>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
import { Card } from 'tdesign-react'
|
||||
import { StartupSettingsCard } from './StartupSettingsCard'
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<Card title='设置'>
|
||||
<div style={{ textAlign: 'center', padding: '40px 0', color: 'var(--td-text-color-placeholder)' }}>
|
||||
设置功能开发中...
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
return <StartupSettingsCard />
|
||||
}
|
||||
|
||||
@@ -92,3 +92,49 @@ export interface ApiErrorResponse {
|
||||
error: string
|
||||
code?: string
|
||||
}
|
||||
|
||||
export interface StartupServerConfig {
|
||||
port: number
|
||||
readTimeout: string
|
||||
writeTimeout: string
|
||||
}
|
||||
|
||||
export interface StartupDatabaseConfig {
|
||||
driver: string
|
||||
path: string
|
||||
host: string
|
||||
port: number
|
||||
user: string
|
||||
password: string
|
||||
dbname: string
|
||||
maxIdleConns: number
|
||||
maxOpenConns: number
|
||||
connMaxLifetime: string
|
||||
}
|
||||
|
||||
export interface StartupLogConfig {
|
||||
level: string
|
||||
path: string
|
||||
maxSize: number
|
||||
maxBackups: number
|
||||
maxAge: number
|
||||
compress: boolean
|
||||
}
|
||||
|
||||
export interface StartupConfig {
|
||||
server: StartupServerConfig
|
||||
database: StartupDatabaseConfig
|
||||
log: StartupLogConfig
|
||||
}
|
||||
|
||||
export interface StartupSettings {
|
||||
mode: 'server' | 'desktop'
|
||||
editable: boolean
|
||||
configPath: string
|
||||
restartRequired: boolean
|
||||
config: StartupConfig
|
||||
}
|
||||
|
||||
export type SaveStartupSettingsInput = {
|
||||
config: StartupConfig
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user