Files
goods/public-frontend/src/components/Account.tsx
T
rosemariejebbjtxbfp d58f46bc80
CI / Go (api) (pull_request) Failing after 32s
CI / Python (ingestion) (pull_request) Failing after 31s
CI / Migrations (postgres) (pull_request) Failing after 32s
feat(public-frontend): 公开前端 UI 视觉升级
引入统一品牌主题(色阶/字体/阴影/动效),重做首页 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>
2026-06-24 03:16:41 +00:00

193 lines
6.8 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.
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>
);
}