mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-12 02:54:44 +08:00
Merge pull request #853 from touwaeriol/pr/swipe-select-admin-tables
feat(frontend): 为后台账号管理和 IP 管理增加拖筐选中能力
This commit is contained in:
@@ -131,8 +131,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #table>
|
||||
<AccountBulkActionsBar :selected-ids="selIds" @delete="handleBulkDelete" @edit="showBulkEdit = true" @clear="selIds = []" @select-page="selectPage" @toggle-schedulable="handleBulkToggleSchedulable" />
|
||||
<div ref="accountTableRef">
|
||||
<AccountBulkActionsBar :selected-ids="selIds" @delete="handleBulkDelete" @edit="showBulkEdit = true" @clear="clearSelection" @select-page="selectPage" @toggle-schedulable="handleBulkToggleSchedulable" />
|
||||
<div ref="accountTableRef" class="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<DataTable
|
||||
:columns="cols"
|
||||
:data="accounts"
|
||||
@@ -288,6 +288,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import { useTableLoader } from '@/composables/useTableLoader'
|
||||
import { useSwipeSelect } from '@/composables/useSwipeSelect'
|
||||
import { useTableSelection } from '@/composables/useTableSelection'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
import TablePageLayout from '@/components/layout/TablePageLayout.vue'
|
||||
import DataTable from '@/components/common/DataTable.vue'
|
||||
@@ -322,17 +323,11 @@ const authStore = useAuthStore()
|
||||
|
||||
const proxies = ref<Proxy[]>([])
|
||||
const groups = ref<AdminGroup[]>([])
|
||||
const selIds = ref<number[]>([])
|
||||
const accountTableRef = ref<HTMLElement | null>(null)
|
||||
useSwipeSelect(accountTableRef, {
|
||||
isSelected: (id) => selIds.value.includes(id),
|
||||
select: (id) => { if (!selIds.value.includes(id)) selIds.value.push(id) },
|
||||
deselect: (id) => { selIds.value = selIds.value.filter(x => x !== id) }
|
||||
})
|
||||
const selPlatforms = computed<AccountPlatform[]>(() => {
|
||||
const platforms = new Set(
|
||||
accounts.value
|
||||
.filter(a => selIds.value.includes(a.id))
|
||||
.filter(a => isSelected(a.id))
|
||||
.map(a => a.platform)
|
||||
)
|
||||
return [...platforms]
|
||||
@@ -340,7 +335,7 @@ const selPlatforms = computed<AccountPlatform[]>(() => {
|
||||
const selTypes = computed<AccountType[]>(() => {
|
||||
const types = new Set(
|
||||
accounts.value
|
||||
.filter(a => selIds.value.includes(a.id))
|
||||
.filter(a => isSelected(a.id))
|
||||
.map(a => a.type)
|
||||
)
|
||||
return [...types]
|
||||
@@ -565,6 +560,29 @@ const {
|
||||
initialParams: { platform: '', type: '', status: '', group: '', search: '' }
|
||||
})
|
||||
|
||||
const {
|
||||
selectedIds: selIds,
|
||||
allVisibleSelected,
|
||||
isSelected,
|
||||
setSelectedIds,
|
||||
select,
|
||||
deselect,
|
||||
toggle: toggleSel,
|
||||
clear: clearSelection,
|
||||
removeMany: removeSelectedAccounts,
|
||||
toggleVisible,
|
||||
selectVisible: selectPage
|
||||
} = useTableSelection<Account>({
|
||||
rows: accounts,
|
||||
getId: (account) => account.id
|
||||
})
|
||||
|
||||
useSwipeSelect(accountTableRef, {
|
||||
isSelected,
|
||||
select,
|
||||
deselect
|
||||
})
|
||||
|
||||
const resetAutoRefreshCache = () => {
|
||||
autoRefreshETag.value = null
|
||||
}
|
||||
@@ -866,24 +884,11 @@ const openMenu = (a: Account, e: MouseEvent) => {
|
||||
|
||||
menu.show = true
|
||||
}
|
||||
const toggleSel = (id: number) => { const i = selIds.value.indexOf(id); if(i === -1) selIds.value.push(id); else selIds.value.splice(i, 1) }
|
||||
const allVisibleSelected = computed(() => {
|
||||
if (accounts.value.length === 0) return false
|
||||
return accounts.value.every(account => selIds.value.includes(account.id))
|
||||
})
|
||||
const toggleSelectAllVisible = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (target.checked) {
|
||||
const next = new Set(selIds.value)
|
||||
accounts.value.forEach(account => next.add(account.id))
|
||||
selIds.value = Array.from(next)
|
||||
return
|
||||
}
|
||||
const visibleIds = new Set(accounts.value.map(account => account.id))
|
||||
selIds.value = selIds.value.filter(id => !visibleIds.has(id))
|
||||
toggleVisible(target.checked)
|
||||
}
|
||||
const selectPage = () => { selIds.value = [...new Set([...selIds.value, ...accounts.value.map(a => a.id)])] }
|
||||
const handleBulkDelete = async () => { if(!confirm(t('common.confirm'))) return; try { await Promise.all(selIds.value.map(id => adminAPI.accounts.delete(id))); selIds.value = []; reload() } catch (error) { console.error('Failed to bulk delete accounts:', error) } }
|
||||
const handleBulkDelete = async () => { if(!confirm(t('common.confirm'))) return; try { await Promise.all(selIds.value.map(id => adminAPI.accounts.delete(id))); clearSelection(); reload() } catch (error) { console.error('Failed to bulk delete accounts:', error) } }
|
||||
const updateSchedulableInList = (accountIds: number[], schedulable: boolean) => {
|
||||
if (accountIds.length === 0) return
|
||||
const idSet = new Set(accountIds)
|
||||
@@ -956,7 +961,7 @@ const handleBulkToggleSchedulable = async (schedulable: boolean) => {
|
||||
const { successIds, failedIds, successCount, failedCount, hasIds, hasCounts } = normalizeBulkSchedulableResult(result, accountIds)
|
||||
if (!hasIds && !hasCounts) {
|
||||
appStore.showError(t('admin.accounts.bulkSchedulableResultUnknown'))
|
||||
selIds.value = accountIds
|
||||
setSelectedIds(accountIds)
|
||||
load().catch((error) => {
|
||||
console.error('Failed to refresh accounts:', error)
|
||||
})
|
||||
@@ -976,16 +981,17 @@ const handleBulkToggleSchedulable = async (schedulable: boolean) => {
|
||||
? t('admin.accounts.bulkSchedulablePartial', { success: successCount, failed: failedCount })
|
||||
: t('admin.accounts.bulkSchedulableResultUnknown')
|
||||
appStore.showError(message)
|
||||
selIds.value = failedIds.length > 0 ? failedIds : accountIds
|
||||
setSelectedIds(failedIds.length > 0 ? failedIds : accountIds)
|
||||
} else {
|
||||
selIds.value = hasIds ? [] : accountIds
|
||||
if (hasIds) clearSelection()
|
||||
else setSelectedIds(accountIds)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to bulk toggle schedulable:', error)
|
||||
appStore.showError(t('common.error'))
|
||||
}
|
||||
}
|
||||
const handleBulkUpdated = () => { showBulkEdit.value = false; selIds.value = []; reload() }
|
||||
const handleBulkUpdated = () => { showBulkEdit.value = false; clearSelection(); reload() }
|
||||
const handleDataImported = () => { showImportData.value = false; reload() }
|
||||
const accountMatchesCurrentFilters = (account: Account) => {
|
||||
if (params.platform && account.platform !== params.platform) return false
|
||||
@@ -1031,7 +1037,7 @@ const patchAccountInList = (updatedAccount: Account) => {
|
||||
if (!accountMatchesCurrentFilters(mergedAccount)) {
|
||||
accounts.value = accounts.value.filter(account => account.id !== mergedAccount.id)
|
||||
syncPaginationAfterLocalRemoval()
|
||||
selIds.value = selIds.value.filter(id => id !== mergedAccount.id)
|
||||
removeSelectedAccounts([mergedAccount.id])
|
||||
if (menu.acc?.id === mergedAccount.id) {
|
||||
menu.show = false
|
||||
menu.acc = null
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
</template>
|
||||
|
||||
<template #table>
|
||||
<div ref="proxyTableRef">
|
||||
<div ref="proxyTableRef" class="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<DataTable :columns="columns" :data="proxies" :loading="loading">
|
||||
<template #header-select>
|
||||
<input
|
||||
@@ -883,6 +883,7 @@ import Icon from '@/components/icons/Icon.vue'
|
||||
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { useSwipeSelect } from '@/composables/useSwipeSelect'
|
||||
import { useTableSelection } from '@/composables/useTableSelection'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
@@ -961,12 +962,25 @@ const testingProxyIds = ref<Set<number>>(new Set())
|
||||
const qualityCheckingProxyIds = ref<Set<number>>(new Set())
|
||||
const batchTesting = ref(false)
|
||||
const batchQualityChecking = ref(false)
|
||||
const selectedProxyIds = ref<Set<number>>(new Set())
|
||||
const proxyTableRef = ref<HTMLElement | null>(null)
|
||||
const {
|
||||
selectedSet: selectedProxyIds,
|
||||
selectedCount,
|
||||
allVisibleSelected,
|
||||
isSelected,
|
||||
select,
|
||||
deselect,
|
||||
clear: clearSelectedProxies,
|
||||
removeMany: removeSelectedProxies,
|
||||
toggleVisible
|
||||
} = useTableSelection<Proxy>({
|
||||
rows: proxies,
|
||||
getId: (proxy) => proxy.id
|
||||
})
|
||||
useSwipeSelect(proxyTableRef, {
|
||||
isSelected: (id) => selectedProxyIds.value.has(id),
|
||||
select: (id) => { const next = new Set(selectedProxyIds.value); next.add(id); selectedProxyIds.value = next },
|
||||
deselect: (id) => { const next = new Set(selectedProxyIds.value); next.delete(id); selectedProxyIds.value = next }
|
||||
isSelected,
|
||||
select,
|
||||
deselect
|
||||
})
|
||||
const accountsProxy = ref<Proxy | null>(null)
|
||||
const proxyAccounts = ref<ProxyAccountSummary[]>([])
|
||||
@@ -977,12 +991,6 @@ const showQualityReportDialog = ref(false)
|
||||
const qualityReportProxy = ref<Proxy | null>(null)
|
||||
const qualityReport = ref<ProxyQualityCheckResult | null>(null)
|
||||
|
||||
const selectedCount = computed(() => selectedProxyIds.value.size)
|
||||
const allVisibleSelected = computed(() => {
|
||||
if (proxies.value.length === 0) return false
|
||||
return proxies.value.every((proxy) => selectedProxyIds.value.has(proxy.id))
|
||||
})
|
||||
|
||||
// Batch import state
|
||||
const createMode = ref<'standard' | 'batch'>('standard')
|
||||
const batchInput = ref('')
|
||||
@@ -1029,26 +1037,16 @@ const isAbortError = (error: unknown) => {
|
||||
|
||||
const toggleSelectRow = (id: number, event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const next = new Set(selectedProxyIds.value)
|
||||
if (target.checked) {
|
||||
next.add(id)
|
||||
} else {
|
||||
next.delete(id)
|
||||
select(id)
|
||||
return
|
||||
}
|
||||
selectedProxyIds.value = next
|
||||
deselect(id)
|
||||
}
|
||||
|
||||
const toggleSelectAllVisible = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const next = new Set(selectedProxyIds.value)
|
||||
for (const proxy of proxies.value) {
|
||||
if (target.checked) {
|
||||
next.add(proxy.id)
|
||||
} else {
|
||||
next.delete(proxy.id)
|
||||
}
|
||||
}
|
||||
selectedProxyIds.value = next
|
||||
toggleVisible(target.checked)
|
||||
}
|
||||
|
||||
const loadProxies = async () => {
|
||||
@@ -1740,11 +1738,7 @@ const confirmDelete = async () => {
|
||||
await adminAPI.proxies.delete(deletingProxy.value.id)
|
||||
appStore.showSuccess(t('admin.proxies.proxyDeleted'))
|
||||
showDeleteDialog.value = false
|
||||
if (selectedProxyIds.value.has(deletingProxy.value.id)) {
|
||||
const next = new Set(selectedProxyIds.value)
|
||||
next.delete(deletingProxy.value.id)
|
||||
selectedProxyIds.value = next
|
||||
}
|
||||
removeSelectedProxies([deletingProxy.value.id])
|
||||
deletingProxy.value = null
|
||||
loadProxies()
|
||||
} catch (error: any) {
|
||||
@@ -1771,7 +1765,7 @@ const confirmBatchDelete = async () => {
|
||||
appStore.showInfo(t('admin.proxies.batchDeleteSkipped', { skipped }))
|
||||
}
|
||||
|
||||
selectedProxyIds.value = new Set()
|
||||
clearSelectedProxies()
|
||||
showBatchDeleteDialog.value = false
|
||||
loadProxies()
|
||||
} catch (error: any) {
|
||||
|
||||
Reference in New Issue
Block a user