614 lines
34 KiB
TypeScript
614 lines
34 KiB
TypeScript
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Slider } from "@/components/ui/slider"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Separator } from "@/components/ui/separator"
|
|
import { Switch } from "@/components/ui/switch"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import { Checkbox } from "@/components/ui/checkbox"
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
import { useState } from "react"
|
|
import { Config, defaultConfig } from "@/types/config"
|
|
import {
|
|
Settings,
|
|
Volume2,
|
|
FlashlightIcon as Brightness4,
|
|
Wifi,
|
|
Database,
|
|
Shield,
|
|
Monitor,
|
|
Bell,
|
|
Server,
|
|
Lock,
|
|
Zap,
|
|
HardDrive,
|
|
Cpu,
|
|
} from "lucide-react"
|
|
|
|
export default function Control() {
|
|
const [config, setConfig] = useState<Config>(defaultConfig)
|
|
|
|
const updateConfig = (path: string, value: any) => {
|
|
setConfig(prev => {
|
|
const newConfig = { ...prev }
|
|
const keys = path.split('.')
|
|
let current: any = newConfig
|
|
|
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
current = current[keys[i]]
|
|
}
|
|
|
|
current[keys[keys.length - 1]] = value
|
|
return newConfig
|
|
})
|
|
}
|
|
|
|
const updateNestedConfig = (path: string, value: any) => {
|
|
updateConfig(path, value)
|
|
}
|
|
|
|
return (
|
|
<ScrollArea className="flex-1 h-full">
|
|
<div className="min-h-screen p-6">
|
|
<div className="mx-auto max-w-7xl space-y-6">
|
|
<div className="grid gap-6 lg:grid-cols-2">
|
|
{/* App Configuration */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Settings className="h-5 w-5" />
|
|
应用配置
|
|
</CardTitle>
|
|
<CardDescription>应用基本设置</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="app-name">应用名称</Label>
|
|
<Input
|
|
id="app-name"
|
|
value={config.app.name}
|
|
onChange={(e) => updateConfig('app.name', e.target.value)}
|
|
placeholder="输入应用名称"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="app-version">版本号</Label>
|
|
<Input
|
|
id="app-version"
|
|
value={config.app.version}
|
|
onChange={(e) => updateConfig('app.version', e.target.value)}
|
|
placeholder="输入版本号"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="app-debug">调试模式</Label>
|
|
<Switch
|
|
id="app-debug"
|
|
checked={config.app.debug}
|
|
onCheckedChange={(checked) => updateConfig('app.debug', checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label>时区</Label>
|
|
<Select value={config.app.timezone} onValueChange={(value) => updateConfig('app.timezone', value)}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="UTC">UTC</SelectItem>
|
|
<SelectItem value="Asia/Shanghai">中国标准时间</SelectItem>
|
|
<SelectItem value="America/New_York">美国东部时间</SelectItem>
|
|
<SelectItem value="Europe/London">英国时间</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Database Configuration */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Database className="h-5 w-5" />
|
|
数据库配置
|
|
</CardTitle>
|
|
<CardDescription>数据库连接设置</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="db-connections">最大连接数: {config.database.max_connections}</Label>
|
|
<Slider
|
|
id="db-connections"
|
|
value={[config.database.max_connections]}
|
|
onValueChange={(value) => updateConfig('database.max_connections', value[0])}
|
|
min={5}
|
|
max={100}
|
|
step={5}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="db-timeout">连接超时: {config.database.connection_timeout}秒</Label>
|
|
<Slider
|
|
id="db-timeout"
|
|
value={[config.database.connection_timeout]}
|
|
onValueChange={(value) => updateConfig('database.connection_timeout', value[0])}
|
|
min={10}
|
|
max={120}
|
|
step={5}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Kafka Configuration */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Server className="h-5 w-5" />
|
|
Kafka配置
|
|
</CardTitle>
|
|
<CardDescription>消息队列设置</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="kafka-retries">最大重试次数: {config.kafka.max_retries}</Label>
|
|
<Slider
|
|
id="kafka-retries"
|
|
value={[config.kafka.max_retries]}
|
|
onValueChange={(value) => updateConfig('kafka.max_retries', value[0])}
|
|
min={1}
|
|
max={10}
|
|
step={1}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="kafka-delay">重试延迟: {config.kafka.retry_delay}毫秒</Label>
|
|
<Slider
|
|
id="kafka-delay"
|
|
value={[config.kafka.retry_delay]}
|
|
onValueChange={(value) => updateConfig('kafka.retry_delay', value[0])}
|
|
min={100}
|
|
max={5000}
|
|
step={100}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Security Configuration */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Shield className="h-5 w-5" />
|
|
安全配置
|
|
</CardTitle>
|
|
<CardDescription>安全相关设置</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="session-timeout">会话超时: {config.security.session_timeout}秒</Label>
|
|
<Slider
|
|
id="session-timeout"
|
|
value={[config.security.session_timeout]}
|
|
onValueChange={(value) => updateConfig('security.session_timeout', value[0])}
|
|
min={1800}
|
|
max={7200}
|
|
step={300}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="login-attempts">最大登录尝试次数: {config.security.max_login_attempts}</Label>
|
|
<Slider
|
|
id="login-attempts"
|
|
value={[config.security.max_login_attempts]}
|
|
onValueChange={(value) => updateConfig('security.max_login_attempts', value[0])}
|
|
min={3}
|
|
max={10}
|
|
step={1}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Logging Configuration */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Monitor className="h-5 w-5" />
|
|
日志配置
|
|
</CardTitle>
|
|
<CardDescription>日志记录设置</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="space-y-3">
|
|
<Label>日志级别</Label>
|
|
<Select value={config.logging.level} onValueChange={(value) => updateConfig('logging.level', value)}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="debug">Debug</SelectItem>
|
|
<SelectItem value="info">Info</SelectItem>
|
|
<SelectItem value="warn">Warning</SelectItem>
|
|
<SelectItem value="error">Error</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="log-files">最大日志文件数: {config.logging.max_files}</Label>
|
|
<Slider
|
|
id="log-files"
|
|
value={[config.logging.max_files]}
|
|
onValueChange={(value) => updateConfig('logging.max_files', value[0])}
|
|
min={5}
|
|
max={50}
|
|
step={5}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Cache Configuration */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<HardDrive className="h-5 w-5" />
|
|
缓存配置
|
|
</CardTitle>
|
|
<CardDescription>缓存系统设置</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="cache-ttl">缓存TTL: {config.cache.ttl}秒</Label>
|
|
<Slider
|
|
id="cache-ttl"
|
|
value={[config.cache.ttl]}
|
|
onValueChange={(value) => updateConfig('cache.ttl', value[0])}
|
|
min={60}
|
|
max={3600}
|
|
step={60}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="cache-size">最大缓存大小: {config.cache.max_size}</Label>
|
|
<Slider
|
|
id="cache-size"
|
|
value={[config.cache.max_size]}
|
|
onValueChange={(value) => updateConfig('cache.max_size', value[0])}
|
|
min={100}
|
|
max={10000}
|
|
step={100}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Site Configuration */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Wifi className="h-5 w-5" />
|
|
站点配置
|
|
</CardTitle>
|
|
<CardDescription>网站基本设置</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="site-name">站点名称</Label>
|
|
<Input
|
|
id="site-name"
|
|
value={config.site.name}
|
|
onChange={(e) => updateConfig('site.name', e.target.value)}
|
|
placeholder="输入站点名称"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label>默认语言</Label>
|
|
<Select value={config.site.locale_default} onValueChange={(value) => updateConfig('site.locale_default', value)}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="zh-CN">中文</SelectItem>
|
|
<SelectItem value="en">English</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="logo-url">Logo URL</Label>
|
|
<Input
|
|
id="logo-url"
|
|
value={config.site.brand.logo_url}
|
|
onChange={(e) => updateConfig('site.brand.logo_url', e.target.value)}
|
|
placeholder="输入Logo URL"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="primary-color">主色调</Label>
|
|
<Input
|
|
id="primary-color"
|
|
type="color"
|
|
value={config.site.brand.primary_color}
|
|
onChange={(e) => updateConfig('site.brand.primary_color', e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="dark-mode">默认深色模式</Label>
|
|
<Switch
|
|
id="dark-mode"
|
|
checked={config.site.brand.dark_mode_default}
|
|
onCheckedChange={(checked) => updateConfig('site.brand.dark_mode_default', checked)}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Notice Configuration */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Bell className="h-5 w-5" />
|
|
通知配置
|
|
</CardTitle>
|
|
<CardDescription>系统通知设置</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="banner-enabled">启用横幅通知</Label>
|
|
<Switch
|
|
id="banner-enabled"
|
|
checked={config.notice.banner.enabled}
|
|
onCheckedChange={(checked) => updateConfig('notice.banner.enabled', checked)}
|
|
/>
|
|
</div>
|
|
|
|
{config.notice.banner.enabled && (
|
|
<>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="banner-text-zh">中文横幅文本</Label>
|
|
<Input
|
|
id="banner-text-zh"
|
|
value={config.notice.banner.text["zh-CN"]}
|
|
onChange={(e) => updateConfig('notice.banner.text.zh-CN', e.target.value)}
|
|
placeholder="输入中文横幅文本"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="banner-text-en">English Banner Text</Label>
|
|
<Input
|
|
id="banner-text-en"
|
|
value={config.notice.banner.text["en"]}
|
|
onChange={(e) => updateConfig('notice.banner.text.en', e.target.value)}
|
|
placeholder="Enter English banner text"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Maintenance Configuration */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Cpu className="h-5 w-5" />
|
|
维护配置
|
|
</CardTitle>
|
|
<CardDescription>系统维护设置</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="maintenance-enabled">启用维护模式</Label>
|
|
<Switch
|
|
id="maintenance-enabled"
|
|
checked={config.maintenance.window.enabled}
|
|
onCheckedChange={(checked) => updateConfig('maintenance.window.enabled', checked)}
|
|
/>
|
|
</div>
|
|
|
|
{config.maintenance.window.enabled && (
|
|
<>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="maintenance-start">维护开始时间</Label>
|
|
<Input
|
|
id="maintenance-start"
|
|
type="datetime-local"
|
|
value={config.maintenance.window.start_time.replace('Z', '')}
|
|
onChange={(e) => updateConfig('maintenance.window.start_time', e.target.value + 'Z')}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="maintenance-end">维护结束时间</Label>
|
|
<Input
|
|
id="maintenance-end"
|
|
type="datetime-local"
|
|
value={config.maintenance.window.end_time.replace('Z', '')}
|
|
onChange={(e) => updateConfig('maintenance.window.end_time', e.target.value + 'Z')}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="maintenance-message-zh">中文维护消息</Label>
|
|
<Textarea
|
|
id="maintenance-message-zh"
|
|
value={config.maintenance.window.message["zh-CN"]}
|
|
onChange={(e) => updateConfig('maintenance.window.message.zh-CN', e.target.value)}
|
|
placeholder="输入中文维护消息"
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="maintenance-message-en">English Maintenance Message</Label>
|
|
<Textarea
|
|
id="maintenance-message-en"
|
|
value={config.maintenance.window.message["en"]}
|
|
onChange={(e) => updateConfig('maintenance.window.message.en', e.target.value)}
|
|
placeholder="Enter English maintenance message"
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Operations Configuration */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Zap className="h-5 w-5" />
|
|
运营配置
|
|
</CardTitle>
|
|
<CardDescription>运营功能设置</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="registration-enabled">启用用户注册</Label>
|
|
<Switch
|
|
id="registration-enabled"
|
|
checked={config.ops.features.registration_enabled}
|
|
onCheckedChange={(checked) => updateConfig('ops.features.registration_enabled', checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="invite-code-required">需要邀请码</Label>
|
|
<Switch
|
|
id="invite-code-required"
|
|
checked={config.ops.features.invite_code_required}
|
|
onCheckedChange={(checked) => updateConfig('ops.features.invite_code_required', checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="email-verification">邮箱验证</Label>
|
|
<Switch
|
|
id="email-verification"
|
|
checked={config.ops.features.email_verification}
|
|
onCheckedChange={(checked) => updateConfig('ops.features.email_verification', checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="max-users">最大用户数: {config.ops.limits.max_users}</Label>
|
|
<Slider
|
|
id="max-users"
|
|
value={[config.ops.limits.max_users]}
|
|
onValueChange={(value) => updateConfig('ops.limits.max_users', value[0])}
|
|
min={100}
|
|
max={10000}
|
|
step={100}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="max-invite-codes">每用户最大邀请码数: {config.ops.limits.max_invite_codes_per_user}</Label>
|
|
<Slider
|
|
id="max-invite-codes"
|
|
value={[config.ops.limits.max_invite_codes_per_user]}
|
|
onValueChange={(value) => updateConfig('ops.limits.max_invite_codes_per_user', value[0])}
|
|
min={1}
|
|
max={50}
|
|
step={1}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="session-timeout">会话超时: {config.ops.limits.session_timeout_hours}小时</Label>
|
|
<Slider
|
|
id="session-timeout"
|
|
value={[config.ops.limits.session_timeout_hours]}
|
|
onValueChange={(value) => updateConfig('ops.limits.session_timeout_hours', value[0])}
|
|
min={1}
|
|
max={168}
|
|
step={1}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Settings className="h-5 w-5" />
|
|
配置操作
|
|
</CardTitle>
|
|
<CardDescription>保存、重置和导出配置</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex flex-wrap gap-3">
|
|
<Button className="flex items-center gap-2" onClick={() => {
|
|
// console.log('Applying configuration:', config)
|
|
alert('配置应用成功!')
|
|
}}>
|
|
<Zap className="h-4 w-4" />
|
|
应用所有设置
|
|
</Button>
|
|
<Button variant="outline" onClick={() => setConfig(defaultConfig)}>
|
|
重置为默认值
|
|
</Button>
|
|
<Button variant="outline" onClick={() => {
|
|
const dataStr = JSON.stringify(config, null, 2)
|
|
const dataBlob = new Blob([dataStr], { type: 'application/json' })
|
|
const url = URL.createObjectURL(dataBlob)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = 'config.json'
|
|
link.click()
|
|
URL.revokeObjectURL(url)
|
|
}}>
|
|
导出配置
|
|
</Button>
|
|
<Button variant="outline" onClick={() => {
|
|
const input = document.createElement('input')
|
|
input.type = 'file'
|
|
input.accept = '.json'
|
|
input.onchange = (e) => {
|
|
const file = (e.target as HTMLInputElement).files?.[0]
|
|
if (file) {
|
|
const reader = new FileReader()
|
|
reader.onload = (e) => {
|
|
try {
|
|
const importedConfig = JSON.parse(e.target?.result as string)
|
|
setConfig(importedConfig)
|
|
} catch (error) {
|
|
console.error('Failed to parse config file:', error)
|
|
alert('无效的配置文件')
|
|
}
|
|
}
|
|
reader.readAsText(file)
|
|
}
|
|
}
|
|
input.click()
|
|
}}>
|
|
导入配置
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</ScrollArea>
|
|
)
|
|
}
|