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
bdc426a7
"backend/ent/vscode:/vscode.git/clone" did not exist on "fad04ca99535e68a5797a7656d21fa2be17a5e5c"
Commit
bdc426a7
authored
Jan 18, 2026
by
yangjianbo
Browse files
Merge branch 'main' into dev
parents
771baa66
32fff379
Changes
44
Hide whitespace changes
Inline
Side-by-side
frontend/src/i18n/locales/zh.ts
View file @
bdc426a7
...
...
@@ -160,6 +160,7 @@ export default {
notAvailable
:
'
不可用
'
,
now
:
'
现在
'
,
unknown
:
'
未知
'
,
minutes
:
'
分钟
'
,
time
:
{
never
:
'
从未
'
,
justNow
:
'
刚刚
'
,
...
...
@@ -1134,7 +1135,7 @@ export default {
platformType
:
'
平台/类型
'
,
platform
:
'
平台
'
,
type
:
'
类型
'
,
c
oncurrencyStatus
:
'
并发
'
,
c
apacity
:
'
容量
'
,
notes
:
'
备注
'
,
priority
:
'
优先级
'
,
billingRateMultiplier
:
'
账号倍率
'
,
...
...
@@ -1144,10 +1145,23 @@ export default {
todayStats
:
'
今日统计
'
,
groups
:
'
分组
'
,
usageWindows
:
'
用量窗口
'
,
proxy
:
'
代理
'
,
lastUsed
:
'
最近使用
'
,
expiresAt
:
'
过期时间
'
,
actions
:
'
操作
'
},
// 容量状态提示
capacity
:
{
windowCost
:
{
blocked
:
'
5h窗口费用超限,账号暂停调度
'
,
stickyOnly
:
'
5h窗口费用达阈值,仅允许粘性会话
'
,
normal
:
'
5h窗口费用正常
'
},
sessions
:
{
full
:
'
活跃会话已满,新会话需等待(空闲超时:{idle}分钟)
'
,
normal
:
'
活跃会话正常(空闲超时:{idle}分钟)
'
}
},
clearRateLimit
:
'
清除速率限制
'
,
testConnection
:
'
测试连接
'
,
reAuthorize
:
'
重新授权
'
,
...
...
@@ -1383,6 +1397,31 @@ export default {
interceptWarmupRequestsDesc
:
'
启用后,标题生成等预热请求将返回 mock 响应,不消耗上游 token
'
,
autoPauseOnExpired
:
'
过期自动暂停调度
'
,
autoPauseOnExpiredDesc
:
'
启用后,账号过期将自动暂停调度
'
,
// Quota control (Anthropic OAuth/SetupToken only)
quotaControl
:
{
title
:
'
配额控制
'
,
hint
:
'
仅适用于 Anthropic OAuth/Setup Token 账号
'
,
windowCost
:
{
label
:
'
5h窗口费用控制
'
,
hint
:
'
限制账号在5小时窗口内的费用使用
'
,
limit
:
'
费用阈值
'
,
limitPlaceholder
:
'
50
'
,
limitHint
:
'
达到阈值后不参与新请求调度
'
,
stickyReserve
:
'
粘性预留额度
'
,
stickyReservePlaceholder
:
'
10
'
,
stickyReserveHint
:
'
为粘性会话预留的额外额度
'
},
sessionLimit
:
{
label
:
'
会话数量控制
'
,
hint
:
'
限制同时活跃的会话数量
'
,
maxSessions
:
'
最大会话数
'
,
maxSessionsPlaceholder
:
'
3
'
,
maxSessionsHint
:
'
同时活跃的最大会话数量
'
,
idleTimeout
:
'
空闲超时
'
,
idleTimeoutPlaceholder
:
'
5
'
,
idleTimeoutHint
:
'
会话空闲超时后自动释放
'
}
},
expired
:
'
已过期
'
,
proxy
:
'
代理
'
,
noProxy
:
'
无代理
'
,
...
...
frontend/src/types/index.ts
View file @
bdc426a7
...
...
@@ -471,6 +471,18 @@ export interface Account {
session_window_start
:
string
|
null
session_window_end
:
string
|
null
session_window_status
:
'
allowed
'
|
'
allowed_warning
'
|
'
rejected
'
|
null
// 5h窗口费用控制(仅 Anthropic OAuth/SetupToken 账号有效)
window_cost_limit
?:
number
|
null
window_cost_sticky_reserve
?:
number
|
null
// 会话数量控制(仅 Anthropic OAuth/SetupToken 账号有效)
max_sessions
?:
number
|
null
session_idle_timeout_minutes
?:
number
|
null
// 运行时状态(仅当启用对应限制时返回)
current_window_cost
?:
number
|
null
// 当前窗口费用
active_sessions
?:
number
|
null
// 当前活跃会话数
}
// Account Usage types
...
...
frontend/src/views/admin/AccountsView.vue
View file @
bdc426a7
...
...
@@ -15,7 +15,40 @@
@
refresh=
"load"
@
sync=
"showSync = 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>
</template>
<
template
#table
>
...
...
@@ -34,15 +67,8 @@
<
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
#cell-capacity=
"{ row }"
>
<AccountCapacityCell
:account=
"row"
/>
</
template
>
<
template
#cell-status=
"{ row }"
>
<AccountStatusIndicator
:account=
"row"
@
show-temp-unsched=
"handleShowTempUnsched"
/>
...
...
@@ -61,6 +87,15 @@
<
template
#cell-usage=
"{ row }"
>
<AccountUsageCell
:account=
"row"
/>
</
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 }"
>
<span
class=
"text-sm font-mono text-gray-700 dark:text-gray-300"
>
{{
(
row
.
rate_multiplier
??
1
).
toFixed
(
2
)
}}
x
...
...
@@ -148,7 +183,9 @@ import AccountStatusIndicator from '@/components/account/AccountStatusIndicator.
import
AccountUsageCell
from
'
@/components/account/AccountUsageCell.vue
'
import
AccountTodayStatsCell
from
'
@/components/account/AccountTodayStatsCell.vue
'
import
AccountGroupsCell
from
'
@/components/account/AccountGroupsCell.vue
'
import
AccountCapacityCell
from
'
@/components/account/AccountCapacityCell.vue
'
import
PlatformTypeBadge
from
'
@/components/common/PlatformTypeBadge.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
{
formatDateTime
,
formatRelativeTime
}
from
'
@/utils/format
'
import
type
{
Account
,
Proxy
,
Group
}
from
'
@/types
'
...
...
@@ -177,17 +214,59 @@ 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
})
// 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
>
({
fetchFn
:
adminAPI
.
accounts
.
list
,
initialParams
:
{
platform
:
''
,
type
:
''
,
status
:
''
,
search
:
''
}
})
const
cols
=
computed
(()
=>
{
// All available columns
const
allColumns
=
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
:
'
c
oncurrenc
y
'
,
label
:
t
(
'
admin.accounts.columns.c
oncurrencyStatus
'
),
sortable
:
false
},
{
key
:
'
c
apacit
y
'
,
label
:
t
(
'
admin.accounts.columns.c
apacity
'
),
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
}
...
...
@@ -195,11 +274,12 @@ const cols = computed(() => {
if
(
!
authStore
.
isSimpleMode
)
{
c
.
push
({
key
:
'
groups
'
,
label
:
t
(
'
admin.accounts.columns.groups
'
),
sortable
:
false
})
}
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
},
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
},
{
key
:
'
expires_at
'
,
label
:
t
(
'
admin.accounts.columns.expiresAt
'
),
sortable
:
true
},
{
key
:
'
notes
'
,
label
:
t
(
'
admin.accounts.columns.notes
'
),
sortable
:
false
},
{
key
:
'
actions
'
,
label
:
t
(
'
admin.accounts.columns.actions
'
),
sortable
:
false
}
...
...
@@ -207,6 +287,18 @@ const cols = computed(() => {
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
openMenu
=
(
a
:
Account
,
e
:
MouseEvent
)
=>
{
menu
.
acc
=
a
...
...
@@ -409,12 +501,21 @@ const isExpired = (value: number | null) => {
return
value
*
1000
<=
Date
.
now
()
}
// 滚动时关闭
菜单
// 滚动时关闭
操作菜单(不关闭列设置下拉菜单)
const
handleScroll
=
()
=>
{
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
()
=>
{
loadSavedColumns
()
load
()
try
{
const
[
p
,
g
]
=
await
Promise
.
all
([
adminAPI
.
proxies
.
getAll
(),
adminAPI
.
groups
.
getAll
()])
...
...
@@ -424,9 +525,11 @@ onMounted(async () => {
console
.
error
(
'
Failed to load proxies/groups:
'
,
error
)
}
window
.
addEventListener
(
'
scroll
'
,
handleScroll
,
true
)
document
.
addEventListener
(
'
click
'
,
handleClickOutside
)
})
onUnmounted
(()
=>
{
window
.
removeEventListener
(
'
scroll
'
,
handleScroll
,
true
)
document
.
removeEventListener
(
'
click
'
,
handleClickOutside
)
})
</
script
>
frontend/src/views/admin/SubscriptionsView.vue
View file @
bdc426a7
...
...
@@ -85,6 +85,57 @@
<!-- Right: Actions -->
<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
@
click=
"loadSubscriptions"
:disabled=
"loading"
...
...
@@ -110,12 +161,18 @@
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"
>
{{
row
.
user
?.
email
?.
charAt
(
0
).
toUpperCase
()
||
'
?
'
}}
{{
userColumnMode
===
'
email
'
?
(
row
.
user
?.
email
?.
charAt
(
0
).
toUpperCase
()
||
'
?
'
)
:
(
row
.
user
?.
username
?.
charAt
(
0
).
toUpperCase
()
||
'
?
'
)
}}
</span>
</div>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
row
.
user
?.
email
||
t
(
'
admin.redeem.userPrefix
'
,
{
id
:
row
.
user_id
}
)
}}
<
/span
>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
userColumnMode
===
'
email
'
?
(
row
.
user
?.
email
||
t
(
'
admin.redeem.userPrefix
'
,
{
id
:
row
.
user_id
}
))
:
(
row
.
user
?.
username
||
'
-
'
)
}}
<
/span
>
<
/div
>
<
/template
>
...
...
@@ -545,8 +602,43 @@ import Icon from '@/components/icons/Icon.vue'
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
const
columns
=
computed
<
Column
[]
>
(()
=>
[
{
key
:
'
user
'
,
label
:
t
(
'
admin.subscriptions.columns.user
'
),
sortable
:
true
}
,
// User column display mode: 'email' or 'username'
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
:
'
usage
'
,
label
:
t
(
'
admin.subscriptions.columns.usage
'
),
sortable
:
false
}
,
{
key
:
'
expires_at
'
,
label
:
t
(
'
admin.subscriptions.columns.expires
'
),
sortable
:
true
}
,
...
...
@@ -554,6 +646,69 @@ const columns = computed<Column[]>(() => [
{
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
const
statusOptions
=
computed
(()
=>
[
{
value
:
''
,
label
:
t
(
'
admin.subscriptions.allStatus
'
)
}
,
...
...
@@ -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
target
=
event
.
target
as
HTMLElement
if
(
!
target
.
closest
(
'
[data-assign-user-search]
'
))
showUserDropdown
.
value
=
false
if
(
!
target
.
closest
(
'
[data-filter-user-search]
'
))
showFilterUserDropdown
.
value
=
false
if
(
columnDropdownRef
.
value
&&
!
columnDropdownRef
.
value
.
contains
(
target
))
{
showColumnDropdown
.
value
=
false
}
}
onMounted
(()
=>
{
loadUserColumnMode
()
loadSavedColumns
()
loadSubscriptions
()
loadGroups
()
document
.
addEventListener
(
'
click
'
,
handleClickOutside
)
...
...
Prev
1
2
3
Next
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