将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。 当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包), M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
99 lines
4.2 KiB
TypeScript
99 lines
4.2 KiB
TypeScript
'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>
|
||
);
|
||
}
|