feat(webui): 账号列表添加搜索过滤功能

- 后端 GET /admin/accounts 支持 ?q= 参数,大小写不敏感匹配 identifier/email/mobile
- 前端搜索框内嵌于标题栏按钮行(测试全部按钮前)
- 搜索时重置到第 1 页,分页 total 反映过滤后数量
- 无匹配结果时显示专属提示文案(中英文)
This commit is contained in:
root
2026-02-28 09:57:19 +08:00
parent 0869ea56cd
commit d97b86e0ee
6 changed files with 1120 additions and 1079 deletions

View File

@@ -1,117 +1,121 @@
import { useI18n } from '../../i18n'
import { useAccountsData } from './useAccountsData'
import { useAccountActions } from './useAccountActions'
import QueueCards from './QueueCards'
import ApiKeysPanel from './ApiKeysPanel'
import AccountsTable from './AccountsTable'
import AddKeyModal from './AddKeyModal'
import AddAccountModal from './AddAccountModal'
export default function AccountManagerContainer({ config, onRefresh, onMessage, authFetch }) {
const { t } = useI18n()
const apiFetch = authFetch || fetch
const {
queueStatus,
keysExpanded,
setKeysExpanded,
accounts,
page,
pageSize,
totalPages,
totalAccounts,
loadingAccounts,
fetchAccounts,
changePageSize,
resolveAccountIdentifier,
} = useAccountsData({ apiFetch })
const {
showAddKey,
setShowAddKey,
showAddAccount,
setShowAddAccount,
newKey,
setNewKey,
copiedKey,
setCopiedKey,
newAccount,
setNewAccount,
loading,
testing,
testingAll,
batchProgress,
addKey,
deleteKey,
addAccount,
deleteAccount,
testAccount,
testAllAccounts,
} = useAccountActions({
apiFetch,
t,
onMessage,
onRefresh,
config,
fetchAccounts,
resolveAccountIdentifier,
})
return (
<div className="space-y-6">
<QueueCards queueStatus={queueStatus} t={t} />
<ApiKeysPanel
t={t}
config={config}
keysExpanded={keysExpanded}
setKeysExpanded={setKeysExpanded}
setShowAddKey={setShowAddKey}
copiedKey={copiedKey}
setCopiedKey={setCopiedKey}
onDeleteKey={deleteKey}
/>
<AccountsTable
t={t}
accounts={accounts}
loadingAccounts={loadingAccounts}
testing={testing}
testingAll={testingAll}
batchProgress={batchProgress}
totalAccounts={totalAccounts}
page={page}
pageSize={pageSize}
totalPages={totalPages}
resolveAccountIdentifier={resolveAccountIdentifier}
onTestAll={testAllAccounts}
onShowAddAccount={() => setShowAddAccount(true)}
onTestAccount={testAccount}
onDeleteAccount={deleteAccount}
onPrevPage={() => fetchAccounts(page - 1)}
onNextPage={() => fetchAccounts(page + 1)}
onPageSizeChange={changePageSize}
/>
<AddKeyModal
show={showAddKey}
t={t}
newKey={newKey}
setNewKey={setNewKey}
loading={loading}
onClose={() => setShowAddKey(false)}
onAdd={addKey}
/>
<AddAccountModal
show={showAddAccount}
t={t}
newAccount={newAccount}
setNewAccount={setNewAccount}
loading={loading}
onClose={() => setShowAddAccount(false)}
onAdd={addAccount}
/>
</div>
)
}
import { useI18n } from '../../i18n'
import { useAccountsData } from './useAccountsData'
import { useAccountActions } from './useAccountActions'
import QueueCards from './QueueCards'
import ApiKeysPanel from './ApiKeysPanel'
import AccountsTable from './AccountsTable'
import AddKeyModal from './AddKeyModal'
import AddAccountModal from './AddAccountModal'
export default function AccountManagerContainer({ config, onRefresh, onMessage, authFetch }) {
const { t } = useI18n()
const apiFetch = authFetch || fetch
const {
queueStatus,
keysExpanded,
setKeysExpanded,
accounts,
page,
pageSize,
totalPages,
totalAccounts,
loadingAccounts,
fetchAccounts,
changePageSize,
resolveAccountIdentifier,
searchQuery,
handleSearchChange,
} = useAccountsData({ apiFetch })
const {
showAddKey,
setShowAddKey,
showAddAccount,
setShowAddAccount,
newKey,
setNewKey,
copiedKey,
setCopiedKey,
newAccount,
setNewAccount,
loading,
testing,
testingAll,
batchProgress,
addKey,
deleteKey,
addAccount,
deleteAccount,
testAccount,
testAllAccounts,
} = useAccountActions({
apiFetch,
t,
onMessage,
onRefresh,
config,
fetchAccounts,
resolveAccountIdentifier,
})
return (
<div className="space-y-6">
<QueueCards queueStatus={queueStatus} t={t} />
<ApiKeysPanel
t={t}
config={config}
keysExpanded={keysExpanded}
setKeysExpanded={setKeysExpanded}
setShowAddKey={setShowAddKey}
copiedKey={copiedKey}
setCopiedKey={setCopiedKey}
onDeleteKey={deleteKey}
/>
<AccountsTable
t={t}
accounts={accounts}
loadingAccounts={loadingAccounts}
testing={testing}
testingAll={testingAll}
batchProgress={batchProgress}
totalAccounts={totalAccounts}
page={page}
pageSize={pageSize}
totalPages={totalPages}
resolveAccountIdentifier={resolveAccountIdentifier}
onTestAll={testAllAccounts}
onShowAddAccount={() => setShowAddAccount(true)}
onTestAccount={testAccount}
onDeleteAccount={deleteAccount}
onPrevPage={() => fetchAccounts(page - 1)}
onNextPage={() => fetchAccounts(page + 1)}
onPageSizeChange={changePageSize}
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
/>
<AddKeyModal
show={showAddKey}
t={t}
newKey={newKey}
setNewKey={setNewKey}
loading={loading}
onClose={() => setShowAddKey(false)}
onAdd={addKey}
/>
<AddAccountModal
show={showAddAccount}
t={t}
newAccount={newAccount}
setNewAccount={setNewAccount}
loading={loading}
onClose={() => setShowAddAccount(false)}
onAdd={addAccount}
/>
</div>
)
}

View File

@@ -1,182 +1,191 @@
import { useState } from 'react'
import { ChevronLeft, ChevronRight, Check, Copy, Play, Plus, Trash2 } from 'lucide-react'
import clsx from 'clsx'
export default function AccountsTable({
t,
accounts,
loadingAccounts,
testing,
testingAll,
batchProgress,
totalAccounts,
page,
pageSize,
totalPages,
resolveAccountIdentifier,
onTestAll,
onShowAddAccount,
onTestAccount,
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">
<div>
<h2 className="text-lg font-semibold">{t('accountManager.accountsTitle')}</h2>
<p className="text-sm text-muted-foreground">{t('accountManager.accountsDesc')}</p>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={onTestAll}
disabled={testingAll || totalAccounts === 0}
className="flex items-center px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border disabled:opacity-50"
>
{testingAll ? <span className="animate-spin mr-2"></span> : <Play className="w-3 h-3 mr-2" />}
{t('accountManager.testAll')}
</button>
<button
onClick={onShowAddAccount}
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium text-sm shadow-sm"
>
<Plus className="w-4 h-4" />
{t('accountManager.addAccount')}
</button>
</div>
</div>
{testingAll && batchProgress.total > 0 && (
<div className="p-4 border-b border-border bg-muted/30">
<div className="flex items-center justify-between text-sm mb-2">
<span className="font-medium">{t('accountManager.testingAllAccounts')}</span>
<span className="text-muted-foreground">{batchProgress.current} / {batchProgress.total}</span>
</div>
<div className="w-full bg-muted rounded-full h-2 overflow-hidden mb-4">
<div
className="bg-primary h-full transition-all duration-300"
style={{ width: `${(batchProgress.current / batchProgress.total) * 100}%` }}
/>
</div>
{batchProgress.results.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 max-h-32 overflow-y-auto custom-scrollbar">
{batchProgress.results.map((r, i) => (
<div key={i} className={clsx(
"text-xs px-2 py-1 rounded border truncate",
r.success ? "bg-emerald-500/10 border-emerald-500/20 text-emerald-500" : "bg-destructive/10 border-destructive/20 text-destructive"
)}>
{r.success ? '✓' : '✗'} {r.id}
</div>
))}
</div>
)}
</div>
)}
<div className="divide-y divide-border">
{loadingAccounts ? (
<div className="p-8 text-center text-muted-foreground">{t('actions.loading')}</div>
) : accounts.length > 0 ? (
accounts.map((acc, i) => {
const id = resolveAccountIdentifier(acc)
return (
<div key={i} className="p-4 flex flex-col md:flex-row md:items-center justify-between gap-4 hover:bg-muted/50 transition-colors">
<div className="flex items-center gap-3 min-w-0">
<div className={clsx(
"w-2 h-2 rounded-full shrink-0",
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 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.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}
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2 self-start lg:self-auto ml-5 lg:ml-0">
<button
onClick={() => onTestAccount(id)}
disabled={testing[id]}
className="px-2 lg:px-3 py-1 lg:py-1.5 text-[10px] lg:text-xs font-medium border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50"
>
{testing[id] ? t('actions.testing') : t('actions.test')}
</button>
<button
onClick={() => onDeleteAccount(id)}
className="p-1 lg:p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors"
>
<Trash2 className="w-3.5 h-3.5 lg:w-4 lg:h-4" />
</button>
</div>
</div>
)
})
) : (
<div className="p-8 text-center text-muted-foreground">{t('accountManager.noAccounts')}</div>
)}
</div>
{totalPages > 1 && (
<div className="p-4 border-t border-border flex items-center justify-between">
<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
onClick={onPrevPage}
disabled={page <= 1 || loadingAccounts}
className="p-2 border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-sm font-medium px-2">{page} / {totalPages}</span>
<button
onClick={onNextPage}
disabled={page >= totalPages || loadingAccounts}
className="p-2 border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
)
}
import { useState } from 'react'
import { ChevronLeft, ChevronRight, Check, Copy, Play, Plus, Trash2 } from 'lucide-react'
import clsx from 'clsx'
export default function AccountsTable({
t,
accounts,
loadingAccounts,
testing,
testingAll,
batchProgress,
totalAccounts,
page,
pageSize,
totalPages,
resolveAccountIdentifier,
onTestAll,
onShowAddAccount,
onTestAccount,
onDeleteAccount,
onPrevPage,
onNextPage,
onPageSizeChange,
searchQuery,
onSearchChange,
}) {
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">
<div>
<h2 className="text-lg font-semibold">{t('accountManager.accountsTitle')}</h2>
<p className="text-sm text-muted-foreground">{t('accountManager.accountsDesc')}</p>
</div>
<div className="flex flex-wrap gap-2">
<input
type="text"
value={searchQuery}
onChange={e => onSearchChange(e.target.value)}
placeholder={t('accountManager.searchPlaceholder')}
className="px-3 py-1.5 text-sm bg-muted border border-border rounded-lg focus:outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground"
/>
<button
onClick={onTestAll}
disabled={testingAll || totalAccounts === 0}
className="flex items-center px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border disabled:opacity-50"
>
{testingAll ? <span className="animate-spin mr-2"></span> : <Play className="w-3 h-3 mr-2" />}
{t('accountManager.testAll')}
</button>
<button
onClick={onShowAddAccount}
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium text-sm shadow-sm"
>
<Plus className="w-4 h-4" />
{t('accountManager.addAccount')}
</button>
</div>
</div>
{testingAll && batchProgress.total > 0 && (
<div className="p-4 border-b border-border bg-muted/30">
<div className="flex items-center justify-between text-sm mb-2">
<span className="font-medium">{t('accountManager.testingAllAccounts')}</span>
<span className="text-muted-foreground">{batchProgress.current} / {batchProgress.total}</span>
</div>
<div className="w-full bg-muted rounded-full h-2 overflow-hidden mb-4">
<div
className="bg-primary h-full transition-all duration-300"
style={{ width: `${(batchProgress.current / batchProgress.total) * 100}%` }}
/>
</div>
{batchProgress.results.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 max-h-32 overflow-y-auto custom-scrollbar">
{batchProgress.results.map((r, i) => (
<div key={i} className={clsx(
"text-xs px-2 py-1 rounded border truncate",
r.success ? "bg-emerald-500/10 border-emerald-500/20 text-emerald-500" : "bg-destructive/10 border-destructive/20 text-destructive"
)}>
{r.success ? '✓' : '✗'} {r.id}
</div>
))}
</div>
)}
</div>
)}
<div className="divide-y divide-border">
{loadingAccounts ? (
<div className="p-8 text-center text-muted-foreground">{t('actions.loading')}</div>
) : accounts.length > 0 ? (
accounts.map((acc, i) => {
const id = resolveAccountIdentifier(acc)
return (
<div key={i} className="p-4 flex flex-col md:flex-row md:items-center justify-between gap-4 hover:bg-muted/50 transition-colors">
<div className="flex items-center gap-3 min-w-0">
<div className={clsx(
"w-2 h-2 rounded-full shrink-0",
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 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.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}
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2 self-start lg:self-auto ml-5 lg:ml-0">
<button
onClick={() => onTestAccount(id)}
disabled={testing[id]}
className="px-2 lg:px-3 py-1 lg:py-1.5 text-[10px] lg:text-xs font-medium border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50"
>
{testing[id] ? t('actions.testing') : t('actions.test')}
</button>
<button
onClick={() => onDeleteAccount(id)}
className="p-1 lg:p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors"
>
<Trash2 className="w-3.5 h-3.5 lg:w-4 lg:h-4" />
</button>
</div>
</div>
)
})
) : (
<div className="p-8 text-center text-muted-foreground">{searchQuery ? t('accountManager.searchNoResults') : t('accountManager.noAccounts')}</div>
)}
</div>
{totalPages > 1 && (
<div className="p-4 border-t border-border flex items-center justify-between">
<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
onClick={onPrevPage}
disabled={page <= 1 || loadingAccounts}
className="p-2 border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-sm font-medium px-2">{page} / {totalPages}</span>
<button
onClick={onNextPage}
disabled={page >= totalPages || loadingAccounts}
className="p-2 border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
)
}

View File

@@ -1,75 +1,86 @@
import { useEffect, useState } from 'react'
export function useAccountsData({ apiFetch }) {
const [queueStatus, setQueueStatus] = useState(null)
const [keysExpanded, setKeysExpanded] = useState(false)
const [accounts, setAccounts] = useState([])
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [totalPages, setTotalPages] = useState(1)
const [totalAccounts, setTotalAccounts] = useState(0)
const [loadingAccounts, setLoadingAccounts] = useState(false)
const resolveAccountIdentifier = (acc) => {
if (!acc || typeof acc !== 'object') return ''
return String(acc.identifier || acc.email || acc.mobile || '').trim()
}
const fetchAccounts = async (targetPage = page, targetPageSize = pageSize) => {
setLoadingAccounts(true)
try {
const res = await apiFetch(`/admin/accounts?page=${targetPage}&page_size=${targetPageSize}`)
if (res.ok) {
const data = await res.json()
setAccounts(data.items || [])
setTotalPages(data.total_pages || 1)
setTotalAccounts(data.total || 0)
setPage(data.page || 1)
}
} catch (e) {
console.error('Failed to fetch accounts:', e)
} finally {
setLoadingAccounts(false)
}
}
const changePageSize = (newSize) => {
setPageSize(newSize)
fetchAccounts(1, newSize)
}
const fetchQueueStatus = async () => {
try {
const res = await apiFetch('/admin/queue/status')
if (res.ok) {
const data = await res.json()
setQueueStatus(data)
}
} catch (e) {
console.error('Failed to fetch queue status:', e)
}
}
useEffect(() => {
fetchAccounts()
fetchQueueStatus()
const interval = setInterval(fetchQueueStatus, 5000)
return () => clearInterval(interval)
}, [])
return {
queueStatus,
keysExpanded,
setKeysExpanded,
accounts,
page,
pageSize,
totalPages,
totalAccounts,
loadingAccounts,
fetchAccounts,
changePageSize,
resolveAccountIdentifier,
}
}
import { useEffect, useState } from 'react'
export function useAccountsData({ apiFetch }) {
const [queueStatus, setQueueStatus] = useState(null)
const [keysExpanded, setKeysExpanded] = useState(false)
const [accounts, setAccounts] = useState([])
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [totalPages, setTotalPages] = useState(1)
const [totalAccounts, setTotalAccounts] = useState(0)
const [loadingAccounts, setLoadingAccounts] = useState(false)
const resolveAccountIdentifier = (acc) => {
if (!acc || typeof acc !== 'object') return ''
return String(acc.identifier || acc.email || acc.mobile || '').trim()
}
const [searchQuery, setSearchQuery] = useState('')
const fetchAccounts = async (targetPage = page, targetPageSize = pageSize, targetQuery = searchQuery) => {
setLoadingAccounts(true)
try {
let url = `/admin/accounts?page=${targetPage}&page_size=${targetPageSize}`
if (targetQuery.trim()) url += `&q=${encodeURIComponent(targetQuery.trim())}`
const res = await apiFetch(url)
if (res.ok) {
const data = await res.json()
setAccounts(data.items || [])
setTotalPages(data.total_pages || 1)
setTotalAccounts(data.total || 0)
setPage(data.page || 1)
}
} catch (e) {
console.error('Failed to fetch accounts:', e)
} finally {
setLoadingAccounts(false)
}
}
const changePageSize = (newSize) => {
setPageSize(newSize)
fetchAccounts(1, newSize)
}
const handleSearchChange = (query) => {
setSearchQuery(query)
fetchAccounts(1, pageSize, query)
}
const fetchQueueStatus = async () => {
try {
const res = await apiFetch('/admin/queue/status')
if (res.ok) {
const data = await res.json()
setQueueStatus(data)
}
} catch (e) {
console.error('Failed to fetch queue status:', e)
}
}
useEffect(() => {
fetchAccounts()
fetchQueueStatus()
const interval = setInterval(fetchQueueStatus, 5000)
return () => clearInterval(interval)
}, [])
return {
queueStatus,
keysExpanded,
setKeysExpanded,
accounts,
page,
pageSize,
totalPages,
totalAccounts,
loadingAccounts,
fetchAccounts,
changePageSize,
resolveAccountIdentifier,
searchQuery,
handleSearchChange,
}
}