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
445bfdf2
Unverified
Commit
445bfdf2
authored
Mar 02, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 02, 2026
Browse files
Merge pull request #706 from PMExtra/feat/default-subscriptions-on-user-create
feat(settings): add default subscriptions for new users
parents
fc5b9c82
0fba1901
Changes
22
Hide whitespace changes
Inline
Side-by-side
frontend/src/i18n/locales/zh.ts
View file @
445bfdf2
...
...
@@ -3725,7 +3725,14 @@ export default {
defaultBalance
:
'
默认余额
'
,
defaultBalanceHint
:
'
新用户的初始余额
'
,
defaultConcurrency
:
'
默认并发数
'
,
defaultConcurrencyHint
:
'
新用户的最大并发请求数
'
defaultConcurrencyHint
:
'
新用户的最大并发请求数
'
,
defaultSubscriptions
:
'
默认订阅列表
'
,
defaultSubscriptionsHint
:
'
新用户创建或注册时自动分配这些订阅
'
,
addDefaultSubscription
:
'
添加默认订阅
'
,
defaultSubscriptionsEmpty
:
'
未配置默认订阅。新用户不会自动获得订阅套餐。
'
,
defaultSubscriptionsDuplicate
:
'
默认订阅存在重复分组:{groupId}。每个分组只能出现一次。
'
,
subscriptionGroup
:
'
订阅分组
'
,
subscriptionValidityDays
:
'
有效期(天)
'
},
claudeCode
:
{
title
:
'
Claude Code 设置
'
,
...
...
frontend/src/views/admin/SettingsView.vue
View file @
445bfdf2
...
...
@@ -579,7 +579,7 @@
{{ t('admin.settings.defaults.description') }}
</p>
</div>
<div
class=
"p-6"
>
<div
class=
"
space-y-6
p-6"
>
<div
class=
"grid grid-cols-1 gap-6 md:grid-cols-2"
>
<div>
<label
class=
"mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
...
...
@@ -613,6 +613,98 @@
</p>
</div>
</div>
<div
class=
"border-t border-gray-100 pt-4 dark:border-dark-700"
>
<div
class=
"mb-3 flex items-center justify-between"
>
<div>
<label
class=
"font-medium text-gray-900 dark:text-white"
>
{{ t('admin.settings.defaults.defaultSubscriptions') }}
</label>
<p
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{ t('admin.settings.defaults.defaultSubscriptionsHint') }}
</p>
</div>
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
@
click=
"addDefaultSubscription"
:disabled=
"subscriptionGroups.length === 0"
>
{{ t('admin.settings.defaults.addDefaultSubscription') }}
</button>
</div>
<div
v-if=
"form.default_subscriptions.length === 0"
class=
"rounded border border-dashed border-gray-300 px-4 py-3 text-sm text-gray-500 dark:border-dark-600 dark:text-gray-400"
>
{{ t('admin.settings.defaults.defaultSubscriptionsEmpty') }}
</div>
<div
v-else
class=
"space-y-3"
>
<div
v-for=
"(item, index) in form.default_subscriptions"
:key=
"`default-sub-${index}`"
class=
"grid grid-cols-1 gap-3 rounded border border-gray-200 p-3 md:grid-cols-[1fr_160px_auto] dark:border-dark-600"
>
<div>
<label
class=
"mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{{ t('admin.settings.defaults.subscriptionGroup') }}
</label>
<Select
v-model=
"item.group_id"
class=
"default-sub-group-select"
:options=
"defaultSubscriptionGroupOptions"
:placeholder=
"t('admin.settings.defaults.subscriptionGroup')"
>
<
template
#selected=
"{ option }"
>
<GroupBadge
v-if=
"option"
:name=
"(option as unknown as DefaultSubscriptionGroupOption).label"
:platform=
"(option as unknown as DefaultSubscriptionGroupOption).platform"
:subscription-type=
"(option as unknown as DefaultSubscriptionGroupOption).subscriptionType"
:rate-multiplier=
"(option as unknown as DefaultSubscriptionGroupOption).rate"
/>
<span
v-else
class=
"text-gray-400"
>
{{
t
(
'
admin.settings.defaults.subscriptionGroup
'
)
}}
</span>
</
template
>
<
template
#option=
"{ option, selected }"
>
<GroupOptionItem
:name=
"(option as unknown as DefaultSubscriptionGroupOption).label"
:platform=
"(option as unknown as DefaultSubscriptionGroupOption).platform"
:subscription-type=
"(option as unknown as DefaultSubscriptionGroupOption).subscriptionType"
:rate-multiplier=
"(option as unknown as DefaultSubscriptionGroupOption).rate"
:description=
"(option as unknown as DefaultSubscriptionGroupOption).description"
:selected=
"selected"
/>
</
template
>
</Select>
</div>
<div>
<label
class=
"mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{{ t('admin.settings.defaults.subscriptionValidityDays') }}
</label>
<input
v-model.number=
"item.validity_days"
type=
"number"
min=
"1"
max=
"36500"
class=
"input h-[42px]"
/>
</div>
<div
class=
"flex items-end"
>
<button
type=
"button"
class=
"btn btn-secondary default-sub-delete-btn w-full text-red-600 hover:text-red-700 dark:text-red-400"
@
click=
"removeDefaultSubscription(index)"
>
{{ t('common.delete') }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
...
...
@@ -1157,9 +1249,17 @@
import
{
ref
,
reactive
,
computed
,
onMounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
adminAPI
}
from
'
@/api
'
import
type
{
SystemSettings
,
UpdateSettingsRequest
}
from
'
@/api/admin/settings
'
import
type
{
SystemSettings
,
UpdateSettingsRequest
,
DefaultSubscriptionSetting
}
from
'
@/api/admin/settings
'
import
type
{
AdminGroup
}
from
'
@/types
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
GroupBadge
from
'
@/components/common/GroupBadge.vue
'
import
GroupOptionItem
from
'
@/components/common/GroupOptionItem.vue
'
import
Toggle
from
'
@/components/common/Toggle.vue
'
import
{
useClipboard
}
from
'
@/composables/useClipboard
'
import
{
useAppStore
}
from
'
@/stores
'
...
...
@@ -1181,6 +1281,7 @@ const adminApiKeyExists = ref(false)
const
adminApiKeyMasked
=
ref
(
''
)
const
adminApiKeyOperating
=
ref
(
false
)
const
newAdminApiKey
=
ref
(
''
)
const
subscriptionGroups
=
ref
<
AdminGroup
[]
>
([])
// Stream Timeout 状态
const
streamTimeoutLoading
=
ref
(
true
)
...
...
@@ -1193,6 +1294,16 @@ const streamTimeoutForm = reactive({
threshold_window_minutes
:
10
})
interface
DefaultSubscriptionGroupOption
{
value
:
number
label
:
string
description
:
string
|
null
platform
:
AdminGroup
[
'
platform
'
]
subscriptionType
:
AdminGroup
[
'
subscription_type
'
]
rate
:
number
[
key
:
string
]:
unknown
}
type
SettingsForm
=
SystemSettings
&
{
smtp_password
:
string
turnstile_secret_key
:
string
...
...
@@ -1209,6 +1320,7 @@ const form = reactive<SettingsForm>({
totp_encryption_key_configured
:
false
,
default_balance
:
0
,
default_concurrency
:
1
,
default_subscriptions
:
[],
site_name
:
'
Sub2API
'
,
site_logo
:
''
,
site_subtitle
:
'
Subscription to API Conversion Platform
'
,
...
...
@@ -1257,6 +1369,17 @@ const form = reactive<SettingsForm>({
min_claude_code_version
:
''
})
const
defaultSubscriptionGroupOptions
=
computed
<
DefaultSubscriptionGroupOption
[]
>
(()
=>
subscriptionGroups
.
value
.
map
((
group
)
=>
({
value
:
group
.
id
,
label
:
group
.
name
,
description
:
group
.
description
,
platform
:
group
.
platform
,
subscriptionType
:
group
.
subscription_type
,
rate
:
group
.
rate_multiplier
}))
)
// LinuxDo OAuth redirect URL suggestion
const
linuxdoRedirectUrlSuggestion
=
computed
(()
=>
{
if
(
typeof
window
===
'
undefined
'
)
return
''
...
...
@@ -1316,6 +1439,14 @@ async function loadSettings() {
try
{
const
settings
=
await
adminAPI
.
settings
.
getSettings
()
Object
.
assign
(
form
,
settings
)
form
.
default_subscriptions
=
Array
.
isArray
(
settings
.
default_subscriptions
)
?
settings
.
default_subscriptions
.
filter
((
item
)
=>
item
.
group_id
>
0
&&
item
.
validity_days
>
0
)
.
map
((
item
)
=>
({
group_id
:
item
.
group_id
,
validity_days
:
item
.
validity_days
}))
:
[]
form
.
smtp_password
=
''
form
.
turnstile_secret_key
=
''
form
.
linuxdo_connect_client_secret
=
''
...
...
@@ -1328,9 +1459,60 @@ async function loadSettings() {
}
}
async
function
loadSubscriptionGroups
()
{
try
{
const
groups
=
await
adminAPI
.
groups
.
getAll
()
subscriptionGroups
.
value
=
groups
.
filter
(
(
group
)
=>
group
.
subscription_type
===
'
subscription
'
&&
group
.
status
===
'
active
'
)
}
catch
(
error
)
{
console
.
error
(
'
Failed to load subscription groups:
'
,
error
)
subscriptionGroups
.
value
=
[]
}
}
function
addDefaultSubscription
()
{
if
(
subscriptionGroups
.
value
.
length
===
0
)
return
const
existing
=
new
Set
(
form
.
default_subscriptions
.
map
((
item
)
=>
item
.
group_id
))
const
candidate
=
subscriptionGroups
.
value
.
find
((
group
)
=>
!
existing
.
has
(
group
.
id
))
if
(
!
candidate
)
return
form
.
default_subscriptions
.
push
({
group_id
:
candidate
.
id
,
validity_days
:
30
})
}
function
removeDefaultSubscription
(
index
:
number
)
{
form
.
default_subscriptions
.
splice
(
index
,
1
)
}
async
function
saveSettings
()
{
saving
.
value
=
true
try
{
const
normalizedDefaultSubscriptions
=
form
.
default_subscriptions
.
filter
((
item
)
=>
item
.
group_id
>
0
&&
item
.
validity_days
>
0
)
.
map
((
item
:
DefaultSubscriptionSetting
)
=>
({
group_id
:
item
.
group_id
,
validity_days
:
Math
.
min
(
36500
,
Math
.
max
(
1
,
Math
.
floor
(
item
.
validity_days
)))
}))
const
seenGroupIDs
=
new
Set
<
number
>
()
const
duplicateDefaultSubscription
=
normalizedDefaultSubscriptions
.
find
((
item
)
=>
{
if
(
seenGroupIDs
.
has
(
item
.
group_id
))
{
return
true
}
seenGroupIDs
.
add
(
item
.
group_id
)
return
false
})
if
(
duplicateDefaultSubscription
)
{
appStore
.
showError
(
t
(
'
admin.settings.defaults.defaultSubscriptionsDuplicate
'
,
{
groupId
:
duplicateDefaultSubscription
.
group_id
})
)
return
}
const
payload
:
UpdateSettingsRequest
=
{
registration_enabled
:
form
.
registration_enabled
,
email_verify_enabled
:
form
.
email_verify_enabled
,
...
...
@@ -1340,6 +1522,7 @@ async function saveSettings() {
totp_enabled
:
form
.
totp_enabled
,
default_balance
:
form
.
default_balance
,
default_concurrency
:
form
.
default_concurrency
,
default_subscriptions
:
normalizedDefaultSubscriptions
,
site_name
:
form
.
site_name
,
site_logo
:
form
.
site_logo
,
site_subtitle
:
form
.
site_subtitle
,
...
...
@@ -1538,7 +1721,18 @@ async function saveStreamTimeoutSettings() {
onMounted
(()
=>
{
loadSettings
()
loadSubscriptionGroups
()
loadAdminApiKey
()
loadStreamTimeoutSettings
()
})
</
script
>
<
style
scoped
>
.default-sub-group-select
:deep
(
.select-trigger
)
{
@apply
h-[42px];
}
.default-sub-delete-btn
{
@apply
h-[42px];
}
</
style
>
Prev
1
2
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