Files
beige/frontend/src/components/config/ApiKeysTab.tsx
yangqianqian 6a2632da70 baseline: Clover 独立仓库首次基线提交
将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。
当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包),
M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 11:30:22 +08:00

99 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}