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
32fff379
Unverified
Commit
32fff379
authored
Jan 18, 2026
by
Wesley Liddick
Committed by
GitHub
Jan 18, 2026
Browse files
Merge pull request #326 from geminiwen/main
feat(admin): 添加账号管理和订阅管理的列设置功能
parents
2b02c663
45e8598d
Changes
5
Hide whitespace changes
Inline
Side-by-side
frontend/src/components/admin/account/AccountTableActions.vue
View file @
32fff379
<
template
>
<
template
>
<div
class=
"flex flex-wrap items-center gap-3"
>
<div
class=
"flex flex-wrap items-center gap-3"
>
<slot
name=
"before"
></slot>
<button
@
click=
"$emit('refresh')"
:disabled=
"loading"
class=
"btn btn-secondary"
>
<button
@
click=
"$emit('refresh')"
:disabled=
"loading"
class=
"btn btn-secondary"
>
<Icon
name=
"refresh"
size=
"md"
:class=
"[loading ? 'animate-spin' : '']"
/>
<Icon
name=
"refresh"
size=
"md"
:class=
"[loading ? 'animate-spin' : '']"
/>
</button>
</button>
...
...
frontend/src/i18n/locales/en.ts
View file @
32fff379
...
@@ -673,6 +673,7 @@ export default {
...
@@ -673,6 +673,7 @@ export default {
updating
:
'
Updating...
'
,
updating
:
'
Updating...
'
,
columns
:
{
columns
:
{
user
:
'
User
'
,
user
:
'
User
'
,
email
:
'
Email
'
,
username
:
'
Username
'
,
username
:
'
Username
'
,
notes
:
'
Notes
'
,
notes
:
'
Notes
'
,
role
:
'
Role
'
,
role
:
'
Role
'
,
...
@@ -1093,6 +1094,7 @@ export default {
...
@@ -1093,6 +1094,7 @@ export default {
todayStats
:
'
Today Stats
'
,
todayStats
:
'
Today Stats
'
,
groups
:
'
Groups
'
,
groups
:
'
Groups
'
,
usageWindows
:
'
Usage Windows
'
,
usageWindows
:
'
Usage Windows
'
,
proxy
:
'
Proxy
'
,
lastUsed
:
'
Last Used
'
,
lastUsed
:
'
Last Used
'
,
expiresAt
:
'
Expires At
'
,
expiresAt
:
'
Expires At
'
,
actions
:
'
Actions
'
actions
:
'
Actions
'
...
...
frontend/src/i18n/locales/zh.ts
View file @
32fff379
...
@@ -1142,6 +1142,7 @@ export default {
...
@@ -1142,6 +1142,7 @@ export default {
todayStats
:
'
今日统计
'
,
todayStats
:
'
今日统计
'
,
groups
:
'
分组
'
,
groups
:
'
分组
'
,
usageWindows
:
'
用量窗口
'
,
usageWindows
:
'
用量窗口
'
,
proxy
:
'
代理
'
,
lastUsed
:
'
最近使用
'
,
lastUsed
:
'
最近使用
'
,
expiresAt
:
'
过期时间
'
,
expiresAt
:
'
过期时间
'
,
actions
:
'
操作
'
actions
:
'
操作
'
...
...
frontend/src/views/admin/AccountsView.vue
View file @
32fff379
...
@@ -15,7 +15,40 @@
...
@@ -15,7 +15,40 @@
@
refresh=
"load"
@
refresh=
"load"
@
sync=
"showSync = true"
@
sync=
"showSync = true"
@
create=
"showCreate = true"
@
create=
"showCreate = true"
/>
>
<template
#before
>
<!-- Column Settings Dropdown -->
<div
class=
"relative"
ref=
"columnDropdownRef"
>
<button
@
click=
"showColumnDropdown = !showColumnDropdown"
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
>
</AccountTableActions>
</div>
</div>
</template>
</template>
<
template
#table
>
<
template
#table
>
...
@@ -54,6 +87,15 @@
...
@@ -54,6 +87,15 @@
<
template
#cell-usage=
"{ row }"
>
<
template
#cell-usage=
"{ row }"
>
<AccountUsageCell
:account=
"row"
/>
<AccountUsageCell
:account=
"row"
/>
</
template
>
</
template
>
<
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
>
<
template
#cell-rate_multiplier=
"{ row }"
>
<
template
#cell-rate_multiplier=
"{ row }"
>
<span
class=
"text-sm font-mono text-gray-700 dark:text-gray-300"
>
<span
class=
"text-sm font-mono text-gray-700 dark:text-gray-300"
>
{{
(
row
.
rate_multiplier
??
1
).
toFixed
(
2
)
}}
x
{{
(
row
.
rate_multiplier
??
1
).
toFixed
(
2
)
}}
x
...
@@ -143,6 +185,7 @@ import AccountTodayStatsCell from '@/components/account/AccountTodayStatsCell.vu
...
@@ -143,6 +185,7 @@ import AccountTodayStatsCell from '@/components/account/AccountTodayStatsCell.vu
import
AccountGroupsCell
from
'
@/components/account/AccountGroupsCell.vue
'
import
AccountGroupsCell
from
'
@/components/account/AccountGroupsCell.vue
'
import
AccountCapacityCell
from
'
@/components/account/AccountCapacityCell.vue
'
import
AccountCapacityCell
from
'
@/components/account/AccountCapacityCell.vue
'
import
PlatformTypeBadge
from
'
@/components/common/PlatformTypeBadge.vue
'
import
PlatformTypeBadge
from
'
@/components/common/PlatformTypeBadge.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
{
formatDateTime
,
formatRelativeTime
}
from
'
@/utils/format
'
import
{
formatDateTime
,
formatRelativeTime
}
from
'
@/utils/format
'
import
type
{
Account
,
Proxy
,
Group
}
from
'
@/types
'
import
type
{
Account
,
Proxy
,
Group
}
from
'
@/types
'
...
@@ -171,12 +214,54 @@ const statsAcc = ref<Account | null>(null)
...
@@ -171,12 +214,54 @@ const statsAcc = ref<Account | null>(null)
const
togglingSchedulable
=
ref
<
number
|
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
})
const
menu
=
reactive
<
{
show
:
boolean
,
acc
:
Account
|
null
,
pos
:{
top
:
number
,
left
:
number
}
|
null
}
>
({
show
:
false
,
acc
:
null
,
pos
:
null
})
// 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
'
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
)
}
}
const
toggleColumn
=
(
key
:
string
)
=>
{
if
(
hiddenColumns
.
has
(
key
))
{
hiddenColumns
.
delete
(
key
)
}
else
{
hiddenColumns
.
add
(
key
)
}
saveColumnsToStorage
()
}
const
isColumnVisible
=
(
key
:
string
)
=>
!
hiddenColumns
.
has
(
key
)
const
{
items
:
accounts
,
loading
,
params
,
pagination
,
load
,
reload
,
debouncedReload
,
handlePageChange
,
handlePageSizeChange
}
=
useTableLoader
<
Account
,
any
>
({
const
{
items
:
accounts
,
loading
,
params
,
pagination
,
load
,
reload
,
debouncedReload
,
handlePageChange
,
handlePageSizeChange
}
=
useTableLoader
<
Account
,
any
>
({
fetchFn
:
adminAPI
.
accounts
.
list
,
fetchFn
:
adminAPI
.
accounts
.
list
,
initialParams
:
{
platform
:
''
,
type
:
''
,
status
:
''
,
search
:
''
}
initialParams
:
{
platform
:
''
,
type
:
''
,
status
:
''
,
search
:
''
}
})
})
const
cols
=
computed
(()
=>
{
// All available columns
const
allColumns
=
computed
(()
=>
{
const
c
=
[
const
c
=
[
{
key
:
'
select
'
,
label
:
''
,
sortable
:
false
},
{
key
:
'
select
'
,
label
:
''
,
sortable
:
false
},
{
key
:
'
name
'
,
label
:
t
(
'
admin.accounts.columns.name
'
),
sortable
:
true
},
{
key
:
'
name
'
,
label
:
t
(
'
admin.accounts.columns.name
'
),
sortable
:
true
},
...
@@ -189,11 +274,12 @@ const cols = computed(() => {
...
@@ -189,11 +274,12 @@ const cols = computed(() => {
if
(
!
authStore
.
isSimpleMode
)
{
if
(
!
authStore
.
isSimpleMode
)
{
c
.
push
({
key
:
'
groups
'
,
label
:
t
(
'
admin.accounts.columns.groups
'
),
sortable
:
false
})
c
.
push
({
key
:
'
groups
'
,
label
:
t
(
'
admin.accounts.columns.groups
'
),
sortable
:
false
})
}
}
c
.
push
(
c
.
push
(
{
key
:
'
usage
'
,
label
:
t
(
'
admin.accounts.columns.usageWindows
'
),
sortable
:
false
},
{
key
:
'
usage
'
,
label
:
t
(
'
admin.accounts.columns.usageWindows
'
),
sortable
:
false
},
{
key
:
'
priority
'
,
label
:
t
(
'
admin.accounts.columns.priority
'
),
sortable
:
true
},
{
key
:
'
proxy
'
,
label
:
t
(
'
admin.accounts.columns.proxy
'
),
sortable
:
false
},
{
key
:
'
rate_multiplier
'
,
label
:
t
(
'
admin.accounts.columns.billingRateMultiplier
'
),
sortable
:
true
},
{
key
:
'
priority
'
,
label
:
t
(
'
admin.accounts.columns.priority
'
),
sortable
:
true
},
{
key
:
'
last_used_at
'
,
label
:
t
(
'
admin.accounts.columns.lastUsed
'
),
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
},
{
key
:
'
expires_at
'
,
label
:
t
(
'
admin.accounts.columns.expiresAt
'
),
sortable
:
true
},
{
key
:
'
expires_at
'
,
label
:
t
(
'
admin.accounts.columns.expiresAt
'
),
sortable
:
true
},
{
key
:
'
notes
'
,
label
:
t
(
'
admin.accounts.columns.notes
'
),
sortable
:
false
},
{
key
:
'
notes
'
,
label
:
t
(
'
admin.accounts.columns.notes
'
),
sortable
:
false
},
{
key
:
'
actions
'
,
label
:
t
(
'
admin.accounts.columns.actions
'
),
sortable
:
false
}
{
key
:
'
actions
'
,
label
:
t
(
'
admin.accounts.columns.actions
'
),
sortable
:
false
}
...
@@ -201,6 +287,18 @@ const cols = computed(() => {
...
@@ -201,6 +287,18 @@ const cols = computed(() => {
return
c
return
c
})
})
// 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
)
)
)
const
handleEdit
=
(
a
:
Account
)
=>
{
edAcc
.
value
=
a
;
showEdit
.
value
=
true
}
const
handleEdit
=
(
a
:
Account
)
=>
{
edAcc
.
value
=
a
;
showEdit
.
value
=
true
}
const
openMenu
=
(
a
:
Account
,
e
:
MouseEvent
)
=>
{
const
openMenu
=
(
a
:
Account
,
e
:
MouseEvent
)
=>
{
menu
.
acc
=
a
menu
.
acc
=
a
...
@@ -403,12 +501,21 @@ const isExpired = (value: number | null) => {
...
@@ -403,12 +501,21 @@ const isExpired = (value: number | null) => {
return
value
*
1000
<=
Date
.
now
()
return
value
*
1000
<=
Date
.
now
()
}
}
// 滚动时关闭
菜单
// 滚动时关闭
操作菜单(不关闭列设置下拉菜单)
const
handleScroll
=
()
=>
{
const
handleScroll
=
()
=>
{
menu
.
show
=
false
menu
.
show
=
false
}
}
// 点击外部关闭列设置下拉菜单
const
handleClickOutside
=
(
event
:
MouseEvent
)
=>
{
const
target
=
event
.
target
as
HTMLElement
if
(
columnDropdownRef
.
value
&&
!
columnDropdownRef
.
value
.
contains
(
target
))
{
showColumnDropdown
.
value
=
false
}
}
onMounted
(
async
()
=>
{
onMounted
(
async
()
=>
{
loadSavedColumns
()
load
()
load
()
try
{
try
{
const
[
p
,
g
]
=
await
Promise
.
all
([
adminAPI
.
proxies
.
getAll
(),
adminAPI
.
groups
.
getAll
()])
const
[
p
,
g
]
=
await
Promise
.
all
([
adminAPI
.
proxies
.
getAll
(),
adminAPI
.
groups
.
getAll
()])
...
@@ -418,9 +525,11 @@ onMounted(async () => {
...
@@ -418,9 +525,11 @@ onMounted(async () => {
console
.
error
(
'
Failed to load proxies/groups:
'
,
error
)
console
.
error
(
'
Failed to load proxies/groups:
'
,
error
)
}
}
window
.
addEventListener
(
'
scroll
'
,
handleScroll
,
true
)
window
.
addEventListener
(
'
scroll
'
,
handleScroll
,
true
)
document
.
addEventListener
(
'
click
'
,
handleClickOutside
)
})
})
onUnmounted
(()
=>
{
onUnmounted
(()
=>
{
window
.
removeEventListener
(
'
scroll
'
,
handleScroll
,
true
)
window
.
removeEventListener
(
'
scroll
'
,
handleScroll
,
true
)
document
.
removeEventListener
(
'
click
'
,
handleClickOutside
)
})
})
</
script
>
</
script
>
frontend/src/views/admin/SubscriptionsView.vue
View file @
32fff379
...
@@ -85,6 +85,57 @@
...
@@ -85,6 +85,57 @@
<!-- Right: Actions -->
<!-- Right: Actions -->
<div
class=
"ml-auto flex flex-wrap items-center justify-end gap-3"
>
<div
class=
"ml-auto flex flex-wrap items-center justify-end gap-3"
>
<!-- Column Settings Dropdown -->
<div
class=
"relative"
ref=
"columnDropdownRef"
>
<button
@
click=
"showColumnDropdown = !showColumnDropdown"
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=
"p-2"
>
<!-- User column mode selection -->
<div
class=
"mb-2 border-b border-gray-200 pb-2 dark:border-gray-700"
>
<div
class=
"px-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.subscriptions.columns.user
'
)
}}
</div>
<button
@
click=
"setUserColumnMode('email')"
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.users.columns.email
'
)
}}
</span>
<Icon
v-if=
"userColumnMode === 'email'"
name=
"check"
size=
"sm"
class=
"text-primary-500"
/>
</button>
<button
@
click=
"setUserColumnMode('username')"
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.users.columns.username
'
)
}}
</span>
<Icon
v-if=
"userColumnMode === 'username'"
name=
"check"
size=
"sm"
class=
"text-primary-500"
/>
</button>
</div>
<!-- Other columns toggle -->
<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>
<button
<button
@
click=
"loadSubscriptions"
@
click=
"loadSubscriptions"
:disabled=
"loading"
:disabled=
"loading"
...
@@ -110,12 +161,18 @@
...
@@ -110,12 +161,18 @@
class=
"flex h-8 w-8 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900/30"
class=
"flex h-8 w-8 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900/30"
>
>
<span
class=
"text-sm font-medium text-primary-700 dark:text-primary-300"
>
<span
class=
"text-sm font-medium text-primary-700 dark:text-primary-300"
>
{{
row
.
user
?.
email
?.
charAt
(
0
).
toUpperCase
()
||
'
?
'
}}
{{
userColumnMode
===
'
email
'
?
(
row
.
user
?.
email
?.
charAt
(
0
).
toUpperCase
()
||
'
?
'
)
:
(
row
.
user
?.
username
?.
charAt
(
0
).
toUpperCase
()
||
'
?
'
)
}}
</span>
</span>
</div>
</div>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
<span
class=
"font-medium text-gray-900 dark:text-white"
>
row
.
user
?.
email
||
t
(
'
admin.redeem.userPrefix
'
,
{
id
:
row
.
user_id
}
)
{{
userColumnMode
===
'
email
'
}}
<
/span
>
?
(
row
.
user
?.
email
||
t
(
'
admin.redeem.userPrefix
'
,
{
id
:
row
.
user_id
}
))
:
(
row
.
user
?.
username
||
'
-
'
)
}}
<
/span
>
<
/div
>
<
/div
>
<
/template
>
<
/template
>
...
@@ -545,8 +602,43 @@ import Icon from '@/components/icons/Icon.vue'
...
@@ -545,8 +602,43 @@ import Icon from '@/components/icons/Icon.vue'
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
const
appStore
=
useAppStore
()
const
columns
=
computed
<
Column
[]
>
(()
=>
[
// User column display mode: 'email' or 'username'
{
key
:
'
user
'
,
label
:
t
(
'
admin.subscriptions.columns.user
'
),
sortable
:
true
}
,
const
userColumnMode
=
ref
<
'
email
'
|
'
username
'
>
(
'
email
'
)
const
USER_COLUMN_MODE_KEY
=
'
subscription-user-column-mode
'
const
loadUserColumnMode
=
()
=>
{
try
{
const
saved
=
localStorage
.
getItem
(
USER_COLUMN_MODE_KEY
)
if
(
saved
===
'
email
'
||
saved
===
'
username
'
)
{
userColumnMode
.
value
=
saved
}
}
catch
(
e
)
{
console
.
error
(
'
Failed to load user column mode:
'
,
e
)
}
}
const
saveUserColumnMode
=
()
=>
{
try
{
localStorage
.
setItem
(
USER_COLUMN_MODE_KEY
,
userColumnMode
.
value
)
}
catch
(
e
)
{
console
.
error
(
'
Failed to save user column mode:
'
,
e
)
}
}
const
setUserColumnMode
=
(
mode
:
'
email
'
|
'
username
'
)
=>
{
userColumnMode
.
value
=
mode
saveUserColumnMode
()
}
// All available columns
const
allColumns
=
computed
<
Column
[]
>
(()
=>
[
{
key
:
'
user
'
,
label
:
userColumnMode
.
value
===
'
email
'
?
t
(
'
admin.subscriptions.columns.user
'
)
:
t
(
'
admin.users.columns.username
'
),
sortable
:
true
}
,
{
key
:
'
group
'
,
label
:
t
(
'
admin.subscriptions.columns.group
'
),
sortable
:
true
}
,
{
key
:
'
group
'
,
label
:
t
(
'
admin.subscriptions.columns.group
'
),
sortable
:
true
}
,
{
key
:
'
usage
'
,
label
:
t
(
'
admin.subscriptions.columns.usage
'
),
sortable
:
false
}
,
{
key
:
'
usage
'
,
label
:
t
(
'
admin.subscriptions.columns.usage
'
),
sortable
:
false
}
,
{
key
:
'
expires_at
'
,
label
:
t
(
'
admin.subscriptions.columns.expires
'
),
sortable
:
true
}
,
{
key
:
'
expires_at
'
,
label
:
t
(
'
admin.subscriptions.columns.expires
'
),
sortable
:
true
}
,
...
@@ -554,6 +646,69 @@ const columns = computed<Column[]>(() => [
...
@@ -554,6 +646,69 @@ const columns = computed<Column[]>(() => [
{
key
:
'
actions
'
,
label
:
t
(
'
admin.subscriptions.columns.actions
'
),
sortable
:
false
}
{
key
:
'
actions
'
,
label
:
t
(
'
admin.subscriptions.columns.actions
'
),
sortable
:
false
}
])
])
// Columns that can be toggled (exclude user and actions which are always visible)
const
toggleableColumns
=
computed
(()
=>
allColumns
.
value
.
filter
(
col
=>
col
.
key
!==
'
user
'
&&
col
.
key
!==
'
actions
'
)
)
// Hidden columns set
const
hiddenColumns
=
reactive
<
Set
<
string
>>
(
new
Set
())
// Default hidden columns
const
DEFAULT_HIDDEN_COLUMNS
:
string
[]
=
[]
// localStorage key
const
HIDDEN_COLUMNS_KEY
=
'
subscription-hidden-columns
'
// Load saved column settings
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
))
}
}
// Save column settings to localStorage
const
saveColumnsToStorage
=
()
=>
{
try
{
localStorage
.
setItem
(
HIDDEN_COLUMNS_KEY
,
JSON
.
stringify
([...
hiddenColumns
]))
}
catch
(
e
)
{
console
.
error
(
'
Failed to save columns:
'
,
e
)
}
}
// Toggle column visibility
const
toggleColumn
=
(
key
:
string
)
=>
{
if
(
hiddenColumns
.
has
(
key
))
{
hiddenColumns
.
delete
(
key
)
}
else
{
hiddenColumns
.
add
(
key
)
}
saveColumnsToStorage
()
}
// Check if column is visible
const
isColumnVisible
=
(
key
:
string
)
=>
!
hiddenColumns
.
has
(
key
)
// Filtered columns for display
const
columns
=
computed
<
Column
[]
>
(()
=>
allColumns
.
value
.
filter
(
col
=>
col
.
key
===
'
user
'
||
col
.
key
===
'
actions
'
||
!
hiddenColumns
.
has
(
col
.
key
)
)
)
// Column dropdown state
const
showColumnDropdown
=
ref
(
false
)
const
columnDropdownRef
=
ref
<
HTMLElement
|
null
>
(
null
)
// Filter options
// Filter options
const
statusOptions
=
computed
(()
=>
[
const
statusOptions
=
computed
(()
=>
[
{
value
:
''
,
label
:
t
(
'
admin.subscriptions.allStatus
'
)
}
,
{
value
:
''
,
label
:
t
(
'
admin.subscriptions.allStatus
'
)
}
,
...
@@ -949,14 +1104,19 @@ const formatResetTime = (windowStart: string, period: 'daily' | 'weekly' | 'mont
...
@@ -949,14 +1104,19 @@ const formatResetTime = (windowStart: string, period: 'daily' | 'weekly' | 'mont
}
}
}
}
// Handle click outside to close
user
dropdown
// Handle click outside to close dropdown
s
const
handleClickOutside
=
(
event
:
MouseEvent
)
=>
{
const
handleClickOutside
=
(
event
:
MouseEvent
)
=>
{
const
target
=
event
.
target
as
HTMLElement
const
target
=
event
.
target
as
HTMLElement
if
(
!
target
.
closest
(
'
[data-assign-user-search]
'
))
showUserDropdown
.
value
=
false
if
(
!
target
.
closest
(
'
[data-assign-user-search]
'
))
showUserDropdown
.
value
=
false
if
(
!
target
.
closest
(
'
[data-filter-user-search]
'
))
showFilterUserDropdown
.
value
=
false
if
(
!
target
.
closest
(
'
[data-filter-user-search]
'
))
showFilterUserDropdown
.
value
=
false
if
(
columnDropdownRef
.
value
&&
!
columnDropdownRef
.
value
.
contains
(
target
))
{
showColumnDropdown
.
value
=
false
}
}
}
onMounted
(()
=>
{
onMounted
(()
=>
{
loadUserColumnMode
()
loadSavedColumns
()
loadSubscriptions
()
loadSubscriptions
()
loadGroups
()
loadGroups
()
document
.
addEventListener
(
'
click
'
,
handleClickOutside
)
document
.
addEventListener
(
'
click
'
,
handleClickOutside
)
...
...
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