From 326582e25bbd8a0208c8ab945289446c6443aa2d Mon Sep 17 00:00:00 2001 From: cong <3135055939@qq.com> Date: Sat, 4 Apr 2026 22:39:07 +0800 Subject: [PATCH] feat(contribution): improve quota readability and add one-click key generate --- api/contribution.py | 26 +++++++++ frontend/src/pages/Settings.tsx | 97 ++++++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 19 deletions(-) diff --git a/api/contribution.py b/api/contribution.py index 10dcda7..2bc3c0e 100644 --- a/api/contribution.py +++ b/api/contribution.py @@ -35,6 +35,11 @@ class ContributionRedeemRequest(ContributionProxyRequest): amount_usd: float = Field(..., gt=0) +class ContributionGenerateKeyRequest(BaseModel): + server_url: str | None = None + name: str | None = None + + def _resolve_server_url(server_url: str | None) -> str: raw = str(server_url or config_store.get("contribution_server_url", "") or "").strip() if not raw: @@ -228,3 +233,24 @@ def redeem(body: ContributionRedeemRequest): "endpoint": "/public/redeem", "data": data, } + + +@router.post("/generate-key") +def generate_key(body: ContributionGenerateKeyRequest): + server_url = _resolve_server_url(body.server_url) + payload: dict[str, Any] | None = None + name = str(body.name or "").strip() + if name: + payload = {"name": name} + + data = _request_json( + "POST", + server_url, + "/public/generate", + payload=payload, + ) + return { + "ok": True, + "endpoint": "/public/generate", + "data": data, + } diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 818f883..1707caf 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -483,11 +483,17 @@ function pickNumber(value: Record | null, keys: string[]): numb return null } -function maskSecretValue(value: string): string { - const text = String(value || '').trim() - if (!text) return '-' - if (text.length <= 8) return `${'*'.repeat(Math.max(0, text.length - 2))}${text.slice(-2)}` - return `${text.slice(0, 4)}****${text.slice(-4)}` +function formatDisplayNumber(value: number | null, digits = 0): string { + if (value === null || !Number.isFinite(value)) return '-' + return value.toLocaleString('zh-CN', { + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }) +} + +function formatDisplayPercent(value: number | null): string { + if (value === null || !Number.isFinite(value)) return '-' + return `${value.toFixed(2)}%` } function ConfigField({ field }: { field: FieldConfig }) { @@ -1068,8 +1074,8 @@ function ContributionPanel({ }) { const [loadingStats, setLoadingStats] = useState(false) const [redeeming, setRedeeming] = useState(false) + const [creatingKey, setCreatingKey] = useState(false) const [redeemAmount, setRedeemAmount] = useState(CONTRIBUTION_REDEEM_OPTIONS[0]) - const [showKey, setShowKey] = useState(false) const [statsResponse, setStatsResponse] = useState | null>(null) const [redeemResponse, setRedeemResponse] = useState | null>(null) const [statsError, setStatsError] = useState('') @@ -1093,8 +1099,15 @@ function ContributionPanel({ const settlementAmount = pickNumber(keyInfo, ['settlement_amount_usd', 'settlement_amount', 'settled_amount_usd']) ?? pickNumber(rawData, ['settlement_amount_usd', 'settlement_amount']) + const serverQuotaAccountCount = pickNumber(serverInfo, ['quota_account_count']) + const serverQuotaTotal = pickNumber(serverInfo, ['quota_total']) + const serverQuotaUsed = pickNumber(serverInfo, ['quota_used']) + const serverQuotaRemaining = pickNumber(serverInfo, ['quota_remaining']) + const serverQuotaUsedPercent = pickNumber(serverInfo, ['quota_used_percent']) + const serverQuotaRemainingPercent = pickNumber(serverInfo, ['quota_remaining_percent']) + const serverQuotaRemainingAccounts = pickNumber(serverInfo, ['quota_remaining_accounts']) - const fetchStats = async (silent = false) => { + const fetchStats = async (silent = false, keyOverride?: string) => { if (!contributionEnabled) { if (!silent) message.warning('请先开启贡献功能') return @@ -1111,7 +1124,7 @@ function ContributionPanel({ method: 'POST', body: JSON.stringify({ server_url: contributionServerUrl, - key: contributionKey, + key: keyOverride ?? contributionKey, }), }) setStatsResponse(asRecord(data)) @@ -1139,7 +1152,7 @@ function ContributionPanel({ return } if (!contributionKey) { - message.error('请先填写贡献 key') + message.error('请先填写 API Key') return } @@ -1171,6 +1184,36 @@ function ContributionPanel({ }) } + const doGenerateKey = async () => { + if (!contributionServerUrl) { + message.error('请先填写服务器地址') + return + } + setCreatingKey(true) + try { + const result = await apiFetch('/contribution/generate-key', { + method: 'POST', + body: JSON.stringify({ + server_url: contributionServerUrl, + }), + }) + const payload = asRecord(asRecord(result)?.data) + const generated = pickString(payload, ['key', 'api_key', 'public_key']) + if (!generated) { + throw new Error('服务端未返回可用 key') + } + form.setFieldValue('contribution_key', generated) + message.success('已新建并填充 API Key,请点击保存配置') + if (contributionEnabled) { + await fetchStats(true, generated) + } + } catch (e: any) { + message.error(String(e?.message || '请求新建 key 失败')) + } finally { + setCreatingKey(false) + } + } + return (
@@ -1184,8 +1227,21 @@ function ContributionPanel({ > - - + + { void doGenerateKey() }} + style={{ paddingInline: 0 }} + > + 没有key?请求新建 + + )} + />