feat(web): add account list and form components
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
135
web/client/src/components/accounts/AccountForm.tsx
Normal file
135
web/client/src/components/accounts/AccountForm.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import type { Account } from '@/types';
|
||||
|
||||
interface Props {
|
||||
account?: Account;
|
||||
onSave: (data: Partial<Account>) => void;
|
||||
onDelete?: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function AccountForm({ account, onSave, onDelete, onCancel }: Props) {
|
||||
const [form, setForm] = useState({
|
||||
id: account?.id || '',
|
||||
name: account?.name || '',
|
||||
description: account?.description || '',
|
||||
defaultFormat: account?.defaultFormat || '9:16',
|
||||
imageModel: account?.imageModel || 'gemini',
|
||||
videoModel: account?.videoModel || 'veo3-fast',
|
||||
ttsVoice: account?.ttsVoice || '',
|
||||
ttsInstruction: account?.ttsInstruction || '',
|
||||
});
|
||||
|
||||
const handleChange = (key: string, value: string) => setForm((f) => ({ ...f, [key]: value }));
|
||||
|
||||
return (
|
||||
<div className="max-w-lg space-y-4">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{account ? `编辑账户: ${account.name}` : '创建新账户'}
|
||||
</h2>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-zinc-500">账户 ID</label>
|
||||
<Input
|
||||
value={form.id}
|
||||
onChange={(e) => handleChange('id', e.target.value)}
|
||||
disabled={!!account}
|
||||
placeholder="my-account"
|
||||
className="mt-1 bg-zinc-900 border-zinc-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-zinc-500">名称</label>
|
||||
<Input
|
||||
value={form.name}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
placeholder="账户名称"
|
||||
className="mt-1 bg-zinc-900 border-zinc-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-zinc-500">描述</label>
|
||||
<Input
|
||||
value={form.description}
|
||||
onChange={(e) => handleChange('description', e.target.value)}
|
||||
placeholder="简短描述..."
|
||||
className="mt-1 bg-zinc-900 border-zinc-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-xs 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-800 bg-zinc-900 px-3 text-sm"
|
||||
>
|
||||
<option value="9:16">9:16 竖屏</option>
|
||||
<option value="16:9">16:9 横屏</option>
|
||||
<option value="1:1">1:1 方形</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-zinc-500">生图模型</label>
|
||||
<select
|
||||
value={form.imageModel}
|
||||
onChange={(e) => handleChange('imageModel', e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-md border border-zinc-800 bg-zinc-900 px-3 text-sm"
|
||||
>
|
||||
<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 text-zinc-500">视频模型</label>
|
||||
<select
|
||||
value={form.videoModel}
|
||||
onChange={(e) => handleChange('videoModel', e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-md border border-zinc-800 bg-zinc-900 px-3 text-sm"
|
||||
>
|
||||
<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>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-zinc-500">TTS 语音</label>
|
||||
<Input
|
||||
value={form.ttsVoice}
|
||||
onChange={(e) => handleChange('ttsVoice', e.target.value)}
|
||||
placeholder="cosyvoice-xxx"
|
||||
className="mt-1 bg-zinc-900 border-zinc-800 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-zinc-500">TTS 指令</label>
|
||||
<textarea
|
||||
value={form.ttsInstruction}
|
||||
onChange={(e) => handleChange('ttsInstruction', e.target.value)}
|
||||
rows={3}
|
||||
placeholder="用沉稳有力的男性声音朗读..."
|
||||
className="mt-1 w-full rounded-md border border-zinc-800 bg-zinc-900 px-3 py-2 text-sm resize-y"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button onClick={() => onSave(form)}>保存</Button>
|
||||
{onDelete && (
|
||||
<Button variant="destructive" onClick={onDelete}>删除</Button>
|
||||
)}
|
||||
<Button variant="ghost" onClick={onCancel}>取消</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,61 @@
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useAccounts } from '@/hooks/useAccounts';
|
||||
import { AccountForm } from './AccountForm';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import type { Account } from '@/types';
|
||||
|
||||
export function AccountList() {
|
||||
return <div className="p-6 text-zinc-500">账户管理</div>;
|
||||
const { accounts, create, update, remove } = useAccounts();
|
||||
const [editing, setEditing] = useState<Account | null>(null);
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
<ScrollArea className="w-60 border-r border-zinc-800 p-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-medium text-zinc-300">账户列表</h2>
|
||||
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => setCreating(true)}>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
{accounts.map((a) => (
|
||||
<button
|
||||
key={a.id}
|
||||
onClick={() => setEditing(a)}
|
||||
className="w-full text-left px-3 py-2 rounded-md text-sm hover:bg-zinc-800/50 text-zinc-400 hover:text-zinc-200 transition-colors"
|
||||
>
|
||||
<div className="font-medium truncate">{a.name}</div>
|
||||
<div className="text-xs text-zinc-600">{a.imageModel} · {a.defaultFormat}</div>
|
||||
</button>
|
||||
))}
|
||||
</ScrollArea>
|
||||
|
||||
<div className="flex-1 p-6">
|
||||
{(editing || creating) ? (
|
||||
<AccountForm
|
||||
account={creating ? undefined : editing!}
|
||||
onSave={(data) => {
|
||||
if (creating) {
|
||||
create(data).then(() => setCreating(false));
|
||||
} else {
|
||||
update(editing!.id, data).then(() => setEditing(null));
|
||||
}
|
||||
}}
|
||||
onDelete={editing ? () => {
|
||||
if (confirm(`确定删除账户「${editing.name}」?`)) {
|
||||
remove(editing.id).then(() => setEditing(null));
|
||||
}
|
||||
} : undefined}
|
||||
onCancel={() => { setEditing(null); setCreating(false); }}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-zinc-600 text-sm">
|
||||
选择一个账户或创建新账户
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
20
web/client/src/hooks/useAccounts.ts
Normal file
20
web/client/src/hooks/useAccounts.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
import type { Account } from '@/types';
|
||||
|
||||
export function useAccounts() {
|
||||
const [accounts, setAccounts] = useState<Account[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
api.listAccounts().then(setAccounts).finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => { refresh(); }, [refresh]);
|
||||
|
||||
const create = (data: Partial<Account>) => api.createAccount(data).then(refresh);
|
||||
const update = (id: string, data: Partial<Account>) => api.updateAccount(id, data).then(refresh);
|
||||
const remove = (id: string) => api.deleteAccount(id).then(refresh);
|
||||
|
||||
return { accounts, loading, refresh, create, update, remove };
|
||||
}
|
||||
Reference in New Issue
Block a user