mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(codex-oauth): show per-account usage in Auth Center
Each ChatGPT (Codex OAuth) account under Settings → 认证 now displays its own subscription usage — reset countdowns and per-window progress bars — directly in the account list, instead of usage only being visible on the active provider card. - Add useCodexOauthQuotaByAccountId(accountId) and refactor useCodexOauthQuota to delegate to it (shared query key → cache reuse) - Add CodexOauthAccountQuota, a thin per-account wrapper that reuses the existing SubscriptionQuotaView expanded layout (same look and 5-state handling as provider cards), with a light spinner on first load - Render it under each account row in CodexOAuthSection; fetch once when the Auth Center opens, manual refresh available (no polling) Copilot is intentionally left out — same as before, this is Codex-only.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useCodexOauthQuotaByAccountId } from "@/lib/query/subscription";
|
||||
import { SubscriptionQuotaView } from "@/components/SubscriptionQuotaFooter";
|
||||
|
||||
interface CodexOauthAccountQuotaProps {
|
||||
/** cc-switch 自管的 ChatGPT 账号 ID */
|
||||
accountId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 → 认证中心里,单个 ChatGPT (Codex OAuth) 账号的用量展示。
|
||||
*
|
||||
* 直接按 accountId 查询 cc-switch 自管 OAuth token 的订阅额度,复用
|
||||
* `SubscriptionQuotaView` 的展开布局(进度条 + 重置倒计时 + 刷新按钮),
|
||||
* 因此与供应商卡片里的额度展示保持完全一致的观感与状态处理。
|
||||
*
|
||||
* 面板打开时拉取一次,不轮询;用户可点卡片内的刷新按钮手动更新。
|
||||
*/
|
||||
const CodexOauthAccountQuota: React.FC<CodexOauthAccountQuotaProps> = ({
|
||||
accountId,
|
||||
}) => {
|
||||
const {
|
||||
data: quota,
|
||||
isFetching: loading,
|
||||
refetch,
|
||||
} = useCodexOauthQuotaByAccountId(accountId, {
|
||||
enabled: true,
|
||||
autoQuery: false,
|
||||
});
|
||||
|
||||
// 首次加载占位:SubscriptionQuotaView 在 quota 未就绪时返回 null,
|
||||
// 这里给一个语言无关的 spinner,避免开面板后出现空白跳变。
|
||||
if (loading && !quota) {
|
||||
return (
|
||||
<div className="mt-3 flex items-center justify-center py-2 text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SubscriptionQuotaView
|
||||
quota={quota}
|
||||
loading={loading}
|
||||
refetch={refetch}
|
||||
appIdForExpiredHint="codex_oauth"
|
||||
inline={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodexOauthAccountQuota;
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useCodexOauth } from "./hooks/useCodexOauth";
|
||||
import { copyText } from "@/lib/clipboard";
|
||||
import CodexOauthAccountQuota from "@/components/CodexOauthAccountQuota";
|
||||
|
||||
interface CodexOAuthSectionProps {
|
||||
className?: string;
|
||||
@@ -178,47 +179,50 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
{accounts.map((account) => (
|
||||
<div
|
||||
key={account.id}
|
||||
className="flex items-center justify-between p-2 rounded-md border bg-muted/30"
|
||||
className="space-y-2 p-2 rounded-md border bg-muted/30"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">{account.login}</span>
|
||||
{defaultAccountId === account.id && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{t("codexOauth.defaultAccount", "默认")}
|
||||
</Badge>
|
||||
)}
|
||||
{selectedAccountId === account.id && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{t("codexOauth.selected", "已选中")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{defaultAccountId !== account.id && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">{account.login}</span>
|
||||
{defaultAccountId === account.id && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{t("codexOauth.defaultAccount", "默认")}
|
||||
</Badge>
|
||||
)}
|
||||
{selectedAccountId === account.id && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{t("codexOauth.selected", "已选中")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{defaultAccountId !== account.id && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs text-muted-foreground"
|
||||
onClick={() => setDefaultAccount(account.id)}
|
||||
disabled={isSettingDefaultAccount}
|
||||
>
|
||||
{t("codexOauth.setAsDefault", "设为默认")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs text-muted-foreground"
|
||||
onClick={() => setDefaultAccount(account.id)}
|
||||
disabled={isSettingDefaultAccount}
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-red-500"
|
||||
onClick={(e) => handleRemoveAccount(account.id, e)}
|
||||
disabled={isRemovingAccount}
|
||||
title={t("codexOauth.removeAccount", "移除账号")}
|
||||
>
|
||||
{t("codexOauth.setAsDefault", "设为默认")}
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-red-500"
|
||||
onClick={(e) => handleRemoveAccount(account.id, e)}
|
||||
disabled={isRemovingAccount}
|
||||
title={t("codexOauth.removeAccount", "移除账号")}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<CodexOauthAccountQuota accountId={account.id} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -45,20 +45,18 @@ export interface UseCodexOauthQuotaOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Codex OAuth (ChatGPT Plus/Pro 反代) 订阅额度查询 hook
|
||||
* Codex OAuth 订阅额度查询 hook(按账号 ID)
|
||||
*
|
||||
* 与 `useSubscriptionQuota` 平行:数据走 cc-switch 自管的 OAuth token,
|
||||
* 而不是 Codex CLI 的 ~/.codex/auth.json。
|
||||
*
|
||||
* Query key 包含 accountId,多张卡片绑定到同一账号时会自动去重共享请求。
|
||||
* 直接以 cc-switch 自管的 ChatGPT 账号 ID 查询额度,供认证中心里逐个账号
|
||||
* 展示用量时复用。Query key 与 `useCodexOauthQuota` 一致,绑定到同一账号的
|
||||
* 供应商卡片与账号列表会自动去重共享同一份请求缓存。
|
||||
* accountId 为 null 时使用 "default" 占位,让后端 fallback 到默认账号。
|
||||
*/
|
||||
export function useCodexOauthQuota(
|
||||
meta: ProviderMeta | undefined,
|
||||
export function useCodexOauthQuotaByAccountId(
|
||||
accountId: string | null,
|
||||
options: UseCodexOauthQuotaOptions = {},
|
||||
) {
|
||||
const { enabled = true, autoQuery = false } = options;
|
||||
const accountId = resolveManagedAccountId(meta, PROVIDER_TYPES.CODEX_OAUTH);
|
||||
return useQuery({
|
||||
queryKey: ["codex_oauth", "quota", accountId ?? "default"],
|
||||
queryFn: () => subscriptionApi.getCodexOauthQuota(accountId),
|
||||
@@ -70,3 +68,18 @@ export function useCodexOauthQuota(
|
||||
retry: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Codex OAuth (ChatGPT Plus/Pro 反代) 订阅额度查询 hook
|
||||
*
|
||||
* 与 `useSubscriptionQuota` 平行:数据走 cc-switch 自管的 OAuth token,
|
||||
* 而不是 Codex CLI 的 ~/.codex/auth.json。账号 ID 从供应商 meta 的
|
||||
* authBinding 中解析,再委托给 `useCodexOauthQuotaByAccountId`。
|
||||
*/
|
||||
export function useCodexOauthQuota(
|
||||
meta: ProviderMeta | undefined,
|
||||
options: UseCodexOauthQuotaOptions = {},
|
||||
) {
|
||||
const accountId = resolveManagedAccountId(meta, PROVIDER_TYPES.CODEX_OAUTH);
|
||||
return useCodexOauthQuotaByAccountId(accountId, options);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user