mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-02 07:25:26 +08:00
Merge pull request #59 from ronghuaxueleng/feature/account-improvements
feat: 账号测试状态持久化、分页选择器、点击账号名复制
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
"testingAllAccounts": "正在测试所有账号...",
|
||||
"sessionActive": "已建立会话",
|
||||
"reauthRequired": "需重新登录",
|
||||
"testStatusFailed": "上次测试失败",
|
||||
"noAccounts": "未找到任何账号",
|
||||
"modalAddKeyTitle": "添加 API 密钥",
|
||||
"newKeyLabel": "新密钥值",
|
||||
|
||||
Reference in New Issue
Block a user