Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
ff74f517
Commit
ff74f517
authored
Jan 21, 2026
by
ducky
Browse files
feat(frontend): 账号表格默认排序/持久化 + 自动刷新 + 更多菜单外部关闭
parent
39fad63c
Changes
6
Show whitespace changes
Inline
Side-by-side
frontend/src/components/admin/account/AccountActionMenu.vue
View file @
ff74f517
<
template
>
<Teleport
to=
"body"
>
<div
v-if=
"show && position"
class=
"action-menu-content fixed z-[9999] w-52 overflow-hidden rounded-xl bg-white shadow-lg ring-1 ring-black/5 dark:bg-dark-800"
:style=
"
{ top: position.top + 'px', left: position.left + 'px' }">
<div
v-if=
"show && position"
>
<!-- Backdrop: click anywhere outside to close -->
<div
class=
"fixed inset-0 z-[9998]"
@
click=
"emit('close')"
></div>
<div
class=
"action-menu-content fixed z-[9999] w-52 overflow-hidden rounded-xl bg-white shadow-lg ring-1 ring-black/5 dark:bg-dark-800"
:style=
"
{ top: position.top + 'px', left: position.left + 'px' }"
@click.stop
>
<div
class=
"py-1"
>
<template
v-if=
"account"
>
<button
@
click=
"$emit('test', account); $emit('close')"
class=
"flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-dark-700"
>
...
...
@@ -33,18 +40,39 @@
</template>
</div>
</div>
</div>
</Teleport>
</template>
<
script
setup
lang=
"ts"
>
import
{
computed
}
from
'
vue
'
import
{
computed
,
watch
,
onUnmounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
Icon
}
from
'
@/components/icons
'
import
type
{
Account
}
from
'
@/types
'
const
props
=
defineProps
<
{
show
:
boolean
;
account
:
Account
|
null
;
position
:
{
top
:
number
;
left
:
number
}
|
null
}
>
()
defineEmits
([
'
close
'
,
'
test
'
,
'
stats
'
,
'
reauth
'
,
'
refresh-token
'
,
'
reset-status
'
,
'
clear-rate-limit
'
])
const
emit
=
defineEmits
([
'
close
'
,
'
test
'
,
'
stats
'
,
'
reauth
'
,
'
refresh-token
'
,
'
reset-status
'
,
'
clear-rate-limit
'
])
const
{
t
}
=
useI18n
()
const
isRateLimited
=
computed
(()
=>
props
.
account
?.
rate_limit_reset_at
&&
new
Date
(
props
.
account
.
rate_limit_reset_at
)
>
new
Date
())
const
isOverloaded
=
computed
(()
=>
props
.
account
?.
overload_until
&&
new
Date
(
props
.
account
.
overload_until
)
>
new
Date
())
const
handleKeydown
=
(
event
:
KeyboardEvent
)
=>
{
if
(
event
.
key
===
'
Escape
'
)
emit
(
'
close
'
)
}
watch
(
()
=>
props
.
show
,
(
visible
)
=>
{
if
(
visible
)
{
window
.
addEventListener
(
'
keydown
'
,
handleKeydown
)
}
else
{
window
.
removeEventListener
(
'
keydown
'
,
handleKeydown
)
}
},
{
immediate
:
true
}
)
onUnmounted
(()
=>
{
window
.
removeEventListener
(
'
keydown
'
,
handleKeydown
)
})
</
script
>
frontend/src/components/common/DataTable.vue
View file @
ff74f517
...
...
@@ -279,18 +279,143 @@ interface Props {
expandableActions
?:
boolean
actionsCount
?:
number
// 操作按钮总数,用于判断是否需要展开功能
rowKey
?:
string
|
((
row
:
any
)
=>
string
|
number
)
/**
* Default sort configuration (only applied when there is no persisted sort state)
*/
defaultSortKey
?:
string
defaultSortOrder
?:
'
asc
'
|
'
desc
'
/**
* Persist sort state (key + order) to localStorage using this key.
* If provided, DataTable will load the stored sort state on mount.
*/
sortStorageKey
?:
string
}
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
loading
:
false
,
stickyFirstColumn
:
true
,
stickyActionsColumn
:
true
,
expandableActions
:
true
expandableActions
:
true
,
defaultSortOrder
:
'
asc
'
})
const
sortKey
=
ref
<
string
>
(
''
)
const
sortOrder
=
ref
<
'
asc
'
|
'
desc
'
>
(
'
asc
'
)
const
actionsExpanded
=
ref
(
false
)
type
PersistedSortState
=
{
key
:
string
order
:
'
asc
'
|
'
desc
'
}
const
collator
=
new
Intl
.
Collator
(
undefined
,
{
numeric
:
true
,
sensitivity
:
'
base
'
})
const
getSortableKeys
=
()
=>
{
const
keys
=
new
Set
<
string
>
()
for
(
const
col
of
props
.
columns
)
{
if
(
col
.
sortable
)
keys
.
add
(
col
.
key
)
}
return
keys
}
const
normalizeSortKey
=
(
candidate
:
string
)
=>
{
if
(
!
candidate
)
return
''
const
sortableKeys
=
getSortableKeys
()
return
sortableKeys
.
has
(
candidate
)
?
candidate
:
''
}
const
normalizeSortOrder
=
(
candidate
:
any
):
'
asc
'
|
'
desc
'
=>
{
return
candidate
===
'
desc
'
?
'
desc
'
:
'
asc
'
}
const
readPersistedSortState
=
():
PersistedSortState
|
null
=>
{
if
(
!
props
.
sortStorageKey
)
return
null
try
{
const
raw
=
localStorage
.
getItem
(
props
.
sortStorageKey
)
if
(
!
raw
)
return
null
const
parsed
=
JSON
.
parse
(
raw
)
as
Partial
<
PersistedSortState
>
const
key
=
normalizeSortKey
(
typeof
parsed
.
key
===
'
string
'
?
parsed
.
key
:
''
)
if
(
!
key
)
return
null
return
{
key
,
order
:
normalizeSortOrder
(
parsed
.
order
)
}
}
catch
(
e
)
{
console
.
error
(
'
[DataTable] Failed to read persisted sort state:
'
,
e
)
return
null
}
}
const
writePersistedSortState
=
(
state
:
PersistedSortState
)
=>
{
if
(
!
props
.
sortStorageKey
)
return
try
{
localStorage
.
setItem
(
props
.
sortStorageKey
,
JSON
.
stringify
(
state
))
}
catch
(
e
)
{
console
.
error
(
'
[DataTable] Failed to persist sort state:
'
,
e
)
}
}
const
resolveInitialSortState
=
():
PersistedSortState
|
null
=>
{
const
persisted
=
readPersistedSortState
()
if
(
persisted
)
return
persisted
const
key
=
normalizeSortKey
(
props
.
defaultSortKey
||
''
)
if
(
!
key
)
return
null
return
{
key
,
order
:
normalizeSortOrder
(
props
.
defaultSortOrder
)
}
}
const
applySortState
=
(
state
:
PersistedSortState
|
null
)
=>
{
if
(
!
state
)
return
sortKey
.
value
=
state
.
key
sortOrder
.
value
=
state
.
order
}
const
isNullishOrEmpty
=
(
value
:
any
)
=>
value
===
null
||
value
===
undefined
||
value
===
''
const
toFiniteNumberOrNull
=
(
value
:
any
):
number
|
null
=>
{
if
(
typeof
value
===
'
number
'
)
return
Number
.
isFinite
(
value
)
?
value
:
null
if
(
typeof
value
===
'
boolean
'
)
return
value
?
1
:
0
if
(
typeof
value
===
'
string
'
)
{
const
trimmed
=
value
.
trim
()
if
(
!
trimmed
)
return
null
const
n
=
Number
(
trimmed
)
return
Number
.
isFinite
(
n
)
?
n
:
null
}
return
null
}
const
toSortableString
=
(
value
:
any
):
string
=>
{
if
(
value
===
null
||
value
===
undefined
)
return
''
if
(
typeof
value
===
'
string
'
)
return
value
if
(
typeof
value
===
'
number
'
||
typeof
value
===
'
boolean
'
)
return
String
(
value
)
if
(
value
instanceof
Date
)
return
value
.
toISOString
()
try
{
return
JSON
.
stringify
(
value
)
}
catch
{
return
String
(
value
)
}
}
const
compareSortValues
=
(
a
:
any
,
b
:
any
):
number
=>
{
const
aEmpty
=
isNullishOrEmpty
(
a
)
const
bEmpty
=
isNullishOrEmpty
(
b
)
if
(
aEmpty
&&
bEmpty
)
return
0
if
(
aEmpty
)
return
1
if
(
bEmpty
)
return
-
1
const
aNum
=
toFiniteNumberOrNull
(
a
)
const
bNum
=
toFiniteNumberOrNull
(
b
)
if
(
aNum
!==
null
&&
bNum
!==
null
)
{
if
(
aNum
===
bNum
)
return
0
return
aNum
<
bNum
?
-
1
:
1
}
const
aStr
=
toSortableString
(
a
)
const
bStr
=
toSortableString
(
b
)
const
res
=
collator
.
compare
(
aStr
,
bStr
)
if
(
res
===
0
)
return
0
return
res
<
0
?
-
1
:
1
}
const
resolveRowKey
=
(
row
:
any
,
index
:
number
)
=>
{
if
(
typeof
props
.
rowKey
===
'
function
'
)
{
const
key
=
props
.
rowKey
(
row
)
...
...
@@ -334,15 +459,18 @@ const handleSort = (key: string) => {
const
sortedData
=
computed
(()
=>
{
if
(
!
sortKey
.
value
||
!
props
.
data
)
return
props
.
data
return
[...
props
.
data
].
sort
((
a
,
b
)
=>
{
const
aVal
=
a
[
sortKey
.
value
]
const
bVal
=
b
[
sortKey
.
value
]
if
(
aVal
===
bVal
)
return
0
const
key
=
sortKey
.
value
const
order
=
sortOrder
.
value
const
comparison
=
aVal
>
bVal
?
1
:
-
1
return
sortOrder
.
value
===
'
asc
'
?
comparison
:
-
comparison
// Stable sort (tie-break with original index) to avoid jitter when values are equal.
return
props
.
data
.
map
((
row
,
index
)
=>
({
row
,
index
}))
.
sort
((
a
,
b
)
=>
{
const
cmp
=
compareSortValues
(
a
.
row
?.[
key
],
b
.
row
?.[
key
])
if
(
cmp
!==
0
)
return
order
===
'
asc
'
?
cmp
:
-
cmp
return
a
.
index
-
b
.
index
})
.
map
(
item
=>
item
.
row
)
})
const
hasActionsColumn
=
computed
(()
=>
{
...
...
@@ -396,6 +524,51 @@ const getAdaptivePaddingClass = () => {
return
'
px-6
'
// 24px (原始值)
}
}
// Init + keep persisted sort state consistent with current columns
const
didInitSort
=
ref
(
false
)
onMounted
(()
=>
{
const
initial
=
resolveInitialSortState
()
applySortState
(
initial
)
didInitSort
.
value
=
true
})
watch
(
()
=>
props
.
columns
,
()
=>
{
// If current sort key is no longer sortable/visible, fall back to default/persisted.
const
normalized
=
normalizeSortKey
(
sortKey
.
value
)
if
(
!
sortKey
.
value
)
{
const
initial
=
resolveInitialSortState
()
applySortState
(
initial
)
return
}
if
(
!
normalized
)
{
const
fallback
=
resolveInitialSortState
()
if
(
fallback
)
{
applySortState
(
fallback
)
}
else
{
sortKey
.
value
=
''
sortOrder
.
value
=
'
asc
'
}
}
},
{
deep
:
true
}
)
watch
(
[
sortKey
,
sortOrder
],
([
nextKey
,
nextOrder
])
=>
{
if
(
!
didInitSort
.
value
)
return
if
(
!
props
.
sortStorageKey
)
return
const
key
=
normalizeSortKey
(
nextKey
)
if
(
!
key
)
return
writePersistedSortState
({
key
,
order
:
normalizeSortOrder
(
nextOrder
)
})
},
{
flush
:
'
post
'
}
)
</
script
>
<
style
scoped
>
...
...
frontend/src/components/common/README.md
View file @
ff74f517
...
...
@@ -13,6 +13,9 @@ A generic data table component with sorting, loading states, and custom cell ren
-
`columns: Column[]`
- Array of column definitions with key, label, sortable, and formatter
-
`data: any[]`
- Array of data objects to display
-
`loading?: boolean`
- Show loading skeleton
-
`defaultSortKey?: string`
- Default sort key (only used if no persisted sort state)
-
`defaultSortOrder?: 'asc' | 'desc'`
- Default sort order (default:
`asc`
)
-
`sortStorageKey?: string`
- Persist sort state (key + order) to localStorage
-
`rowKey?: string | (row: any) => string | number`
- Row key field or resolver (defaults to
`row.id`
, falls back to index)
**Slots:**
...
...
frontend/src/i18n/locales/en.ts
View file @
ff74f517
...
...
@@ -1022,6 +1022,13 @@ export default {
title
:
'
Account Management
'
,
description
:
'
Manage AI platform accounts and credentials
'
,
createAccount
:
'
Create Account
'
,
autoRefresh
:
'
Auto Refresh
'
,
enableAutoRefresh
:
'
Enable auto refresh
'
,
refreshInterval5s
:
'
5 seconds
'
,
refreshInterval10s
:
'
10 seconds
'
,
refreshInterval15s
:
'
15 seconds
'
,
refreshInterval30s
:
'
30 seconds
'
,
autoRefreshCountdown
:
'
Auto refresh: {seconds}s
'
,
syncFromCrs
:
'
Sync from CRS
'
,
syncFromCrsTitle
:
'
Sync Accounts from CRS
'
,
syncFromCrsDesc
:
...
...
frontend/src/i18n/locales/zh.ts
View file @
ff74f517
...
...
@@ -1096,6 +1096,13 @@ export default {
title
:
'
账号管理
'
,
description
:
'
管理 AI 平台账号和 Cookie
'
,
createAccount
:
'
添加账号
'
,
autoRefresh
:
'
自动刷新
'
,
enableAutoRefresh
:
'
启用自动刷新
'
,
refreshInterval5s
:
'
5 秒
'
,
refreshInterval10s
:
'
10 秒
'
,
refreshInterval15s
:
'
15 秒
'
,
refreshInterval30s
:
'
30 秒
'
,
autoRefreshCountdown
:
'
自动刷新:{seconds}s
'
,
syncFromCrs
:
'
从 CRS 同步
'
,
syncFromCrsTitle
:
'
从 CRS 同步账号
'
,
syncFromCrsDesc
:
...
...
frontend/src/views/admin/AccountsView.vue
View file @
ff74f517
...
...
@@ -17,10 +17,58 @@
@
create=
"showCreate = true"
>
<template
#after
>
<!-- 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
>
<!--
Column
Settings
Dropdown
-->
<
div
class
=
"
relative
"
ref
=
"
columnDropdownRef
"
>
<
button
@
click=
"showColumnDropdown = !showColumnDropdown"
@
click
=
"
showColumnDropdown = !showColumnDropdown;
showAutoRefreshDropdown = false
"
class
=
"
btn btn-secondary px-2 md:px-3
"
:
title
=
"
t('admin.users.columnSettings')
"
>
...
...
@@ -53,7 +101,15 @@
<
/template
>
<
template
#
table
>
<
AccountBulkActionsBar
:
selected
-
ids
=
"
selIds
"
@
delete
=
"
handleBulkDelete
"
@
edit
=
"
showBulkEdit = true
"
@
clear
=
"
selIds = []
"
@
select
-
page
=
"
selectPage
"
@
toggle
-
schedulable
=
"
handleBulkToggleSchedulable
"
/>
<DataTable
:columns=
"cols"
:data=
"accounts"
:loading=
"loading"
row-key=
"id"
>
<
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
"
>
<
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
>
...
...
@@ -161,6 +217,7 @@
<
script
setup
lang
=
"
ts
"
>
import
{
ref
,
reactive
,
computed
,
onMounted
,
onUnmounted
}
from
'
vue
'
import
{
useIntervalFn
}
from
'
@vueuse/core
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
...
...
@@ -221,6 +278,26 @@ const hiddenColumns = reactive<Set<string>>(new Set())
const
DEFAULT_HIDDEN_COLUMNS
=
[
'
proxy
'
,
'
notes
'
,
'
priority
'
,
'
rate_multiplier
'
]
const
HIDDEN_COLUMNS_KEY
=
'
account-hidden-columns
'
// 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`
}
const
loadSavedColumns
=
()
=>
{
try
{
const
saved
=
localStorage
.
getItem
(
HIDDEN_COLUMNS_KEY
)
...
...
@@ -244,6 +321,60 @@ const saveColumnsToStorage = () => {
}
}
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
}
}
const
toggleColumn
=
(
key
:
string
)
=>
{
if
(
hiddenColumns
.
has
(
key
))
{
hiddenColumns
.
delete
(
key
)
...
...
@@ -260,6 +391,44 @@ const { items: accounts, loading, params, pagination, load, reload, debouncedRel
initialParams
:
{
platform
:
''
,
type
:
''
,
status
:
''
,
search
:
''
}
}
)
const
isAnyModalOpen
=
computed
(()
=>
{
return
(
showCreate
.
value
||
showEdit
.
value
||
showSync
.
value
||
showBulkEdit
.
value
||
showTempUnsched
.
value
||
showDeleteDialog
.
value
||
showReAuth
.
value
||
showTest
.
value
||
showStats
.
value
)
}
)
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
}
)
// All available columns
const
allColumns
=
computed
(()
=>
{
const
c
=
[
...
...
@@ -512,10 +681,12 @@ const handleClickOutside = (event: MouseEvent) => {
if
(
columnDropdownRef
.
value
&&
!
columnDropdownRef
.
value
.
contains
(
target
))
{
showColumnDropdown
.
value
=
false
}
if
(
autoRefreshDropdownRef
.
value
&&
!
autoRefreshDropdownRef
.
value
.
contains
(
target
))
{
showAutoRefreshDropdown
.
value
=
false
}
}
onMounted
(
async
()
=>
{
loadSavedColumns
()
load
()
try
{
const
[
p
,
g
]
=
await
Promise
.
all
([
adminAPI
.
proxies
.
getAll
(),
adminAPI
.
groups
.
getAll
()])
...
...
@@ -526,6 +697,13 @@ onMounted(async () => {
}
window
.
addEventListener
(
'
scroll
'
,
handleScroll
,
true
)
document
.
addEventListener
(
'
click
'
,
handleClickOutside
)
if
(
autoRefreshEnabled
.
value
)
{
autoRefreshCountdown
.
value
=
autoRefreshIntervalSeconds
.
value
resumeAutoRefresh
()
}
else
{
pauseAutoRefresh
()
}
}
)
onUnmounted
(()
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment