link0518
重构
b88ce1b
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Save, Server, Shield, Sliders, MessageSquare, AlertCircle, Loader2 } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import { cn } from '../lib/utils';
export default function Settings() {
const { token: adminToken } = useAuth();
const [settings, setSettings] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [message, setMessage] = useState({ type: '', content: '' });
useEffect(() => {
const fetchSettings = async () => {
try {
const res = await fetch('/admin/settings', {
headers: { 'X-Admin-Token': adminToken }
});
const data = await res.json();
// Map nested backend data to flat UI state
setSettings({
port: data.server?.port,
host: data.server?.host,
apiKey: data.security?.apiKey,
adminPassword: data.security?.adminPassword,
maxRequestSize: data.security?.maxRequestSize,
temperature: data.defaults?.temperature,
topP: data.defaults?.top_p,
topK: data.defaults?.top_k,
maxTokens: data.defaults?.max_tokens,
systemInstruction: data.systemInstruction
});
} catch (error) {
console.error('Failed to fetch settings', error);
setMessage({ type: 'error', content: '加载设置失败' });
} finally {
setIsLoading(false);
}
};
fetchSettings();
}, [adminToken]);
const handleChange = (key, value) => {
setSettings(prev => ({ ...prev, [key]: value }));
};
const handleSave = async () => {
setIsSaving(true);
setMessage({ type: '', content: '' });
try {
// Map flat UI state back to nested backend structure
const payload = {
server: {
port: settings.port,
host: settings.host
},
security: {
apiKey: settings.apiKey,
adminPassword: settings.adminPassword,
maxRequestSize: settings.maxRequestSize
},
defaults: {
temperature: settings.temperature,
top_p: settings.topP,
top_k: settings.topK,
max_tokens: settings.maxTokens
},
systemInstruction: settings.systemInstruction
};
const res = await fetch('/admin/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Admin-Token': adminToken
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.success) {
setMessage({ type: 'success', content: '设置保存成功' });
} else {
setMessage({ type: 'error', content: '保存失败' });
}
} catch (error) {
setMessage({ type: 'error', content: '保存失败: ' + error.message });
} finally {
setIsSaving(false);
}
};
if (isLoading) return <div className="p-12 text-center text-zinc-400">加载中...</div>;
return (
<div className="space-y-6 max-w-4xl mx-auto pb-12">
<div className="flex justify-between items-center sticky top-0 bg-zinc-50/90 backdrop-blur-sm py-4 z-10">
<div>
<h2 className="text-2xl font-semibold text-zinc-900 tracking-tight">系统设置</h2>
<p className="text-zinc-500">配置服务器参数和模型默认值</p>
</div>
<button
onClick={handleSave}
disabled={isSaving}
className="flex items-center gap-2 px-6 py-2.5 bg-zinc-900 hover:bg-zinc-800 text-white font-medium rounded-xl transition-colors disabled:opacity-50 shadow-sm"
>
{isSaving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
{isSaving ? '保存中...' : '保存设置'}
</button>
</div>
<AnimatePresence>
{message.content && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className={cn(
"flex items-center gap-2 p-4 rounded-xl text-sm font-medium border",
message.type === 'error'
? "bg-red-50 text-red-600 border-red-100"
: "bg-emerald-50 text-emerald-600 border-emerald-100"
)}
>
<AlertCircle className="w-4 h-4" />
{message.content}
</motion.div>
)}
</AnimatePresence>
{/* Server Config */}
<div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm">
<h3 className="font-semibold text-zinc-900 mb-6 flex items-center gap-2 text-base">
<Server className="w-5 h-5 text-zinc-900" />
服务器配置
</h3>
<div className="grid md:grid-cols-2 gap-6">
<FormInput
label="服务端口"
value={settings.port || ''}
onChange={v => handleChange('port', v)}
placeholder="8045"
type="number"
/>
<FormInput
label="监听地址"
value={settings.host || ''}
onChange={v => handleChange('host', v)}
placeholder="0.0.0.0"
/>
</div>
</div>
{/* Security Config */}
<div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm">
<h3 className="font-semibold text-zinc-900 mb-6 flex items-center gap-2 text-base">
<Shield className="w-5 h-5 text-zinc-900" />
安全配置
</h3>
<div className="space-y-6">
<FormInput
label="默认 API 密钥"
value={settings.apiKey || ''}
onChange={v => handleChange('apiKey', v)}
placeholder="sk-test"
helper="此密钥不受频率限制约束,用于测试或内部使用"
/>
<FormInput
label="管理员密码"
value={settings.adminPassword || ''}
onChange={v => handleChange('adminPassword', v)}
placeholder="admin123"
type="password"
/>
<FormInput
label="最大请求体大小"
value={settings.maxRequestSize || ''}
onChange={v => handleChange('maxRequestSize', v)}
placeholder="50mb"
/>
</div>
</div>
{/* Model Defaults */}
<div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm">
<h3 className="font-semibold text-zinc-900 mb-6 flex items-center gap-2 text-base">
<Sliders className="w-5 h-5 text-zinc-900" />
模型默认参数
</h3>
<div className="grid md:grid-cols-2 gap-6">
<FormInput
label="Temperature"
value={settings.temperature || ''}
onChange={v => handleChange('temperature', parseFloat(v))}
type="number" step="0.1" min="0" max="2"
/>
<FormInput
label="Top P"
value={settings.topP || ''}
onChange={v => handleChange('topP', parseFloat(v))}
type="number" step="0.01" min="0" max="1"
/>
<FormInput
label="Top K"
value={settings.topK || ''}
onChange={v => handleChange('topK', parseInt(v))}
type="number" min="1"
/>
<FormInput
label="最大 Token 数"
value={settings.maxTokens || ''}
onChange={v => handleChange('maxTokens', parseInt(v))}
type="number" min="1"
/>
</div>
</div>
{/* System Instruction */}
<div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm">
<h3 className="font-semibold text-zinc-900 mb-6 flex items-center gap-2 text-base">
<MessageSquare className="w-5 h-5 text-zinc-900" />
系统指令
</h3>
<div>
<label className="block text-sm font-medium text-zinc-700 mb-2">System Instruction</label>
<textarea
value={settings.systemInstruction || ''}
onChange={e => handleChange('systemInstruction', e.target.value)}
rows={5}
placeholder="输入系统提示词..."
className="w-full px-4 py-3 bg-zinc-50 border border-zinc-200 rounded-xl focus:ring-2 focus:ring-zinc-900/5 focus:border-zinc-900 outline-none transition-all resize-y text-sm placeholder:text-zinc-400"
/>
</div>
</div>
</div>
);
}
function FormInput({ label, value, onChange, type = "text", placeholder, helper, ...props }) {
return (
<div>
<label className="block text-sm font-medium text-zinc-700 mb-2">{label}</label>
<input
type={type}
value={value}
onChange={e => onChange(e.target.value)}
placeholder={placeholder}
className="w-full px-4 py-2.5 bg-zinc-50 border border-zinc-200 rounded-xl focus:ring-2 focus:ring-zinc-900/5 focus:border-zinc-900 outline-none transition-all text-sm placeholder:text-zinc-400"
{...props}
/>
{helper && <p className="mt-1.5 text-xs text-zinc-400">{helper}</p>}
</div>
);
}