Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 43 additions & 38 deletions apps/src/app/accounts/accounts-page-helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export interface QuotaProgressProps {

export interface QuotaSummaryItem extends QuotaProgressProps {
id: string;
resetDurationMode?: "hours" | "days";
}

export interface AccountEditorState {
Expand All @@ -137,6 +138,7 @@ export interface AccountEditorState {
currentTags: string;
currentNote: string;
currentSort: number;
currentForceEnabled: boolean;
currentQuotaPrimaryWindowTokens: number | null;
currentQuotaSecondaryWindowTokens: number | null;
}
Expand Down Expand Up @@ -216,27 +218,29 @@ function QuotaProgress({

export function QuotaOverviewCell({ items }: { items: QuotaSummaryItem[] }) {
const { t } = useI18n();
const summaryItems = items.slice(0, 2);

return (
<Tooltip>
<TooltipTrigger render={<div />} className="block min-w-0 cursor-help">
<div className="rounded-xl border border-primary/5 bg-accent/10 px-3 py-2.5">
<div className="flex items-center gap-3">
{summaryItems.map((item) => (
<div key={item.id} className="min-w-0 flex-1 space-y-1">
<div className="flex items-center justify-between text-[11px] leading-4">
<div className="account-pool-quota-grid">
{items.map((item) => (
<div
key={item.id}
className="account-pool-quota-item min-w-0 rounded-lg border border-border/40 bg-background/20 p-2"
>
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-start gap-1 text-[11px] leading-4">
<span
className={fitLongTextClassName(
item.label,
"min-w-0 max-w-full break-words text-muted-foreground [overflow-wrap:anywhere]",
"min-w-0 break-words text-muted-foreground [overflow-wrap:anywhere]",
"text-[11px]",
)}
title={item.label}
>
{item.label}
</span>
<span className="font-medium text-foreground/80">
<span className="shrink-0 whitespace-nowrap font-medium text-foreground/80">
{item.remainPercent == null
? (item.emptyText ?? "--")
: `${item.remainPercent}%`}
Expand All @@ -259,38 +263,32 @@ export function QuotaOverviewCell({ items }: { items: QuotaSummaryItem[] }) {
: "bg-green-500"
}
/>
</div>
))}
</div>
<div className="mt-1.5 grid grid-cols-2 gap-3 text-[11px] text-muted-foreground">
{summaryItems.map((item) => (
<div
key={`${item.id}-reset`}
className="min-w-0 space-y-0.5"
>
<span
className={fitLongTextClassName(
formatTsFromSeconds(
<div className="mt-1.5 space-y-0.5 text-[11px] text-muted-foreground">
<span
className={fitLongTextClassName(
formatTsFromSeconds(
item.resetsAt,
item.emptyResetText ?? t("未知"),
),
"block min-w-0 max-w-full break-all leading-tight [overflow-wrap:anywhere]",
"text-[10px]",
)}
>
{formatTsFromSeconds(
item.resetsAt,
item.emptyResetText ?? t("未知"),
)}
</span>
<span className="block min-w-0 max-w-full break-words whitespace-normal leading-tight text-foreground/70 [overflow-wrap:anywhere]">
{formatRemainingDurationFromSeconds(
item.resetsAt,
item.resetDurationMode ??
(item.id.endsWith("-primary") ? "hours" : "days"),
item.emptyResetText ?? t("未知"),
),
"block min-w-0 break-words leading-tight [overflow-wrap:anywhere]",
"text-[11px]",
)}
>
{formatTsFromSeconds(
item.resetsAt,
item.emptyResetText ?? t("未知"),
)}
</span>
<span className="block whitespace-nowrap leading-tight text-foreground/70">
{formatRemainingDurationFromSeconds(
item.resetsAt,
item.id.endsWith("-primary") ? "hours" : "days",
item.emptyResetText ?? t("未知"),
)}
{t("后刷新")}
</span>
)}
{t("后刷新")}
</span>
</div>
</div>
))}
</div>
Expand Down Expand Up @@ -438,6 +436,8 @@ export function formatAccountStatusReasonLabel(
return t("工作区已停用");
case "usage_limit_exhausted":
return t("额度已耗尽");
case "manual_force_enable":
return t("手动强制开启");
default:
return reasonCode;
}
Expand Down Expand Up @@ -465,7 +465,7 @@ export function AccountStatusCell({ account }: { account: Account }) {
/>
<span
className={cn(
"min-w-0 max-w-full break-words whitespace-normal text-xs font-semibold leading-4",
"min-w-0 max-w-full break-words whitespace-normal text-xs font-semibold leading-4 [overflow-wrap:anywhere]",
account.isAvailable
? "text-green-600 dark:text-green-400"
: "text-red-600 dark:text-red-400",
Expand Down Expand Up @@ -714,6 +714,7 @@ export function buildQuotaSummaryItems(
caption: t("标准模型窗口"),
emptyText: secondaryWindowOnly ? t("未提供") : "--",
emptyResetText: secondaryWindowOnly ? t("未提供") : t("未知"),
resetDurationMode: "hours",
},
{
id: `${account.id}-secondary`,
Expand All @@ -725,6 +726,7 @@ export function buildQuotaSummaryItems(
caption: t("长周期窗口"),
emptyText: primaryWindowOnly ? t("未提供") : "--",
emptyResetText: primaryWindowOnly ? t("未提供") : t("未知"),
resetDurationMode: "days",
},
...extraUsageRows.map((item) => ({
id: item.id,
Expand All @@ -736,6 +738,9 @@ export function buildQuotaSummaryItems(
caption: t(item.windowLabel, item.windowLabelValues),
emptyText: "--",
emptyResetText: t("未知"),
resetDurationMode: item.windowLabel.includes("天")
? ("days" as const)
: ("hours" as const),
})),
];
}
Expand Down
66 changes: 60 additions & 6 deletions apps/src/app/accounts/accounts-page-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export interface AccountsPageViewProps {
tagsDraft: string;
noteDraft: string;
sortDraft: string;
forceEnabledDraft: boolean;
quotaPrimaryDraft: string;
quotaSecondaryDraft: string;
isRefreshingAllAccounts: boolean;
Expand Down Expand Up @@ -213,6 +214,7 @@ export interface AccountsPageViewProps {
setTagsDraft: Dispatch<SetStateAction<string>>;
setNoteDraft: Dispatch<SetStateAction<string>>;
setSortDraft: Dispatch<SetStateAction<string>>;
setForceEnabledDraft: Dispatch<SetStateAction<boolean>>;
setQuotaPrimaryDraft: Dispatch<SetStateAction<string>>;
setQuotaSecondaryDraft: Dispatch<SetStateAction<string>>;
setPage: Dispatch<SetStateAction<number>>;
Expand Down Expand Up @@ -256,6 +258,7 @@ export interface AccountsPageViewProps {
refreshAccount: (accountId: string) => void;
clearPreferredAccount: (accountId: string) => void;
setPreferredAccount: (accountId: string) => void;
toggleForceEnabled: (account: Account) => Promise<void>;
toggleAccountStatus: (
accountId: string,
enabled: boolean,
Expand Down Expand Up @@ -307,6 +310,7 @@ export function AccountsPageView(props: AccountsPageViewProps) {
tagsDraft,
noteDraft,
sortDraft,
forceEnabledDraft,
quotaPrimaryDraft,
quotaSecondaryDraft,
isRefreshingAllAccounts,
Expand Down Expand Up @@ -343,6 +347,7 @@ export function AccountsPageView(props: AccountsPageViewProps) {
setTagsDraft,
setNoteDraft,
setSortDraft,
setForceEnabledDraft,
setQuotaPrimaryDraft,
setQuotaSecondaryDraft,
setPage,
Expand Down Expand Up @@ -384,9 +389,13 @@ export function AccountsPageView(props: AccountsPageViewProps) {
onAccountTestFinished,
clearPreferredAccount,
setPreferredAccount,
toggleForceEnabled,
toggleAccountStatus,
} = props;

const forceToggleBlocked = ["disabled", "inactive", "unavailable", "banned"].includes(
String(currentEditingAccount?.status || "").trim().toLowerCase(),
);
const accountProxyBusy =
isProxySettingsLoading || isSavingAccountProxy || isClearingAccountProxy;
const selectedProxyProfile =
Expand Down Expand Up @@ -445,6 +454,18 @@ export function AccountsPageView(props: AccountsPageViewProps) {
const isAtListTop = accounts[0]?.id === account.id;
const isAtListBottom =
accounts[accounts.length - 1]?.id === account.id;
const normalizedAccountStatus = account.status.trim().toLowerCase();
const isForceEnabled = normalizedAccountStatus === "force_enabled";
const forceToggleBlocked = [
"disabled",
"inactive",
"unavailable",
"banned",
].includes(normalizedAccountStatus);
const isForceToggleBusy =
isUpdatingManyStatuses ||
isUpdatingProfileAccountId === account.id ||
isUpdatingStatusAccountId === account.id;

return (
<div className="table-action-cell gap-1">
Expand Down Expand Up @@ -565,6 +586,21 @@ export function AccountsPageView(props: AccountsPageViewProps) {
{t("测试账号")}
</DropdownMenuItem>
) : null}
<DropdownMenuItem
className="gap-2"
disabled={
!isServiceReady || forceToggleBlocked || isForceToggleBusy
}
onClick={() => void toggleForceEnabled(account)}
>
{isForceEnabled ? (
<PowerOff className="h-4 w-4" />
) : (
<Power className="h-4 w-4" />
)}
{isForceEnabled ? t("取消强制开启") : t("强制开启")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="gap-2"
disabled={
Expand Down Expand Up @@ -1092,7 +1128,7 @@ export function AccountsPageView(props: AccountsPageViewProps) {
<colgroup>
<col className="account-pool-col-select" />
<col className="account-pool-col-info" />
<col />
<col className="account-pool-col-quota" />
<col className="account-pool-col-order" />
<col className="account-pool-col-proxy" />
<col className="account-pool-col-status" />
Expand All @@ -1110,13 +1146,13 @@ export function AccountsPageView(props: AccountsPageViewProps) {
onCheckedChange={toggleSelectAllVisible}
/>
</TableHead>
<TableHead className="w-[clamp(280px,34vw,440px)] min-w-[280px] max-w-[440px] whitespace-normal">
<TableHead className="w-[360px] min-w-[320px] max-w-[360px] whitespace-normal">
{t("账号信息")}
</TableHead>
<TableHead className="min-w-[300px] text-center">
{t("额度详情")}
</TableHead>
<TableHead className="w-[132px]">{t("顺序")}</TableHead>
<TableHead className="w-[168px]">{t("顺序")}</TableHead>
<TableHead className="min-w-[180px]">{t("账号代理")}</TableHead>
<TableHead className="account-pool-status-head whitespace-normal">
{t("状态")}
Expand Down Expand Up @@ -1181,7 +1217,7 @@ export function AccountsPageView(props: AccountsPageViewProps) {
onCheckedChange={() => toggleSelect(account.id)}
/>
</TableCell>
<TableCell className="w-[clamp(280px,34vw,440px)] min-w-[280px] max-w-[440px] whitespace-normal align-top">
<TableCell className="w-[360px] min-w-[320px] max-w-[360px] whitespace-normal align-top">
<AccountInfoCell
account={account}
isPreferred={account.preferred}
Expand Down Expand Up @@ -1223,8 +1259,8 @@ export function AccountsPageView(props: AccountsPageViewProps) {
/>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<TableCell className="align-middle whitespace-nowrap">
<div className="flex min-w-max flex-nowrap items-center gap-1">
<span className="min-w-8 rounded-md bg-muted/60 px-2 py-1 text-center font-mono text-xs font-semibold tabular-nums">
{account.priority}
</span>
Expand Down Expand Up @@ -1270,6 +1306,7 @@ export function AccountsPageView(props: AccountsPageViewProps) {
isUpdatingProfileAccountId === account.id
}
onClick={() => openAccountEditor(account)}
aria-label={t("编辑账号信息")}
title={t("编辑账号信息")}
>
<PencilLine className="h-4 w-4" />
Expand Down Expand Up @@ -1724,6 +1761,23 @@ export function AccountsPageView(props: AccountsPageViewProps) {
/>
</div>
</div>
<div className="flex items-start justify-between gap-4 rounded-xl border border-amber-500/20 bg-amber-500/5 px-3 py-3">
<div className="min-w-0 space-y-1">
<Label htmlFor="account-force-enabled-switch">
{t("额度耗尽后仍使用账号")}
</Label>
<p className="text-[11px] leading-4 text-muted-foreground">
{t("开启后忽略 5h/7d 耗尽状态,继续把该账号加入网关候选;默认关闭。")}
</p>
</div>
<Switch
id="account-force-enabled-switch"
aria-label={t("额度耗尽后仍使用账号")}
checked={forceEnabledDraft}
disabled={Boolean(isUpdatingProfileAccountId) || forceToggleBlocked}
onCheckedChange={setForceEnabledDraft}
/>
</div>
<div className="grid gap-3 rounded-xl bg-muted/20 px-3 py-3 text-[11px] text-muted-foreground sm:grid-cols-2">
<div className="space-y-1">
<div>{t("账号 ID")}</div>
Expand Down
Loading