d58f46bc80
引入统一品牌主题(色阶/字体/阴影/动效),重做首页 hero、卡片、导航与页脚,统一各页面的卡片/输入框/按钮样式。 - tailwind: 新增 brand 色阶、Inter 字体、card/glow 阴影与 fade-up 动效 - index.html: 引入 Inter 字体与 theme-color/description meta - index.css: 双径向渐变背景 + @layer 组件类(.card/.input/.btn-primary 等) - App/Home/ProductView/Contribute/ApiDocs/Account: 套用新设计系统 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
193 lines
6.8 KiB
TypeScript
193 lines
6.8 KiB
TypeScript
import { useState } from "react";
|
||
import { Check, Copy, KeyRound, AlertTriangle } from "lucide-react";
|
||
import { api, type AccountInfo, type KeyResponse } from "../api";
|
||
|
||
function KeyReveal({ data }: { data: KeyResponse }) {
|
||
const [copied, setCopied] = useState(false);
|
||
return (
|
||
<div className="mt-4 rounded-lg border border-brand-100 bg-brand-50 p-4">
|
||
<div className="flex items-start gap-2 text-amber-700 text-sm">
|
||
<AlertTriangle className="w-4 h-4 mt-0.5 shrink-0" />
|
||
<span>请立即复制保存此密钥,它只显示这一次,无法再次查看。</span>
|
||
</div>
|
||
<div className="mt-3 flex items-center gap-2">
|
||
<code className="flex-1 break-all bg-white border rounded-md px-3 py-2 text-sm font-mono text-gray-800">
|
||
{data.api_key}
|
||
</code>
|
||
<button
|
||
onClick={async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(data.api_key);
|
||
setCopied(true);
|
||
setTimeout(() => setCopied(false), 1200);
|
||
} catch {
|
||
/* clipboard unavailable */
|
||
}
|
||
}}
|
||
className="text-gray-400 hover:text-gray-600 shrink-0"
|
||
title="复制"
|
||
>
|
||
{copied ? <Check className="w-5 h-5 text-brand-600" /> : <Copy className="w-5 h-5" />}
|
||
</button>
|
||
</div>
|
||
<div className="mt-3 text-sm text-gray-600">
|
||
配额:每分钟 <strong>{data.rate_limit_per_min}</strong> 次 · 累计{" "}
|
||
<strong>{data.quota_total.toLocaleString()}</strong> 次
|
||
</div>
|
||
<div className="mt-2 text-xs text-gray-500">
|
||
调用时通过请求头携带:
|
||
<code className="font-mono">X-API-Key: {data.api_key.slice(0, 12)}…</code>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function Account() {
|
||
const [mode, setMode] = useState<"register" | "manage">("register");
|
||
const [email, setEmail] = useState("");
|
||
const [password, setPassword] = useState("");
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [issued, setIssued] = useState<KeyResponse | null>(null);
|
||
const [info, setInfo] = useState<AccountInfo | null>(null);
|
||
|
||
const reset = () => {
|
||
setError(null);
|
||
setIssued(null);
|
||
setInfo(null);
|
||
};
|
||
|
||
async function submit(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
reset();
|
||
if (password.length < 8) {
|
||
setError("密码至少需要 8 位");
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
try {
|
||
if (mode === "register") {
|
||
setIssued(await api.register(email, password));
|
||
} else {
|
||
setInfo(await api.account(email, password));
|
||
}
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "操作失败");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
async function regenerate() {
|
||
reset();
|
||
setLoading(true);
|
||
try {
|
||
setIssued(await api.regenerate(email, password));
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "操作失败");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="max-w-xl mx-auto space-y-5">
|
||
<div className="card p-5">
|
||
<h1 className="flex items-center gap-2 text-2xl font-bold text-gray-800">
|
||
<KeyRound className="w-6 h-6 text-brand-600" /> API 密钥
|
||
</h1>
|
||
<p className="mt-2 text-sm text-gray-600 leading-relaxed">
|
||
匿名调用免费,但每个来源 IP 累计共 <strong>1000</strong> 次。注册一个账号即可自助领取专属 API
|
||
密钥,获得更高的每分钟频率与累计调用配额。
|
||
</p>
|
||
|
||
<div className="mt-4 inline-flex rounded-md border bg-gray-50 p-0.5 text-sm">
|
||
<button
|
||
className={`px-4 py-1.5 rounded ${
|
||
mode === "register" ? "bg-white shadow-sm text-brand-700" : "text-gray-500"
|
||
}`}
|
||
onClick={() => {
|
||
setMode("register");
|
||
reset();
|
||
}}
|
||
>
|
||
注册领取
|
||
</button>
|
||
<button
|
||
className={`px-4 py-1.5 rounded ${
|
||
mode === "manage" ? "bg-white shadow-sm text-brand-700" : "text-gray-500"
|
||
}`}
|
||
onClick={() => {
|
||
setMode("manage");
|
||
reset();
|
||
}}
|
||
>
|
||
查看 / 重置
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={submit} className="mt-4 space-y-3">
|
||
<div>
|
||
<label className="block text-sm text-gray-600 mb-1">邮箱</label>
|
||
<input
|
||
type="email"
|
||
required
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
placeholder="you@example.com"
|
||
className="w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm text-gray-600 mb-1">密码(至少 8 位)</label>
|
||
<input
|
||
type="password"
|
||
required
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
placeholder="••••••••"
|
||
className="w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||
/>
|
||
</div>
|
||
{error && <div className="text-sm text-red-600">{error}</div>}
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
className="w-full bg-brand-600 text-white rounded-md py-2 text-sm font-medium hover:bg-brand-700 disabled:opacity-50"
|
||
>
|
||
{loading ? "处理中…" : mode === "register" ? "注册并领取密钥" : "查询账号"}
|
||
</button>
|
||
</form>
|
||
|
||
{issued && <KeyReveal data={issued} />}
|
||
|
||
{info && (
|
||
<div className="mt-4 rounded-lg border bg-gray-50 p-4 text-sm text-gray-700 space-y-1.5">
|
||
<div>
|
||
邮箱:<span className="font-medium">{info.email}</span>
|
||
</div>
|
||
<div>
|
||
密钥前缀:<code className="font-mono">{info.key_prefix}…</code>
|
||
</div>
|
||
<div>
|
||
每分钟频率:<strong>{info.rate_limit_per_min}</strong> 次
|
||
</div>
|
||
<div>
|
||
累计配额:已用 <strong>{info.quota_used.toLocaleString()}</strong> /{" "}
|
||
{info.quota_total.toLocaleString()} 次(剩余{" "}
|
||
<strong className="text-brand-600">{info.quota_remaining.toLocaleString()}</strong>)
|
||
</div>
|
||
<button
|
||
onClick={regenerate}
|
||
disabled={loading}
|
||
className="mt-2 text-brand-600 hover:underline disabled:opacity-50"
|
||
>
|
||
忘记密钥?重置并生成新密钥
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|