Spaces:
Paused
Paused
File size: 7,707 Bytes
4f2665c 4a940a5 b7d4394 268c5a4 4f2665c b7d4394 e6a1587 4ebb914 268c5a4 b7d4394 268c5a4 b7d4394 e6a1587 268c5a4 4f2665c 268c5a4 e6a1587 b7d4394 e6a1587 b7d4394 e6a1587 7516302 df56b50 268c5a4 e6a1587 b7d4394 4f2665c b7d4394 268c5a4 b7d4394 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | import { useState, useCallback, useEffect } from "preact/hooks";
import { useI18n, useT } from "../../../shared/i18n/context";
import { AccountCard } from "./AccountCard";
import { AccountImportExport } from "./AccountImportExport";
import type { Account, ProxyEntry, QuotaWarning } from "../../../shared/types";
interface AccountListProps {
accounts: Account[];
loading: boolean;
onDelete: (id: string) => Promise<string | null>;
onRefresh: () => void;
refreshing: boolean;
lastUpdated: Date | null;
proxies?: ProxyEntry[];
onProxyChange?: (accountId: string, proxyId: string) => void;
onExport?: (selectedIds?: string[]) => Promise<void>;
onImport?: (file: File) => Promise<{ success: boolean; added: number; updated: number; failed: number; errors: string[] }>;
}
export function AccountList({ accounts, loading, onDelete, onRefresh, refreshing, lastUpdated, proxies, onProxyChange, onExport, onImport }: AccountListProps) {
const t = useT();
const { lang } = useI18n();
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [warnings, setWarnings] = useState<QuotaWarning[]>([]);
// Poll quota warnings
useEffect(() => {
const fetchWarnings = async () => {
try {
const resp = await fetch("/auth/quota/warnings");
const data = await resp.json();
setWarnings(data.warnings || []);
} catch { /* ignore */ }
};
fetchWarnings();
const timer = setInterval(fetchWarnings, 30_000);
return () => clearInterval(timer);
}, []);
const toggleSelect = useCallback((id: string) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
}, []);
const toggleSelectAll = useCallback(() => {
setSelectedIds((prev) => {
if (prev.size === accounts.length) return new Set();
return new Set(accounts.map((a) => a.id));
});
}, [accounts]);
const updatedAtText = lastUpdated
? lastUpdated.toLocaleTimeString(lang === "zh" ? "zh-CN" : "en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" })
: null;
return (
<section class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-1">
<h2 class="text-[0.95rem] font-bold tracking-tight">{t("connectedAccounts")}</h2>
<p class="text-slate-500 dark:text-text-dim text-[0.8rem]">{t("connectedAccountsDesc")}</p>
</div>
<div class="flex items-center gap-2 shrink-0">
{updatedAtText && (
<span class="text-[0.75rem] text-slate-400 dark:text-text-dim hidden sm:inline">
{t("updatedAt")} {updatedAtText}
</span>
)}
<a
href="#/account-management"
class="text-[0.75rem] text-primary hover:text-primary/80 font-medium transition-colors"
>
{t("manageAccounts")} →
</a>
<a
href="#/usage-stats"
class="text-[0.75rem] text-primary hover:text-primary/80 font-medium transition-colors"
>
{t("usageStats")} →
</a>
{onExport && onImport && (
<AccountImportExport onExport={onExport} onImport={onImport} selectedIds={selectedIds} />
)}
{accounts.length > 0 && (
<button
onClick={toggleSelectAll}
title={selectedIds.size === accounts.length ? t("deselectAll") : t("selectAll")}
class="p-1.5 text-slate-400 dark:text-text-dim hover:text-primary transition-colors rounded-md hover:bg-primary/10"
>
<svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
{selectedIds.size === accounts.length ? (
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
) : (
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
)}
</svg>
</button>
)}
<button
onClick={onRefresh}
disabled={refreshing}
title={t("refresh")}
class="p-1.5 text-slate-400 dark:text-text-dim hover:text-primary transition-colors rounded-md hover:bg-primary/10 disabled:opacity-40 disabled:cursor-not-allowed"
>
<svg
class={`size-[18px] ${refreshing ? "animate-spin" : ""}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
</button>
</div>
</div>
{/* Quota warning banners */}
{warnings.filter((w) => w.level === "critical").length > 0 && (
<div class="px-4 py-2.5 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800/30 text-red-700 dark:text-red-400 text-sm flex items-center gap-2">
<svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
<span>
{t("quotaCriticalWarning").replace("{count}", String(warnings.filter((w) => w.level === "critical").length))}
</span>
</div>
)}
{warnings.filter((w) => w.level === "warning").length > 0 && warnings.filter((w) => w.level === "critical").length === 0 && (
<div class="px-4 py-2.5 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/30 text-amber-700 dark:text-amber-400 text-sm flex items-center gap-2">
<svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
<span>
{t("quotaWarning").replace("{count}", String(warnings.filter((w) => w.level === "warning").length))}
</span>
</div>
)}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{loading ? (
<div class="md:col-span-2 text-center py-8 text-slate-400 dark:text-text-dim text-sm bg-white dark:bg-card-dark border border-gray-200 dark:border-border-dark rounded-xl transition-colors">
{t("loadingAccounts")}
</div>
) : accounts.length === 0 ? (
<div class="md:col-span-2 text-center py-8 text-slate-400 dark:text-text-dim text-sm bg-white dark:bg-card-dark border border-gray-200 dark:border-border-dark rounded-xl transition-colors">
{t("noAccounts")}
</div>
) : (
accounts.map((acct, i) => (
<AccountCard key={acct.id} account={acct} index={i} onDelete={onDelete} proxies={proxies} onProxyChange={onProxyChange} selected={selectedIds.has(acct.id)} onToggleSelect={toggleSelect} />
))
)}
</div>
</section>
);
}
|