Merge pull request #59 from ronghuaxueleng/feature/account-improvements

feat: 账号测试状态持久化、分页选择器、点击账号名复制
This commit is contained in:
CJACK.
2026-02-27 23:16:05 +08:00
committed by GitHub
10 changed files with 83 additions and 14 deletions

View File

@@ -16,6 +16,7 @@ type ConfigStore interface {
Accounts() []config.Account
FindAccount(identifier string) (config.Account, bool)
UpdateAccountToken(identifier, token string) error
UpdateAccountTestStatus(identifier, status string) error
Update(mutator func(*config.Config) error) error
ExportJSONAndBase64() (string, string, error)
IsEnvBacked() bool

View File

@@ -56,6 +56,7 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
"has_password": acc.Password != "",
"has_token": token != "",
"token_preview": preview,
"test_status": acc.TestStatus,
})
}
writeJSON(w, http.StatusOK, map[string]any{"items": items, "total": total, "page": page, "page_size": pageSize, "total_pages": totalPages})

View File

@@ -88,7 +88,15 @@ func runAccountTestsConcurrently(accounts []config.Account, maxConcurrency int,
func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, message string) map[string]any {
start := time.Now()
result := map[string]any{"account": acc.Identifier(), "success": false, "response_time": 0, "message": "", "model": model}
identifier := acc.Identifier()
result := map[string]any{"account": identifier, "success": false, "response_time": 0, "message": "", "model": model}
defer func() {
status := "failed"
if ok, _ := result["success"].(bool); ok {
status = "ok"
}
_ = h.Store.UpdateAccountTestStatus(identifier, status)
}()
token := strings.TrimSpace(acc.Token)
if token == "" {
newToken, err := h.DS.Login(ctx, acc)

View File

@@ -18,10 +18,11 @@ type Config struct {
}
type Account struct {
Email string `json:"email,omitempty"`
Mobile string `json:"mobile,omitempty"`
Password string `json:"password,omitempty"`
Token string `json:"token,omitempty"`
Email string `json:"email,omitempty"`
Mobile string `json:"mobile,omitempty"`
Password string `json:"password,omitempty"`
Token string `json:"token,omitempty"`
TestStatus string `json:"test_status,omitempty"`
}
type CompatConfig struct {

View File

@@ -97,6 +97,18 @@ func (s *Store) FindAccount(identifier string) (Account, bool) {
return Account{}, false
}
func (s *Store) UpdateAccountTestStatus(identifier, status string) error {
identifier = strings.TrimSpace(identifier)
s.mu.Lock()
defer s.mu.Unlock()
idx, ok := s.findAccountIndexLocked(identifier)
if !ok {
return errors.New("account not found")
}
s.cfg.Accounts[idx].TestStatus = status
return s.saveLocked()
}
func (s *Store) UpdateAccountToken(identifier, token string) error {
identifier = strings.TrimSpace(identifier)
s.mu.Lock()

View File

@@ -17,10 +17,12 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
setKeysExpanded,
accounts,
page,
pageSize,
totalPages,
totalAccounts,
loadingAccounts,
fetchAccounts,
changePageSize,
resolveAccountIdentifier,
} = useAccountsData({ apiFetch })
@@ -79,6 +81,7 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
batchProgress={batchProgress}
totalAccounts={totalAccounts}
page={page}
pageSize={pageSize}
totalPages={totalPages}
resolveAccountIdentifier={resolveAccountIdentifier}
onTestAll={testAllAccounts}
@@ -87,6 +90,7 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
onDeleteAccount={deleteAccount}
onPrevPage={() => fetchAccounts(page - 1)}
onNextPage={() => fetchAccounts(page + 1)}
onPageSizeChange={changePageSize}
/>
<AddKeyModal

View File

@@ -1,4 +1,5 @@
import { ChevronLeft, ChevronRight, Play, Plus, Trash2 } from 'lucide-react'
import { useState } from 'react'
import { ChevronLeft, ChevronRight, Check, Copy, Play, Plus, Trash2 } from 'lucide-react'
import clsx from 'clsx'
export default function AccountsTable({
@@ -10,6 +11,7 @@ export default function AccountsTable({
batchProgress,
totalAccounts,
page,
pageSize,
totalPages,
resolveAccountIdentifier,
onTestAll,
@@ -18,7 +20,16 @@ export default function AccountsTable({
onDeleteAccount,
onPrevPage,
onNextPage,
onPageSizeChange,
}) {
const [copiedId, setCopiedId] = useState(null)
const copyId = (id) => {
navigator.clipboard.writeText(id).then(() => {
setCopiedId(id)
setTimeout(() => setCopiedId(null), 1500)
})
}
return (
<div className="bg-card border border-border rounded-xl overflow-hidden shadow-sm">
<div className="p-6 border-b border-border flex flex-col md:flex-row md:items-center justify-between gap-4">
@@ -83,12 +94,23 @@ export default function AccountsTable({
<div className="flex items-center gap-3 min-w-0">
<div className={clsx(
"w-2 h-2 rounded-full shrink-0",
acc.has_token ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" : "bg-amber-500"
acc.test_status === 'failed' ? "bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.5)]" :
(acc.test_status === 'ok' || acc.has_token) ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" :
"bg-amber-500"
)} />
<div className="min-w-0">
<div className="font-medium truncate">{id || '-'}</div>
<div
className="font-medium truncate flex items-center gap-1.5 cursor-pointer hover:text-primary transition-colors group"
onClick={() => copyId(id)}
>
<span className="truncate">{id || '-'}</span>
{copiedId === id
? <Check className="w-3 h-3 text-emerald-500 shrink-0" />
: <Copy className="w-3 h-3 opacity-0 group-hover:opacity-50 shrink-0 transition-opacity" />
}
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
<span>{acc.has_token ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')}</span>
<span>{acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : (acc.test_status === 'ok' || acc.has_token) ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')}</span>
{acc.token_preview && (
<span className="font-mono bg-muted px-1.5 py-0.5 rounded text-[10px]">
{acc.token_preview}
@@ -122,8 +144,19 @@ export default function AccountsTable({
{totalPages > 1 && (
<div className="p-4 border-t border-border flex items-center justify-between">
<div className="text-sm text-muted-foreground">
{t('accountManager.pageInfo', { current: page, total: totalPages, count: totalAccounts })}
<div className="flex items-center gap-3">
<div className="text-sm text-muted-foreground">
{t('accountManager.pageInfo', { current: page, total: totalPages, count: totalAccounts })}
</div>
<select
value={pageSize}
onChange={e => onPageSizeChange(Number(e.target.value))}
className="text-sm border border-border rounded-md px-2 py-1 bg-background text-foreground"
>
{[10, 20, 50, 100, 500, 1000, 2000, 5000].map(s => (
<option key={s} value={s}>{s}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<button

View File

@@ -6,7 +6,7 @@ export function useAccountsData({ apiFetch }) {
const [accounts, setAccounts] = useState([])
const [page, setPage] = useState(1)
const [pageSize] = useState(10)
const [pageSize, setPageSize] = useState(10)
const [totalPages, setTotalPages] = useState(1)
const [totalAccounts, setTotalAccounts] = useState(0)
const [loadingAccounts, setLoadingAccounts] = useState(false)
@@ -16,10 +16,10 @@ export function useAccountsData({ apiFetch }) {
return String(acc.identifier || acc.email || acc.mobile || '').trim()
}
const fetchAccounts = async (targetPage = page) => {
const fetchAccounts = async (targetPage = page, targetPageSize = pageSize) => {
setLoadingAccounts(true)
try {
const res = await apiFetch(`/admin/accounts?page=${targetPage}&page_size=${pageSize}`)
const res = await apiFetch(`/admin/accounts?page=${targetPage}&page_size=${targetPageSize}`)
if (res.ok) {
const data = await res.json()
setAccounts(data.items || [])
@@ -34,6 +34,11 @@ export function useAccountsData({ apiFetch }) {
}
}
const changePageSize = (newSize) => {
setPageSize(newSize)
fetchAccounts(1, newSize)
}
const fetchQueueStatus = async () => {
try {
const res = await apiFetch('/admin/queue/status')
@@ -59,10 +64,12 @@ export function useAccountsData({ apiFetch }) {
setKeysExpanded,
accounts,
page,
pageSize,
totalPages,
totalAccounts,
loadingAccounts,
fetchAccounts,
changePageSize,
resolveAccountIdentifier,
}
}

View File

@@ -113,6 +113,7 @@
"testingAllAccounts": "Testing all accounts...",
"sessionActive": "Session active",
"reauthRequired": "Re-auth required",
"testStatusFailed": "Last test failed",
"noAccounts": "No accounts found.",
"modalAddKeyTitle": "Add API key",
"newKeyLabel": "New key value",

View File

@@ -113,6 +113,7 @@
"testingAllAccounts": "正在测试所有账号...",
"sessionActive": "已建立会话",
"reauthRequired": "需重新登录",
"testStatusFailed": "上次测试失败",
"noAccounts": "未找到任何账号",
"modalAddKeyTitle": "添加 API 密钥",
"newKeyLabel": "新密钥值",