refactor(web): router-based navigation, light theme, form config
- Replace Zustand activeView with React Router (NavLink + Routes) - White/light modern theme with indigo accents - Sidebar with Chinese labels under icons - ConfigForm with individual form fields (no JSON textareas) - Account switching with context injection into chat - Fix duplicate conversation creation with useRef guard - Asset gallery: smaller 6-column grid with date labels - All components updated to light color scheme Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,56 +1,189 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
export function ConfigForm() {
|
||||
const [configs, setConfigs] = useState<Record<string, string>>({});
|
||||
const [form, setForm] = useState({
|
||||
model: '',
|
||||
baseUrl: '',
|
||||
authToken: '',
|
||||
defaultImageModel: '',
|
||||
defaultVideoModel: '',
|
||||
defaultFormat: '',
|
||||
ossEndpoint: '',
|
||||
ossBucket: '',
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api.getConfigs().then((list) => {
|
||||
const map: Record<string, string> = {};
|
||||
list.forEach((c) => { map[c.key] = JSON.stringify(c.value, null, 2); });
|
||||
setConfigs(map);
|
||||
const next = { ...form };
|
||||
for (const item of list) {
|
||||
try {
|
||||
const v = item.value as Record<string, string>;
|
||||
if (item.key === 'api_keys') {
|
||||
if (v.ANTHROPIC_MODEL) next.model = v.ANTHROPIC_MODEL;
|
||||
if (v.ANTHROPIC_BASE_URL) next.baseUrl = v.ANTHROPIC_BASE_URL;
|
||||
if (v.ANTHROPIC_AUTH_TOKEN) next.authToken = v.ANTHROPIC_AUTH_TOKEN;
|
||||
}
|
||||
if (item.key === 'defaults') {
|
||||
if (v.imageModel) next.defaultImageModel = v.imageModel;
|
||||
if (v.videoModel) next.defaultVideoModel = v.videoModel;
|
||||
if (v.format) next.defaultFormat = v.format;
|
||||
}
|
||||
if (item.key === 'endpoints') {
|
||||
if (v.ossEndpoint) next.ossEndpoint = v.ossEndpoint;
|
||||
if (v.ossBucket) next.ossBucket = v.ossBucket;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
setForm(next);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSave = async (key: string, raw: string) => {
|
||||
const handleChange = (key: string, value: string) => {
|
||||
setForm((f) => ({ ...f, [key]: value }));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.saveConfig(key, JSON.parse(raw));
|
||||
} catch { alert('Invalid JSON'); }
|
||||
await api.saveConfig('api_keys', {
|
||||
ANTHROPIC_MODEL: form.model,
|
||||
ANTHROPIC_BASE_URL: form.baseUrl,
|
||||
ANTHROPIC_AUTH_TOKEN: form.authToken,
|
||||
});
|
||||
await api.saveConfig('defaults', {
|
||||
imageModel: form.defaultImageModel,
|
||||
videoModel: form.defaultVideoModel,
|
||||
format: form.defaultFormat,
|
||||
});
|
||||
await api.saveConfig('endpoints', {
|
||||
ossEndpoint: form.ossEndpoint,
|
||||
ossBucket: form.ossBucket,
|
||||
});
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
} catch {}
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const configKeys = [
|
||||
{ key: 'api_keys', label: 'API 密钥' },
|
||||
{ key: 'defaults', label: '默认参数' },
|
||||
{ key: 'endpoints', label: '服务端点' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="max-w-lg p-6 space-y-6">
|
||||
<h2 className="text-lg font-semibold">设置</h2>
|
||||
{configKeys.map(({ key, label }) => (
|
||||
<div key={key}>
|
||||
<label className="text-xs text-zinc-500">{label}</label>
|
||||
<textarea
|
||||
value={configs[key] || '{}'}
|
||||
onChange={(e) => setConfigs((c) => ({ ...c, [key]: e.target.value }))}
|
||||
rows={6}
|
||||
className="mt-1 w-full rounded-md border border-zinc-800 bg-zinc-900 px-3 py-2 text-sm font-mono resize-y"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="mt-1"
|
||||
disabled={saving}
|
||||
onClick={() => handleSave(key, configs[key])}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<div className="max-w-xl mx-auto p-6 space-y-8 overflow-auto">
|
||||
<h2 className="text-lg font-semibold text-zinc-800">设置</h2>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-zinc-500 uppercase tracking-wide">API 配置</h3>
|
||||
<div className="space-y-3 bg-white rounded-lg border border-zinc-200 p-4">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-zinc-500">模型</label>
|
||||
<Input
|
||||
value={form.model}
|
||||
onChange={(e) => handleChange('model', e.target.value)}
|
||||
placeholder="deepseek-v4-pro[1m]"
|
||||
className="mt-1 bg-zinc-50 border-zinc-200"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-zinc-500">Base URL</label>
|
||||
<Input
|
||||
value={form.baseUrl}
|
||||
onChange={(e) => handleChange('baseUrl', e.target.value)}
|
||||
placeholder="https://api.deepseek.com/anthropic"
|
||||
className="mt-1 bg-zinc-50 border-zinc-200 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-zinc-500">Auth Token</label>
|
||||
<Input
|
||||
value={form.authToken}
|
||||
onChange={(e) => handleChange('authToken', e.target.value)}
|
||||
type="password"
|
||||
placeholder="sk-..."
|
||||
className="mt-1 bg-zinc-50 border-zinc-200 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-zinc-500 uppercase tracking-wide">默认参数</h3>
|
||||
<div className="space-y-3 bg-white rounded-lg border border-zinc-200 p-4">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-zinc-500">默认生图模型</label>
|
||||
<select
|
||||
value={form.defaultImageModel}
|
||||
onChange={(e) => handleChange('defaultImageModel', e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm"
|
||||
>
|
||||
<option value="">未设置</option>
|
||||
<option value="gemini">Gemini</option>
|
||||
<option value="mj">Midjourney</option>
|
||||
<option value="gpt">GPT Image</option>
|
||||
<option value="kling">Kling</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-zinc-500">默认视频模型</label>
|
||||
<select
|
||||
value={form.defaultVideoModel}
|
||||
onChange={(e) => handleChange('defaultVideoModel', e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm"
|
||||
>
|
||||
<option value="">未设置</option>
|
||||
<option value="veo3-fast">Veo3 Fast</option>
|
||||
<option value="veo3-fast-frames">Veo3 Fast Frames</option>
|
||||
<option value="kling">Kling</option>
|
||||
<option value="grok">Grok</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-zinc-500">默认画幅</label>
|
||||
<select
|
||||
value={form.defaultFormat}
|
||||
onChange={(e) => handleChange('defaultFormat', e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm"
|
||||
>
|
||||
<option value="">未设置</option>
|
||||
<option value="9:16">9:16 竖屏</option>
|
||||
<option value="16:9">16:9 横屏</option>
|
||||
<option value="1:1">1:1 方形</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-zinc-500 uppercase tracking-wide">OSS 存储</h3>
|
||||
<div className="space-y-3 bg-white rounded-lg border border-zinc-200 p-4">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-zinc-500">Endpoint</label>
|
||||
<Input
|
||||
value={form.ossEndpoint}
|
||||
onChange={(e) => handleChange('ossEndpoint', e.target.value)}
|
||||
placeholder="https://oss-cn-hangzhou.aliyuncs.com"
|
||||
className="mt-1 bg-zinc-50 border-zinc-200 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-zinc-500">Bucket</label>
|
||||
<Input
|
||||
value={form.ossBucket}
|
||||
onChange={(e) => handleChange('ossBucket', e.target.value)}
|
||||
placeholder="my-bucket"
|
||||
className="mt-1 bg-zinc-50 border-zinc-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Button onClick={handleSave} disabled={saving} className="w-full">
|
||||
{saved ? '已保存' : saving ? '保存中...' : '保存设置'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user