2025-12-18 13:50:39 +08:00
< template >
< AppLayout >
2025-12-27 10:50:25 +08:00
< TablePageLayout >
2026-01-05 01:00:00 +08:00
< template # filters >
< div class = "flex flex-wrap-reverse items-start justify-between gap-3" >
2026-01-04 21:46:23 -08:00
< AccountTableFilters
v - model : searchQuery = "params.search"
: filters = "params"
2026-02-12 03:47:06 +08:00
: groups = "groups"
2026-01-06 19:20:05 +08:00
@ update : filters = "(newFilters) => Object.assign(params, newFilters)"
2026-01-09 17:58:21 +08:00
@ change = "debouncedReload"
2026-01-04 21:46:23 -08:00
@ update : searchQuery = "debouncedReload"
/ >
< AccountTableActions
: loading = "loading"
@ refresh = "load"
@ sync = "showSync = true"
@ create = "showCreate = true"
2026-01-18 11:57:07 +08:00
>
2026-01-20 00:18:51 +08:00
< template # after >
2026-01-21 22:43:25 +08:00
<!-- Auto Refresh Dropdown -- >
< div class = "relative" ref = "autoRefreshDropdownRef" >
< button
@ click = "
showAutoRefreshDropdown = ! showAutoRefreshDropdown ;
showColumnDropdown = false
"
class = "btn btn-secondary px-2 md:px-3"
: title = "t('admin.accounts.autoRefresh')"
>
< Icon name = "refresh" size = "sm" : class = "[autoRefreshEnabled ? 'animate-spin' : '']" / >
< span class = "hidden md:inline" >
{ {
autoRefreshEnabled
? t ( 'admin.accounts.autoRefreshCountdown' , { seconds : autoRefreshCountdown } )
: t ( 'admin.accounts.autoRefresh' )
} }
< / span >
< / button >
< div
v - if = "showAutoRefreshDropdown"
class = "absolute right-0 z-50 mt-2 w-56 origin-top-right rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
>
< div class = "p-2" >
< button
@ click = "setAutoRefreshEnabled(!autoRefreshEnabled)"
class = "flex w-full items-center justify-between rounded-md px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
>
< span > { { t ( 'admin.accounts.enableAutoRefresh' ) } } < / span >
< Icon v-if = "autoRefreshEnabled" name="check" size="sm" class="text-primary-500" / >
< / button >
< div class = "my-1 border-t border-gray-100 dark:border-gray-700" > < / div >
< button
v - for = "sec in autoRefreshIntervals"
: key = "sec"
@ click = "setAutoRefreshInterval(sec)"
class = "flex w-full items-center justify-between rounded-md px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
>
< span > { { autoRefreshIntervalLabel ( sec ) } } < / span >
< Icon v-if = "autoRefreshIntervalSeconds === sec" name="check" size="sm" class="text-primary-500" / >
< / button >
< / div >
< / div >
< / div >
2026-02-06 16:38:06 +08:00
<!-- Error Passthrough Rules -- >
< button
@ click = "showErrorPassthrough = true"
class = "btn btn-secondary"
: title = "t('admin.errorPassthrough.title')"
>
< Icon name = "shield" size = "md" class = "mr-1.5" / >
< span class = "hidden md:inline" > { { t ( 'admin.errorPassthrough.title' ) } } < / span >
< / button >
2026-01-18 11:57:07 +08:00
<!-- Column Settings Dropdown -- >
< div class = "relative" ref = "columnDropdownRef" >
< button
2026-01-21 22:43:25 +08:00
@ click = "
showColumnDropdown = ! showColumnDropdown ;
showAutoRefreshDropdown = false
"
2026-01-18 11:57:07 +08:00
class = "btn btn-secondary px-2 md:px-3"
: title = "t('admin.users.columnSettings')"
>
< svg class = "h-4 w-4 md:mr-1.5" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" stroke -width = " 1.5 " >
< path stroke -linecap = " round " stroke -linejoin = " round " d = "M9 4.5v15m6-15v15m-10.875 0h15.75c.621 0 1.125-.504 1.125-1.125V5.625c0-.621-.504-1.125-1.125-1.125H4.125C3.504 4.5 3 5.004 3 5.625v12.75c0 .621.504 1.125 1.125 1.125z" / >
< / svg >
< span class = "hidden md:inline" > { { t ( 'admin.users.columnSettings' ) } } < / span >
< / button >
<!-- Dropdown menu -- >
< div
v - if = "showColumnDropdown"
class = "absolute right-0 z-50 mt-2 w-48 origin-top-right rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
>
< div class = "max-h-80 overflow-y-auto p-2" >
< button
v - for = "col in toggleableColumns"
: key = "col.key"
@ click = "toggleColumn(col.key)"
class = "flex w-full items-center justify-between rounded-md px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
>
< span > { { col . label } } < / span >
< Icon v-if = "isColumnVisible(col.key)" name="check" size="sm" class="text-primary-500" / >
< / button >
< / div >
< / div >
< / div >
< / template >
2026-02-05 18:35:00 +08:00
< template # beforeCreate >
2026-02-05 17:46:08 +08:00
< button @click ="showImportData = true" class = "btn btn-secondary" >
{ { t ( 'admin.accounts.dataImport' ) } }
< / button >
< button @click ="openExportDataDialog" class = "btn btn-secondary" >
{ { selIds . length ? t ( 'admin.accounts.dataExportSelected' ) : t ( 'admin.accounts.dataExport' ) } }
< / button >
< / template >
2026-01-18 11:57:07 +08:00
< / AccountTableActions >
2026-01-05 01:00:00 +08:00
< / div >
< / template >
2025-12-27 10:50:25 +08:00
< template # table >
2026-01-09 19:26:32 +08:00
< AccountBulkActionsBar :selected-ids = "selIds" @delete ="handleBulkDelete" @edit ="showBulkEdit = true" @clear ="selIds = []" @ select -page = " selectPage " @ toggle -schedulable = " handleBulkToggleSchedulable " / >
2026-01-21 22:43:25 +08:00
< DataTable
: columns = "cols"
: data = "accounts"
: loading = "loading"
row - key = "id"
default - sort - key = "name"
default - sort - order = "asc"
: sort - storage - key = "ACCOUNT_SORT_STORAGE_KEY"
>
2026-02-05 18:23:49 +08:00
< template # header -select >
< input
type = "checkbox"
class = "h-4 w-4 cursor-pointer rounded border-gray-300 text-primary-600 focus:ring-primary-500"
: checked = "allVisibleSelected"
@ click . stop
@ change = "toggleSelectAllVisible($event)"
/ >
< / template >
2026-01-05 00:38:23 +08:00
< template # cell -select = " { row } " >
< input type = "checkbox" :checked = "selIds.includes(row.id)" @change ="toggleSel(row.id)" class = "rounded border-gray-300 text-primary-600 focus:ring-primary-500" / >
< / template >
2026-01-23 15:17:47 +08:00
< template # cell -name = " { row , value } " >
< div class = "flex flex-col" >
< span class = "font-medium text-gray-900 dark:text-white" > { { value } } < / span >
< span
v - if = "row.extra?.email_address"
class = "text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]"
: title = "row.extra.email_address"
>
{ { row . extra . email _address } }
< / span >
< / div >
2026-01-05 00:38:23 +08:00
< / template >
2026-01-05 14:07:33 +08:00
< template # cell -notes = " { value } " >
< span v-if = "value" :title="value" class="block max-w-xs truncate text-sm text-gray-600 dark:text-gray-300" > {{ value }} < / span >
< span v-else class = "text-sm text-gray-400 dark:text-dark-500" > - < / span >
< / template >
2026-01-05 00:38:23 +08:00
< template # cell -platform_type = " { row } " >
< PlatformTypeBadge :platform = "row.platform" :type = "row.type" / >
< / template >
2026-01-16 23:36:52 +08:00
< template # cell -capacity = " { row } " >
< AccountCapacityCell :account = "row" / >
2026-01-05 00:38:23 +08:00
< / template >
< template # cell -status = " { row } " >
< AccountStatusIndicator :account = "row" @ show -temp -unsched = " handleShowTempUnsched " / >
< / template >
< template # cell -schedulable = " { row } " >
< button @click ="handleToggleSchedulable(row)" : disabled = "togglingSchedulable === row.id" class = "relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:focus:ring-offset-dark-800" : class = "[row.schedulable ? 'bg-primary-500 hover:bg-primary-600' : 'bg-gray-200 hover:bg-gray-300 dark:bg-dark-600 dark:hover:bg-dark-500']" : title = "row.schedulable ? t('admin.accounts.schedulableEnabled') : t('admin.accounts.schedulableDisabled')" >
< span class = "pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" : class = "[row.schedulable ? 'translate-x-4' : 'translate-x-0']" / >
< / button >
< / template >
< template # cell -today_stats = " { row } " >
< AccountTodayStatsCell :account = "row" / >
< / template >
< template # cell -groups = " { row } " >
2026-01-12 16:08:44 +08:00
< AccountGroupsCell :groups = "row.groups" :max-display = "4" / >
2026-01-05 00:38:23 +08:00
< / template >
< template # cell -usage = " { row } " >
< AccountUsageCell :account = "row" / >
< / template >
2026-01-18 11:57:07 +08:00
< template # cell -proxy = " { row } " >
< div v-if = "row.proxy" class="flex items-center gap-2" >
< span class = "text-sm text-gray-700 dark:text-gray-300" > { { row . proxy . name } } < / span >
< span v-if = "row.proxy.country_code" class="text-xs text-gray-500 dark:text-gray-400" >
( { { row . proxy . country _code } } )
< / span >
< / div >
< span v-else class = "text-sm text-gray-400 dark:text-dark-500" > - < / span >
< / template >
2026-01-15 15:14:44 +08:00
< template # cell -rate_multiplier = " { row } " >
< span class = "text-sm font-mono text-gray-700 dark:text-gray-300" >
{ { ( row . rate _multiplier ? ? 1 ) . toFixed ( 2 ) } } x
< / span >
< / template >
2026-01-05 00:38:23 +08:00
< template # cell -priority = " { value } " >
< span class = "text-sm text-gray-700 dark:text-gray-300" > { { value } } < / span >
< / template >
< template # cell -last_used_at = " { value } " >
< span class = "text-sm text-gray-500 dark:text-dark-400" > { { formatRelativeTime ( value ) } } < / span >
< / template >
2026-01-07 16:59:35 +08:00
< template # cell -expires_at = " { row , value } " >
< div class = "flex flex-col items-start gap-1" >
< span class = "text-sm text-gray-500 dark:text-dark-400" > { { formatExpiresAt ( value ) } } < / span >
< div v-if = "isExpired(value) || (row.auto_pause_on_expired && value)" class="flex items-center gap-1" >
< span
v - if = "isExpired(value)"
class = "inline-flex items-center rounded-md bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
>
{ { t ( 'admin.accounts.expired' ) } }
< / span >
< span
v - if = "row.auto_pause_on_expired && value"
class = "inline-flex items-center rounded-md bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300"
>
{ { t ( 'admin.accounts.autoPauseOnExpired' ) } }
< / span >
< / div >
< / div >
< / template >
2026-01-05 00:38:23 +08:00
< template # cell -actions = " { row } " >
< div class = "flex items-center gap-1" >
< button @click ="handleEdit(row)" class = "flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:text-primary-400" >
< svg class = "h-4 w-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" stroke -width = " 1.5 " > < path stroke -linecap = " round " stroke -linejoin = " round " d = "M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" / > < / svg >
< span class = "text-xs" > { { t ( 'common.edit' ) } } < / span >
< / button >
< button @click ="handleDelete(row)" class = "flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 dark:hover:text-red-400" >
< svg class = "h-4 w-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" stroke -width = " 1.5 " > < path stroke -linecap = " round " stroke -linejoin = " round " d = "M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" / > < / svg >
< span class = "text-xs" > { { t ( 'common.delete' ) } } < / span >
< / button >
< button @click ="openMenu(row, $event)" class = "flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-dark-700 dark:hover:text-white" >
< svg class = "h-4 w-4" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" stroke -width = " 1.5 " > < path stroke -linecap = " round " stroke -linejoin = " round " d = "M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" / > < / svg >
< span class = "text-xs" > { { t ( 'common.more' ) } } < / span >
< / button >
< / div >
< / template >
2025-12-18 13:50:39 +08:00
< / DataTable >
2025-12-27 10:50:25 +08:00
< / template >
2026-01-09 17:38:45 +08:00
< template # pagination > < Pagination v-if = "pagination.total > 0" :page="pagination.page" :total="pagination.total" :page-size="pagination.page_size" @update:page="handlePageChange" @update:pageSize="handlePageSizeChange" / > < / template >
2025-12-27 10:50:25 +08:00
< / TablePageLayout >
2026-01-04 22:29:19 +08:00
< CreateAccountModal :show = "showCreate" :proxies = "proxies" :groups = "groups" @close ="showCreate = false" @created ="reload" / >
refactor(frontend): comprehensive split of large view files into modular components
- Split UsersView.vue into UserCreateModal, UserEditModal, UserApiKeysModal, etc.
- Split UsageView.vue into UsageStatsCards, UsageFilters, UsageTable, etc.
- Split DashboardView.vue into UserDashboardStats, UserDashboardCharts, etc.
- Split AccountsView.vue into AccountTableActions, AccountTableFilters, etc.
- Standardized TypeScript types across new components to resolve implicit 'any' and 'never[]' errors.
- Improved overall frontend maintainability and code clarity.
2026-01-04 22:17:27 +08:00
< EditAccountModal :show = "showEdit" :account = "edAcc" :proxies = "proxies" :groups = "groups" @close ="showEdit = false" @updated ="load" / >
2026-01-05 00:38:23 +08:00
< ReAuthAccountModal :show = "showReAuth" :account = "reAuthAcc" @close ="closeReAuthModal" @reauthorized ="load" / >
< AccountTestModal :show = "showTest" :account = "testingAcc" @close ="closeTestModal" / >
< AccountStatsModal :show = "showStats" :account = "statsAcc" @close ="closeStatsModal" / >
< AccountActionMenu :show = "menu.show" :account = "menu.acc" :position = "menu.pos" @close ="menu.show = false" @test ="handleTest" @stats ="handleViewStats" @reauth ="handleReAuth" @ refresh -token = " handleRefresh " @ reset -status = " handleResetStatus " @ clear -rate -limit = " handleClearRateLimit " / >
2026-01-04 22:29:19 +08:00
< SyncFromCrsModal :show = "showSync" @close ="showSync = false" @synced ="reload" / >
2026-02-05 17:46:08 +08:00
< ImportDataModal :show = "showImportData" @close ="showImportData = false" @imported ="handleDataImported" / >
refactor(frontend): comprehensive split of large view files into modular components
- Split UsersView.vue into UserCreateModal, UserEditModal, UserApiKeysModal, etc.
- Split UsageView.vue into UsageStatsCards, UsageFilters, UsageTable, etc.
- Split DashboardView.vue into UserDashboardStats, UserDashboardCharts, etc.
- Split AccountsView.vue into AccountTableActions, AccountTableFilters, etc.
- Standardized TypeScript types across new components to resolve implicit 'any' and 'never[]' errors.
- Improved overall frontend maintainability and code clarity.
2026-01-04 22:17:27 +08:00
< BulkEditAccountModal :show = "showBulkEdit" :account-ids = "selIds" :proxies = "proxies" :groups = "groups" @close ="showBulkEdit = false" @updated ="handleBulkUpdated" / >
2026-01-05 00:38:23 +08:00
< TempUnschedStatusModal :show = "showTempUnsched" :account = "tempUnschedAcc" @close ="showTempUnsched = false" @reset ="handleTempUnschedReset" / >
< ConfirmDialog :show = "showDeleteDialog" :title = "t('admin.accounts.deleteAccount')" : message = "t('admin.accounts.deleteConfirm', { name: deletingAcc?.name })" :confirm-text = "t('common.delete')" :cancel-text = "t('common.cancel')" :danger = "true" @confirm ="confirmDelete" @cancel ="showDeleteDialog = false" / >
2026-02-05 17:46:08 +08:00
< ConfirmDialog :show = "showExportDataDialog" :title = "t('admin.accounts.dataExport')" :message = "t('admin.accounts.dataExportConfirmMessage')" :confirm-text = "t('admin.accounts.dataExportConfirm')" :cancel-text = "t('common.cancel')" @confirm ="handleExportData" @cancel ="showExportDataDialog = false" >
< label class = "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300" >
< input type = "checkbox" class = "h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500" v-model = "includeProxyOnExport" / >
< span > { { t ( 'admin.accounts.dataExportIncludeProxies' ) } } < / span >
< / label >
< / ConfirmDialog >
2026-02-05 21:52:54 +08:00
< ErrorPassthroughRulesModal :show = "showErrorPassthrough" @close ="showErrorPassthrough = false" / >
2025-12-18 13:50:39 +08:00
< / AppLayout >
< / template >
< script setup lang = "ts" >
2026-01-15 15:14:44 +08:00
import { ref , reactive , computed , onMounted , onUnmounted } from 'vue'
2026-01-21 22:43:25 +08:00
import { useIntervalFn } from '@vueuse/core'
2026-01-05 00:38:23 +08:00
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { useAuthStore } from '@/stores/auth'
import { adminAPI } from '@/api/admin'
2026-01-04 22:29:19 +08:00
import { useTableLoader } from '@/composables/useTableLoader'
2026-01-05 00:38:23 +08:00
import AppLayout from '@/components/layout/AppLayout.vue'
import TablePageLayout from '@/components/layout/TablePageLayout.vue'
import DataTable from '@/components/common/DataTable.vue'
import Pagination from '@/components/common/Pagination.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import { CreateAccountModal , EditAccountModal , BulkEditAccountModal , SyncFromCrsModal , TempUnschedStatusModal } from '@/components/account'
import AccountTableActions from '@/components/admin/account/AccountTableActions.vue'
import AccountTableFilters from '@/components/admin/account/AccountTableFilters.vue'
import AccountBulkActionsBar from '@/components/admin/account/AccountBulkActionsBar.vue'
import AccountActionMenu from '@/components/admin/account/AccountActionMenu.vue'
2026-02-05 17:46:08 +08:00
import ImportDataModal from '@/components/admin/account/ImportDataModal.vue'
2026-01-05 00:38:23 +08:00
import ReAuthAccountModal from '@/components/admin/account/ReAuthAccountModal.vue'
import AccountTestModal from '@/components/admin/account/AccountTestModal.vue'
import AccountStatsModal from '@/components/admin/account/AccountStatsModal.vue'
2025-12-18 13:50:39 +08:00
import AccountStatusIndicator from '@/components/account/AccountStatusIndicator.vue'
2026-01-05 00:38:23 +08:00
import AccountUsageCell from '@/components/account/AccountUsageCell.vue'
import AccountTodayStatsCell from '@/components/account/AccountTodayStatsCell.vue'
2026-01-12 16:08:44 +08:00
import AccountGroupsCell from '@/components/account/AccountGroupsCell.vue'
2026-01-16 23:36:52 +08:00
import AccountCapacityCell from '@/components/account/AccountCapacityCell.vue'
2026-01-05 00:38:23 +08:00
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
2026-01-18 11:57:07 +08:00
import Icon from '@/components/icons/Icon.vue'
2026-02-05 21:52:54 +08:00
import ErrorPassthroughRulesModal from '@/components/admin/ErrorPassthroughRulesModal.vue'
2026-01-07 16:59:35 +08:00
import { formatDateTime , formatRelativeTime } from '@/utils/format'
2026-01-19 18:58:42 +08:00
import type { Account , Proxy , AdminGroup } from '@/types'
2025-12-24 16:56:48 -08:00
2026-01-05 00:38:23 +08:00
const { t } = useI18n ( )
const appStore = useAppStore ( )
const authStore = useAuthStore ( )
const proxies = ref < Proxy [ ] > ( [ ] )
2026-01-19 18:58:42 +08:00
const groups = ref < AdminGroup [ ] > ( [ ] )
2026-01-05 00:38:23 +08:00
const selIds = ref < number [ ] > ( [ ] )
const showCreate = ref ( false )
const showEdit = ref ( false )
const showSync = ref ( false )
2026-02-05 17:46:08 +08:00
const showImportData = ref ( false )
const showExportDataDialog = ref ( false )
const includeProxyOnExport = ref ( true )
2026-01-05 00:38:23 +08:00
const showBulkEdit = ref ( false )
const showTempUnsched = ref ( false )
const showDeleteDialog = ref ( false )
const showReAuth = ref ( false )
const showTest = ref ( false )
const showStats = ref ( false )
2026-02-05 21:52:54 +08:00
const showErrorPassthrough = ref ( false )
2026-01-05 00:38:23 +08:00
const edAcc = ref < Account | null > ( null )
const tempUnschedAcc = ref < Account | null > ( null )
const deletingAcc = ref < Account | null > ( null )
const reAuthAcc = ref < Account | null > ( null )
const testingAcc = ref < Account | null > ( null )
const statsAcc = ref < Account | null > ( null )
const togglingSchedulable = ref < number | null > ( null )
const menu = reactive < { show : boolean , acc : Account | null , pos : { top : number , left : number } | null } > ( { show : false , acc : null , pos : null } )
2026-02-05 17:46:08 +08:00
const exportingData = ref ( false )
2026-01-04 22:29:19 +08:00
2026-01-18 11:57:07 +08:00
// Column settings
const showColumnDropdown = ref ( false )
const columnDropdownRef = ref < HTMLElement | null > ( null )
const hiddenColumns = reactive < Set < string > > ( new Set ( ) )
const DEFAULT _HIDDEN _COLUMNS = [ 'proxy' , 'notes' , 'priority' , 'rate_multiplier' ]
const HIDDEN _COLUMNS _KEY = 'account-hidden-columns'
2026-01-21 22:43:25 +08:00
// Sorting settings
const ACCOUNT _SORT _STORAGE _KEY = 'account-table-sort'
// Auto refresh settings
const showAutoRefreshDropdown = ref ( false )
const autoRefreshDropdownRef = ref < HTMLElement | null > ( null )
const AUTO _REFRESH _STORAGE _KEY = 'account-auto-refresh'
const autoRefreshIntervals = [ 5 , 10 , 15 , 30 ] as const
const autoRefreshEnabled = ref ( false )
const autoRefreshIntervalSeconds = ref < ( typeof autoRefreshIntervals ) [ number ] > ( 30 )
const autoRefreshCountdown = ref ( 0 )
const autoRefreshIntervalLabel = ( sec : number ) => {
if ( sec === 5 ) return t ( 'admin.accounts.refreshInterval5s' )
if ( sec === 10 ) return t ( 'admin.accounts.refreshInterval10s' )
if ( sec === 15 ) return t ( 'admin.accounts.refreshInterval15s' )
if ( sec === 30 ) return t ( 'admin.accounts.refreshInterval30s' )
return ` ${ sec } s `
}
2026-01-18 11:57:07 +08:00
const loadSavedColumns = ( ) => {
try {
const saved = localStorage . getItem ( HIDDEN _COLUMNS _KEY )
if ( saved ) {
const parsed = JSON . parse ( saved ) as string [ ]
parsed . forEach ( key => hiddenColumns . add ( key ) )
} else {
DEFAULT _HIDDEN _COLUMNS . forEach ( key => hiddenColumns . add ( key ) )
}
} catch ( e ) {
console . error ( 'Failed to load saved columns:' , e )
DEFAULT _HIDDEN _COLUMNS . forEach ( key => hiddenColumns . add ( key ) )
}
}
const saveColumnsToStorage = ( ) => {
try {
localStorage . setItem ( HIDDEN _COLUMNS _KEY , JSON . stringify ( [ ... hiddenColumns ] ) )
} catch ( e ) {
console . error ( 'Failed to save columns:' , e )
}
}
2026-01-21 22:43:25 +08:00
const loadSavedAutoRefresh = ( ) => {
try {
const saved = localStorage . getItem ( AUTO _REFRESH _STORAGE _KEY )
if ( ! saved ) return
const parsed = JSON . parse ( saved ) as { enabled ? : boolean ; interval _seconds ? : number }
autoRefreshEnabled . value = parsed . enabled === true
const interval = Number ( parsed . interval _seconds )
if ( autoRefreshIntervals . includes ( interval as any ) ) {
autoRefreshIntervalSeconds . value = interval as any
}
} catch ( e ) {
console . error ( 'Failed to load saved auto refresh settings:' , e )
}
}
const saveAutoRefreshToStorage = ( ) => {
try {
localStorage . setItem (
AUTO _REFRESH _STORAGE _KEY ,
JSON . stringify ( {
enabled : autoRefreshEnabled . value ,
interval _seconds : autoRefreshIntervalSeconds . value
} )
)
} catch ( e ) {
console . error ( 'Failed to save auto refresh settings:' , e )
}
}
if ( typeof window !== 'undefined' ) {
loadSavedColumns ( )
loadSavedAutoRefresh ( )
}
const setAutoRefreshEnabled = ( enabled : boolean ) => {
autoRefreshEnabled . value = enabled
saveAutoRefreshToStorage ( )
if ( enabled ) {
autoRefreshCountdown . value = autoRefreshIntervalSeconds . value
resumeAutoRefresh ( )
} else {
pauseAutoRefresh ( )
autoRefreshCountdown . value = 0
}
}
const setAutoRefreshInterval = ( seconds : ( typeof autoRefreshIntervals ) [ number ] ) => {
autoRefreshIntervalSeconds . value = seconds
saveAutoRefreshToStorage ( )
if ( autoRefreshEnabled . value ) {
autoRefreshCountdown . value = seconds
}
}
2026-01-18 11:57:07 +08:00
const toggleColumn = ( key : string ) => {
if ( hiddenColumns . has ( key ) ) {
hiddenColumns . delete ( key )
} else {
hiddenColumns . add ( key )
}
saveColumnsToStorage ( )
}
const isColumnVisible = ( key : string ) => ! hiddenColumns . has ( key )
2026-01-09 17:38:45 +08:00
const { items : accounts , loading , params , pagination , load , reload , debouncedReload , handlePageChange , handlePageSizeChange } = useTableLoader < Account , any > ( {
2026-01-04 22:29:19 +08:00
fetchFn : adminAPI . accounts . list ,
2026-02-12 03:47:06 +08:00
initialParams : { platform : '' , type : '' , status : '' , group : '' , search : '' }
2026-01-04 22:29:19 +08:00
} )
2025-12-18 13:50:39 +08:00
2026-01-21 22:43:25 +08:00
const isAnyModalOpen = computed ( ( ) => {
return (
showCreate . value ||
showEdit . value ||
showSync . value ||
2026-02-05 17:46:08 +08:00
showImportData . value ||
showExportDataDialog . value ||
2026-01-21 22:43:25 +08:00
showBulkEdit . value ||
showTempUnsched . value ||
showDeleteDialog . value ||
showReAuth . value ||
showTest . value ||
2026-02-06 11:33:45 +08:00
showStats . value ||
showErrorPassthrough . value
2026-01-21 22:43:25 +08:00
)
} )
const { pause : pauseAutoRefresh , resume : resumeAutoRefresh } = useIntervalFn (
async ( ) => {
if ( ! autoRefreshEnabled . value ) return
if ( document . hidden ) return
if ( loading . value ) return
if ( isAnyModalOpen . value ) return
if ( menu . show ) return
if ( autoRefreshCountdown . value <= 0 ) {
autoRefreshCountdown . value = autoRefreshIntervalSeconds . value
try {
await load ( )
} catch ( e ) {
console . error ( 'Auto refresh failed:' , e )
}
return
}
autoRefreshCountdown . value -= 1
} ,
1000 ,
{ immediate : false }
)
2026-01-18 11:57:07 +08:00
// All available columns
const allColumns = computed ( ( ) => {
2026-01-05 00:38:23 +08:00
const c = [
{ key : 'select' , label : '' , sortable : false } ,
{ key : 'name' , label : t ( 'admin.accounts.columns.name' ) , sortable : true } ,
{ key : 'platform_type' , label : t ( 'admin.accounts.columns.platformType' ) , sortable : false } ,
2026-01-16 23:36:52 +08:00
{ key : 'capacity' , label : t ( 'admin.accounts.columns.capacity' ) , sortable : false } ,
2026-01-05 00:38:23 +08:00
{ key : 'status' , label : t ( 'admin.accounts.columns.status' ) , sortable : true } ,
{ key : 'schedulable' , label : t ( 'admin.accounts.columns.schedulable' ) , sortable : true } ,
{ key : 'today_stats' , label : t ( 'admin.accounts.columns.todayStats' ) , sortable : false }
]
if ( ! authStore . isSimpleMode ) {
c . push ( { key : 'groups' , label : t ( 'admin.accounts.columns.groups' ) , sortable : false } )
}
2026-01-18 11:57:07 +08:00
c . push (
{ key : 'usage' , label : t ( 'admin.accounts.columns.usageWindows' ) , sortable : false } ,
{ key : 'proxy' , label : t ( 'admin.accounts.columns.proxy' ) , sortable : false } ,
{ key : 'priority' , label : t ( 'admin.accounts.columns.priority' ) , sortable : true } ,
{ key : 'rate_multiplier' , label : t ( 'admin.accounts.columns.billingRateMultiplier' ) , sortable : true } ,
{ key : 'last_used_at' , label : t ( 'admin.accounts.columns.lastUsed' ) , sortable : true } ,
2026-01-07 16:59:35 +08:00
{ key : 'expires_at' , label : t ( 'admin.accounts.columns.expiresAt' ) , sortable : true } ,
2026-01-05 14:07:33 +08:00
{ key : 'notes' , label : t ( 'admin.accounts.columns.notes' ) , sortable : false } ,
2026-01-05 00:38:23 +08:00
{ key : 'actions' , label : t ( 'admin.accounts.columns.actions' ) , sortable : false }
)
return c
} )
2025-12-23 10:01:58 +08:00
2026-01-18 11:57:07 +08:00
// Columns that can be toggled (exclude select, name, and actions)
const toggleableColumns = computed ( ( ) =>
allColumns . value . filter ( col => col . key !== 'select' && col . key !== 'name' && col . key !== 'actions' )
)
// Filtered columns based on visibility
const cols = computed ( ( ) =>
allColumns . value . filter ( col =>
col . key === 'select' || col . key === 'name' || col . key === 'actions' || ! hiddenColumns . has ( col . key )
)
)
refactor(frontend): comprehensive split of large view files into modular components
- Split UsersView.vue into UserCreateModal, UserEditModal, UserApiKeysModal, etc.
- Split UsageView.vue into UsageStatsCards, UsageFilters, UsageTable, etc.
- Split DashboardView.vue into UserDashboardStats, UserDashboardCharts, etc.
- Split AccountsView.vue into AccountTableActions, AccountTableFilters, etc.
- Standardized TypeScript types across new components to resolve implicit 'any' and 'never[]' errors.
- Improved overall frontend maintainability and code clarity.
2026-01-04 22:17:27 +08:00
const handleEdit = ( a : Account ) => { edAcc . value = a ; showEdit . value = true }
2026-01-15 15:14:44 +08:00
const openMenu = ( a : Account , e : MouseEvent ) => {
menu . acc = a
const target = e . currentTarget as HTMLElement
if ( target ) {
const rect = target . getBoundingClientRect ( )
const menuWidth = 200
const menuHeight = 240
const padding = 8
const viewportWidth = window . innerWidth
const viewportHeight = window . innerHeight
let left , top
if ( viewportWidth < 768 ) {
// 居中显示,水平位置
left = Math . max ( padding , Math . min (
rect . left + rect . width / 2 - menuWidth / 2 ,
viewportWidth - menuWidth - padding
) )
// 优先显示在按钮下方
top = rect . bottom + 4
// 如果下方空间不够,显示在上方
if ( top + menuHeight > viewportHeight - padding ) {
top = rect . top - menuHeight - 4
// 如果上方也不够,就贴在视口顶部
if ( top < padding ) {
top = padding
}
}
} else {
left = Math . max ( padding , Math . min (
e . clientX - menuWidth ,
viewportWidth - menuWidth - padding
) )
top = e . clientY
if ( top + menuHeight > viewportHeight - padding ) {
top = viewportHeight - menuHeight - padding
}
}
menu . pos = { top , left }
} else {
menu . pos = { top : e . clientY , left : e . clientX - 200 }
}
menu . show = true
}
refactor(frontend): comprehensive split of large view files into modular components
- Split UsersView.vue into UserCreateModal, UserEditModal, UserApiKeysModal, etc.
- Split UsageView.vue into UsageStatsCards, UsageFilters, UsageTable, etc.
- Split DashboardView.vue into UserDashboardStats, UserDashboardCharts, etc.
- Split AccountsView.vue into AccountTableActions, AccountTableFilters, etc.
- Standardized TypeScript types across new components to resolve implicit 'any' and 'never[]' errors.
- Improved overall frontend maintainability and code clarity.
2026-01-04 22:17:27 +08:00
const toggleSel = ( id : number ) => { const i = selIds . value . indexOf ( id ) ; if ( i === - 1 ) selIds . value . push ( id ) ; else selIds . value . splice ( i , 1 ) }
2026-02-05 18:23:49 +08:00
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 ) )
}
2026-01-05 00:38:23 +08:00
const selectPage = ( ) => { selIds . value = [ ... new Set ( [ ... selIds . value , ... accounts . value . map ( a => a . id ) ] ) ] }
2026-01-06 12:42:06 +08:00
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 ) } }
2026-01-11 20:22:17 +08:00
const updateSchedulableInList = ( accountIds : number [ ] , schedulable : boolean ) => {
if ( accountIds . length === 0 ) return
const idSet = new Set ( accountIds )
accounts . value = accounts . value . map ( ( account ) => ( idSet . has ( account . id ) ? { ... account , schedulable } : account ) )
}
const normalizeBulkSchedulableResult = (
result : {
success ? : number
failed ? : number
success _ids ? : number [ ]
failed _ids ? : number [ ]
results ? : Array < { account _id : number ; success : boolean } >
} ,
accountIds : number [ ]
) => {
const responseSuccessIds = Array . isArray ( result . success _ids ) ? result . success _ids : [ ]
const responseFailedIds = Array . isArray ( result . failed _ids ) ? result . failed _ids : [ ]
if ( responseSuccessIds . length > 0 || responseFailedIds . length > 0 ) {
return {
successIds : responseSuccessIds ,
failedIds : responseFailedIds ,
successCount : typeof result . success === 'number' ? result . success : responseSuccessIds . length ,
failedCount : typeof result . failed === 'number' ? result . failed : responseFailedIds . length ,
hasIds : true ,
hasCounts : true
}
}
const results = Array . isArray ( result . results ) ? result . results : [ ]
if ( results . length > 0 ) {
const successIds = results . filter ( item => item . success ) . map ( item => item . account _id )
const failedIds = results . filter ( item => ! item . success ) . map ( item => item . account _id )
return {
successIds ,
failedIds ,
successCount : typeof result . success === 'number' ? result . success : successIds . length ,
failedCount : typeof result . failed === 'number' ? result . failed : failedIds . length ,
hasIds : true ,
hasCounts : true
}
}
const hasExplicitCounts = typeof result . success === 'number' || typeof result . failed === 'number'
const successCount = typeof result . success === 'number' ? result . success : 0
const failedCount = typeof result . failed === 'number' ? result . failed : 0
if ( hasExplicitCounts && failedCount === 0 && successCount === accountIds . length && accountIds . length > 0 ) {
return {
successIds : accountIds ,
failedIds : [ ] ,
successCount ,
failedCount ,
hasIds : true ,
hasCounts : true
}
}
return {
successIds : [ ] ,
failedIds : [ ] ,
successCount ,
failedCount ,
hasIds : false ,
hasCounts : hasExplicitCounts
}
}
2026-01-09 19:26:32 +08:00
const handleBulkToggleSchedulable = async ( schedulable : boolean ) => {
2026-01-11 20:22:17 +08:00
const accountIds = [ ... selIds . value ]
2026-01-09 19:26:32 +08:00
try {
2026-01-11 20:22:17 +08:00
const result = await adminAPI . accounts . bulkUpdate ( accountIds , { schedulable } )
const { successIds , failedIds , successCount , failedCount , hasIds , hasCounts } = normalizeBulkSchedulableResult ( result , accountIds )
if ( ! hasIds && ! hasCounts ) {
appStore . showError ( t ( 'admin.accounts.bulkSchedulableResultUnknown' ) )
selIds . value = accountIds
load ( ) . catch ( ( error ) => {
console . error ( 'Failed to refresh accounts:' , error )
} )
return
}
if ( successIds . length > 0 ) {
updateSchedulableInList ( successIds , schedulable )
}
if ( successCount > 0 && failedCount === 0 ) {
const message = schedulable
? t ( 'admin.accounts.bulkSchedulableEnabled' , { count : successCount } )
: t ( 'admin.accounts.bulkSchedulableDisabled' , { count : successCount } )
appStore . showSuccess ( message )
}
if ( failedCount > 0 ) {
const message = hasCounts || hasIds
? t ( 'admin.accounts.bulkSchedulablePartial' , { success : successCount , failed : failedCount } )
: t ( 'admin.accounts.bulkSchedulableResultUnknown' )
appStore . showError ( message )
selIds . value = failedIds . length > 0 ? failedIds : accountIds
} else {
selIds . value = hasIds ? [ ] : accountIds
}
2026-01-09 19:26:32 +08:00
} catch ( error ) {
2026-01-11 20:22:17 +08:00
console . error ( 'Failed to bulk toggle schedulable:' , error )
2026-01-09 19:26:32 +08:00
appStore . showError ( t ( 'common.error' ) )
}
}
2026-01-04 22:29:19 +08:00
const handleBulkUpdated = ( ) => { showBulkEdit . value = false ; selIds . value = [ ] ; reload ( ) }
2026-02-05 17:46:08 +08:00
const handleDataImported = ( ) => { showImportData . value = false ; reload ( ) }
const formatExportTimestamp = ( ) => {
const now = new Date ( )
const pad2 = ( value : number ) => String ( value ) . padStart ( 2 , '0' )
return ` ${ now . getFullYear ( ) } ${ pad2 ( now . getMonth ( ) + 1 ) } ${ pad2 ( now . getDate ( ) ) } ${ pad2 ( now . getHours ( ) ) } ${ pad2 ( now . getMinutes ( ) ) } ${ pad2 ( now . getSeconds ( ) ) } `
}
const openExportDataDialog = ( ) => {
includeProxyOnExport . value = true
showExportDataDialog . value = true
}
const handleExportData = async ( ) => {
if ( exportingData . value ) return
exportingData . value = true
try {
const dataPayload = await adminAPI . accounts . exportData (
selIds . value . length > 0
? { ids : selIds . value , includeProxies : includeProxyOnExport . value }
: {
includeProxies : includeProxyOnExport . value ,
filters : {
platform : params . platform ,
type : params . type ,
status : params . status ,
search : params . search
}
}
)
const timestamp = formatExportTimestamp ( )
const filename = ` sub2api-account- ${ timestamp } .json `
const blob = new Blob ( [ JSON . stringify ( dataPayload , null , 2 ) ] , { type : 'application/json' } )
const url = URL . createObjectURL ( blob )
const link = document . createElement ( 'a' )
link . href = url
link . download = filename
link . click ( )
URL . revokeObjectURL ( url )
appStore . showSuccess ( t ( 'admin.accounts.dataExported' ) )
} catch ( error : any ) {
appStore . showError ( error ? . message || t ( 'admin.accounts.dataExportFailed' ) )
} finally {
exportingData . value = false
showExportDataDialog . value = false
}
}
2026-01-05 00:38:23 +08:00
const closeTestModal = ( ) => { showTest . value = false ; testingAcc . value = null }
const closeStatsModal = ( ) => { showStats . value = false ; statsAcc . value = null }
const closeReAuthModal = ( ) => { showReAuth . value = false ; reAuthAcc . value = null }
const handleTest = ( a : Account ) => { testingAcc . value = a ; showTest . value = true }
const handleViewStats = ( a : Account ) => { statsAcc . value = a ; showStats . value = true }
const handleReAuth = ( a : Account ) => { reAuthAcc . value = a ; showReAuth . value = true }
2026-01-06 12:42:06 +08:00
const handleRefresh = async ( a : Account ) => { try { await adminAPI . accounts . refreshCredentials ( a . id ) ; load ( ) } catch ( error ) { console . error ( 'Failed to refresh credentials:' , error ) } }
const handleResetStatus = async ( a : Account ) => { try { await adminAPI . accounts . clearError ( a . id ) ; appStore . showSuccess ( t ( 'common.success' ) ) ; load ( ) } catch ( error ) { console . error ( 'Failed to reset status:' , error ) } }
2026-01-06 12:50:51 +08:00
const handleClearRateLimit = async ( a : Account ) => { try { await adminAPI . accounts . clearRateLimit ( a . id ) ; appStore . showSuccess ( t ( 'common.success' ) ) ; load ( ) } catch ( error ) { console . error ( 'Failed to clear rate limit:' , error ) } }
2026-01-05 00:38:23 +08:00
const handleDelete = ( a : Account ) => { deletingAcc . value = a ; showDeleteDialog . value = true }
2026-01-06 12:42:06 +08:00
const confirmDelete = async ( ) => { if ( ! deletingAcc . value ) return ; try { await adminAPI . accounts . delete ( deletingAcc . value . id ) ; showDeleteDialog . value = false ; deletingAcc . value = null ; reload ( ) } catch ( error ) { console . error ( 'Failed to delete account:' , error ) } }
2026-01-11 20:22:17 +08:00
const handleToggleSchedulable = async ( a : Account ) => {
const nextSchedulable = ! a . schedulable
togglingSchedulable . value = a . id
try {
const updated = await adminAPI . accounts . setSchedulable ( a . id , nextSchedulable )
updateSchedulableInList ( [ a . id ] , updated ? . schedulable ? ? nextSchedulable )
} catch ( error ) {
console . error ( 'Failed to toggle schedulable:' , error )
appStore . showError ( t ( 'admin.accounts.failedToToggleSchedulable' ) )
} finally {
togglingSchedulable . value = null
}
}
2026-01-05 00:38:23 +08:00
const handleShowTempUnsched = ( a : Account ) => { tempUnschedAcc . value = a ; showTempUnsched . value = true }
2026-01-06 12:42:06 +08:00
const handleTempUnschedReset = async ( ) => { if ( ! tempUnschedAcc . value ) return ; try { await adminAPI . accounts . clearError ( tempUnschedAcc . value . id ) ; showTempUnsched . value = false ; tempUnschedAcc . value = null ; load ( ) } catch ( error ) { console . error ( 'Failed to reset temp unscheduled:' , error ) } }
2026-01-07 16:59:35 +08:00
const formatExpiresAt = ( value : number | null ) => {
if ( ! value ) return '-'
return formatDateTime (
new Date ( value * 1000 ) ,
{
year : 'numeric' ,
month : '2-digit' ,
day : '2-digit' ,
hour : '2-digit' ,
minute : '2-digit' ,
hour12 : false
} ,
'sv-SE'
)
}
const isExpired = ( value : number | null ) => {
if ( ! value ) return false
return value * 1000 <= Date . now ( )
}
refactor(frontend): comprehensive split of large view files into modular components
- Split UsersView.vue into UserCreateModal, UserEditModal, UserApiKeysModal, etc.
- Split UsageView.vue into UsageStatsCards, UsageFilters, UsageTable, etc.
- Split DashboardView.vue into UserDashboardStats, UserDashboardCharts, etc.
- Split AccountsView.vue into AccountTableActions, AccountTableFilters, etc.
- Standardized TypeScript types across new components to resolve implicit 'any' and 'never[]' errors.
- Improved overall frontend maintainability and code clarity.
2026-01-04 22:17:27 +08:00
2026-01-18 11:57:07 +08:00
// 滚动时关闭操作菜单(不关闭列设置下拉菜单)
2026-01-15 15:14:44 +08:00
const handleScroll = ( ) => {
menu . show = false
}
2026-01-18 11:57:07 +08:00
// 点击外部关闭列设置下拉菜单
const handleClickOutside = ( event : MouseEvent ) => {
const target = event . target as HTMLElement
if ( columnDropdownRef . value && ! columnDropdownRef . value . contains ( target ) ) {
showColumnDropdown . value = false
}
2026-01-21 22:43:25 +08:00
if ( autoRefreshDropdownRef . value && ! autoRefreshDropdownRef . value . contains ( target ) ) {
showAutoRefreshDropdown . value = false
}
2026-01-18 11:57:07 +08:00
}
2026-01-15 22:07:23 +08:00
onMounted ( async ( ) => {
load ( )
try {
const [ p , g ] = await Promise . all ( [ adminAPI . proxies . getAll ( ) , adminAPI . groups . getAll ( ) ] )
proxies . value = p
groups . value = g
} catch ( error ) {
console . error ( 'Failed to load proxies/groups:' , error )
}
window . addEventListener ( 'scroll' , handleScroll , true )
2026-01-18 11:57:07 +08:00
document . addEventListener ( 'click' , handleClickOutside )
2026-01-21 22:43:25 +08:00
if ( autoRefreshEnabled . value ) {
autoRefreshCountdown . value = autoRefreshIntervalSeconds . value
resumeAutoRefresh ( )
} else {
pauseAutoRefresh ( )
}
2026-01-15 22:07:23 +08:00
} )
2026-01-15 15:14:44 +08:00
onUnmounted ( ( ) => {
window . removeEventListener ( 'scroll' , handleScroll , true )
2026-01-18 11:57:07 +08:00
document . removeEventListener ( 'click' , handleClickOutside )
2026-01-15 15:14:44 +08:00
} )
2026-01-05 00:38:23 +08:00
< / script >