mosaicmap/app/admin/common/control.tsx
2025-08-19 22:49:28 +08:00

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>
)
}