baseline: Clover 独立仓库首次基线提交
将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。 当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包), M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
98
frontend/src/components/config/ApiKeysTab.tsx
Normal file
98
frontend/src/components/config/ApiKeysTab.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
'use client';
|
||||
// ApiKeysTab — API Key 录入(只录不显余额,CLAUDE.md红线)
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
import { ApiKey, CreateApiKeyRequest, PaginatedResponse, ApiError } from '@/types/index';
|
||||
import { getErrorAction } from '@/types/errors';
|
||||
|
||||
export function ApiKeysTab() {
|
||||
const [keys, setKeys] = useState<ApiKey[]>([]);
|
||||
const [form, setForm] = useState<CreateApiKeyRequest>({ provider: 'openai', api_key: '' });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => { loadKeys(); }, []);
|
||||
|
||||
async function loadKeys() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await api.get<PaginatedResponse<ApiKey>>('/api/v1/api-keys');
|
||||
setKeys(data.items);
|
||||
} catch { /* silent */ } finally { setLoading(false); }
|
||||
}
|
||||
|
||||
async function handleAdd(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!form.api_key.trim()) { setError('请填写 API Key'); return; }
|
||||
setSaving(true); setError('');
|
||||
try {
|
||||
await api.post('/api/v1/api-keys', form);
|
||||
setForm({ provider: 'openai', api_key: '' });
|
||||
await loadKeys();
|
||||
} catch (err) {
|
||||
const apiErr = err as ApiError;
|
||||
setError(apiErr?.message || getErrorAction(apiErr?.code).message);
|
||||
} finally { setSaving(false); }
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
if (!confirm('确认删除此 Key?')) return;
|
||||
try {
|
||||
await api.delete(`/api/v1/api-keys/${id}`);
|
||||
setKeys((ks) => ks.filter((k) => k.id !== id));
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm text-text-secondary bg-brand-cream-dark px-4 py-3 rounded-lg">
|
||||
API Key 由各人自录,余额和用量请到中转站后台查看。
|
||||
</p>
|
||||
<form onSubmit={handleAdd} className="flex gap-3 items-end">
|
||||
<div>
|
||||
<label htmlFor="provider" className="block text-sm font-medium text-text-primary mb-1">Provider</label>
|
||||
<select id="provider" value={form.provider}
|
||||
onChange={(e) => setForm((f) => ({ ...f, provider: e.target.value }))}
|
||||
className="border border-border-default rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand-orange">
|
||||
<option value="openai">OpenAI (gpt-image-2)</option>
|
||||
<option value="gemini">Gemini</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label htmlFor="api-key-input" className="block text-sm font-medium text-text-primary mb-1">API Key</label>
|
||||
<input id="api-key-input" type="password" value={form.api_key}
|
||||
onChange={(e) => setForm((f) => ({ ...f, api_key: e.target.value }))}
|
||||
placeholder="sk-..."
|
||||
className="w-full border border-border-default rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand-orange"
|
||||
aria-label="输入 API Key"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" disabled={saving} aria-label="保存 API Key"
|
||||
className="bg-brand-orange text-white rounded-lg px-4 py-2 text-sm font-medium hover:bg-orange-600 disabled:opacity-50">
|
||||
{saving ? '保存中…' : '保存'}
|
||||
</button>
|
||||
</form>
|
||||
{error && <p role="alert" className="text-status-error text-sm">{error}</p>}
|
||||
{loading ? (
|
||||
<div className="text-text-secondary text-sm">加载中…</div>
|
||||
) : (
|
||||
<ul className="divide-y divide-border-default" aria-label="已录入的 API Key 列表">
|
||||
{keys.map((k) => (
|
||||
<li key={k.id} className="flex items-center justify-between py-3 text-sm">
|
||||
<span>
|
||||
<span className="font-medium">{k.provider}</span>
|
||||
<span className="text-text-secondary ml-2">…{k.key_last4}</span>
|
||||
</span>
|
||||
<button onClick={() => handleDelete(k.id)} aria-label={`删除 ${k.provider} Key`}
|
||||
className="text-status-error text-xs hover:underline">删除</button>
|
||||
</li>
|
||||
))}
|
||||
{keys.length === 0 && (
|
||||
<li className="py-3 text-text-tertiary text-sm">暂无 Key,请先录入</li>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user