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-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-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-11 20:22:17 +08:00
< DataTable :columns = "cols" :data = "accounts" :loading = "loading" row -key = " id " >
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 >
< template # cell -name = " { value } " >
< span class = "font-medium text-gray-900 dark:text-white" > { { value } } < / span >
< / 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 >
< template # cell -concurrency = " { row } " >
< div class = "flex items-center gap-1.5" >
< span : class = "['inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium', (row.current_concurrency || 0) >= row.concurrency ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' : (row.current_concurrency || 0) > 0 ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400' : 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400']" >
< svg class = "h-3 w-3" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" stroke -width = " 2 " > < path stroke -linecap = " round " stroke -linejoin = " round " d = "M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" / > < / svg >
< span class = "font-mono" > { { row . current _concurrency || 0 } } < / span >
< span class = "text-gray-400 dark:text-gray-500" > / < / span >
< span class = "font-mono" > { { row . concurrency } } < / span >
< / span >
< / div >
< / 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-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" / >
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" / >
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-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'
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-05 00:38:23 +08:00
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
2026-01-07 16:59:35 +08:00
import { formatDateTime , formatRelativeTime } from '@/utils/format'
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
import type { Account , Proxy , Group } 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 [ ] > ( [ ] )
const groups = ref < Group [ ] > ( [ ] )
const selIds = ref < number [ ] > ( [ ] )
const showCreate = ref ( false )
const showEdit = ref ( false )
const showSync = ref ( false )
const showBulkEdit = ref ( false )
const showTempUnsched = ref ( false )
const showDeleteDialog = ref ( false )
const showReAuth = ref ( false )
const showTest = ref ( false )
const showStats = ref ( false )
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-01-04 22:29:19 +08:00
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-01-05 00:38:23 +08:00
initialParams : { platform : '' , type : '' , status : '' , search : '' }
2026-01-04 22:29:19 +08:00
} )
2025-12-18 13:50:39 +08:00
2026-01-05 00:38:23 +08:00
const cols = computed ( ( ) => {
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 } ,
{ key : 'concurrency' , label : t ( 'admin.accounts.columns.concurrencyStatus' ) , sortable : false } ,
{ 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-15 15:14:44 +08:00
c . push (
{ key : 'usage' , label : t ( 'admin.accounts.columns.usageWindows' ) , 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
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-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-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-15 15:14:44 +08:00
// 滚动时关闭菜单
const handleScroll = ( ) => {
menu . show = false
}
2026-01-15 21:31:55 +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-15 15:14:44 +08:00
onUnmounted ( ( ) => {
window . removeEventListener ( 'scroll' , handleScroll , true )
} )
2026-01-05 00:38:23 +08:00
< / script >