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
de9b9c9d
"frontend/src/vscode:/vscode.git/clone" did not exist on "c520de11de6172e018abbdfdd096c3cfc653e771"
Commit
de9b9c9d
authored
Apr 09, 2026
by
IanShaw027
Browse files
feat(admin): 增加分组 messages 调度映射配置界面
parent
d765359f
Changes
1
Hide whitespace changes
Inline
Side-by-side
frontend/src/views/admin/GroupsView.vue
View file @
de9b9c9d
...
@@ -2,7 +2,9 @@
...
@@ -2,7 +2,9 @@
<AppLayout>
<AppLayout>
<TablePageLayout>
<TablePageLayout>
<template
#filters
>
<template
#filters
>
<div
class=
"flex flex-col justify-between gap-4 lg:flex-row lg:items-start"
>
<div
class=
"flex flex-col justify-between gap-4 lg:flex-row lg:items-start"
>
<!-- Left: fuzzy search + filters (can wrap to multiple lines) -->
<!-- Left: fuzzy search + filters (can wrap to multiple lines) -->
<div
class=
"flex flex-1 flex-wrap items-center gap-3"
>
<div
class=
"flex flex-1 flex-wrap items-center gap-3"
>
<div
class=
"relative w-full sm:w-64"
>
<div
class=
"relative w-full sm:w-64"
>
...
@@ -19,38 +21,44 @@
...
@@ -19,38 +21,44 @@
@
input=
"handleSearch"
@
input=
"handleSearch"
/>
/>
</div>
</div>
<Select
<Select
v-model=
"filters.platform"
v-model=
"filters.platform"
:options=
"platformFilterOptions"
:options=
"platformFilterOptions"
:placeholder=
"t('admin.groups.allPlatforms')"
:placeholder=
"t('admin.groups.allPlatforms')"
class=
"w-44"
class=
"w-44"
@
change=
"loadGroups"
@
change=
"loadGroups"
/>
/>
<Select
<Select
v-model=
"filters.status"
v-model=
"filters.status"
:options=
"statusOptions"
:options=
"statusOptions"
:placeholder=
"t('admin.groups.allStatus')"
:placeholder=
"t('admin.groups.allStatus')"
class=
"w-40"
class=
"w-40"
@
change=
"loadGroups"
@
change=
"loadGroups"
/>
/>
<Select
<Select
v-model=
"filters.is_exclusive"
v-model=
"filters.is_exclusive"
:options=
"exclusiveOptions"
:options=
"exclusiveOptions"
:placeholder=
"t('admin.groups.allGroups')"
:placeholder=
"t('admin.groups.allGroups')"
class=
"w-44"
class=
"w-44"
@
change=
"loadGroups"
@
change=
"loadGroups"
/>
/>
</div>
</div>
<!-- Right: actions -->
<!-- Right: actions -->
<div
class=
"flex w-full flex-shrink-0 flex-wrap items-center justify-end gap-3 lg:w-auto"
>
<div
class=
"flex w-full flex-shrink-0 flex-wrap items-center justify-end gap-3 lg:w-auto"
>
<button
<button
@
click=
"loadGroups"
@
click=
"loadGroups"
:disabled=
"loading"
:disabled=
"loading"
class=
"btn btn-secondary"
class=
"btn btn-secondary"
:title=
"t('common.refresh')"
:title=
"t('common.refresh')"
>
>
<Icon
name=
"refresh"
size=
"md"
:class=
"loading ? 'animate-spin' : ''"
/>
<Icon
name=
"refresh"
size=
"md"
:class=
"loading ? 'animate-spin' : ''"
/>
</button>
</button>
<button
<button
@
click=
"openSortModal"
@
click=
"openSortModal"
...
@@ -58,7 +66,7 @@
...
@@ -58,7 +66,7 @@
:title=
"t('admin.groups.sortOrder')"
:title=
"t('admin.groups.sortOrder')"
>
>
<Icon
name=
"arrowsUpDown"
size=
"md"
class=
"mr-2"
/>
<Icon
name=
"arrowsUpDown"
size=
"md"
class=
"mr-2"
/>
{{
t
(
'
admin.groups.sortOrder
'
)
}}
{{
t
(
"
admin.groups.sortOrder
"
)
}}
</button>
</button>
<button
<button
@
click=
"showCreateModal = true"
@
click=
"showCreateModal = true"
...
@@ -66,7 +74,7 @@
...
@@ -66,7 +74,7 @@
data-tour=
"groups-create-btn"
data-tour=
"groups-create-btn"
>
>
<Icon
name=
"plus"
size=
"md"
class=
"mr-2"
/>
<Icon
name=
"plus"
size=
"md"
class=
"mr-2"
/>
{{
t
(
'
admin.groups.createGroup
'
)
}}
{{
t
(
"
admin.groups.createGroup
"
)
}}
</button>
</button>
</div>
</div>
</div>
</div>
...
@@ -75,7 +83,9 @@
...
@@ -75,7 +83,9 @@
<
template
#table
>
<
template
#table
>
<DataTable
:columns=
"columns"
:data=
"groups"
:loading=
"loading"
>
<DataTable
:columns=
"columns"
:data=
"groups"
:loading=
"loading"
>
<template
#cell-name
="
{ value }">
<template
#cell-name
="
{ value }">
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
value
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
value
}}
</span>
</
template
>
</
template
>
<
template
#cell-platform=
"{ value }"
>
<
template
#cell-platform=
"{ value }"
>
...
@@ -88,11 +98,11 @@
...
@@ -88,11 +98,11 @@
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
: value === 'antigravity'
: value === 'antigravity'
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
,
]"
]"
>
>
<PlatformIcon
:platform=
"value"
size=
"xs"
/>
<PlatformIcon
:platform=
"value"
size=
"xs"
/>
{{
t
(
'
admin.groups.platforms.
'
+
value
)
}}
{{
t
(
"
admin.groups.platforms.
"
+
value
)
}}
</span>
</span>
</
template
>
</
template
>
...
@@ -104,13 +114,13 @@
...
@@ -104,13 +114,13 @@
'inline-block rounded-full px-2 py-0.5 text-xs font-medium',
'inline-block rounded-full px-2 py-0.5 text-xs font-medium',
row.subscription_type === 'subscription'
row.subscription_type === 'subscription'
? 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
? 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
,
]"
]"
>
>
{{
{{
row
.
subscription_type
===
'
subscription
'
row
.
subscription_type
===
"
subscription
"
?
t
(
'
admin.groups.subscription.subscription
'
)
?
t
(
"
admin.groups.subscription.subscription
"
)
:
t
(
'
admin.groups.subscription.standard
'
)
:
t
(
"
admin.groups.subscription.standard
"
)
}}
}}
</span>
</span>
<!-- Subscription Limits - compact single line -->
<!-- Subscription Limits - compact single line -->
...
@@ -119,18 +129,29 @@
...
@@ -119,18 +129,29 @@
class=
"text-xs text-gray-500 dark:text-gray-400"
class=
"text-xs text-gray-500 dark:text-gray-400"
>
>
<template
<template
v-if=
"row.daily_limit_usd || row.weekly_limit_usd || row.monthly_limit_usd"
v-if=
"
row.daily_limit_usd ||
row.weekly_limit_usd ||
row.monthly_limit_usd
"
>
>
<span
v-if=
"row.daily_limit_usd"
<span
v-if=
"row.daily_limit_usd"
>
$
{{
row
.
daily_limit_usd
}}
/
{{
t
(
'
admin.groups.limitDay
'
)
}}
</span
>
$
{{
row
.
daily_limit_usd
}}
/
{{
t
(
"
admin.groups.limitDay
"
)
}}
</span
>
>
<span
<span
v-if=
"row.daily_limit_usd && (row.weekly_limit_usd || row.monthly_limit_usd)"
v-if=
"
row.daily_limit_usd &&
(row.weekly_limit_usd || row.monthly_limit_usd)
"
class=
"mx-1 text-gray-300 dark:text-gray-600"
class=
"mx-1 text-gray-300 dark:text-gray-600"
>
·
</span
>
·
</span
>
>
<span
v-if=
"row.weekly_limit_usd"
<span
v-if=
"row.weekly_limit_usd"
>
$
{{
row
.
weekly_limit_usd
}}
/
{{
t
(
'
admin.groups.limitWeek
'
)
}}
</span
>
$
{{
row
.
weekly_limit_usd
}}
/
{{
t
(
"
admin.groups.limitWeek
"
)
}}
</span
>
>
<span
<span
v-if=
"row.weekly_limit_usd && row.monthly_limit_usd"
v-if=
"row.weekly_limit_usd && row.monthly_limit_usd"
...
@@ -138,42 +159,75 @@
...
@@ -138,42 +159,75 @@
>
·
</span
>
·
</span
>
>
<span
v-if=
"row.monthly_limit_usd"
<span
v-if=
"row.monthly_limit_usd"
>
$
{{
row
.
monthly_limit_usd
}}
/
{{
t
(
'
admin.groups.limitMonth
'
)
}}
</span
>
$
{{
row
.
monthly_limit_usd
}}
/
{{
t
(
"
admin.groups.limitMonth
"
)
}}
</span
>
>
</
template
>
</
template
>
<span
v-else
class=
"text-gray-400 dark:text-gray-500"
>
{{
<span
v-else
class=
"text-gray-400 dark:text-gray-500"
>
{{
t(
'
admin.groups.subscription.noLimit
'
)
t(
"
admin.groups.subscription.noLimit
"
)
}}
</span>
}}
</span>
</div>
</div>
</div>
</div>
</template>
</template>
<
template
#cell-rate_multiplier=
"{ value }"
>
<
template
#cell-rate_multiplier=
"{ value }"
>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{
value
}}
x
</span>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{
value
}}
x
</span
>
</
template
>
</
template
>
<
template
#cell-is_exclusive=
"{ value }"
>
<
template
#cell-is_exclusive=
"{ value }"
>
<span
:class=
"['badge', value ? 'badge-primary' : 'badge-gray']"
>
<span
:class=
"['badge', value ? 'badge-primary' : 'badge-gray']"
>
{{
value
?
t
(
'
admin.groups.exclusive
'
)
:
t
(
'
admin.groups.public
'
)
}}
{{
value
?
t
(
"
admin.groups.exclusive
"
)
:
t
(
"
admin.groups.public
"
)
}}
</span>
</span>
</
template
>
</
template
>
<
template
#cell-account_count=
"{ row }"
>
<
template
#cell-account_count=
"{ row }"
>
<div
class=
"space-y-0.5 text-xs"
>
<div
class=
"space-y-0.5 text-xs"
>
<div>
<div>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.groups.accountsAvailable
'
)
}}
</span>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
<span
class=
"ml-1 font-medium text-emerald-600 dark:text-emerald-400"
>
{{
(
row
.
active_account_count
||
0
)
-
(
row
.
rate_limited_account_count
||
0
)
}}
</span>
t
(
"
admin.groups.accountsAvailable
"
)
<span
class=
"ml-1 inline-flex items-center rounded bg-gray-100 px-1.5 py-0.5 font-medium text-gray-800 dark:bg-dark-600 dark:text-gray-300"
>
{{
t
(
'
admin.groups.accountsUnit
'
)
}}
</span>
}}
</span>
<span
class=
"ml-1 font-medium text-emerald-600 dark:text-emerald-400"
>
{{
(
row
.
active_account_count
||
0
)
-
(
row
.
rate_limited_account_count
||
0
)
}}
</span
>
<span
class=
"ml-1 inline-flex items-center rounded bg-gray-100 px-1.5 py-0.5 font-medium text-gray-800 dark:bg-dark-600 dark:text-gray-300"
>
{{
t
(
"
admin.groups.accountsUnit
"
)
}}
</span
>
</div>
</div>
<div
v-if=
"row.rate_limited_account_count"
>
<div
v-if=
"row.rate_limited_account_count"
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.groups.accountsRateLimited
'
)
}}
</span>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
<span
class=
"ml-1 font-medium text-amber-600 dark:text-amber-400"
>
{{
row
.
rate_limited_account_count
}}
</span>
t
(
"
admin.groups.accountsRateLimited
"
)
<span
class=
"ml-1 inline-flex items-center rounded bg-gray-100 px-1.5 py-0.5 font-medium text-gray-800 dark:bg-dark-600 dark:text-gray-300"
>
{{
t
(
'
admin.groups.accountsUnit
'
)
}}
</span>
}}
</span>
<span
class=
"ml-1 font-medium text-amber-600 dark:text-amber-400"
>
{{
row
.
rate_limited_account_count
}}
</span
>
<span
class=
"ml-1 inline-flex items-center rounded bg-gray-100 px-1.5 py-0.5 font-medium text-gray-800 dark:bg-dark-600 dark:text-gray-300"
>
{{
t
(
"
admin.groups.accountsUnit
"
)
}}
</span
>
</div>
</div>
<div>
<div>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.groups.accountsTotal
'
)
}}
</span>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
<span
class=
"ml-1 font-medium text-gray-700 dark:text-gray-300"
>
{{
row
.
account_count
||
0
}}
</span>
t
(
"
admin.groups.accountsTotal
"
)
<span
class=
"ml-1 inline-flex items-center rounded bg-gray-100 px-1.5 py-0.5 font-medium text-gray-800 dark:bg-dark-600 dark:text-gray-300"
>
{{
t
(
'
admin.groups.accountsUnit
'
)
}}
</span>
}}
</span>
<span
class=
"ml-1 font-medium text-gray-700 dark:text-gray-300"
>
{{
row
.
account_count
||
0
}}
</span
>
<span
class=
"ml-1 inline-flex items-center rounded bg-gray-100 px-1.5 py-0.5 font-medium text-gray-800 dark:bg-dark-600 dark:text-gray-300"
>
{{
t
(
"
admin.groups.accountsUnit
"
)
}}
</span
>
</div>
</div>
</div>
</div>
</
template
>
</
template
>
...
@@ -195,19 +249,36 @@
...
@@ -195,19 +249,36 @@
<div
v-if=
"usageLoading"
class=
"text-xs text-gray-400"
>
—
</div>
<div
v-if=
"usageLoading"
class=
"text-xs text-gray-400"
>
—
</div>
<div
v-else
class=
"space-y-0.5 text-xs"
>
<div
v-else
class=
"space-y-0.5 text-xs"
>
<div
class=
"text-gray-500 dark:text-gray-400"
>
<div
class=
"text-gray-500 dark:text-gray-400"
>
<span
class=
"text-gray-400 dark:text-gray-500"
>
{{
t
(
'
admin.groups.usageToday
'
)
}}
</span>
<span
class=
"text-gray-400 dark:text-gray-500"
>
{{
<span
class=
"ml-1 font-medium text-gray-700 dark:text-gray-300"
>
$
{{
formatCost
(
usageMap
.
get
(
row
.
id
)?.
today_cost
??
0
)
}}
</span>
t
(
"
admin.groups.usageToday
"
)
}}
</span>
<span
class=
"ml-1 font-medium text-gray-700 dark:text-gray-300"
>
$
{{
formatCost
(
usageMap
.
get
(
row
.
id
)?.
today_cost
??
0
)
}}
</span
>
</div>
</div>
<div
class=
"text-gray-500 dark:text-gray-400"
>
<div
class=
"text-gray-500 dark:text-gray-400"
>
<span
class=
"text-gray-400 dark:text-gray-500"
>
{{
t
(
'
admin.groups.usageTotal
'
)
}}
</span>
<span
class=
"text-gray-400 dark:text-gray-500"
>
{{
<span
class=
"ml-1 font-medium text-gray-700 dark:text-gray-300"
>
$
{{
formatCost
(
usageMap
.
get
(
row
.
id
)?.
total_cost
??
0
)
}}
</span>
t
(
"
admin.groups.usageTotal
"
)
}}
</span>
<span
class=
"ml-1 font-medium text-gray-700 dark:text-gray-300"
>
$
{{
formatCost
(
usageMap
.
get
(
row
.
id
)?.
total_cost
??
0
)
}}
</span
>
</div>
</div>
</div>
</div>
</
template
>
</
template
>
<
template
#cell-status=
"{ value }"
>
<
template
#cell-status=
"{ value }"
>
<span
:class=
"['badge', value === 'active' ? 'badge-success' : 'badge-danger']"
>
<span
{{
t
(
'
admin.accounts.status.
'
+
value
)
}}
:class=
"[
'badge',
value === 'active' ? 'badge-success' : 'badge-danger',
]"
>
{{
t
(
"
admin.accounts.status.
"
+
value
)
}}
</span>
</span>
</
template
>
</
template
>
...
@@ -218,21 +289,23 @@
...
@@ -218,21 +289,23 @@
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"
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"
>
>
<Icon
name=
"edit"
size=
"sm"
/>
<Icon
name=
"edit"
size=
"sm"
/>
<span
class=
"text-xs"
>
{{
t
(
'
common.edit
'
)
}}
</span>
<span
class=
"text-xs"
>
{{
t
(
"
common.edit
"
)
}}
</span>
</button>
</button>
<button
<button
@
click=
"handleRateMultipliers(row)"
@
click=
"handleRateMultipliers(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-purple-600 dark:hover:bg-dark-700 dark:hover:text-purple-400"
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-purple-600 dark:hover:bg-dark-700 dark:hover:text-purple-400"
>
>
<Icon
name=
"dollar"
size=
"sm"
/>
<Icon
name=
"dollar"
size=
"sm"
/>
<span
class=
"text-xs"
>
{{
t
(
'
admin.groups.rateMultipliers
'
)
}}
</span>
<span
class=
"text-xs"
>
{{
t
(
"
admin.groups.rateMultipliers
"
)
}}
</span>
</button>
</button>
<button
<button
@
click=
"handleDelete(row)"
@
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"
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"
>
>
<Icon
name=
"trash"
size=
"sm"
/>
<Icon
name=
"trash"
size=
"sm"
/>
<span
class=
"text-xs"
>
{{
t
(
'
common.delete
'
)
}}
</span>
<span
class=
"text-xs"
>
{{
t
(
"
common.delete
"
)
}}
</span>
</button>
</button>
</div>
</div>
</
template
>
</
template
>
...
@@ -267,9 +340,13 @@
...
@@ -267,9 +340,13 @@
width=
"normal"
width=
"normal"
@
close=
"closeCreateModal"
@
close=
"closeCreateModal"
>
>
<form
id=
"create-group-form"
@
submit.prevent=
"handleCreateGroup"
class=
"space-y-5"
>
<form
id=
"create-group-form"
@
submit.prevent=
"handleCreateGroup"
class=
"space-y-5"
>
<div>
<div>
<label
class=
"input-label"
>
{{ t(
'
admin.groups.form.name
'
) }}
</label>
<label
class=
"input-label"
>
{{ t(
"
admin.groups.form.name
"
) }}
</label>
<input
<input
v-model=
"createForm.name"
v-model=
"createForm.name"
type=
"text"
type=
"text"
...
@@ -280,7 +357,9 @@
...
@@ -280,7 +357,9 @@
/>
/>
</div>
</div>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.form.description') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.form.description")
}}
</label>
<textarea
<textarea
v-model=
"createForm.description"
v-model=
"createForm.description"
rows=
"3"
rows=
"3"
...
@@ -289,20 +368,22 @@
...
@@ -289,20 +368,22 @@
></textarea>
></textarea>
</div>
</div>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.form.platform') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.form.platform")
}}
</label>
<Select
<Select
v-model=
"createForm.platform"
v-model=
"createForm.platform"
:options=
"platformOptions"
:options=
"platformOptions"
data-tour=
"group-form-platform"
data-tour=
"group-form-platform"
@
change=
"createForm.copy_accounts_from_group_ids = []"
@
change=
"createForm.copy_accounts_from_group_ids = []"
/>
/>
<p
class=
"input-hint"
>
{{ t(
'
admin.groups.platformHint
'
) }}
</p>
<p
class=
"input-hint"
>
{{ t(
"
admin.groups.platformHint
"
) }}
</p>
</div>
</div>
<!-- 从分组复制账号 -->
<!-- 从分组复制账号 -->
<div
v-if=
"copyAccountsGroupOptions.length > 0"
>
<div
v-if=
"copyAccountsGroupOptions.length > 0"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t(
'
admin.groups.copyAccounts.title
'
) }}
{{ t(
"
admin.groups.copyAccounts.title
"
) }}
</label>
</label>
<div
class=
"group relative inline-flex"
>
<div
class=
"group relative inline-flex"
>
<Icon
<Icon
...
@@ -311,27 +392,44 @@
...
@@ -311,27 +392,44 @@
:stroke-width=
"2"
:stroke-width=
"2"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
/>
<div
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
{{ t(
'
admin.groups.copyAccounts.tooltip
'
) }}
{{ t(
"
admin.groups.copyAccounts.tooltip
"
) }}
</p>
</p>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 已选分组标签 -->
<!-- 已选分组标签 -->
<div
v-if=
"createForm.copy_accounts_from_group_ids.length > 0"
class=
"flex flex-wrap gap-1.5 mb-2"
>
<div
v-if=
"createForm.copy_accounts_from_group_ids.length > 0"
class=
"flex flex-wrap gap-1.5 mb-2"
>
<span
<span
v-for=
"groupId in createForm.copy_accounts_from_group_ids"
v-for=
"groupId in createForm.copy_accounts_from_group_ids"
:key=
"groupId"
:key=
"groupId"
class=
"inline-flex items-center gap-1 rounded-full bg-primary-100 px-2.5 py-1 text-xs font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
class=
"inline-flex items-center gap-1 rounded-full bg-primary-100 px-2.5 py-1 text-xs font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
>
>
{{ copyAccountsGroupOptions.find(o => o.value === groupId)?.label || `#${groupId}` }}
{{
copyAccountsGroupOptions.find((o) => o.value === groupId)
?.label || `#${groupId}`
}}
<button
<button
type=
"button"
type=
"button"
@
click=
"createForm.copy_accounts_from_group_ids = createForm.copy_accounts_from_group_ids.filter(id => id !== groupId)"
@
click=
"
createForm.copy_accounts_from_group_ids =
createForm.copy_accounts_from_group_ids.filter(
(id) => id !== groupId,
)
"
class=
"ml-0.5 text-primary-500 hover:text-primary-700 dark:hover:text-primary-200"
class=
"ml-0.5 text-primary-500 hover:text-primary-700 dark:hover:text-primary-200"
>
>
<Icon
name=
"x"
size=
"xs"
/>
<Icon
name=
"x"
size=
"xs"
/>
...
@@ -341,28 +439,39 @@
...
@@ -341,28 +439,39 @@
<!-- 分组选择下拉 -->
<!-- 分组选择下拉 -->
<select
<select
class=
"input"
class=
"input"
@
change=
"(e) => {
@
change=
"
const val = Number((e.target as HTMLSelectElement).value)
(e) => {
if (val && !createForm.copy_accounts_from_group_ids.includes(val)) {
const val = Number((e.target as HTMLSelectElement).value);
createForm.copy_accounts_from_group_ids.push(val)
if (
val &&
!createForm.copy_accounts_from_group_ids.includes(val)
) {
createForm.copy_accounts_from_group_ids.push(val);
}
(e.target as HTMLSelectElement).value = '';
}
}
(e.target as HTMLSelectElement).value = ''
"
}"
>
>
<option
value=
""
>
{{ t('admin.groups.copyAccounts.selectPlaceholder') }}
</option>
<option
value=
""
>
{{ t("admin.groups.copyAccounts.selectPlaceholder") }}
</option>
<option
<option
v-for=
"opt in copyAccountsGroupOptions"
v-for=
"opt in copyAccountsGroupOptions"
:key=
"opt.value"
:key=
"opt.value"
:value=
"opt.value"
:value=
"opt.value"
:disabled=
"createForm.copy_accounts_from_group_ids.includes(opt.value)"
:disabled=
"
createForm.copy_accounts_from_group_ids.includes(opt.value)
"
>
>
{{ opt.label }}
{{ opt.label }}
</option>
</option>
</select>
</select>
<p
class=
"input-hint"
>
{{ t(
'
admin.groups.copyAccounts.hint
'
) }}
</p>
<p
class=
"input-hint"
>
{{ t(
"
admin.groups.copyAccounts.hint
"
) }}
</p>
</div>
</div>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.form.rateMultiplier') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.form.rateMultiplier")
}}
</label>
<input
<input
v-model.number=
"createForm.rate_multiplier"
v-model.number=
"createForm.rate_multiplier"
type=
"number"
type=
"number"
...
@@ -372,12 +481,15 @@
...
@@ -372,12 +481,15 @@
class=
"input"
class=
"input"
data-tour=
"group-form-multiplier"
data-tour=
"group-form-multiplier"
/>
/>
<p
class=
"input-hint"
>
{{ t(
'
admin.groups.rateMultiplierHint
'
) }}
</p>
<p
class=
"input-hint"
>
{{ t(
"
admin.groups.rateMultiplierHint
"
) }}
</p>
</div>
</div>
<div
v-if=
"createForm.subscription_type !== 'subscription'"
data-tour=
"group-form-exclusive"
>
<div
v-if=
"createForm.subscription_type !== 'subscription'"
data-tour=
"group-form-exclusive"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t(
'
admin.groups.form.exclusive
'
) }}
{{ t(
"
admin.groups.form.exclusive
"
) }}
</label>
</label>
<!-- Help Tooltip -->
<!-- Help Tooltip -->
<div
class=
"group relative inline-flex"
>
<div
class=
"group relative inline-flex"
>
...
@@ -388,20 +500,32 @@
...
@@ -388,20 +500,32 @@
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
/>
<!-- Tooltip Popover -->
<!-- Tooltip Popover -->
<div
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
<p
class=
"mb-2 text-xs font-medium"
>
{{ t('admin.groups.exclusiveTooltip.title') }}
</p>
>
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
<p
class=
"mb-2 text-xs font-medium"
>
{{ t("admin.groups.exclusiveTooltip.title") }}
</p>
<p
class=
"mb-2 text-xs leading-relaxed text-gray-300"
>
<p
class=
"mb-2 text-xs leading-relaxed text-gray-300"
>
{{ t(
'
admin.groups.exclusiveTooltip.description
'
) }}
{{ t(
"
admin.groups.exclusiveTooltip.description
"
) }}
</p>
</p>
<div
class=
"rounded bg-gray-800 p-2 dark:bg-gray-700"
>
<div
class=
"rounded bg-gray-800 p-2 dark:bg-gray-700"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<span
class=
"inline-flex items-center gap-1 text-primary-400"
><Icon
name=
"lightbulb"
size=
"xs"
/>
{{ t('admin.groups.exclusiveTooltip.example') }}
</span>
<span
{{ t('admin.groups.exclusiveTooltip.exampleContent') }}
class=
"inline-flex items-center gap-1 text-primary-400"
><Icon
name=
"lightbulb"
size=
"xs"
/>
{{ t("admin.groups.exclusiveTooltip.example") }}
</span
>
{{ t("admin.groups.exclusiveTooltip.exampleContent") }}
</p>
</p>
</div>
</div>
<!-- Arrow -->
<!-- Arrow -->
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -412,18 +536,24 @@
...
@@ -412,18 +536,24 @@
@
click=
"createForm.is_exclusive = !createForm.is_exclusive"
@
click=
"createForm.is_exclusive = !createForm.is_exclusive"
:class=
"[
:class=
"[
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
createForm.is_exclusive ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
createForm.is_exclusive
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600',
]"
]"
>
>
<span
<span
:class=
"[
:class=
"[
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
createForm.is_exclusive ? 'translate-x-6' : 'translate-x-1'
createForm.is_exclusive ? 'translate-x-6' : 'translate-x-1'
,
]"
]"
/>
/>
</button>
</button>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{ createForm.is_exclusive ? t('admin.groups.exclusive') : t('admin.groups.public') }}
{{
createForm.is_exclusive
? t("admin.groups.exclusive")
: t("admin.groups.public")
}}
</span>
</span>
</div>
</div>
</div>
</div>
...
@@ -431,9 +561,16 @@
...
@@ -431,9 +561,16 @@
<!-- Subscription Configuration -->
<!-- Subscription Configuration -->
<div
class=
"mt-4 border-t pt-4"
>
<div
class=
"mt-4 border-t pt-4"
>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.subscription.type') }}
</label>
<label
class=
"input-label"
>
{{
<Select
v-model=
"createForm.subscription_type"
:options=
"subscriptionTypeOptions"
/>
t("admin.groups.subscription.type")
<p
class=
"input-hint"
>
{{ t('admin.groups.subscription.typeHint') }}
</p>
}}
</label>
<Select
v-model=
"createForm.subscription_type"
:options=
"subscriptionTypeOptions"
/>
<p
class=
"input-hint"
>
{{ t("admin.groups.subscription.typeHint") }}
</p>
</div>
</div>
<!-- Subscription limits (only show when subscription type is selected) -->
<!-- Subscription limits (only show when subscription type is selected) -->
...
@@ -442,7 +579,9 @@
...
@@ -442,7 +579,9 @@
class=
"space-y-4 border-l-2 border-primary-200 pl-4 dark:border-primary-800"
class=
"space-y-4 border-l-2 border-primary-200 pl-4 dark:border-primary-800"
>
>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.subscription.dailyLimit') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.subscription.dailyLimit")
}}
</label>
<input
<input
v-model.number=
"createForm.daily_limit_usd"
v-model.number=
"createForm.daily_limit_usd"
type=
"number"
type=
"number"
...
@@ -453,7 +592,9 @@
...
@@ -453,7 +592,9 @@
/>
/>
</div>
</div>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.subscription.weeklyLimit') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.subscription.weeklyLimit")
}}
</label>
<input
<input
v-model.number=
"createForm.weekly_limit_usd"
v-model.number=
"createForm.weekly_limit_usd"
type=
"number"
type=
"number"
...
@@ -464,7 +605,9 @@
...
@@ -464,7 +605,9 @@
/>
/>
</div>
</div>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.subscription.monthlyLimit') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.subscription.monthlyLimit")
}}
</label>
<input
<input
v-model.number=
"createForm.monthly_limit_usd"
v-model.number=
"createForm.monthly_limit_usd"
type=
"number"
type=
"number"
...
@@ -478,12 +621,20 @@
...
@@ -478,12 +621,20 @@
</div>
</div>
<!-- 图片生成计费配置(antigravity 和 gemini 平台) -->
<!-- 图片生成计费配置(antigravity 和 gemini 平台) -->
<div
v-if=
"createForm.platform === 'antigravity' || createForm.platform === 'gemini'"
class=
"border-t pt-4"
>
<div
<label
class=
"block mb-2 font-medium text-gray-700 dark:text-gray-300"
>
v-if=
"
{{ t('admin.groups.imagePricing.title') }}
createForm.platform === 'antigravity' ||
createForm.platform === 'gemini'
"
class=
"border-t pt-4"
>
<label
class=
"block mb-2 font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.groups.imagePricing.title") }}
</label>
</label>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mb-3"
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mb-3"
>
{{ t(
'
admin.groups.imagePricing.description
'
) }}
{{ t(
"
admin.groups.imagePricing.description
"
) }}
</p>
</p>
<div
class=
"grid grid-cols-3 gap-3"
>
<div
class=
"grid grid-cols-3 gap-3"
>
<div>
<div>
...
@@ -522,13 +673,11 @@
...
@@ -522,13 +673,11 @@
</div>
</div>
</div>
</div>
<!-- 支持的模型系列(仅 antigravity 平台) -->
<!-- 支持的模型系列(仅 antigravity 平台) -->
<div
v-if=
"createForm.platform === 'antigravity'"
class=
"border-t pt-4"
>
<div
v-if=
"createForm.platform === 'antigravity'"
class=
"border-t pt-4"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t(
'
admin.groups.supportedScopes.title
'
) }}
{{ t(
"
admin.groups.supportedScopes.title
"
) }}
</label>
</label>
<!-- Help Tooltip -->
<!-- Help Tooltip -->
<div
class=
"group relative inline-flex"
>
<div
class=
"group relative inline-flex"
>
...
@@ -538,12 +687,18 @@
...
@@ -538,12 +687,18 @@
:stroke-width=
"2"
:stroke-width=
"2"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
/>
<div
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
{{ t(
'
admin.groups.supportedScopes.tooltip
'
) }}
{{ t(
"
admin.groups.supportedScopes.tooltip
"
) }}
</p>
</p>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -556,35 +711,47 @@
...
@@ -556,35 +711,47 @@
@
change=
"toggleCreateScope('claude')"
@
change=
"toggleCreateScope('claude')"
class=
"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
class=
"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
/>
/>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{ t('admin.groups.supportedScopes.claude') }}
</span>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{
t("admin.groups.supportedScopes.claude")
}}
</span>
</label>
</label>
<label
class=
"flex items-center gap-2 cursor-pointer"
>
<label
class=
"flex items-center gap-2 cursor-pointer"
>
<input
<input
type=
"checkbox"
type=
"checkbox"
:checked=
"createForm.supported_model_scopes.includes('gemini_text')"
:checked=
"
createForm.supported_model_scopes.includes('gemini_text')
"
@
change=
"toggleCreateScope('gemini_text')"
@
change=
"toggleCreateScope('gemini_text')"
class=
"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
class=
"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
/>
/>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{ t('admin.groups.supportedScopes.geminiText') }}
</span>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{
t("admin.groups.supportedScopes.geminiText")
}}
</span>
</label>
</label>
<label
class=
"flex items-center gap-2 cursor-pointer"
>
<label
class=
"flex items-center gap-2 cursor-pointer"
>
<input
<input
type=
"checkbox"
type=
"checkbox"
:checked=
"createForm.supported_model_scopes.includes('gemini_image')"
:checked=
"
createForm.supported_model_scopes.includes('gemini_image')
"
@
change=
"toggleCreateScope('gemini_image')"
@
change=
"toggleCreateScope('gemini_image')"
class=
"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
class=
"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
/>
/>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{ t('admin.groups.supportedScopes.geminiImage') }}
</span>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{
t("admin.groups.supportedScopes.geminiImage")
}}
</span>
</label>
</label>
</div>
</div>
<p
class=
"mt-2 text-xs text-gray-500 dark:text-gray-400"
>
{{ t('admin.groups.supportedScopes.hint') }}
</p>
<p
class=
"mt-2 text-xs text-gray-500 dark:text-gray-400"
>
{{ t("admin.groups.supportedScopes.hint") }}
</p>
</div>
</div>
<!-- MCP XML 协议注入(仅 antigravity 平台) -->
<!-- MCP XML 协议注入(仅 antigravity 平台) -->
<div
v-if=
"createForm.platform === 'antigravity'"
class=
"border-t pt-4"
>
<div
v-if=
"createForm.platform === 'antigravity'"
class=
"border-t pt-4"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t(
'
admin.groups.mcpXml.title
'
) }}
{{ t(
"
admin.groups.mcpXml.title
"
) }}
</label>
</label>
<div
class=
"group relative inline-flex"
>
<div
class=
"group relative inline-flex"
>
<Icon
<Icon
...
@@ -593,12 +760,18 @@
...
@@ -593,12 +760,18 @@
:stroke-width=
"2"
:stroke-width=
"2"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
/>
<div
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
{{ t(
'
admin.groups.mcpXml.tooltip
'
) }}
{{ t(
"
admin.groups.mcpXml.tooltip
"
) }}
</p>
</p>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -609,18 +782,24 @@
...
@@ -609,18 +782,24 @@
@
click=
"createForm.mcp_xml_inject = !createForm.mcp_xml_inject"
@
click=
"createForm.mcp_xml_inject = !createForm.mcp_xml_inject"
:class=
"[
:class=
"[
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
createForm.mcp_xml_inject ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
createForm.mcp_xml_inject
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600',
]"
]"
>
>
<span
<span
:class=
"[
:class=
"[
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
createForm.mcp_xml_inject ? 'translate-x-6' : 'translate-x-1'
createForm.mcp_xml_inject ? 'translate-x-6' : 'translate-x-1'
,
]"
]"
/>
/>
</button>
</button>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{ createForm.mcp_xml_inject ? t('admin.groups.mcpXml.enabled') : t('admin.groups.mcpXml.disabled') }}
{{
createForm.mcp_xml_inject
? t("admin.groups.mcpXml.enabled")
: t("admin.groups.mcpXml.disabled")
}}
</span>
</span>
</div>
</div>
</div>
</div>
...
@@ -629,7 +808,7 @@
...
@@ -629,7 +808,7 @@
<div
v-if=
"createForm.platform === 'anthropic'"
class=
"border-t pt-4"
>
<div
v-if=
"createForm.platform === 'anthropic'"
class=
"border-t pt-4"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t(
'
admin.groups.claudeCode.title
'
) }}
{{ t(
"
admin.groups.claudeCode.title
"
) }}
</label>
</label>
<!-- Help Tooltip -->
<!-- Help Tooltip -->
<div
class=
"group relative inline-flex"
>
<div
class=
"group relative inline-flex"
>
...
@@ -639,12 +818,18 @@
...
@@ -639,12 +818,18 @@
:stroke-width=
"2"
:stroke-width=
"2"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
/>
<div
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
{{ t(
'
admin.groups.claudeCode.tooltip
'
) }}
{{ t(
"
admin.groups.claudeCode.tooltip
"
) }}
</p>
</p>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -652,97 +837,321 @@
...
@@ -652,97 +837,321 @@
<div
class=
"flex items-center gap-3"
>
<div
class=
"flex items-center gap-3"
>
<button
<button
type=
"button"
type=
"button"
@
click=
"createForm.claude_code_only = !createForm.claude_code_only"
@
click=
"
createForm.claude_code_only = !createForm.claude_code_only
"
:class=
"[
:class=
"[
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
createForm.claude_code_only ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
createForm.claude_code_only
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600',
]"
]"
>
>
<span
<span
:class=
"[
:class=
"[
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
createForm.claude_code_only ? 'translate-x-6' : 'translate-x-1'
createForm.claude_code_only
? 'translate-x-6'
: 'translate-x-1',
]"
]"
/>
/>
</button>
</button>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{ createForm.claude_code_only ? t('admin.groups.claudeCode.enabled') : t('admin.groups.claudeCode.disabled') }}
{{
createForm.claude_code_only
? t("admin.groups.claudeCode.enabled")
: t("admin.groups.claudeCode.disabled")
}}
</span>
</span>
</div>
</div>
<!-- 降级分组选择(仅当启用 claude_code_only 时显示) -->
<!-- 降级分组选择(仅当启用 claude_code_only 时显示) -->
<div
v-if=
"createForm.claude_code_only"
class=
"mt-3"
>
<div
v-if=
"createForm.claude_code_only"
class=
"mt-3"
>
<label
class=
"input-label"
>
{{ t('admin.groups.claudeCode.fallbackGroup') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.claudeCode.fallbackGroup")
}}
</label>
<Select
<Select
v-model=
"createForm.fallback_group_id"
v-model=
"createForm.fallback_group_id"
:options=
"fallbackGroupOptions"
:options=
"fallbackGroupOptions"
:placeholder=
"t('admin.groups.claudeCode.noFallback')"
:placeholder=
"t('admin.groups.claudeCode.noFallback')"
/>
/>
<p
class=
"input-hint"
>
{{ t('admin.groups.claudeCode.fallbackHint') }}
</p>
<p
class=
"input-hint"
>
{{ t("admin.groups.claudeCode.fallbackHint") }}
</p>
</div>
</div>
</div>
</div>
<!-- OpenAI Messages 调度配置(仅 openai 平台) -->
<!-- OpenAI Messages 调度配置(仅 openai 平台) -->
<div
v-if=
"createForm.platform === 'openai'"
class=
"border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
>
<div
<h4
class=
"text-sm font-medium text-gray-700 dark:text-gray-300 mb-3"
>
{{ t('admin.groups.openaiMessages.title') }}
</h4>
v-if=
"createForm.platform === 'openai'"
class=
"border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
>
<h4
class=
"text-sm font-medium text-gray-700 dark:text-gray-300 mb-3"
>
{{ t("admin.groups.openaiMessages.title") }}
</h4>
<!-- 允许 Messages 调度开关 -->
<!-- 允许 Messages 调度开关 -->
<div
class=
"flex items-center justify-between"
>
<div
class=
"flex items-center justify-between"
>
<label
class=
"text-sm text-gray-600 dark:text-gray-400"
>
{{ t('admin.groups.openaiMessages.allowDispatch') }}
</label>
<label
class=
"text-sm text-gray-600 dark:text-gray-400"
>
{{
t("admin.groups.openaiMessages.allowDispatch")
}}
</label>
<button
<button
type=
"button"
type=
"button"
@
click=
"createForm.allow_messages_dispatch = !createForm.allow_messages_dispatch"
@
click=
"
createForm.allow_messages_dispatch =
!createForm.allow_messages_dispatch
"
class=
"relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
class=
"relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
:class=
"
:class=
"
createForm.allow_messages_dispatch ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
createForm.allow_messages_dispatch
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600'
"
"
>
>
<span
<span
class=
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
class=
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class=
"
:class=
"
createForm.allow_messages_dispatch ? 'translate-x-6' : 'translate-x-1'
createForm.allow_messages_dispatch
? 'translate-x-6'
: 'translate-x-1'
"
"
/>
/>
</button>
</button>
</div>
</div>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mt-1"
>
{{ t('admin.groups.openaiMessages.allowDispatchHint') }}
</p>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mt-1"
>
{{ t("admin.groups.openaiMessages.allowDispatchHint") }}
</p>
<!-- 默认映射模型(仅当开关打开时显示) -->
<div
v-if=
"createForm.allow_messages_dispatch"
class=
"mt-3"
>
<div
v-if=
"createForm.allow_messages_dispatch"
class=
"mt-3"
>
<label
class=
"input-label"
>
{{ t('admin.groups.openaiMessages.defaultModel') }}
</label>
<div
<input
class=
"relative overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-dark-600 dark:bg-dark-800"
v-model=
"createForm.default_mapped_model"
>
type=
"text"
<div
:placeholder=
"t('admin.groups.openaiMessages.defaultModelPlaceholder')"
class=
"border-b border-gray-100 bg-gray-50/80 px-4 py-3 dark:border-dark-700 dark:bg-dark-700/50"
class=
"input"
>
/>
<div
class=
"flex items-center gap-2"
>
<p
class=
"input-hint"
>
{{ t('admin.groups.openaiMessages.defaultModelHint') }}
</p>
<div
class=
"h-2 w-2 rounded-full bg-blue-500"
></div>
<label
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
{{
t("admin.groups.openaiMessages.familyMappingTitle")
}}
</label
>
</div>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
{{ t("admin.groups.openaiMessages.familyMappingHint") }}
</p>
</div>
<div
class=
"p-4"
>
<div
class=
"grid gap-4 md:grid-cols-3"
>
<div>
<label
class=
"input-label"
>
{{
t("admin.groups.openaiMessages.opusModel")
}}
</label>
<input
v-model=
"createForm.opus_mapped_model"
type=
"text"
:placeholder=
"
t('admin.groups.openaiMessages.opusModelPlaceholder')
"
class=
"input"
/>
</div>
<div>
<label
class=
"input-label"
>
{{
t("admin.groups.openaiMessages.sonnetModel")
}}
</label>
<input
v-model=
"createForm.sonnet_mapped_model"
type=
"text"
:placeholder=
"
t('admin.groups.openaiMessages.sonnetModelPlaceholder')
"
class=
"input"
/>
</div>
<div>
<label
class=
"input-label"
>
{{
t("admin.groups.openaiMessages.haikuModel")
}}
</label>
<input
v-model=
"createForm.haiku_mapped_model"
type=
"text"
:placeholder=
"
t('admin.groups.openaiMessages.haikuModelPlaceholder')
"
class=
"input"
/>
</div>
</div>
</div>
</div>
<div
class=
"mt-5 relative overflow-hidden rounded-xl border border-primary-200 bg-white shadow-sm dark:border-primary-900/50 dark:bg-dark-800"
>
<div
class=
"border-b border-primary-100 bg-primary-50/80 px-4 py-3 dark:border-primary-900/40 dark:bg-primary-900/20"
>
<div
class=
"flex items-start justify-between gap-3"
>
<div>
<div
class=
"flex items-center gap-2"
>
<div
class=
"h-2 w-2 rounded-full bg-primary-500"
></div>
<label
class=
"text-sm font-medium text-primary-900 dark:text-primary-100"
>
{{
t("admin.groups.openaiMessages.exactMappingTitle")
}}
</label
>
</div>
<p
class=
"mt-1 text-xs text-primary-600/90 dark:text-primary-400/90"
>
{{ t("admin.groups.openaiMessages.exactMappingHint") }}
</p>
</div>
</div>
</div>
<div
class=
"p-4 bg-gray-50/30 dark:bg-dark-800/30"
>
<div
v-if=
"createForm.exact_model_mappings.length === 0"
class=
"flex items-center justify-between gap-3 rounded-xl border-2 border-dashed border-primary-200 bg-white px-5 py-4 text-sm text-primary-700 transition-colors hover:border-primary-300 dark:border-primary-900/40 dark:bg-dark-800 dark:text-primary-300 dark:hover:border-primary-800"
>
<span>
{{
t("admin.groups.openaiMessages.noExactMappings")
}}
</span>
<button
type=
"button"
@
click=
"addCreateMessagesDispatchMapping"
class=
"flex items-center gap-1.5 text-sm font-medium text-primary-600 transition-colors hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
>
<Icon
name=
"plus"
size=
"sm"
/>
{{ t("admin.groups.openaiMessages.addExactMapping") }}
</button>
</div>
<div
v-else
class=
"space-y-3"
>
<div
v-for=
"row in createForm.exact_model_mappings"
:key=
"getCreateMessagesDispatchRowKey(row)"
class=
"group relative rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-all hover:border-primary-300 hover:shadow-md dark:border-dark-600 dark:bg-dark-700 dark:hover:border-primary-700"
>
<div
class=
"flex items-center gap-4"
>
<div
class=
"grid flex-1 gap-4 md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] md:items-start"
>
<div>
<label
class=
"input-label"
>
{{
t("admin.groups.openaiMessages.claudeModel")
}}
</label>
<input
v-model=
"row.claude_model"
type=
"text"
:placeholder=
"
t(
'admin.groups.openaiMessages.claudeModelPlaceholder',
)
"
class=
"input bg-gray-50 focus:bg-white dark:bg-dark-800 dark:focus:bg-dark-900"
/>
</div>
<div
class=
"hidden md:flex md:justify-center md:pt-7 text-primary-300 dark:text-primary-700"
>
<Icon
name=
"arrowRight"
size=
"sm"
class=
"transition-transform group-hover:translate-x-1"
/>
</div>
<div>
<label
class=
"input-label"
>
{{
t("admin.groups.openaiMessages.targetModel")
}}
</label>
<input
v-model=
"row.target_model"
type=
"text"
:placeholder=
"
t(
'admin.groups.openaiMessages.targetModelPlaceholder',
)
"
class=
"input bg-gray-50 focus:bg-white dark:bg-dark-800 dark:focus:bg-dark-900"
/>
</div>
</div>
<button
type=
"button"
@
click=
"removeCreateMessagesDispatchMapping(row)"
class=
"mt-6 flex h-9 w-9 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-900/20 dark:hover:text-red-400"
:title=
"
t('admin.groups.openaiMessages.removeExactMapping')
"
>
<Icon
name=
"trash"
size=
"sm"
/>
</button>
</div>
</div>
<button
type=
"button"
@
click=
"addCreateMessagesDispatchMapping"
class=
"flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed border-gray-300 bg-white py-3 text-sm font-medium text-gray-500 transition-all hover:border-primary-300 hover:bg-primary-50/50 hover:text-primary-600 dark:border-dark-600 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-primary-800 dark:hover:bg-primary-900/20 dark:hover:text-primary-400"
>
<Icon
name=
"plus"
size=
"sm"
/>
{{ t("admin.groups.openaiMessages.addExactMapping") }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 账号过滤控制 (OpenAI/Antigravity/Anthropic/Gemini) -->
<!-- 账号过滤控制 (OpenAI/Antigravity/Anthropic/Gemini) -->
<div
v-if=
"['openai', 'antigravity', 'anthropic', 'gemini'].includes(createForm.platform)"
class=
"border-t border-gray-200 dark:border-dark-400 pt-4 mt-4 space-y-4"
>
<div
<h4
class=
"text-sm font-medium text-gray-700 dark:text-gray-300 mb-3"
>
账号过滤控制
</h4>
v-if=
"
['openai', 'antigravity', 'anthropic', 'gemini'].includes(
createForm.platform,
)
"
class=
"border-t border-gray-200 dark:border-dark-400 pt-4 mt-4 space-y-4"
>
<h4
class=
"text-sm font-medium text-gray-700 dark:text-gray-300 mb-3"
>
账号过滤控制
</h4>
<!-- require_oauth_only toggle -->
<!-- require_oauth_only toggle -->
<div
class=
"flex items-center justify-between"
>
<div
class=
"flex items-center justify-between"
>
<div>
<div>
<label
class=
"text-sm text-gray-600 dark:text-gray-400"
>
仅允许 OAuth 账号
</label>
<label
class=
"text-sm text-gray-600 dark:text-gray-400"
>
仅允许 OAuth 账号
</label
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mt-0.5"
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mt-0.5"
>
{{ createForm.require_oauth_only ? '已启用 — 排除 API Key 类型账号' : '未启用' }}
{{
createForm.require_oauth_only
? "已启用 — 排除 API Key 类型账号"
: "未启用"
}}
</p>
</p>
</div>
</div>
<button
<button
type=
"button"
type=
"button"
@
click=
"createForm.require_oauth_only = !createForm.require_oauth_only"
@
click=
"
createForm.require_oauth_only = !createForm.require_oauth_only
"
class=
"relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
class=
"relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
:class=
"
:class=
"
createForm.require_oauth_only ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
createForm.require_oauth_only
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600'
"
"
>
>
<span
<span
class=
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
class=
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class=
"
:class=
"
createForm.require_oauth_only ? 'translate-x-6' : 'translate-x-1'
createForm.require_oauth_only
? 'translate-x-6'
: 'translate-x-1'
"
"
/>
/>
</button>
</button>
...
@@ -751,23 +1160,35 @@
...
@@ -751,23 +1160,35 @@
<!-- require_privacy_set toggle -->
<!-- require_privacy_set toggle -->
<div
class=
"flex items-center justify-between"
>
<div
class=
"flex items-center justify-between"
>
<div>
<div>
<label
class=
"text-sm text-gray-600 dark:text-gray-400"
>
仅允许隐私保护已设置的账号
</label>
<label
class=
"text-sm text-gray-600 dark:text-gray-400"
>
仅允许隐私保护已设置的账号
</label
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mt-0.5"
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mt-0.5"
>
{{ createForm.require_privacy_set ? '已启用 — Privacy 未设置的账号将被排除' : '未启用' }}
{{
createForm.require_privacy_set
? "已启用 — Privacy 未设置的账号将被排除"
: "未启用"
}}
</p>
</p>
</div>
</div>
<button
<button
type=
"button"
type=
"button"
@
click=
"createForm.require_privacy_set = !createForm.require_privacy_set"
@
click=
"
createForm.require_privacy_set = !createForm.require_privacy_set
"
class=
"relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
class=
"relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
:class=
"
:class=
"
createForm.require_privacy_set ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
createForm.require_privacy_set
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600'
"
"
>
>
<span
<span
class=
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
class=
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class=
"
:class=
"
createForm.require_privacy_set ? 'translate-x-6' : 'translate-x-1'
createForm.require_privacy_set
? 'translate-x-6'
: 'translate-x-1'
"
"
/>
/>
</button>
</button>
...
@@ -776,23 +1197,30 @@
...
@@ -776,23 +1197,30 @@
<!-- 无效请求兜底(仅 anthropic/antigravity 平台,且非订阅分组) -->
<!-- 无效请求兜底(仅 anthropic/antigravity 平台,且非订阅分组) -->
<div
<div
v-if=
"['anthropic', 'antigravity'].includes(createForm.platform) && createForm.subscription_type !== 'subscription'"
v-if=
"
['anthropic', 'antigravity'].includes(createForm.platform) &&
createForm.subscription_type !== 'subscription'
"
class=
"border-t pt-4"
class=
"border-t pt-4"
>
>
<label
class=
"input-label"
>
{{ t('admin.groups.invalidRequestFallback.title') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.invalidRequestFallback.title")
}}
</label>
<Select
<Select
v-model=
"createForm.fallback_group_id_on_invalid_request"
v-model=
"createForm.fallback_group_id_on_invalid_request"
:options=
"invalidRequestFallbackOptions"
:options=
"invalidRequestFallbackOptions"
:placeholder=
"t('admin.groups.invalidRequestFallback.noFallback')"
:placeholder=
"t('admin.groups.invalidRequestFallback.noFallback')"
/>
/>
<p
class=
"input-hint"
>
{{ t('admin.groups.invalidRequestFallback.hint') }}
</p>
<p
class=
"input-hint"
>
{{ t("admin.groups.invalidRequestFallback.hint") }}
</p>
</div>
</div>
<!-- 模型路由配置(仅 anthropic 平台) -->
<!-- 模型路由配置(仅 anthropic 平台) -->
<div
v-if=
"createForm.platform === 'anthropic'"
class=
"border-t pt-4"
>
<div
v-if=
"createForm.platform === 'anthropic'"
class=
"border-t pt-4"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t(
'
admin.groups.modelRouting.title
'
) }}
{{ t(
"
admin.groups.modelRouting.title
"
) }}
</label>
</label>
<!-- Help Tooltip -->
<!-- Help Tooltip -->
<div
class=
"group relative inline-flex"
>
<div
class=
"group relative inline-flex"
>
...
@@ -802,12 +1230,18 @@
...
@@ -802,12 +1230,18 @@
:stroke-width=
"2"
:stroke-width=
"2"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
/>
<div
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-80 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-80 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
{{ t(
'
admin.groups.modelRouting.tooltip
'
) }}
{{ t(
"
admin.groups.modelRouting.tooltip
"
) }}
</p>
</p>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -816,28 +1250,42 @@
...
@@ -816,28 +1250,42 @@
<div
class=
"flex items-center gap-3 mb-3"
>
<div
class=
"flex items-center gap-3 mb-3"
>
<button
<button
type=
"button"
type=
"button"
@
click=
"createForm.model_routing_enabled = !createForm.model_routing_enabled"
@
click=
"
createForm.model_routing_enabled =
!createForm.model_routing_enabled
"
:class=
"[
:class=
"[
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
createForm.model_routing_enabled ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
createForm.model_routing_enabled
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600',
]"
]"
>
>
<span
<span
:class=
"[
:class=
"[
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
createForm.model_routing_enabled ? 'translate-x-6' : 'translate-x-1'
createForm.model_routing_enabled
? 'translate-x-6'
: 'translate-x-1',
]"
]"
/>
/>
</button>
</button>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{ createForm.model_routing_enabled ? t('admin.groups.modelRouting.enabled') : t('admin.groups.modelRouting.disabled') }}
{{
createForm.model_routing_enabled
? t("admin.groups.modelRouting.enabled")
: t("admin.groups.modelRouting.disabled")
}}
</span>
</span>
</div>
</div>
<p
v-if=
"!createForm.model_routing_enabled"
class=
"text-xs text-gray-500 dark:text-gray-400 mb-3"
>
<p
{{ t('admin.groups.modelRouting.disabledHint') }}
v-if=
"!createForm.model_routing_enabled"
class=
"text-xs text-gray-500 dark:text-gray-400 mb-3"
>
{{ t("admin.groups.modelRouting.disabledHint") }}
</p>
</p>
<p
v-else
class=
"text-xs text-gray-500 dark:text-gray-400 mb-3"
>
<p
v-else
class=
"text-xs text-gray-500 dark:text-gray-400 mb-3"
>
{{ t(
'
admin.groups.modelRouting.noRulesHint
'
) }}
{{ t(
"
admin.groups.modelRouting.noRulesHint
"
) }}
</p>
</p>
<!-- 路由规则列表(仅在启用时显示) -->
<!-- 路由规则列表(仅在启用时显示) -->
<div
v-if=
"createForm.model_routing_enabled"
class=
"space-y-3"
>
<div
v-if=
"createForm.model_routing_enabled"
class=
"space-y-3"
>
...
@@ -849,18 +1297,27 @@
...
@@ -849,18 +1297,27 @@
<div
class=
"flex items-start gap-3"
>
<div
class=
"flex items-start gap-3"
>
<div
class=
"flex-1 space-y-2"
>
<div
class=
"flex-1 space-y-2"
>
<div>
<div>
<label
class=
"input-label text-xs"
>
{{ t('admin.groups.modelRouting.modelPattern') }}
</label>
<label
class=
"input-label text-xs"
>
{{
t("admin.groups.modelRouting.modelPattern")
}}
</label>
<input
<input
v-model=
"rule.pattern"
v-model=
"rule.pattern"
type=
"text"
type=
"text"
class=
"input text-sm"
class=
"input text-sm"
:placeholder=
"t('admin.groups.modelRouting.modelPatternPlaceholder')"
:placeholder=
"
t('admin.groups.modelRouting.modelPatternPlaceholder')
"
/>
/>
</div>
</div>
<div>
<div>
<label
class=
"input-label text-xs"
>
{{ t('admin.groups.modelRouting.accounts') }}
</label>
<label
class=
"input-label text-xs"
>
{{
t("admin.groups.modelRouting.accounts")
}}
</label>
<!-- 已选账号标签 -->
<!-- 已选账号标签 -->
<div
v-if=
"rule.accounts.length > 0"
class=
"flex flex-wrap gap-1.5 mb-2"
>
<div
v-if=
"rule.accounts.length > 0"
class=
"flex flex-wrap gap-1.5 mb-2"
>
<span
<span
v-for=
"account in rule.accounts"
v-for=
"account in rule.accounts"
:key=
"account.id"
:key=
"account.id"
...
@@ -879,33 +1336,55 @@
...
@@ -879,33 +1336,55 @@
<!-- 账号搜索输入框 -->
<!-- 账号搜索输入框 -->
<div
class=
"relative account-search-container"
>
<div
class=
"relative account-search-container"
>
<input
<input
v-model=
"accountSearchKeyword[getCreateRuleSearchKey(rule)]"
v-model=
"
accountSearchKeyword[getCreateRuleSearchKey(rule)]
"
type=
"text"
type=
"text"
class=
"input text-sm"
class=
"input text-sm"
:placeholder=
"t('admin.groups.modelRouting.searchAccountPlaceholder')"
:placeholder=
"
t(
'admin.groups.modelRouting.searchAccountPlaceholder',
)
"
@
input=
"searchAccountsByRule(rule)"
@
input=
"searchAccountsByRule(rule)"
@
focus=
"onAccountSearchFocus(rule)"
@
focus=
"onAccountSearchFocus(rule)"
/>
/>
<!-- 搜索结果下拉框 -->
<!-- 搜索结果下拉框 -->
<div
<div
v-if=
"showAccountDropdown[getCreateRuleSearchKey(rule)] && accountSearchResults[getCreateRuleSearchKey(rule)]?.length > 0"
v-if=
"
showAccountDropdown[getCreateRuleSearchKey(rule)] &&
accountSearchResults[getCreateRuleSearchKey(rule)]
?.length > 0
"
class=
"absolute z-50 mt-1 max-h-48 w-full overflow-auto rounded-lg border bg-white shadow-lg dark:border-dark-600 dark:bg-dark-800"
class=
"absolute z-50 mt-1 max-h-48 w-full overflow-auto rounded-lg border bg-white shadow-lg dark:border-dark-600 dark:bg-dark-800"
>
>
<button
<button
v-for=
"account in accountSearchResults[getCreateRuleSearchKey(rule)]"
v-for=
"account in accountSearchResults[
getCreateRuleSearchKey(rule)
]"
:key=
"account.id"
:key=
"account.id"
type=
"button"
type=
"button"
@
click=
"selectAccount(rule, account)"
@
click=
"selectAccount(rule, account)"
class=
"w-full px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-dark-700"
class=
"w-full px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-dark-700"
:class=
"{ 'opacity-50': rule.accounts.some(a => a.id === account.id) }"
:class=
"{
:disabled=
"rule.accounts.some(a => a.id === account.id)"
'opacity-50': rule.accounts.some(
(a) => a.id === account.id,
),
}"
:disabled=
"
rule.accounts.some((a) => a.id === account.id)
"
>
>
<span>
{{ account.name }}
</span>
<span>
{{ account.name }}
</span>
<span
class=
"ml-2 text-xs text-gray-400"
>
#{{ account.id }}
</span>
<span
class=
"ml-2 text-xs text-gray-400"
>
#{{ account.id }}
</span
>
</button>
</button>
</div>
</div>
</div>
</div>
<p
class=
"text-xs text-gray-400 mt-1"
>
{{ t('admin.groups.modelRouting.accountsHint') }}
</p>
<p
class=
"text-xs text-gray-400 mt-1"
>
{{ t("admin.groups.modelRouting.accountsHint") }}
</p>
</div>
</div>
</div>
</div>
<button
<button
...
@@ -927,16 +1406,19 @@
...
@@ -927,16 +1406,19 @@
class=
"mt-3 flex items-center gap-1.5 text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
class=
"mt-3 flex items-center gap-1.5 text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
>
>
<Icon
name=
"plus"
size=
"sm"
/>
<Icon
name=
"plus"
size=
"sm"
/>
{{ t(
'
admin.groups.modelRouting.addRule
'
) }}
{{ t(
"
admin.groups.modelRouting.addRule
"
) }}
</button>
</button>
</div>
</div>
</form>
</form>
<
template
#footer
>
<
template
#footer
>
<div
class=
"flex justify-end gap-3 pt-4"
>
<div
class=
"flex justify-end gap-3 pt-4"
>
<button
@
click=
"closeCreateModal"
type=
"button"
class=
"btn btn-secondary"
>
<button
{{
t
(
'
common.cancel
'
)
}}
@
click=
"closeCreateModal"
type=
"button"
class=
"btn btn-secondary"
>
{{
t
(
"
common.cancel
"
)
}}
</button>
</button>
<button
<button
type=
"submit"
type=
"submit"
...
@@ -965,7 +1447,7 @@
...
@@ -965,7 +1447,7 @@
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
></path>
</svg>
</svg>
{{
submitting
?
t
(
'
admin.groups.creating
'
)
:
t
(
'
common.create
'
)
}}
{{
submitting
?
t
(
"
admin.groups.creating
"
)
:
t
(
"
common.create
"
)
}}
</button>
</button>
</div>
</div>
</
template
>
</
template
>
...
@@ -985,7 +1467,7 @@
...
@@ -985,7 +1467,7 @@
class=
"space-y-5"
class=
"space-y-5"
>
>
<div>
<div>
<label
class=
"input-label"
>
{{ t(
'
admin.groups.form.name
'
) }}
</label>
<label
class=
"input-label"
>
{{ t(
"
admin.groups.form.name
"
) }}
</label>
<input
<input
v-model=
"editForm.name"
v-model=
"editForm.name"
type=
"text"
type=
"text"
...
@@ -995,24 +1477,32 @@
...
@@ -995,24 +1477,32 @@
/>
/>
</div>
</div>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.form.description') }}
</label>
<label
class=
"input-label"
>
{{
<textarea
v-model=
"editForm.description"
rows=
"3"
class=
"input"
></textarea>
t("admin.groups.form.description")
}}
</label>
<textarea
v-model=
"editForm.description"
rows=
"3"
class=
"input"
></textarea>
</div>
</div>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.form.platform') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.form.platform")
}}
</label>
<Select
<Select
v-model=
"editForm.platform"
v-model=
"editForm.platform"
:options=
"platformOptions"
:options=
"platformOptions"
:disabled=
"true"
:disabled=
"true"
data-tour=
"group-form-platform"
data-tour=
"group-form-platform"
/>
/>
<p
class=
"input-hint"
>
{{ t(
'
admin.groups.platformNotEditable
'
) }}
</p>
<p
class=
"input-hint"
>
{{ t(
"
admin.groups.platformNotEditable
"
) }}
</p>
</div>
</div>
<!-- 从分组复制账号(编辑时) -->
<!-- 从分组复制账号(编辑时) -->
<div
v-if=
"copyAccountsGroupOptionsForEdit.length > 0"
>
<div
v-if=
"copyAccountsGroupOptionsForEdit.length > 0"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t(
'
admin.groups.copyAccounts.title
'
) }}
{{ t(
"
admin.groups.copyAccounts.title
"
) }}
</label>
</label>
<div
class=
"group relative inline-flex"
>
<div
class=
"group relative inline-flex"
>
<Icon
<Icon
...
@@ -1021,27 +1511,44 @@
...
@@ -1021,27 +1511,44 @@
:stroke-width=
"2"
:stroke-width=
"2"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
/>
<div
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
{{ t(
'
admin.groups.copyAccounts.tooltipEdit
'
) }}
{{ t(
"
admin.groups.copyAccounts.tooltipEdit
"
) }}
</p>
</p>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 已选分组标签 -->
<!-- 已选分组标签 -->
<div
v-if=
"editForm.copy_accounts_from_group_ids.length > 0"
class=
"flex flex-wrap gap-1.5 mb-2"
>
<div
v-if=
"editForm.copy_accounts_from_group_ids.length > 0"
class=
"flex flex-wrap gap-1.5 mb-2"
>
<span
<span
v-for=
"groupId in editForm.copy_accounts_from_group_ids"
v-for=
"groupId in editForm.copy_accounts_from_group_ids"
:key=
"groupId"
:key=
"groupId"
class=
"inline-flex items-center gap-1 rounded-full bg-primary-100 px-2.5 py-1 text-xs font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
class=
"inline-flex items-center gap-1 rounded-full bg-primary-100 px-2.5 py-1 text-xs font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
>
>
{{ copyAccountsGroupOptionsForEdit.find(o => o.value === groupId)?.label || `#${groupId}` }}
{{
copyAccountsGroupOptionsForEdit.find((o) => o.value === groupId)
?.label || `#${groupId}`
}}
<button
<button
type=
"button"
type=
"button"
@
click=
"editForm.copy_accounts_from_group_ids = editForm.copy_accounts_from_group_ids.filter(id => id !== groupId)"
@
click=
"
editForm.copy_accounts_from_group_ids =
editForm.copy_accounts_from_group_ids.filter(
(id) => id !== groupId,
)
"
class=
"ml-0.5 text-primary-500 hover:text-primary-700 dark:hover:text-primary-200"
class=
"ml-0.5 text-primary-500 hover:text-primary-700 dark:hover:text-primary-200"
>
>
<Icon
name=
"x"
size=
"xs"
/>
<Icon
name=
"x"
size=
"xs"
/>
...
@@ -1051,28 +1558,41 @@
...
@@ -1051,28 +1558,41 @@
<!-- 分组选择下拉 -->
<!-- 分组选择下拉 -->
<select
<select
class=
"input"
class=
"input"
@
change=
"(e) => {
@
change=
"
const val = Number((e.target as HTMLSelectElement).value)
(e) => {
if (val && !editForm.copy_accounts_from_group_ids.includes(val)) {
const val = Number((e.target as HTMLSelectElement).value);
editForm.copy_accounts_from_group_ids.push(val)
if (
val &&
!editForm.copy_accounts_from_group_ids.includes(val)
) {
editForm.copy_accounts_from_group_ids.push(val);
}
(e.target as HTMLSelectElement).value = '';
}
}
(e.target as HTMLSelectElement).value = ''
"
}"
>
>
<option
value=
""
>
{{ t('admin.groups.copyAccounts.selectPlaceholder') }}
</option>
<option
value=
""
>
{{ t("admin.groups.copyAccounts.selectPlaceholder") }}
</option>
<option
<option
v-for=
"opt in copyAccountsGroupOptionsForEdit"
v-for=
"opt in copyAccountsGroupOptionsForEdit"
:key=
"opt.value"
:key=
"opt.value"
:value=
"opt.value"
:value=
"opt.value"
:disabled=
"editForm.copy_accounts_from_group_ids.includes(opt.value)"
:disabled=
"
editForm.copy_accounts_from_group_ids.includes(opt.value)
"
>
>
{{ opt.label }}
{{ opt.label }}
</option>
</option>
</select>
</select>
<p
class=
"input-hint"
>
{{ t('admin.groups.copyAccounts.hintEdit') }}
</p>
<p
class=
"input-hint"
>
{{ t("admin.groups.copyAccounts.hintEdit") }}
</p>
</div>
</div>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.form.rateMultiplier') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.form.rateMultiplier")
}}
</label>
<input
<input
v-model.number=
"editForm.rate_multiplier"
v-model.number=
"editForm.rate_multiplier"
type=
"number"
type=
"number"
...
@@ -1086,7 +1606,7 @@
...
@@ -1086,7 +1606,7 @@
<div
v-if=
"editForm.subscription_type !== 'subscription'"
>
<div
v-if=
"editForm.subscription_type !== 'subscription'"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t(
'
admin.groups.form.exclusive
'
) }}
{{ t(
"
admin.groups.form.exclusive
"
) }}
</label>
</label>
<!-- Help Tooltip -->
<!-- Help Tooltip -->
<div
class=
"group relative inline-flex"
>
<div
class=
"group relative inline-flex"
>
...
@@ -1097,20 +1617,32 @@
...
@@ -1097,20 +1617,32 @@
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
/>
<!-- Tooltip Popover -->
<!-- Tooltip Popover -->
<div
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
<p
class=
"mb-2 text-xs font-medium"
>
{{ t('admin.groups.exclusiveTooltip.title') }}
</p>
>
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
<p
class=
"mb-2 text-xs font-medium"
>
{{ t("admin.groups.exclusiveTooltip.title") }}
</p>
<p
class=
"mb-2 text-xs leading-relaxed text-gray-300"
>
<p
class=
"mb-2 text-xs leading-relaxed text-gray-300"
>
{{ t(
'
admin.groups.exclusiveTooltip.description
'
) }}
{{ t(
"
admin.groups.exclusiveTooltip.description
"
) }}
</p>
</p>
<div
class=
"rounded bg-gray-800 p-2 dark:bg-gray-700"
>
<div
class=
"rounded bg-gray-800 p-2 dark:bg-gray-700"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<span
class=
"inline-flex items-center gap-1 text-primary-400"
><Icon
name=
"lightbulb"
size=
"xs"
/>
{{ t('admin.groups.exclusiveTooltip.example') }}
</span>
<span
{{ t('admin.groups.exclusiveTooltip.exampleContent') }}
class=
"inline-flex items-center gap-1 text-primary-400"
><Icon
name=
"lightbulb"
size=
"xs"
/>
{{ t("admin.groups.exclusiveTooltip.example") }}
</span
>
{{ t("admin.groups.exclusiveTooltip.exampleContent") }}
</p>
</p>
</div>
</div>
<!-- Arrow -->
<!-- Arrow -->
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -1121,36 +1653,46 @@
...
@@ -1121,36 +1653,46 @@
@
click=
"editForm.is_exclusive = !editForm.is_exclusive"
@
click=
"editForm.is_exclusive = !editForm.is_exclusive"
:class=
"[
:class=
"[
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
editForm.is_exclusive ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
editForm.is_exclusive
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600',
]"
]"
>
>
<span
<span
:class=
"[
:class=
"[
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
editForm.is_exclusive ? 'translate-x-6' : 'translate-x-1'
editForm.is_exclusive ? 'translate-x-6' : 'translate-x-1'
,
]"
]"
/>
/>
</button>
</button>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{ editForm.is_exclusive ? t('admin.groups.exclusive') : t('admin.groups.public') }}
{{
editForm.is_exclusive
? t("admin.groups.exclusive")
: t("admin.groups.public")
}}
</span>
</span>
</div>
</div>
</div>
</div>
<div>
<div>
<label
class=
"input-label"
>
{{ t(
'
admin.groups.form.status
'
) }}
</label>
<label
class=
"input-label"
>
{{ t(
"
admin.groups.form.status
"
) }}
</label>
<Select
v-model=
"editForm.status"
:options=
"editStatusOptions"
/>
<Select
v-model=
"editForm.status"
:options=
"editStatusOptions"
/>
</div>
</div>
<!-- Subscription Configuration -->
<!-- Subscription Configuration -->
<div
class=
"mt-4 border-t pt-4"
>
<div
class=
"mt-4 border-t pt-4"
>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.subscription.type') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.subscription.type")
}}
</label>
<Select
<Select
v-model=
"editForm.subscription_type"
v-model=
"editForm.subscription_type"
:options=
"subscriptionTypeOptions"
:options=
"subscriptionTypeOptions"
:disabled=
"true"
:disabled=
"true"
/>
/>
<p
class=
"input-hint"
>
{{ t('admin.groups.subscription.typeNotEditable') }}
</p>
<p
class=
"input-hint"
>
{{ t("admin.groups.subscription.typeNotEditable") }}
</p>
</div>
</div>
<!-- Subscription limits (only show when subscription type is selected) -->
<!-- Subscription limits (only show when subscription type is selected) -->
...
@@ -1159,7 +1701,9 @@
...
@@ -1159,7 +1701,9 @@
class=
"space-y-4 border-l-2 border-primary-200 pl-4 dark:border-primary-800"
class=
"space-y-4 border-l-2 border-primary-200 pl-4 dark:border-primary-800"
>
>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.subscription.dailyLimit') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.subscription.dailyLimit")
}}
</label>
<input
<input
v-model.number=
"editForm.daily_limit_usd"
v-model.number=
"editForm.daily_limit_usd"
type=
"number"
type=
"number"
...
@@ -1170,7 +1714,9 @@
...
@@ -1170,7 +1714,9 @@
/>
/>
</div>
</div>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.subscription.weeklyLimit') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.subscription.weeklyLimit")
}}
</label>
<input
<input
v-model.number=
"editForm.weekly_limit_usd"
v-model.number=
"editForm.weekly_limit_usd"
type=
"number"
type=
"number"
...
@@ -1181,7 +1727,9 @@
...
@@ -1181,7 +1727,9 @@
/>
/>
</div>
</div>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.groups.subscription.monthlyLimit') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.subscription.monthlyLimit")
}}
</label>
<input
<input
v-model.number=
"editForm.monthly_limit_usd"
v-model.number=
"editForm.monthly_limit_usd"
type=
"number"
type=
"number"
...
@@ -1195,12 +1743,20 @@
...
@@ -1195,12 +1743,20 @@
</div>
</div>
<!-- 图片生成计费配置(antigravity 和 gemini 平台) -->
<!-- 图片生成计费配置(antigravity 和 gemini 平台) -->
<div
v-if=
"editForm.platform === 'antigravity' || editForm.platform === 'gemini'"
class=
"border-t pt-4"
>
<div
<label
class=
"block mb-2 font-medium text-gray-700 dark:text-gray-300"
>
v-if=
"
{{ t('admin.groups.imagePricing.title') }}
editForm.platform === 'antigravity' ||
editForm.platform === 'gemini'
"
class=
"border-t pt-4"
>
<label
class=
"block mb-2 font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.groups.imagePricing.title") }}
</label>
</label>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mb-3"
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mb-3"
>
{{ t(
'
admin.groups.imagePricing.description
'
) }}
{{ t(
"
admin.groups.imagePricing.description
"
) }}
</p>
</p>
<div
class=
"grid grid-cols-3 gap-3"
>
<div
class=
"grid grid-cols-3 gap-3"
>
<div>
<div>
...
@@ -1239,13 +1795,11 @@
...
@@ -1239,13 +1795,11 @@
</div>
</div>
</div>
</div>
<!-- 支持的模型系列(仅 antigravity 平台) -->
<!-- 支持的模型系列(仅 antigravity 平台) -->
<div
v-if=
"editForm.platform === 'antigravity'"
class=
"border-t pt-4"
>
<div
v-if=
"editForm.platform === 'antigravity'"
class=
"border-t pt-4"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t(
'
admin.groups.supportedScopes.title
'
) }}
{{ t(
"
admin.groups.supportedScopes.title
"
) }}
</label>
</label>
<!-- Help Tooltip -->
<!-- Help Tooltip -->
<div
class=
"group relative inline-flex"
>
<div
class=
"group relative inline-flex"
>
...
@@ -1255,12 +1809,18 @@
...
@@ -1255,12 +1809,18 @@
:stroke-width=
"2"
:stroke-width=
"2"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
/>
<div
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
{{ t(
'
admin.groups.supportedScopes.tooltip
'
) }}
{{ t(
"
admin.groups.supportedScopes.tooltip
"
) }}
</p>
</p>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -1273,35 +1833,47 @@
...
@@ -1273,35 +1833,47 @@
@
change=
"toggleEditScope('claude')"
@
change=
"toggleEditScope('claude')"
class=
"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
class=
"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
/>
/>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{ t('admin.groups.supportedScopes.claude') }}
</span>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{
t("admin.groups.supportedScopes.claude")
}}
</span>
</label>
</label>
<label
class=
"flex items-center gap-2 cursor-pointer"
>
<label
class=
"flex items-center gap-2 cursor-pointer"
>
<input
<input
type=
"checkbox"
type=
"checkbox"
:checked=
"editForm.supported_model_scopes.includes('gemini_text')"
:checked=
"
editForm.supported_model_scopes.includes('gemini_text')
"
@
change=
"toggleEditScope('gemini_text')"
@
change=
"toggleEditScope('gemini_text')"
class=
"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
class=
"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
/>
/>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{ t('admin.groups.supportedScopes.geminiText') }}
</span>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{
t("admin.groups.supportedScopes.geminiText")
}}
</span>
</label>
</label>
<label
class=
"flex items-center gap-2 cursor-pointer"
>
<label
class=
"flex items-center gap-2 cursor-pointer"
>
<input
<input
type=
"checkbox"
type=
"checkbox"
:checked=
"editForm.supported_model_scopes.includes('gemini_image')"
:checked=
"
editForm.supported_model_scopes.includes('gemini_image')
"
@
change=
"toggleEditScope('gemini_image')"
@
change=
"toggleEditScope('gemini_image')"
class=
"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
class=
"h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-600 dark:bg-dark-700"
/>
/>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{ t('admin.groups.supportedScopes.geminiImage') }}
</span>
<span
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{
t("admin.groups.supportedScopes.geminiImage")
}}
</span>
</label>
</label>
</div>
</div>
<p
class=
"mt-2 text-xs text-gray-500 dark:text-gray-400"
>
{{ t('admin.groups.supportedScopes.hint') }}
</p>
<p
class=
"mt-2 text-xs text-gray-500 dark:text-gray-400"
>
{{ t("admin.groups.supportedScopes.hint") }}
</p>
</div>
</div>
<!-- MCP XML 协议注入(仅 antigravity 平台) -->
<!-- MCP XML 协议注入(仅 antigravity 平台) -->
<div
v-if=
"editForm.platform === 'antigravity'"
class=
"border-t pt-4"
>
<div
v-if=
"editForm.platform === 'antigravity'"
class=
"border-t pt-4"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t(
'
admin.groups.mcpXml.title
'
) }}
{{ t(
"
admin.groups.mcpXml.title
"
) }}
</label>
</label>
<div
class=
"group relative inline-flex"
>
<div
class=
"group relative inline-flex"
>
<Icon
<Icon
...
@@ -1310,12 +1882,18 @@
...
@@ -1310,12 +1882,18 @@
:stroke-width=
"2"
:stroke-width=
"2"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
/>
<div
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
{{ t(
'
admin.groups.mcpXml.tooltip
'
) }}
{{ t(
"
admin.groups.mcpXml.tooltip
"
) }}
</p>
</p>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -1326,18 +1904,24 @@
...
@@ -1326,18 +1904,24 @@
@
click=
"editForm.mcp_xml_inject = !editForm.mcp_xml_inject"
@
click=
"editForm.mcp_xml_inject = !editForm.mcp_xml_inject"
:class=
"[
:class=
"[
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
editForm.mcp_xml_inject ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
editForm.mcp_xml_inject
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600',
]"
]"
>
>
<span
<span
:class=
"[
:class=
"[
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
editForm.mcp_xml_inject ? 'translate-x-6' : 'translate-x-1'
editForm.mcp_xml_inject ? 'translate-x-6' : 'translate-x-1'
,
]"
]"
/>
/>
</button>
</button>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{ editForm.mcp_xml_inject ? t('admin.groups.mcpXml.enabled') : t('admin.groups.mcpXml.disabled') }}
{{
editForm.mcp_xml_inject
? t("admin.groups.mcpXml.enabled")
: t("admin.groups.mcpXml.disabled")
}}
</span>
</span>
</div>
</div>
</div>
</div>
...
@@ -1346,7 +1930,7 @@
...
@@ -1346,7 +1930,7 @@
<div
v-if=
"editForm.platform === 'anthropic'"
class=
"border-t pt-4"
>
<div
v-if=
"editForm.platform === 'anthropic'"
class=
"border-t pt-4"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t(
'
admin.groups.claudeCode.title
'
) }}
{{ t(
"
admin.groups.claudeCode.title
"
) }}
</label>
</label>
<!-- Help Tooltip -->
<!-- Help Tooltip -->
<div
class=
"group relative inline-flex"
>
<div
class=
"group relative inline-flex"
>
...
@@ -1356,12 +1940,18 @@
...
@@ -1356,12 +1940,18 @@
:stroke-width=
"2"
:stroke-width=
"2"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
/>
<div
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
{{ t(
'
admin.groups.claudeCode.tooltip
'
) }}
{{ t(
"
admin.groups.claudeCode.tooltip
"
) }}
</p>
</p>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -1372,94 +1962,314 @@
...
@@ -1372,94 +1962,314 @@
@
click=
"editForm.claude_code_only = !editForm.claude_code_only"
@
click=
"editForm.claude_code_only = !editForm.claude_code_only"
:class=
"[
:class=
"[
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
editForm.claude_code_only ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
editForm.claude_code_only
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600',
]"
]"
>
>
<span
<span
:class=
"[
:class=
"[
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
editForm.claude_code_only ? 'translate-x-6' : 'translate-x-1'
editForm.claude_code_only ? 'translate-x-6' : 'translate-x-1'
,
]"
]"
/>
/>
</button>
</button>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{ editForm.claude_code_only ? t('admin.groups.claudeCode.enabled') : t('admin.groups.claudeCode.disabled') }}
{{
editForm.claude_code_only
? t("admin.groups.claudeCode.enabled")
: t("admin.groups.claudeCode.disabled")
}}
</span>
</span>
</div>
</div>
<!-- 降级分组选择(仅当启用 claude_code_only 时显示) -->
<!-- 降级分组选择(仅当启用 claude_code_only 时显示) -->
<div
v-if=
"editForm.claude_code_only"
class=
"mt-3"
>
<div
v-if=
"editForm.claude_code_only"
class=
"mt-3"
>
<label
class=
"input-label"
>
{{ t('admin.groups.claudeCode.fallbackGroup') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.claudeCode.fallbackGroup")
}}
</label>
<Select
<Select
v-model=
"editForm.fallback_group_id"
v-model=
"editForm.fallback_group_id"
:options=
"fallbackGroupOptionsForEdit"
:options=
"fallbackGroupOptionsForEdit"
:placeholder=
"t('admin.groups.claudeCode.noFallback')"
:placeholder=
"t('admin.groups.claudeCode.noFallback')"
/>
/>
<p
class=
"input-hint"
>
{{ t('admin.groups.claudeCode.fallbackHint') }}
</p>
<p
class=
"input-hint"
>
{{ t("admin.groups.claudeCode.fallbackHint") }}
</p>
</div>
</div>
</div>
</div>
<!-- OpenAI Messages 调度配置(仅 openai 平台) -->
<!-- OpenAI Messages 调度配置(仅 openai 平台) -->
<div
v-if=
"editForm.platform === 'openai'"
class=
"border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
>
<div
<h4
class=
"text-sm font-medium text-gray-700 dark:text-gray-300 mb-3"
>
{{ t('admin.groups.openaiMessages.title') }}
</h4>
v-if=
"editForm.platform === 'openai'"
class=
"border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
>
<h4
class=
"text-sm font-medium text-gray-700 dark:text-gray-300 mb-3"
>
{{ t("admin.groups.openaiMessages.title") }}
</h4>
<!-- 允许 Messages 调度开关 -->
<!-- 允许 Messages 调度开关 -->
<div
class=
"flex items-center justify-between"
>
<div
class=
"flex items-center justify-between"
>
<label
class=
"text-sm text-gray-600 dark:text-gray-400"
>
{{ t('admin.groups.openaiMessages.allowDispatch') }}
</label>
<label
class=
"text-sm text-gray-600 dark:text-gray-400"
>
{{
t("admin.groups.openaiMessages.allowDispatch")
}}
</label>
<button
<button
type=
"button"
type=
"button"
@
click=
"editForm.allow_messages_dispatch = !editForm.allow_messages_dispatch"
@
click=
"
editForm.allow_messages_dispatch =
!editForm.allow_messages_dispatch
"
class=
"relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
class=
"relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
:class=
"
:class=
"
editForm.allow_messages_dispatch ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
editForm.allow_messages_dispatch
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600'
"
"
>
>
<span
<span
class=
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
class=
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class=
"
:class=
"
editForm.allow_messages_dispatch ? 'translate-x-6' : 'translate-x-1'
editForm.allow_messages_dispatch
? 'translate-x-6'
: 'translate-x-1'
"
"
/>
/>
</button>
</button>
</div>
</div>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mt-1"
>
{{ t('admin.groups.openaiMessages.allowDispatchHint') }}
</p>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mt-1"
>
{{ t("admin.groups.openaiMessages.allowDispatchHint") }}
</p>
<!-- 默认映射模型(仅当开关打开时显示) -->
<div
v-if=
"editForm.allow_messages_dispatch"
class=
"mt-3"
>
<div
v-if=
"editForm.allow_messages_dispatch"
class=
"mt-3"
>
<label
class=
"input-label"
>
{{ t('admin.groups.openaiMessages.defaultModel') }}
</label>
<div
<input
class=
"relative overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-dark-600 dark:bg-dark-800"
v-model=
"editForm.default_mapped_model"
>
type=
"text"
<div
:placeholder=
"t('admin.groups.openaiMessages.defaultModelPlaceholder')"
class=
"border-b border-gray-100 bg-gray-50/80 px-4 py-3 dark:border-dark-700 dark:bg-dark-700/50"
class=
"input"
>
/>
<div
class=
"flex items-center gap-2"
>
<p
class=
"input-hint"
>
{{ t('admin.groups.openaiMessages.defaultModelHint') }}
</p>
<div
class=
"h-2 w-2 rounded-full bg-blue-500"
></div>
<label
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
{{
t("admin.groups.openaiMessages.familyMappingTitle")
}}
</label
>
</div>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
{{ t("admin.groups.openaiMessages.familyMappingHint") }}
</p>
</div>
<div
class=
"p-4"
>
<div
class=
"grid gap-4 md:grid-cols-3"
>
<div>
<label
class=
"input-label"
>
{{
t("admin.groups.openaiMessages.opusModel")
}}
</label>
<input
v-model=
"editForm.opus_mapped_model"
type=
"text"
:placeholder=
"
t('admin.groups.openaiMessages.opusModelPlaceholder')
"
class=
"input"
/>
</div>
<div>
<label
class=
"input-label"
>
{{
t("admin.groups.openaiMessages.sonnetModel")
}}
</label>
<input
v-model=
"editForm.sonnet_mapped_model"
type=
"text"
:placeholder=
"
t('admin.groups.openaiMessages.sonnetModelPlaceholder')
"
class=
"input"
/>
</div>
<div>
<label
class=
"input-label"
>
{{
t("admin.groups.openaiMessages.haikuModel")
}}
</label>
<input
v-model=
"editForm.haiku_mapped_model"
type=
"text"
:placeholder=
"
t('admin.groups.openaiMessages.haikuModelPlaceholder')
"
class=
"input"
/>
</div>
</div>
</div>
</div>
<div
class=
"mt-5 relative overflow-hidden rounded-xl border border-primary-200 bg-white shadow-sm dark:border-primary-900/50 dark:bg-dark-800"
>
<div
class=
"border-b border-primary-100 bg-primary-50/80 px-4 py-3 dark:border-primary-900/40 dark:bg-primary-900/20"
>
<div
class=
"flex items-start justify-between gap-3"
>
<div>
<div
class=
"flex items-center gap-2"
>
<div
class=
"h-2 w-2 rounded-full bg-primary-500"
></div>
<label
class=
"text-sm font-medium text-primary-900 dark:text-primary-100"
>
{{
t("admin.groups.openaiMessages.exactMappingTitle")
}}
</label
>
</div>
<p
class=
"mt-1 text-xs text-primary-600/90 dark:text-primary-400/90"
>
{{ t("admin.groups.openaiMessages.exactMappingHint") }}
</p>
</div>
</div>
</div>
<div
class=
"p-4 bg-gray-50/30 dark:bg-dark-800/30"
>
<div
v-if=
"editForm.exact_model_mappings.length === 0"
class=
"flex items-center justify-between gap-3 rounded-xl border-2 border-dashed border-primary-200 bg-white px-5 py-4 text-sm text-primary-700 transition-colors hover:border-primary-300 dark:border-primary-900/40 dark:bg-dark-800 dark:text-primary-300 dark:hover:border-primary-800"
>
<span>
{{
t("admin.groups.openaiMessages.noExactMappings")
}}
</span>
<button
type=
"button"
@
click=
"addEditMessagesDispatchMapping"
class=
"flex items-center gap-1.5 text-sm font-medium text-primary-600 transition-colors hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
>
<Icon
name=
"plus"
size=
"sm"
/>
{{ t("admin.groups.openaiMessages.addExactMapping") }}
</button>
</div>
<div
v-else
class=
"space-y-3"
>
<div
v-for=
"row in editForm.exact_model_mappings"
:key=
"getEditMessagesDispatchRowKey(row)"
class=
"group relative rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-all hover:border-primary-300 hover:shadow-md dark:border-dark-600 dark:bg-dark-700 dark:hover:border-primary-700"
>
<div
class=
"flex items-center gap-4"
>
<div
class=
"grid flex-1 gap-4 md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] md:items-start"
>
<div>
<label
class=
"input-label"
>
{{
t("admin.groups.openaiMessages.claudeModel")
}}
</label>
<input
v-model=
"row.claude_model"
type=
"text"
:placeholder=
"
t(
'admin.groups.openaiMessages.claudeModelPlaceholder',
)
"
class=
"input bg-gray-50 focus:bg-white dark:bg-dark-800 dark:focus:bg-dark-900"
/>
</div>
<div
class=
"hidden md:flex md:justify-center md:pt-7 text-primary-300 dark:text-primary-700"
>
<Icon
name=
"arrowRight"
size=
"sm"
class=
"transition-transform group-hover:translate-x-1"
/>
</div>
<div>
<label
class=
"input-label"
>
{{
t("admin.groups.openaiMessages.targetModel")
}}
</label>
<input
v-model=
"row.target_model"
type=
"text"
:placeholder=
"
t(
'admin.groups.openaiMessages.targetModelPlaceholder',
)
"
class=
"input bg-gray-50 focus:bg-white dark:bg-dark-800 dark:focus:bg-dark-900"
/>
</div>
</div>
<button
type=
"button"
@
click=
"removeEditMessagesDispatchMapping(row)"
class=
"mt-6 flex h-9 w-9 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-900/20 dark:hover:text-red-400"
:title=
"
t('admin.groups.openaiMessages.removeExactMapping')
"
>
<Icon
name=
"trash"
size=
"sm"
/>
</button>
</div>
</div>
<button
type=
"button"
@
click=
"addEditMessagesDispatchMapping"
class=
"flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed border-gray-300 bg-white py-3 text-sm font-medium text-gray-500 transition-all hover:border-primary-300 hover:bg-primary-50/50 hover:text-primary-600 dark:border-dark-600 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-primary-800 dark:hover:bg-primary-900/20 dark:hover:text-primary-400"
>
<Icon
name=
"plus"
size=
"sm"
/>
{{ t("admin.groups.openaiMessages.addExactMapping") }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 账号过滤控制 (OpenAI/Antigravity/Anthropic/Gemini) -->
<!-- 账号过滤控制 (OpenAI/Antigravity/Anthropic/Gemini) -->
<div
v-if=
"['openai', 'antigravity', 'anthropic', 'gemini'].includes(editForm.platform)"
class=
"border-t border-gray-200 dark:border-dark-400 pt-4 mt-4 space-y-4"
>
<div
<h4
class=
"text-sm font-medium text-gray-700 dark:text-gray-300 mb-3"
>
账号过滤控制
</h4>
v-if=
"
['openai', 'antigravity', 'anthropic', 'gemini'].includes(
editForm.platform,
)
"
class=
"border-t border-gray-200 dark:border-dark-400 pt-4 mt-4 space-y-4"
>
<h4
class=
"text-sm font-medium text-gray-700 dark:text-gray-300 mb-3"
>
账号过滤控制
</h4>
<!-- require_oauth_only toggle -->
<!-- require_oauth_only toggle -->
<div
class=
"flex items-center justify-between"
>
<div
class=
"flex items-center justify-between"
>
<div>
<div>
<label
class=
"text-sm text-gray-600 dark:text-gray-400"
>
仅允许 OAuth 账号
</label>
<label
class=
"text-sm text-gray-600 dark:text-gray-400"
>
仅允许 OAuth 账号
</label
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mt-0.5"
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mt-0.5"
>
{{ editForm.require_oauth_only ? '已启用 — 排除 API Key 类型账号' : '未启用' }}
{{
editForm.require_oauth_only
? "已启用 — 排除 API Key 类型账号"
: "未启用"
}}
</p>
</p>
</div>
</div>
<button
<button
type=
"button"
type=
"button"
@
click=
"editForm.require_oauth_only = !editForm.require_oauth_only"
@
click=
"
editForm.require_oauth_only = !editForm.require_oauth_only
"
class=
"relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
class=
"relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
:class=
"
:class=
"
editForm.require_oauth_only ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
editForm.require_oauth_only
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600'
"
"
>
>
<span
<span
class=
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
class=
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class=
"
:class=
"
editForm.require_oauth_only ? 'translate-x-6' : 'translate-x-1'
editForm.require_oauth_only
? 'translate-x-6'
: 'translate-x-1'
"
"
/>
/>
</button>
</button>
...
@@ -1468,23 +2278,35 @@
...
@@ -1468,23 +2278,35 @@
<!-- require_privacy_set toggle -->
<!-- require_privacy_set toggle -->
<div
class=
"flex items-center justify-between"
>
<div
class=
"flex items-center justify-between"
>
<div>
<div>
<label
class=
"text-sm text-gray-600 dark:text-gray-400"
>
仅允许隐私保护已设置的账号
</label>
<label
class=
"text-sm text-gray-600 dark:text-gray-400"
>
仅允许隐私保护已设置的账号
</label
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mt-0.5"
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400 mt-0.5"
>
{{ editForm.require_privacy_set ? '已启用 — Privacy 未设置的账号将被排除' : '未启用' }}
{{
editForm.require_privacy_set
? "已启用 — Privacy 未设置的账号将被排除"
: "未启用"
}}
</p>
</p>
</div>
</div>
<button
<button
type=
"button"
type=
"button"
@
click=
"editForm.require_privacy_set = !editForm.require_privacy_set"
@
click=
"
editForm.require_privacy_set = !editForm.require_privacy_set
"
class=
"relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
class=
"relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
:class=
"
:class=
"
editForm.require_privacy_set ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
editForm.require_privacy_set
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600'
"
"
>
>
<span
<span
class=
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
class=
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class=
"
:class=
"
editForm.require_privacy_set ? 'translate-x-6' : 'translate-x-1'
editForm.require_privacy_set
? 'translate-x-6'
: 'translate-x-1'
"
"
/>
/>
</button>
</button>
...
@@ -1493,23 +2315,30 @@
...
@@ -1493,23 +2315,30 @@
<!-- 无效请求兜底(仅 anthropic/antigravity 平台,且非订阅分组) -->
<!-- 无效请求兜底(仅 anthropic/antigravity 平台,且非订阅分组) -->
<div
<div
v-if=
"['anthropic', 'antigravity'].includes(editForm.platform) && editForm.subscription_type !== 'subscription'"
v-if=
"
['anthropic', 'antigravity'].includes(editForm.platform) &&
editForm.subscription_type !== 'subscription'
"
class=
"border-t pt-4"
class=
"border-t pt-4"
>
>
<label
class=
"input-label"
>
{{ t('admin.groups.invalidRequestFallback.title') }}
</label>
<label
class=
"input-label"
>
{{
t("admin.groups.invalidRequestFallback.title")
}}
</label>
<Select
<Select
v-model=
"editForm.fallback_group_id_on_invalid_request"
v-model=
"editForm.fallback_group_id_on_invalid_request"
:options=
"invalidRequestFallbackOptionsForEdit"
:options=
"invalidRequestFallbackOptionsForEdit"
:placeholder=
"t('admin.groups.invalidRequestFallback.noFallback')"
:placeholder=
"t('admin.groups.invalidRequestFallback.noFallback')"
/>
/>
<p
class=
"input-hint"
>
{{ t('admin.groups.invalidRequestFallback.hint') }}
</p>
<p
class=
"input-hint"
>
{{ t("admin.groups.invalidRequestFallback.hint") }}
</p>
</div>
</div>
<!-- 模型路由配置(仅 anthropic 平台) -->
<!-- 模型路由配置(仅 anthropic 平台) -->
<div
v-if=
"editForm.platform === 'anthropic'"
class=
"border-t pt-4"
>
<div
v-if=
"editForm.platform === 'anthropic'"
class=
"border-t pt-4"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<div
class=
"mb-1.5 flex items-center gap-1"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t(
'
admin.groups.modelRouting.title
'
) }}
{{ t(
"
admin.groups.modelRouting.title
"
) }}
</label>
</label>
<!-- Help Tooltip -->
<!-- Help Tooltip -->
<div
class=
"group relative inline-flex"
>
<div
class=
"group relative inline-flex"
>
...
@@ -1519,12 +2348,18 @@
...
@@ -1519,12 +2348,18 @@
:stroke-width=
"2"
:stroke-width=
"2"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
class=
"cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
/>
/>
<div
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-80 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
class=
"pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-80 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100"
>
<div
class=
"rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
<p
class=
"text-xs leading-relaxed text-gray-300"
>
{{ t(
'
admin.groups.modelRouting.tooltip
'
) }}
{{ t(
"
admin.groups.modelRouting.tooltip
"
) }}
</p>
</p>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
<div
class=
"absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -1533,28 +2368,41 @@
...
@@ -1533,28 +2368,41 @@
<div
class=
"flex items-center gap-3 mb-3"
>
<div
class=
"flex items-center gap-3 mb-3"
>
<button
<button
type=
"button"
type=
"button"
@
click=
"editForm.model_routing_enabled = !editForm.model_routing_enabled"
@
click=
"
editForm.model_routing_enabled = !editForm.model_routing_enabled
"
:class=
"[
:class=
"[
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
editForm.model_routing_enabled ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
editForm.model_routing_enabled
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600',
]"
]"
>
>
<span
<span
:class=
"[
:class=
"[
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
editForm.model_routing_enabled ? 'translate-x-6' : 'translate-x-1'
editForm.model_routing_enabled
? 'translate-x-6'
: 'translate-x-1',
]"
]"
/>
/>
</button>
</button>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
<span
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{ editForm.model_routing_enabled ? t('admin.groups.modelRouting.enabled') : t('admin.groups.modelRouting.disabled') }}
{{
editForm.model_routing_enabled
? t("admin.groups.modelRouting.enabled")
: t("admin.groups.modelRouting.disabled")
}}
</span>
</span>
</div>
</div>
<p
v-if=
"!editForm.model_routing_enabled"
class=
"text-xs text-gray-500 dark:text-gray-400 mb-3"
>
<p
{{ t('admin.groups.modelRouting.disabledHint') }}
v-if=
"!editForm.model_routing_enabled"
class=
"text-xs text-gray-500 dark:text-gray-400 mb-3"
>
{{ t("admin.groups.modelRouting.disabledHint") }}
</p>
</p>
<p
v-else
class=
"text-xs text-gray-500 dark:text-gray-400 mb-3"
>
<p
v-else
class=
"text-xs text-gray-500 dark:text-gray-400 mb-3"
>
{{ t(
'
admin.groups.modelRouting.noRulesHint
'
) }}
{{ t(
"
admin.groups.modelRouting.noRulesHint
"
) }}
</p>
</p>
<!-- 路由规则列表(仅在启用时显示) -->
<!-- 路由规则列表(仅在启用时显示) -->
<div
v-if=
"editForm.model_routing_enabled"
class=
"space-y-3"
>
<div
v-if=
"editForm.model_routing_enabled"
class=
"space-y-3"
>
...
@@ -1566,18 +2414,27 @@
...
@@ -1566,18 +2414,27 @@
<div
class=
"flex items-start gap-3"
>
<div
class=
"flex items-start gap-3"
>
<div
class=
"flex-1 space-y-2"
>
<div
class=
"flex-1 space-y-2"
>
<div>
<div>
<label
class=
"input-label text-xs"
>
{{ t('admin.groups.modelRouting.modelPattern') }}
</label>
<label
class=
"input-label text-xs"
>
{{
t("admin.groups.modelRouting.modelPattern")
}}
</label>
<input
<input
v-model=
"rule.pattern"
v-model=
"rule.pattern"
type=
"text"
type=
"text"
class=
"input text-sm"
class=
"input text-sm"
:placeholder=
"t('admin.groups.modelRouting.modelPatternPlaceholder')"
:placeholder=
"
t('admin.groups.modelRouting.modelPatternPlaceholder')
"
/>
/>
</div>
</div>
<div>
<div>
<label
class=
"input-label text-xs"
>
{{ t('admin.groups.modelRouting.accounts') }}
</label>
<label
class=
"input-label text-xs"
>
{{
t("admin.groups.modelRouting.accounts")
}}
</label>
<!-- 已选账号标签 -->
<!-- 已选账号标签 -->
<div
v-if=
"rule.accounts.length > 0"
class=
"flex flex-wrap gap-1.5 mb-2"
>
<div
v-if=
"rule.accounts.length > 0"
class=
"flex flex-wrap gap-1.5 mb-2"
>
<span
<span
v-for=
"account in rule.accounts"
v-for=
"account in rule.accounts"
:key=
"account.id"
:key=
"account.id"
...
@@ -1596,33 +2453,55 @@
...
@@ -1596,33 +2453,55 @@
<!-- 账号搜索输入框 -->
<!-- 账号搜索输入框 -->
<div
class=
"relative account-search-container"
>
<div
class=
"relative account-search-container"
>
<input
<input
v-model=
"accountSearchKeyword[getEditRuleSearchKey(rule)]"
v-model=
"
accountSearchKeyword[getEditRuleSearchKey(rule)]
"
type=
"text"
type=
"text"
class=
"input text-sm"
class=
"input text-sm"
:placeholder=
"t('admin.groups.modelRouting.searchAccountPlaceholder')"
:placeholder=
"
t(
'admin.groups.modelRouting.searchAccountPlaceholder',
)
"
@
input=
"searchAccountsByRule(rule, true)"
@
input=
"searchAccountsByRule(rule, true)"
@
focus=
"onAccountSearchFocus(rule, true)"
@
focus=
"onAccountSearchFocus(rule, true)"
/>
/>
<!-- 搜索结果下拉框 -->
<!-- 搜索结果下拉框 -->
<div
<div
v-if=
"showAccountDropdown[getEditRuleSearchKey(rule)] && accountSearchResults[getEditRuleSearchKey(rule)]?.length > 0"
v-if=
"
showAccountDropdown[getEditRuleSearchKey(rule)] &&
accountSearchResults[getEditRuleSearchKey(rule)]
?.length > 0
"
class=
"absolute z-50 mt-1 max-h-48 w-full overflow-auto rounded-lg border bg-white shadow-lg dark:border-dark-600 dark:bg-dark-800"
class=
"absolute z-50 mt-1 max-h-48 w-full overflow-auto rounded-lg border bg-white shadow-lg dark:border-dark-600 dark:bg-dark-800"
>
>
<button
<button
v-for=
"account in accountSearchResults[getEditRuleSearchKey(rule)]"
v-for=
"account in accountSearchResults[
getEditRuleSearchKey(rule)
]"
:key=
"account.id"
:key=
"account.id"
type=
"button"
type=
"button"
@
click=
"selectAccount(rule, account, true)"
@
click=
"selectAccount(rule, account, true)"
class=
"w-full px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-dark-700"
class=
"w-full px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-dark-700"
:class=
"{ 'opacity-50': rule.accounts.some(a => a.id === account.id) }"
:class=
"{
:disabled=
"rule.accounts.some(a => a.id === account.id)"
'opacity-50': rule.accounts.some(
(a) => a.id === account.id,
),
}"
:disabled=
"
rule.accounts.some((a) => a.id === account.id)
"
>
>
<span>
{{ account.name }}
</span>
<span>
{{ account.name }}
</span>
<span
class=
"ml-2 text-xs text-gray-400"
>
#{{ account.id }}
</span>
<span
class=
"ml-2 text-xs text-gray-400"
>
#{{ account.id }}
</span
>
</button>
</button>
</div>
</div>
</div>
</div>
<p
class=
"text-xs text-gray-400 mt-1"
>
{{ t('admin.groups.modelRouting.accountsHint') }}
</p>
<p
class=
"text-xs text-gray-400 mt-1"
>
{{ t("admin.groups.modelRouting.accountsHint") }}
</p>
</div>
</div>
</div>
</div>
<button
<button
...
@@ -1644,16 +2523,19 @@
...
@@ -1644,16 +2523,19 @@
class=
"mt-3 flex items-center gap-1.5 text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
class=
"mt-3 flex items-center gap-1.5 text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
>
>
<Icon
name=
"plus"
size=
"sm"
/>
<Icon
name=
"plus"
size=
"sm"
/>
{{ t(
'
admin.groups.modelRouting.addRule
'
) }}
{{ t(
"
admin.groups.modelRouting.addRule
"
) }}
</button>
</button>
</div>
</div>
</form>
</form>
<
template
#footer
>
<
template
#footer
>
<div
class=
"flex justify-end gap-3 pt-4"
>
<div
class=
"flex justify-end gap-3 pt-4"
>
<button
@
click=
"closeEditModal"
type=
"button"
class=
"btn btn-secondary"
>
<button
{{
t
(
'
common.cancel
'
)
}}
@
click=
"closeEditModal"
type=
"button"
class=
"btn btn-secondary"
>
{{
t
(
"
common.cancel
"
)
}}
</button>
</button>
<button
<button
type=
"submit"
type=
"submit"
...
@@ -1682,7 +2564,7 @@
...
@@ -1682,7 +2564,7 @@
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
></path>
</svg>
</svg>
{{
submitting
?
t
(
'
admin.groups.updating
'
)
:
t
(
'
common.update
'
)
}}
{{
submitting
?
t
(
"
admin.groups.updating
"
)
:
t
(
"
common.update
"
)
}}
</button>
</button>
</div>
</div>
</
template
>
</
template
>
...
@@ -1709,7 +2591,7 @@
...
@@ -1709,7 +2591,7 @@
>
>
<div
class=
"space-y-4"
>
<div
class=
"space-y-4"
>
<p
class=
"text-sm text-gray-500 dark:text-gray-400"
>
<p
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{ t(
'
admin.groups.sortOrderHint
'
) }}
{{ t(
"
admin.groups.sortOrderHint
"
) }}
</p>
</p>
<VueDraggable
<VueDraggable
v-model=
"sortableGroups"
v-model=
"sortableGroups"
...
@@ -1725,7 +2607,9 @@
...
@@ -1725,7 +2607,9 @@
<Icon
name=
"menu"
size=
"md"
/>
<Icon
name=
"menu"
size=
"md"
/>
</div>
</div>
<div
class=
"flex-1"
>
<div
class=
"flex-1"
>
<div
class=
"font-medium text-gray-900 dark:text-white"
>
{{ group.name }}
</div>
<div
class=
"font-medium text-gray-900 dark:text-white"
>
{{ group.name }}
</div>
<div
class=
"text-xs text-gray-500 dark:text-gray-400"
>
<div
class=
"text-xs text-gray-500 dark:text-gray-400"
>
<span
<span
:class=
"[
:class=
"[
...
@@ -1736,24 +2620,26 @@
...
@@ -1736,24 +2620,26 @@
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
: group.platform === 'antigravity'
: group.platform === 'antigravity'
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
,
]"
]"
>
>
{{ t(
'
admin.groups.platforms.
'
+ group.platform) }}
{{ t(
"
admin.groups.platforms.
"
+ group.platform) }}
</span>
</span>
</div>
</div>
</div>
</div>
<div
class=
"text-sm text-gray-400"
>
<div
class=
"text-sm text-gray-400"
>
#{{ group.id }}
</div>
#{{ group.id }}
</div>
</div>
</div>
</VueDraggable>
</VueDraggable>
</div>
</div>
<
template
#footer
>
<
template
#footer
>
<div
class=
"flex justify-end gap-3 pt-4"
>
<div
class=
"flex justify-end gap-3 pt-4"
>
<button
@
click=
"closeSortModal"
type=
"button"
class=
"btn btn-secondary"
>
<button
{{
t
(
'
common.cancel
'
)
}}
@
click=
"closeSortModal"
type=
"button"
class=
"btn btn-secondary"
>
{{
t
(
"
common.cancel
"
)
}}
</button>
</button>
<button
<button
@
click=
"saveSortOrder"
@
click=
"saveSortOrder"
...
@@ -1780,7 +2666,7 @@
...
@@ -1780,7 +2666,7 @@
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
></path>
</svg>
</svg>
{{
sortSubmitting
?
t
(
'
common.saving
'
)
:
t
(
'
common.save
'
)
}}
{{
sortSubmitting
?
t
(
"
common.saving
"
)
:
t
(
"
common.save
"
)
}}
</button>
</button>
</div>
</div>
</
template
>
</
template
>
...
@@ -1797,214 +2683,271 @@
...
@@ -1797,214 +2683,271 @@
</template>
</template>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
reactive
,
computed
,
onMounted
,
onUnmounted
,
watch
}
from
'
vue
'
import
{
ref
,
reactive
,
computed
,
onMounted
,
onUnmounted
,
watch
}
from
"
vue
"
;
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
"
vue-i18n
"
;
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAppStore
}
from
"
@/stores/app
"
;
import
{
useOnboardingStore
}
from
'
@/stores/onboarding
'
import
{
useOnboardingStore
}
from
"
@/stores/onboarding
"
;
import
{
adminAPI
}
from
'
@/api/admin
'
import
{
adminAPI
}
from
"
@/api/admin
"
;
import
type
{
AdminGroup
,
GroupPlatform
,
SubscriptionType
}
from
'
@/types
'
import
type
{
AdminGroup
,
GroupPlatform
,
SubscriptionType
}
from
"
@/types
"
;
import
type
{
Column
}
from
'
@/components/common/types
'
import
type
{
Column
}
from
"
@/components/common/types
"
;
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
AppLayout
from
"
@/components/layout/AppLayout.vue
"
;
import
TablePageLayout
from
'
@/components/layout/TablePageLayout.vue
'
import
TablePageLayout
from
"
@/components/layout/TablePageLayout.vue
"
;
import
DataTable
from
'
@/components/common/DataTable.vue
'
import
DataTable
from
"
@/components/common/DataTable.vue
"
;
import
Pagination
from
'
@/components/common/Pagination.vue
'
import
Pagination
from
"
@/components/common/Pagination.vue
"
;
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
BaseDialog
from
"
@/components/common/BaseDialog.vue
"
;
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
ConfirmDialog
from
"
@/components/common/ConfirmDialog.vue
"
;
import
EmptyState
from
'
@/components/common/EmptyState.vue
'
import
EmptyState
from
"
@/components/common/EmptyState.vue
"
;
import
Select
from
'
@/components/common/Select.vue
'
import
Select
from
"
@/components/common/Select.vue
"
;
import
PlatformIcon
from
'
@/components/common/PlatformIcon.vue
'
import
PlatformIcon
from
"
@/components/common/PlatformIcon.vue
"
;
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
"
@/components/icons/Icon.vue
"
;
import
GroupRateMultipliersModal
from
'
@/components/admin/group/GroupRateMultipliersModal.vue
'
import
GroupRateMultipliersModal
from
"
@/components/admin/group/GroupRateMultipliersModal.vue
"
;
import
GroupCapacityBadge
from
'
@/components/common/GroupCapacityBadge.vue
'
import
GroupCapacityBadge
from
"
@/components/common/GroupCapacityBadge.vue
"
;
import
{
VueDraggable
}
from
'
vue-draggable-plus
'
import
{
VueDraggable
}
from
"
vue-draggable-plus
"
;
import
{
createStableObjectKeyResolver
}
from
'
@/utils/stableObjectKey
'
import
{
createStableObjectKeyResolver
}
from
"
@/utils/stableObjectKey
"
;
import
{
useKeyedDebouncedSearch
}
from
'
@/composables/useKeyedDebouncedSearch
'
import
{
useKeyedDebouncedSearch
}
from
"
@/composables/useKeyedDebouncedSearch
"
;
import
{
getPersistedPageSize
}
from
'
@/composables/usePersistedPageSize
'
import
{
getPersistedPageSize
}
from
"
@/composables/usePersistedPageSize
"
;
import
{
const
{
t
}
=
useI18n
()
createDefaultMessagesDispatchFormState
,
const
appStore
=
useAppStore
()
messagesDispatchConfigToFormState
,
const
onboardingStore
=
useOnboardingStore
()
messagesDispatchFormStateToConfig
,
resetMessagesDispatchFormState
,
type
MessagesDispatchMappingRow
,
}
from
"
./groupsMessagesDispatch
"
;
const
{
t
}
=
useI18n
();
const
appStore
=
useAppStore
();
const
onboardingStore
=
useOnboardingStore
();
const
columns
=
computed
<
Column
[]
>
(()
=>
[
const
columns
=
computed
<
Column
[]
>
(()
=>
[
{
key
:
'
name
'
,
label
:
t
(
'
admin.groups.columns.name
'
),
sortable
:
true
},
{
key
:
"
name
"
,
label
:
t
(
"
admin.groups.columns.name
"
),
sortable
:
true
},
{
key
:
'
platform
'
,
label
:
t
(
'
admin.groups.columns.platform
'
),
sortable
:
true
},
{
{
key
:
'
billing_type
'
,
label
:
t
(
'
admin.groups.columns.billingType
'
),
sortable
:
true
},
key
:
"
platform
"
,
{
key
:
'
rate_multiplier
'
,
label
:
t
(
'
admin.groups.columns.rateMultiplier
'
),
sortable
:
true
},
label
:
t
(
"
admin.groups.columns.platform
"
),
{
key
:
'
is_exclusive
'
,
label
:
t
(
'
admin.groups.columns.type
'
),
sortable
:
true
},
sortable
:
true
,
{
key
:
'
account_count
'
,
label
:
t
(
'
admin.groups.columns.accounts
'
),
sortable
:
true
},
},
{
key
:
'
capacity
'
,
label
:
t
(
'
admin.groups.columns.capacity
'
),
sortable
:
false
},
{
{
key
:
'
usage
'
,
label
:
t
(
'
admin.groups.columns.usage
'
),
sortable
:
false
},
key
:
"
billing_type
"
,
{
key
:
'
status
'
,
label
:
t
(
'
admin.groups.columns.status
'
),
sortable
:
true
},
label
:
t
(
"
admin.groups.columns.billingType
"
),
{
key
:
'
actions
'
,
label
:
t
(
'
admin.groups.columns.actions
'
),
sortable
:
false
}
sortable
:
true
,
])
},
{
key
:
"
rate_multiplier
"
,
label
:
t
(
"
admin.groups.columns.rateMultiplier
"
),
sortable
:
true
,
},
{
key
:
"
is_exclusive
"
,
label
:
t
(
"
admin.groups.columns.type
"
),
sortable
:
true
,
},
{
key
:
"
account_count
"
,
label
:
t
(
"
admin.groups.columns.accounts
"
),
sortable
:
true
,
},
{
key
:
"
capacity
"
,
label
:
t
(
"
admin.groups.columns.capacity
"
),
sortable
:
false
,
},
{
key
:
"
usage
"
,
label
:
t
(
"
admin.groups.columns.usage
"
),
sortable
:
false
},
{
key
:
"
status
"
,
label
:
t
(
"
admin.groups.columns.status
"
),
sortable
:
true
},
{
key
:
"
actions
"
,
label
:
t
(
"
admin.groups.columns.actions
"
),
sortable
:
false
},
]);
// Filter options
// Filter options
const
statusOptions
=
computed
(()
=>
[
const
statusOptions
=
computed
(()
=>
[
{
value
:
''
,
label
:
t
(
'
admin.groups.allStatus
'
)
},
{
value
:
""
,
label
:
t
(
"
admin.groups.allStatus
"
)
},
{
value
:
'
active
'
,
label
:
t
(
'
admin.accounts.status.active
'
)
},
{
value
:
"
active
"
,
label
:
t
(
"
admin.accounts.status.active
"
)
},
{
value
:
'
inactive
'
,
label
:
t
(
'
admin.accounts.status.inactive
'
)
}
{
value
:
"
inactive
"
,
label
:
t
(
"
admin.accounts.status.inactive
"
)
}
,
])
])
;
const
exclusiveOptions
=
computed
(()
=>
[
const
exclusiveOptions
=
computed
(()
=>
[
{
value
:
''
,
label
:
t
(
'
admin.groups.allGroups
'
)
},
{
value
:
""
,
label
:
t
(
"
admin.groups.allGroups
"
)
},
{
value
:
'
true
'
,
label
:
t
(
'
admin.groups.exclusive
'
)
},
{
value
:
"
true
"
,
label
:
t
(
"
admin.groups.exclusive
"
)
},
{
value
:
'
false
'
,
label
:
t
(
'
admin.groups.nonExclusive
'
)
}
{
value
:
"
false
"
,
label
:
t
(
"
admin.groups.nonExclusive
"
)
}
,
])
])
;
const
platformOptions
=
computed
(()
=>
[
const
platformOptions
=
computed
(()
=>
[
{
value
:
'
anthropic
'
,
label
:
'
Anthropic
'
},
{
value
:
"
anthropic
"
,
label
:
"
Anthropic
"
},
{
value
:
'
openai
'
,
label
:
'
OpenAI
'
},
{
value
:
"
openai
"
,
label
:
"
OpenAI
"
},
{
value
:
'
gemini
'
,
label
:
'
Gemini
'
},
{
value
:
"
gemini
"
,
label
:
"
Gemini
"
},
{
value
:
'
antigravity
'
,
label
:
'
Antigravity
'
}
{
value
:
"
antigravity
"
,
label
:
"
Antigravity
"
}
,
])
])
;
const
platformFilterOptions
=
computed
(()
=>
[
const
platformFilterOptions
=
computed
(()
=>
[
{
value
:
''
,
label
:
t
(
'
admin.groups.allPlatforms
'
)
},
{
value
:
""
,
label
:
t
(
"
admin.groups.allPlatforms
"
)
},
{
value
:
'
anthropic
'
,
label
:
'
Anthropic
'
},
{
value
:
"
anthropic
"
,
label
:
"
Anthropic
"
},
{
value
:
'
openai
'
,
label
:
'
OpenAI
'
},
{
value
:
"
openai
"
,
label
:
"
OpenAI
"
},
{
value
:
'
gemini
'
,
label
:
'
Gemini
'
},
{
value
:
"
gemini
"
,
label
:
"
Gemini
"
},
{
value
:
'
antigravity
'
,
label
:
'
Antigravity
'
}
{
value
:
"
antigravity
"
,
label
:
"
Antigravity
"
}
,
])
])
;
const
editStatusOptions
=
computed
(()
=>
[
const
editStatusOptions
=
computed
(()
=>
[
{
value
:
'
active
'
,
label
:
t
(
'
admin.accounts.status.active
'
)
},
{
value
:
"
active
"
,
label
:
t
(
"
admin.accounts.status.active
"
)
},
{
value
:
'
inactive
'
,
label
:
t
(
'
admin.accounts.status.inactive
'
)
}
{
value
:
"
inactive
"
,
label
:
t
(
"
admin.accounts.status.inactive
"
)
}
,
])
])
;
const
subscriptionTypeOptions
=
computed
(()
=>
[
const
subscriptionTypeOptions
=
computed
(()
=>
[
{
value
:
'
standard
'
,
label
:
t
(
'
admin.groups.subscription.standard
'
)
},
{
value
:
"
standard
"
,
label
:
t
(
"
admin.groups.subscription.standard
"
)
},
{
value
:
'
subscription
'
,
label
:
t
(
'
admin.groups.subscription.subscription
'
)
}
{
value
:
"
subscription
"
,
label
:
t
(
"
admin.groups.subscription.subscription
"
)
}
,
])
])
;
// 降级分组选项(创建时)- 仅包含 anthropic 平台且未启用 claude_code_only 的分组
// 降级分组选项(创建时)- 仅包含 anthropic 平台且未启用 claude_code_only 的分组
const
fallbackGroupOptions
=
computed
(()
=>
{
const
fallbackGroupOptions
=
computed
(()
=>
{
const
options
:
{
value
:
number
|
null
;
label
:
string
}[]
=
[
const
options
:
{
value
:
number
|
null
;
label
:
string
}[]
=
[
{
value
:
null
,
label
:
t
(
'
admin.groups.claudeCode.noFallback
'
)
}
{
value
:
null
,
label
:
t
(
"
admin.groups.claudeCode.noFallback
"
)
}
,
]
]
;
const
eligibleGroups
=
groups
.
value
.
filter
(
const
eligibleGroups
=
groups
.
value
.
filter
(
(
g
)
=>
g
.
platform
===
'
anthropic
'
&&
!
g
.
claude_code_only
&&
g
.
status
===
'
active
'
(
g
)
=>
)
g
.
platform
===
"
anthropic
"
&&
!
g
.
claude_code_only
&&
g
.
status
===
"
active
"
,
);
eligibleGroups
.
forEach
((
g
)
=>
{
eligibleGroups
.
forEach
((
g
)
=>
{
options
.
push
({
value
:
g
.
id
,
label
:
g
.
name
})
options
.
push
({
value
:
g
.
id
,
label
:
g
.
name
})
;
})
})
;
return
options
return
options
;
})
})
;
// 降级分组选项(编辑时)- 排除自身
// 降级分组选项(编辑时)- 排除自身
const
fallbackGroupOptionsForEdit
=
computed
(()
=>
{
const
fallbackGroupOptionsForEdit
=
computed
(()
=>
{
const
options
:
{
value
:
number
|
null
;
label
:
string
}[]
=
[
const
options
:
{
value
:
number
|
null
;
label
:
string
}[]
=
[
{
value
:
null
,
label
:
t
(
'
admin.groups.claudeCode.noFallback
'
)
}
{
value
:
null
,
label
:
t
(
"
admin.groups.claudeCode.noFallback
"
)
}
,
]
]
;
const
currentId
=
editingGroup
.
value
?.
id
const
currentId
=
editingGroup
.
value
?.
id
;
const
eligibleGroups
=
groups
.
value
.
filter
(
const
eligibleGroups
=
groups
.
value
.
filter
(
(
g
)
=>
g
.
platform
===
'
anthropic
'
&&
!
g
.
claude_code_only
&&
g
.
status
===
'
active
'
&&
g
.
id
!==
currentId
(
g
)
=>
)
g
.
platform
===
"
anthropic
"
&&
!
g
.
claude_code_only
&&
g
.
status
===
"
active
"
&&
g
.
id
!==
currentId
,
);
eligibleGroups
.
forEach
((
g
)
=>
{
eligibleGroups
.
forEach
((
g
)
=>
{
options
.
push
({
value
:
g
.
id
,
label
:
g
.
name
})
options
.
push
({
value
:
g
.
id
,
label
:
g
.
name
})
;
})
})
;
return
options
return
options
;
})
})
;
// 无效请求兜底分组选项(创建时)- 仅包含 anthropic 平台、非订阅且未配置兜底的分组
// 无效请求兜底分组选项(创建时)- 仅包含 anthropic 平台、非订阅且未配置兜底的分组
const
invalidRequestFallbackOptions
=
computed
(()
=>
{
const
invalidRequestFallbackOptions
=
computed
(()
=>
{
const
options
:
{
value
:
number
|
null
;
label
:
string
}[]
=
[
const
options
:
{
value
:
number
|
null
;
label
:
string
}[]
=
[
{
value
:
null
,
label
:
t
(
'
admin.groups.invalidRequestFallback.noFallback
'
)
}
{
value
:
null
,
label
:
t
(
"
admin.groups.invalidRequestFallback.noFallback
"
)
}
,
]
]
;
const
eligibleGroups
=
groups
.
value
.
filter
(
const
eligibleGroups
=
groups
.
value
.
filter
(
(
g
)
=>
(
g
)
=>
g
.
platform
===
'
anthropic
'
&&
g
.
platform
===
"
anthropic
"
&&
g
.
status
===
'
active
'
&&
g
.
status
===
"
active
"
&&
g
.
subscription_type
!==
'
subscription
'
&&
g
.
subscription_type
!==
"
subscription
"
&&
g
.
fallback_group_id_on_invalid_request
===
null
g
.
fallback_group_id_on_invalid_request
===
null
,
)
)
;
eligibleGroups
.
forEach
((
g
)
=>
{
eligibleGroups
.
forEach
((
g
)
=>
{
options
.
push
({
value
:
g
.
id
,
label
:
g
.
name
})
options
.
push
({
value
:
g
.
id
,
label
:
g
.
name
})
;
})
})
;
return
options
return
options
;
})
})
;
// 无效请求兜底分组选项(编辑时)- 排除自身
// 无效请求兜底分组选项(编辑时)- 排除自身
const
invalidRequestFallbackOptionsForEdit
=
computed
(()
=>
{
const
invalidRequestFallbackOptionsForEdit
=
computed
(()
=>
{
const
options
:
{
value
:
number
|
null
;
label
:
string
}[]
=
[
const
options
:
{
value
:
number
|
null
;
label
:
string
}[]
=
[
{
value
:
null
,
label
:
t
(
'
admin.groups.invalidRequestFallback.noFallback
'
)
}
{
value
:
null
,
label
:
t
(
"
admin.groups.invalidRequestFallback.noFallback
"
)
}
,
]
]
;
const
currentId
=
editingGroup
.
value
?.
id
const
currentId
=
editingGroup
.
value
?.
id
;
const
eligibleGroups
=
groups
.
value
.
filter
(
const
eligibleGroups
=
groups
.
value
.
filter
(
(
g
)
=>
(
g
)
=>
g
.
platform
===
'
anthropic
'
&&
g
.
platform
===
"
anthropic
"
&&
g
.
status
===
'
active
'
&&
g
.
status
===
"
active
"
&&
g
.
subscription_type
!==
'
subscription
'
&&
g
.
subscription_type
!==
"
subscription
"
&&
g
.
fallback_group_id_on_invalid_request
===
null
&&
g
.
fallback_group_id_on_invalid_request
===
null
&&
g
.
id
!==
currentId
g
.
id
!==
currentId
,
)
)
;
eligibleGroups
.
forEach
((
g
)
=>
{
eligibleGroups
.
forEach
((
g
)
=>
{
options
.
push
({
value
:
g
.
id
,
label
:
g
.
name
})
options
.
push
({
value
:
g
.
id
,
label
:
g
.
name
})
;
})
})
;
return
options
return
options
;
})
})
;
// 复制账号的源分组选项(创建时)- 仅包含相同平台且有账号的分组
// 复制账号的源分组选项(创建时)- 仅包含相同平台且有账号的分组
const
copyAccountsGroupOptions
=
computed
(()
=>
{
const
copyAccountsGroupOptions
=
computed
(()
=>
{
const
eligibleGroups
=
groups
.
value
.
filter
(
const
eligibleGroups
=
groups
.
value
.
filter
(
(
g
)
=>
g
.
platform
===
createForm
.
platform
&&
(
g
.
account_count
||
0
)
>
0
(
g
)
=>
g
.
platform
===
createForm
.
platform
&&
(
g
.
account_count
||
0
)
>
0
,
)
)
;
return
eligibleGroups
.
map
((
g
)
=>
({
return
eligibleGroups
.
map
((
g
)
=>
({
value
:
g
.
id
,
value
:
g
.
id
,
label
:
`
${
g
.
name
}
(
${
g
.
account_count
||
0
}
个账号)`
label
:
`
${
g
.
name
}
(
${
g
.
account_count
||
0
}
个账号)`
,
}))
}))
;
})
})
;
// 复制账号的源分组选项(编辑时)- 仅包含相同平台且有账号的分组,排除自身
// 复制账号的源分组选项(编辑时)- 仅包含相同平台且有账号的分组,排除自身
const
copyAccountsGroupOptionsForEdit
=
computed
(()
=>
{
const
copyAccountsGroupOptionsForEdit
=
computed
(()
=>
{
const
currentId
=
editingGroup
.
value
?.
id
const
currentId
=
editingGroup
.
value
?.
id
;
const
eligibleGroups
=
groups
.
value
.
filter
(
const
eligibleGroups
=
groups
.
value
.
filter
(
(
g
)
=>
g
.
platform
===
editForm
.
platform
&&
(
g
.
account_count
||
0
)
>
0
&&
g
.
id
!==
currentId
(
g
)
=>
)
g
.
platform
===
editForm
.
platform
&&
(
g
.
account_count
||
0
)
>
0
&&
g
.
id
!==
currentId
,
);
return
eligibleGroups
.
map
((
g
)
=>
({
return
eligibleGroups
.
map
((
g
)
=>
({
value
:
g
.
id
,
value
:
g
.
id
,
label
:
`
${
g
.
name
}
(
${
g
.
account_count
||
0
}
个账号)`
label
:
`
${
g
.
name
}
(
${
g
.
account_count
||
0
}
个账号)`
,
}))
}));
})
});
const
groups
=
ref
<
AdminGroup
[]
>
([])
const
groups
=
ref
<
AdminGroup
[]
>
([]);
const
loading
=
ref
(
false
)
const
loading
=
ref
(
false
);
const
usageMap
=
ref
<
Map
<
number
,
{
today_cost
:
number
;
total_cost
:
number
}
>>
(
new
Map
())
const
usageMap
=
ref
<
Map
<
number
,
{
today_cost
:
number
;
total_cost
:
number
}
>>
(
const
usageLoading
=
ref
(
false
)
new
Map
(),
const
capacityMap
=
ref
<
Map
<
number
,
{
concurrencyUsed
:
number
;
concurrencyMax
:
number
;
sessionsUsed
:
number
;
sessionsMax
:
number
;
rpmUsed
:
number
;
rpmMax
:
number
}
>>
(
new
Map
())
);
const
searchQuery
=
ref
(
''
)
const
usageLoading
=
ref
(
false
);
const
capacityMap
=
ref
<
Map
<
number
,
{
concurrencyUsed
:
number
;
concurrencyMax
:
number
;
sessionsUsed
:
number
;
sessionsMax
:
number
;
rpmUsed
:
number
;
rpmMax
:
number
;
}
>
>
(
new
Map
());
const
searchQuery
=
ref
(
""
);
const
filters
=
reactive
({
const
filters
=
reactive
({
platform
:
''
,
platform
:
""
,
status
:
''
,
status
:
""
,
is_exclusive
:
''
is_exclusive
:
""
,
})
})
;
const
pagination
=
reactive
({
const
pagination
=
reactive
({
page
:
1
,
page
:
1
,
page_size
:
getPersistedPageSize
(),
page_size
:
getPersistedPageSize
(),
total
:
0
,
total
:
0
,
pages
:
0
pages
:
0
,
})
});
let
abortController
:
AbortController
|
null
=
null
let
abortController
:
AbortController
|
null
=
null
;
const
showCreateModal
=
ref
(
false
)
const
showCreateModal
=
ref
(
false
);
const
showEditModal
=
ref
(
false
)
const
showEditModal
=
ref
(
false
);
const
showDeleteDialog
=
ref
(
false
)
const
showDeleteDialog
=
ref
(
false
);
const
showSortModal
=
ref
(
false
)
const
showSortModal
=
ref
(
false
);
const
submitting
=
ref
(
false
)
const
submitting
=
ref
(
false
);
const
sortSubmitting
=
ref
(
false
)
const
sortSubmitting
=
ref
(
false
);
const
editingGroup
=
ref
<
AdminGroup
|
null
>
(
null
)
const
editingGroup
=
ref
<
AdminGroup
|
null
>
(
null
);
const
deletingGroup
=
ref
<
AdminGroup
|
null
>
(
null
)
const
deletingGroup
=
ref
<
AdminGroup
|
null
>
(
null
);
const
showRateMultipliersModal
=
ref
(
false
)
const
showRateMultipliersModal
=
ref
(
false
);
const
rateMultipliersGroup
=
ref
<
AdminGroup
|
null
>
(
null
)
const
rateMultipliersGroup
=
ref
<
AdminGroup
|
null
>
(
null
);
const
sortableGroups
=
ref
<
AdminGroup
[]
>
([])
const
sortableGroups
=
ref
<
AdminGroup
[]
>
([]);
const
createMessagesDispatchDefaults
=
createDefaultMessagesDispatchFormState
();
const
editMessagesDispatchDefaults
=
createDefaultMessagesDispatchFormState
();
const
createForm
=
reactive
({
const
createForm
=
reactive
({
name
:
''
,
name
:
""
,
description
:
''
,
description
:
""
,
platform
:
'
anthropic
'
as
GroupPlatform
,
platform
:
"
anthropic
"
as
GroupPlatform
,
rate_multiplier
:
1.0
,
rate_multiplier
:
1.0
,
is_exclusive
:
false
,
is_exclusive
:
false
,
subscription_type
:
'
standard
'
as
SubscriptionType
,
subscription_type
:
"
standard
"
as
SubscriptionType
,
daily_limit_usd
:
null
as
number
|
null
,
daily_limit_usd
:
null
as
number
|
null
,
weekly_limit_usd
:
null
as
number
|
null
,
weekly_limit_usd
:
null
as
number
|
null
,
monthly_limit_usd
:
null
as
number
|
null
,
monthly_limit_usd
:
null
as
number
|
null
,
...
@@ -2018,68 +2961,89 @@ const createForm = reactive({
...
@@ -2018,68 +2961,89 @@ const createForm = reactive({
fallback_group_id_on_invalid_request
:
null
as
number
|
null
,
fallback_group_id_on_invalid_request
:
null
as
number
|
null
,
// OpenAI Messages 调度配置(仅 openai 平台使用)
// OpenAI Messages 调度配置(仅 openai 平台使用)
allow_messages_dispatch
:
false
,
allow_messages_dispatch
:
false
,
default_mapped_model
:
'
gpt-5.4
'
,
opus_mapped_model
:
createMessagesDispatchDefaults
.
opus_mapped_model
,
sonnet_mapped_model
:
createMessagesDispatchDefaults
.
sonnet_mapped_model
,
haiku_mapped_model
:
createMessagesDispatchDefaults
.
haiku_mapped_model
,
exact_model_mappings
:
[]
as
MessagesDispatchMappingRow
[],
// 账号过滤控制(OpenAI/Antigravity 平台)
// 账号过滤控制(OpenAI/Antigravity 平台)
require_oauth_only
:
false
,
require_oauth_only
:
false
,
require_privacy_set
:
false
,
require_privacy_set
:
false
,
// 模型路由开关
// 模型路由开关
model_routing_enabled
:
false
,
model_routing_enabled
:
false
,
// 支持的模型系列(仅 antigravity 平台)
// 支持的模型系列(仅 antigravity 平台)
supported_model_scopes
:
[
'
claude
'
,
'
gemini_text
'
,
'
gemini_image
'
]
as
string
[],
supported_model_scopes
:
[
"
claude
"
,
"
gemini_text
"
,
"
gemini_image
"
]
as
string
[],
// MCP XML 协议注入开关(仅 antigravity 平台)
// MCP XML 协议注入开关(仅 antigravity 平台)
mcp_xml_inject
:
true
,
mcp_xml_inject
:
true
,
// 从分组复制账号
// 从分组复制账号
copy_accounts_from_group_ids
:
[]
as
number
[]
copy_accounts_from_group_ids
:
[]
as
number
[]
,
})
})
;
// 简单账号类型(用于模型路由选择)
// 简单账号类型(用于模型路由选择)
interface
SimpleAccount
{
interface
SimpleAccount
{
id
:
number
id
:
number
;
name
:
string
name
:
string
;
}
}
// 模型路由规则类型
// 模型路由规则类型
interface
ModelRoutingRule
{
interface
ModelRoutingRule
{
pattern
:
string
pattern
:
string
;
accounts
:
SimpleAccount
[]
// 选中的账号对象数组
accounts
:
SimpleAccount
[]
;
// 选中的账号对象数组
}
}
// 创建表单的模型路由规则
// 创建表单的模型路由规则
const
createModelRoutingRules
=
ref
<
ModelRoutingRule
[]
>
([])
const
createModelRoutingRules
=
ref
<
ModelRoutingRule
[]
>
([])
;
// 编辑表单的模型路由规则
// 编辑表单的模型路由规则
const
editModelRoutingRules
=
ref
<
ModelRoutingRule
[]
>
([])
const
editModelRoutingRules
=
ref
<
ModelRoutingRule
[]
>
([])
;
// 规则对象稳定 key(避免使用 index 导致状态错位)
// 规则对象稳定 key(避免使用 index 导致状态错位)
const
resolveCreateRuleKey
=
createStableObjectKeyResolver
<
ModelRoutingRule
>
(
'
create-rule
'
)
const
resolveCreateRuleKey
=
const
resolveEditRuleKey
=
createStableObjectKeyResolver
<
ModelRoutingRule
>
(
'
edit-rule
'
)
createStableObjectKeyResolver
<
ModelRoutingRule
>
(
"
create-rule
"
);
const
resolveEditRuleKey
=
const
getCreateRuleRenderKey
=
(
rule
:
ModelRoutingRule
)
=>
resolveCreateRuleKey
(
rule
)
createStableObjectKeyResolver
<
ModelRoutingRule
>
(
"
edit-rule
"
);
const
getEditRuleRenderKey
=
(
rule
:
ModelRoutingRule
)
=>
resolveEditRuleKey
(
rule
)
const
resolveCreateMessagesDispatchRowKey
=
createStableObjectKeyResolver
<
MessagesDispatchMappingRow
>
(
const
getCreateRuleSearchKey
=
(
rule
:
ModelRoutingRule
)
=>
`create-
${
resolveCreateRuleKey
(
rule
)}
`
"
create-messages-dispatch-row
"
,
const
getEditRuleSearchKey
=
(
rule
:
ModelRoutingRule
)
=>
`edit-
${
resolveEditRuleKey
(
rule
)}
`
);
const
resolveEditMessagesDispatchRowKey
=
createStableObjectKeyResolver
<
MessagesDispatchMappingRow
>
(
"
edit-messages-dispatch-row
"
,
);
const
getCreateRuleRenderKey
=
(
rule
:
ModelRoutingRule
)
=>
resolveCreateRuleKey
(
rule
);
const
getEditRuleRenderKey
=
(
rule
:
ModelRoutingRule
)
=>
resolveEditRuleKey
(
rule
);
const
getCreateMessagesDispatchRowKey
=
(
row
:
MessagesDispatchMappingRow
)
=>
resolveCreateMessagesDispatchRowKey
(
row
);
const
getEditMessagesDispatchRowKey
=
(
row
:
MessagesDispatchMappingRow
)
=>
resolveEditMessagesDispatchRowKey
(
row
);
const
getCreateRuleSearchKey
=
(
rule
:
ModelRoutingRule
)
=>
`create-
${
resolveCreateRuleKey
(
rule
)}
`
;
const
getEditRuleSearchKey
=
(
rule
:
ModelRoutingRule
)
=>
`edit-
${
resolveEditRuleKey
(
rule
)}
`
;
const
getRuleSearchKey
=
(
rule
:
ModelRoutingRule
,
isEdit
:
boolean
=
false
)
=>
{
const
getRuleSearchKey
=
(
rule
:
ModelRoutingRule
,
isEdit
:
boolean
=
false
)
=>
{
return
isEdit
?
getEditRuleSearchKey
(
rule
)
:
getCreateRuleSearchKey
(
rule
)
return
isEdit
?
getEditRuleSearchKey
(
rule
)
:
getCreateRuleSearchKey
(
rule
)
;
}
}
;
// 账号搜索相关状态
// 账号搜索相关状态
const
accountSearchKeyword
=
ref
<
Record
<
string
,
string
>>
({})
const
accountSearchKeyword
=
ref
<
Record
<
string
,
string
>>
({})
;
const
accountSearchResults
=
ref
<
Record
<
string
,
SimpleAccount
[]
>>
({})
const
accountSearchResults
=
ref
<
Record
<
string
,
SimpleAccount
[]
>>
({})
;
const
showAccountDropdown
=
ref
<
Record
<
string
,
boolean
>>
({})
const
showAccountDropdown
=
ref
<
Record
<
string
,
boolean
>>
({})
;
const
clearAccountSearchStateByKey
=
(
key
:
string
)
=>
{
const
clearAccountSearchStateByKey
=
(
key
:
string
)
=>
{
delete
accountSearchKeyword
.
value
[
key
]
delete
accountSearchKeyword
.
value
[
key
]
;
delete
accountSearchResults
.
value
[
key
]
delete
accountSearchResults
.
value
[
key
]
;
delete
showAccountDropdown
.
value
[
key
]
delete
showAccountDropdown
.
value
[
key
]
;
}
}
;
const
clearAllAccountSearchState
=
()
=>
{
const
clearAllAccountSearchState
=
()
=>
{
accountSearchKeyword
.
value
=
{}
accountSearchKeyword
.
value
=
{}
;
accountSearchResults
.
value
=
{}
accountSearchResults
.
value
=
{}
;
showAccountDropdown
.
value
=
{}
showAccountDropdown
.
value
=
{}
;
}
}
;
const
accountSearchRunner
=
useKeyedDebouncedSearch
<
SimpleAccount
[]
>
({
const
accountSearchRunner
=
useKeyedDebouncedSearch
<
SimpleAccount
[]
>
({
delay
:
300
,
delay
:
300
,
...
@@ -2089,163 +3053,181 @@ const accountSearchRunner = useKeyedDebouncedSearch<SimpleAccount[]>({
...
@@ -2089,163 +3053,181 @@ const accountSearchRunner = useKeyedDebouncedSearch<SimpleAccount[]>({
20
,
20
,
{
{
search
:
keyword
,
search
:
keyword
,
platform
:
'
anthropic
'
platform
:
"
anthropic
"
,
},
},
{
signal
}
{
signal
}
,
)
)
;
return
res
.
items
.
map
((
account
)
=>
({
id
:
account
.
id
,
name
:
account
.
name
}))
return
res
.
items
.
map
((
account
)
=>
({
id
:
account
.
id
,
name
:
account
.
name
}))
;
},
},
onSuccess
:
(
key
,
result
)
=>
{
onSuccess
:
(
key
,
result
)
=>
{
accountSearchResults
.
value
[
key
]
=
result
accountSearchResults
.
value
[
key
]
=
result
;
},
},
onError
:
(
key
)
=>
{
onError
:
(
key
)
=>
{
accountSearchResults
.
value
[
key
]
=
[]
accountSearchResults
.
value
[
key
]
=
[]
;
}
}
,
})
})
;
// 搜索账号(仅限 anthropic 平台)
// 搜索账号(仅限 anthropic 平台)
const
searchAccounts
=
(
key
:
string
)
=>
{
const
searchAccounts
=
(
key
:
string
)
=>
{
accountSearchRunner
.
trigger
(
key
,
accountSearchKeyword
.
value
[
key
]
||
''
)
accountSearchRunner
.
trigger
(
key
,
accountSearchKeyword
.
value
[
key
]
||
""
);
}
}
;
const
searchAccountsByRule
=
(
rule
:
ModelRoutingRule
,
isEdit
:
boolean
=
false
)
=>
{
const
searchAccountsByRule
=
(
searchAccounts
(
getRuleSearchKey
(
rule
,
isEdit
))
rule
:
ModelRoutingRule
,
}
isEdit
:
boolean
=
false
,
)
=>
{
searchAccounts
(
getRuleSearchKey
(
rule
,
isEdit
));
};
// 选择账号
// 选择账号
const
selectAccount
=
(
rule
:
ModelRoutingRule
,
account
:
SimpleAccount
,
isEdit
:
boolean
=
false
)
=>
{
const
selectAccount
=
(
if
(
!
rule
)
return
rule
:
ModelRoutingRule
,
account
:
SimpleAccount
,
isEdit
:
boolean
=
false
,
)
=>
{
if
(
!
rule
)
return
;
// 检查是否已选择
// 检查是否已选择
if
(
!
rule
.
accounts
.
some
(
a
=>
a
.
id
===
account
.
id
))
{
if
(
!
rule
.
accounts
.
some
(
(
a
)
=>
a
.
id
===
account
.
id
))
{
rule
.
accounts
.
push
(
account
)
rule
.
accounts
.
push
(
account
)
;
}
}
// 清空搜索
// 清空搜索
const
key
=
getRuleSearchKey
(
rule
,
isEdit
)
const
key
=
getRuleSearchKey
(
rule
,
isEdit
)
;
accountSearchKeyword
.
value
[
key
]
=
''
accountSearchKeyword
.
value
[
key
]
=
""
;
showAccountDropdown
.
value
[
key
]
=
false
showAccountDropdown
.
value
[
key
]
=
false
;
}
}
;
// 移除已选账号
// 移除已选账号
const
removeSelectedAccount
=
(
rule
:
ModelRoutingRule
,
accountId
:
number
,
_isEdit
:
boolean
=
false
)
=>
{
const
removeSelectedAccount
=
(
if
(
!
rule
)
return
rule
:
ModelRoutingRule
,
accountId
:
number
,
_isEdit
:
boolean
=
false
,
)
=>
{
if
(
!
rule
)
return
;
rule
.
accounts
=
rule
.
accounts
.
filter
(
a
=>
a
.
id
!==
accountId
)
rule
.
accounts
=
rule
.
accounts
.
filter
(
(
a
)
=>
a
.
id
!==
accountId
)
;
}
}
;
// 切换创建表单的模型系列选择
// 切换创建表单的模型系列选择
const
toggleCreateScope
=
(
scope
:
string
)
=>
{
const
toggleCreateScope
=
(
scope
:
string
)
=>
{
const
idx
=
createForm
.
supported_model_scopes
.
indexOf
(
scope
)
const
idx
=
createForm
.
supported_model_scopes
.
indexOf
(
scope
)
;
if
(
idx
===
-
1
)
{
if
(
idx
===
-
1
)
{
createForm
.
supported_model_scopes
.
push
(
scope
)
createForm
.
supported_model_scopes
.
push
(
scope
)
;
}
else
{
}
else
{
createForm
.
supported_model_scopes
.
splice
(
idx
,
1
)
createForm
.
supported_model_scopes
.
splice
(
idx
,
1
)
;
}
}
}
}
;
// 切换编辑表单的模型系列选择
// 切换编辑表单的模型系列选择
const
toggleEditScope
=
(
scope
:
string
)
=>
{
const
toggleEditScope
=
(
scope
:
string
)
=>
{
const
idx
=
editForm
.
supported_model_scopes
.
indexOf
(
scope
)
const
idx
=
editForm
.
supported_model_scopes
.
indexOf
(
scope
)
;
if
(
idx
===
-
1
)
{
if
(
idx
===
-
1
)
{
editForm
.
supported_model_scopes
.
push
(
scope
)
editForm
.
supported_model_scopes
.
push
(
scope
)
;
}
else
{
}
else
{
editForm
.
supported_model_scopes
.
splice
(
idx
,
1
)
editForm
.
supported_model_scopes
.
splice
(
idx
,
1
)
;
}
}
}
}
;
// 处理账号搜索输入框聚焦
// 处理账号搜索输入框聚焦
const
onAccountSearchFocus
=
(
rule
:
ModelRoutingRule
,
isEdit
:
boolean
=
false
)
=>
{
const
onAccountSearchFocus
=
(
const
key
=
getRuleSearchKey
(
rule
,
isEdit
)
rule
:
ModelRoutingRule
,
showAccountDropdown
.
value
[
key
]
=
true
isEdit
:
boolean
=
false
,
)
=>
{
const
key
=
getRuleSearchKey
(
rule
,
isEdit
);
showAccountDropdown
.
value
[
key
]
=
true
;
// 如果没有搜索结果,触发一次搜索
// 如果没有搜索结果,触发一次搜索
if
(
!
accountSearchResults
.
value
[
key
]?.
length
)
{
if
(
!
accountSearchResults
.
value
[
key
]?.
length
)
{
searchAccounts
(
key
)
searchAccounts
(
key
)
;
}
}
}
}
;
// 添加创建表单的路由规则
// 添加创建表单的路由规则
const
addCreateRoutingRule
=
()
=>
{
const
addCreateRoutingRule
=
()
=>
{
createModelRoutingRules
.
value
.
push
({
pattern
:
''
,
accounts
:
[]
})
createModelRoutingRules
.
value
.
push
({
pattern
:
""
,
accounts
:
[]
})
;
}
}
;
// 删除创建表单的路由规则
// 删除创建表单的路由规则
const
removeCreateRoutingRule
=
(
rule
:
ModelRoutingRule
)
=>
{
const
removeCreateRoutingRule
=
(
rule
:
ModelRoutingRule
)
=>
{
const
index
=
createModelRoutingRules
.
value
.
indexOf
(
rule
)
const
index
=
createModelRoutingRules
.
value
.
indexOf
(
rule
)
;
if
(
index
===
-
1
)
return
if
(
index
===
-
1
)
return
;
const
key
=
getCreateRuleSearchKey
(
rule
)
const
key
=
getCreateRuleSearchKey
(
rule
)
;
accountSearchRunner
.
clearKey
(
key
)
accountSearchRunner
.
clearKey
(
key
)
;
clearAccountSearchStateByKey
(
key
)
clearAccountSearchStateByKey
(
key
)
;
createModelRoutingRules
.
value
.
splice
(
index
,
1
)
createModelRoutingRules
.
value
.
splice
(
index
,
1
)
;
}
}
;
// 添加编辑表单的路由规则
// 添加编辑表单的路由规则
const
addEditRoutingRule
=
()
=>
{
const
addEditRoutingRule
=
()
=>
{
editModelRoutingRules
.
value
.
push
({
pattern
:
''
,
accounts
:
[]
})
editModelRoutingRules
.
value
.
push
({
pattern
:
""
,
accounts
:
[]
})
;
}
}
;
// 删除编辑表单的路由规则
// 删除编辑表单的路由规则
const
removeEditRoutingRule
=
(
rule
:
ModelRoutingRule
)
=>
{
const
removeEditRoutingRule
=
(
rule
:
ModelRoutingRule
)
=>
{
const
index
=
editModelRoutingRules
.
value
.
indexOf
(
rule
)
const
index
=
editModelRoutingRules
.
value
.
indexOf
(
rule
)
;
if
(
index
===
-
1
)
return
if
(
index
===
-
1
)
return
;
const
key
=
getEditRuleSearchKey
(
rule
)
const
key
=
getEditRuleSearchKey
(
rule
)
;
accountSearchRunner
.
clearKey
(
key
)
accountSearchRunner
.
clearKey
(
key
)
;
clearAccountSearchStateByKey
(
key
)
clearAccountSearchStateByKey
(
key
)
;
editModelRoutingRules
.
value
.
splice
(
index
,
1
)
editModelRoutingRules
.
value
.
splice
(
index
,
1
)
;
}
}
;
// 将 UI 格式的路由规则转换为 API 格式
// 将 UI 格式的路由规则转换为 API 格式
const
convertRoutingRulesToApiFormat
=
(
rules
:
ModelRoutingRule
[]):
Record
<
string
,
number
[]
>
|
null
=>
{
const
convertRoutingRulesToApiFormat
=
(
const
result
:
Record
<
string
,
number
[]
>
=
{}
rules
:
ModelRoutingRule
[],
let
hasValidRules
=
false
):
Record
<
string
,
number
[]
>
|
null
=>
{
const
result
:
Record
<
string
,
number
[]
>
=
{};
let
hasValidRules
=
false
;
for
(
const
rule
of
rules
)
{
for
(
const
rule
of
rules
)
{
const
pattern
=
rule
.
pattern
.
trim
()
const
pattern
=
rule
.
pattern
.
trim
()
;
if
(
!
pattern
)
continue
if
(
!
pattern
)
continue
;
const
accountIds
=
rule
.
accounts
.
map
(
a
=>
a
.
id
).
filter
(
id
=>
id
>
0
)
const
accountIds
=
rule
.
accounts
.
map
(
(
a
)
=>
a
.
id
).
filter
(
(
id
)
=>
id
>
0
)
;
if
(
accountIds
.
length
>
0
)
{
if
(
accountIds
.
length
>
0
)
{
result
[
pattern
]
=
accountIds
result
[
pattern
]
=
accountIds
;
hasValidRules
=
true
hasValidRules
=
true
;
}
}
}
}
return
hasValidRules
?
result
:
null
return
hasValidRules
?
result
:
null
;
}
}
;
// 将 API 格式的路由规则转换为 UI 格式(需要加载账号名称)
// 将 API 格式的路由规则转换为 UI 格式(需要加载账号名称)
const
convertApiFormatToRoutingRules
=
async
(
apiFormat
:
Record
<
string
,
number
[]
>
|
null
):
Promise
<
ModelRoutingRule
[]
>
=>
{
const
convertApiFormatToRoutingRules
=
async
(
if
(
!
apiFormat
)
return
[]
apiFormat
:
Record
<
string
,
number
[]
>
|
null
,
):
Promise
<
ModelRoutingRule
[]
>
=>
{
if
(
!
apiFormat
)
return
[];
const
rules
:
ModelRoutingRule
[]
=
[]
const
rules
:
ModelRoutingRule
[]
=
[]
;
for
(
const
[
pattern
,
accountIds
]
of
Object
.
entries
(
apiFormat
))
{
for
(
const
[
pattern
,
accountIds
]
of
Object
.
entries
(
apiFormat
))
{
// 加载账号信息
// 加载账号信息
const
accounts
:
SimpleAccount
[]
=
[]
const
accounts
:
SimpleAccount
[]
=
[]
;
for
(
const
id
of
accountIds
)
{
for
(
const
id
of
accountIds
)
{
try
{
try
{
const
account
=
await
adminAPI
.
accounts
.
getById
(
id
)
const
account
=
await
adminAPI
.
accounts
.
getById
(
id
)
;
accounts
.
push
({
id
:
account
.
id
,
name
:
account
.
name
})
accounts
.
push
({
id
:
account
.
id
,
name
:
account
.
name
})
;
}
catch
{
}
catch
{
// 如果账号不存在,仍然显示 ID
// 如果账号不存在,仍然显示 ID
accounts
.
push
({
id
,
name
:
`#
${
id
}
`
})
accounts
.
push
({
id
,
name
:
`#
${
id
}
`
})
;
}
}
}
}
rules
.
push
({
pattern
,
accounts
})
rules
.
push
({
pattern
,
accounts
})
;
}
}
return
rules
return
rules
;
}
}
;
const
editForm
=
reactive
({
const
editForm
=
reactive
({
name
:
''
,
name
:
""
,
description
:
''
,
description
:
""
,
platform
:
'
anthropic
'
as
GroupPlatform
,
platform
:
"
anthropic
"
as
GroupPlatform
,
rate_multiplier
:
1.0
,
rate_multiplier
:
1.0
,
is_exclusive
:
false
,
is_exclusive
:
false
,
status
:
'
active
'
as
'
active
'
|
'
inactive
'
,
status
:
"
active
"
as
"
active
"
|
"
inactive
"
,
subscription_type
:
'
standard
'
as
SubscriptionType
,
subscription_type
:
"
standard
"
as
SubscriptionType
,
daily_limit_usd
:
null
as
number
|
null
,
daily_limit_usd
:
null
as
number
|
null
,
weekly_limit_usd
:
null
as
number
|
null
,
weekly_limit_usd
:
null
as
number
|
null
,
monthly_limit_usd
:
null
as
number
|
null
,
monthly_limit_usd
:
null
as
number
|
null
,
...
@@ -2259,92 +3241,121 @@ const editForm = reactive({
...
@@ -2259,92 +3241,121 @@ const editForm = reactive({
fallback_group_id_on_invalid_request
:
null
as
number
|
null
,
fallback_group_id_on_invalid_request
:
null
as
number
|
null
,
// OpenAI Messages 调度配置(仅 openai 平台使用)
// OpenAI Messages 调度配置(仅 openai 平台使用)
allow_messages_dispatch
:
false
,
allow_messages_dispatch
:
false
,
default_mapped_model
:
''
,
opus_mapped_model
:
editMessagesDispatchDefaults
.
opus_mapped_model
,
sonnet_mapped_model
:
editMessagesDispatchDefaults
.
sonnet_mapped_model
,
haiku_mapped_model
:
editMessagesDispatchDefaults
.
haiku_mapped_model
,
exact_model_mappings
:
[]
as
MessagesDispatchMappingRow
[],
// 账号过滤控制(OpenAI/Antigravity 平台)
// 账号过滤控制(OpenAI/Antigravity 平台)
require_oauth_only
:
false
,
require_oauth_only
:
false
,
require_privacy_set
:
false
,
require_privacy_set
:
false
,
// 模型路由开关
// 模型路由开关
model_routing_enabled
:
false
,
model_routing_enabled
:
false
,
// 支持的模型系列(仅 antigravity 平台)
// 支持的模型系列(仅 antigravity 平台)
supported_model_scopes
:
[
'
claude
'
,
'
gemini_text
'
,
'
gemini_image
'
]
as
string
[],
supported_model_scopes
:
[
"
claude
"
,
"
gemini_text
"
,
"
gemini_image
"
]
as
string
[],
// MCP XML 协议注入开关(仅 antigravity 平台)
// MCP XML 协议注入开关(仅 antigravity 平台)
mcp_xml_inject
:
true
,
mcp_xml_inject
:
true
,
// 从分组复制账号
// 从分组复制账号
copy_accounts_from_group_ids
:
[]
as
number
[]
copy_accounts_from_group_ids
:
[]
as
number
[]
,
})
})
;
// 根据分组类型返回不同的删除确认消息
// 根据分组类型返回不同的删除确认消息
const
deleteConfirmMessage
=
computed
(()
=>
{
const
deleteConfirmMessage
=
computed
(()
=>
{
if
(
!
deletingGroup
.
value
)
{
if
(
!
deletingGroup
.
value
)
{
return
''
return
""
;
}
}
if
(
deletingGroup
.
value
.
subscription_type
===
'
subscription
'
)
{
if
(
deletingGroup
.
value
.
subscription_type
===
"
subscription
"
)
{
return
t
(
'
admin.groups.deleteConfirmSubscription
'
,
{
name
:
deletingGroup
.
value
.
name
})
return
t
(
"
admin.groups.deleteConfirmSubscription
"
,
{
name
:
deletingGroup
.
value
.
name
,
});
}
}
return
t
(
'
admin.groups.deleteConfirm
'
,
{
name
:
deletingGroup
.
value
.
name
})
return
t
(
"
admin.groups.deleteConfirm
"
,
{
name
:
deletingGroup
.
value
.
name
})
;
})
})
;
const
loadGroups
=
async
()
=>
{
const
loadGroups
=
async
()
=>
{
if
(
abortController
)
{
if
(
abortController
)
{
abortController
.
abort
()
abortController
.
abort
()
;
}
}
const
currentController
=
new
AbortController
()
const
currentController
=
new
AbortController
()
;
abortController
=
currentController
abortController
=
currentController
;
const
{
signal
}
=
currentController
const
{
signal
}
=
currentController
;
loading
.
value
=
true
loading
.
value
=
true
;
try
{
try
{
const
response
=
await
adminAPI
.
groups
.
list
(
pagination
.
page
,
pagination
.
page_size
,
{
const
response
=
await
adminAPI
.
groups
.
list
(
platform
:
(
filters
.
platform
as
GroupPlatform
)
||
undefined
,
pagination
.
page
,
status
:
filters
.
status
as
any
,
pagination
.
page_size
,
is_exclusive
:
filters
.
is_exclusive
?
filters
.
is_exclusive
===
'
true
'
:
undefined
,
{
search
:
searchQuery
.
value
.
trim
()
||
undefined
platform
:
(
filters
.
platform
as
GroupPlatform
)
||
undefined
,
},
{
signal
})
status
:
filters
.
status
as
any
,
if
(
signal
.
aborted
)
return
is_exclusive
:
filters
.
is_exclusive
groups
.
value
=
response
.
items
?
filters
.
is_exclusive
===
"
true
"
pagination
.
total
=
response
.
total
:
undefined
,
pagination
.
pages
=
response
.
pages
search
:
searchQuery
.
value
.
trim
()
||
undefined
,
loadUsageSummary
()
},
loadCapacitySummary
()
{
signal
},
);
if
(
signal
.
aborted
)
return
;
groups
.
value
=
response
.
items
;
pagination
.
total
=
response
.
total
;
pagination
.
pages
=
response
.
pages
;
loadUsageSummary
();
loadCapacitySummary
();
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
if
(
signal
.
aborted
||
error
?.
name
===
'
AbortError
'
||
error
?.
code
===
'
ERR_CANCELED
'
)
{
if
(
return
signal
.
aborted
||
error
?.
name
===
"
AbortError
"
||
error
?.
code
===
"
ERR_CANCELED
"
)
{
return
;
}
}
appStore
.
showError
(
t
(
'
admin.groups.failedToLoad
'
))
appStore
.
showError
(
t
(
"
admin.groups.failedToLoad
"
))
;
console
.
error
(
'
Error loading groups:
'
,
error
)
console
.
error
(
"
Error loading groups:
"
,
error
)
;
}
finally
{
}
finally
{
if
(
abortController
===
currentController
&&
!
signal
.
aborted
)
{
if
(
abortController
===
currentController
&&
!
signal
.
aborted
)
{
loading
.
value
=
false
loading
.
value
=
false
;
}
}
}
}
}
}
;
const
formatCost
=
(
cost
:
number
):
string
=>
{
const
formatCost
=
(
cost
:
number
):
string
=>
{
if
(
cost
>=
1000
)
return
cost
.
toFixed
(
0
)
if
(
cost
>=
1000
)
return
cost
.
toFixed
(
0
)
;
if
(
cost
>=
100
)
return
cost
.
toFixed
(
1
)
if
(
cost
>=
100
)
return
cost
.
toFixed
(
1
)
;
return
cost
.
toFixed
(
2
)
return
cost
.
toFixed
(
2
)
;
}
}
;
const
loadUsageSummary
=
async
()
=>
{
const
loadUsageSummary
=
async
()
=>
{
usageLoading
.
value
=
true
usageLoading
.
value
=
true
;
try
{
try
{
const
tz
=
Intl
.
DateTimeFormat
().
resolvedOptions
().
timeZone
const
tz
=
Intl
.
DateTimeFormat
().
resolvedOptions
().
timeZone
;
const
data
=
await
adminAPI
.
groups
.
getUsageSummary
(
tz
)
const
data
=
await
adminAPI
.
groups
.
getUsageSummary
(
tz
)
;
const
map
=
new
Map
<
number
,
{
today_cost
:
number
;
total_cost
:
number
}
>
()
const
map
=
new
Map
<
number
,
{
today_cost
:
number
;
total_cost
:
number
}
>
()
;
for
(
const
item
of
data
)
{
for
(
const
item
of
data
)
{
map
.
set
(
item
.
group_id
,
{
today_cost
:
item
.
today_cost
,
total_cost
:
item
.
total_cost
})
map
.
set
(
item
.
group_id
,
{
today_cost
:
item
.
today_cost
,
total_cost
:
item
.
total_cost
,
});
}
}
usageMap
.
value
=
map
usageMap
.
value
=
map
;
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'
Error loading group usage summary:
'
,
error
)
console
.
error
(
"
Error loading group usage summary:
"
,
error
)
;
}
finally
{
}
finally
{
usageLoading
.
value
=
false
usageLoading
.
value
=
false
;
}
}
}
}
;
const
loadCapacitySummary
=
async
()
=>
{
const
loadCapacitySummary
=
async
()
=>
{
try
{
try
{
const
data
=
await
adminAPI
.
groups
.
getCapacitySummary
()
const
data
=
await
adminAPI
.
groups
.
getCapacitySummary
();
const
map
=
new
Map
<
number
,
{
concurrencyUsed
:
number
;
concurrencyMax
:
number
;
sessionsUsed
:
number
;
sessionsMax
:
number
;
rpmUsed
:
number
;
rpmMax
:
number
}
>
()
const
map
=
new
Map
<
number
,
{
concurrencyUsed
:
number
;
concurrencyMax
:
number
;
sessionsUsed
:
number
;
sessionsMax
:
number
;
rpmUsed
:
number
;
rpmMax
:
number
;
}
>
();
for
(
const
item
of
data
)
{
for
(
const
item
of
data
)
{
map
.
set
(
item
.
group_id
,
{
map
.
set
(
item
.
group_id
,
{
concurrencyUsed
:
item
.
concurrency_used
,
concurrencyUsed
:
item
.
concurrency_used
,
...
@@ -2352,313 +3363,417 @@ const loadCapacitySummary = async () => {
...
@@ -2352,313 +3363,417 @@ const loadCapacitySummary = async () => {
sessionsUsed
:
item
.
sessions_used
,
sessionsUsed
:
item
.
sessions_used
,
sessionsMax
:
item
.
sessions_max
,
sessionsMax
:
item
.
sessions_max
,
rpmUsed
:
item
.
rpm_used
,
rpmUsed
:
item
.
rpm_used
,
rpmMax
:
item
.
rpm_max
rpmMax
:
item
.
rpm_max
,
})
})
;
}
}
capacityMap
.
value
=
map
capacityMap
.
value
=
map
;
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'
Error loading group capacity summary:
'
,
error
)
console
.
error
(
"
Error loading group capacity summary:
"
,
error
)
;
}
}
}
}
;
let
searchTimeout
:
ReturnType
<
typeof
setTimeout
>
let
searchTimeout
:
ReturnType
<
typeof
setTimeout
>
;
const
handleSearch
=
()
=>
{
const
handleSearch
=
()
=>
{
clearTimeout
(
searchTimeout
)
clearTimeout
(
searchTimeout
)
;
searchTimeout
=
setTimeout
(()
=>
{
searchTimeout
=
setTimeout
(()
=>
{
pagination
.
page
=
1
pagination
.
page
=
1
;
loadGroups
()
loadGroups
()
;
},
300
)
},
300
)
;
}
}
;
const
handlePageChange
=
(
page
:
number
)
=>
{
const
handlePageChange
=
(
page
:
number
)
=>
{
pagination
.
page
=
page
pagination
.
page
=
page
;
loadGroups
()
loadGroups
()
;
}
}
;
const
handlePageSizeChange
=
(
pageSize
:
number
)
=>
{
const
handlePageSizeChange
=
(
pageSize
:
number
)
=>
{
pagination
.
page_size
=
pageSize
pagination
.
page_size
=
pageSize
;
pagination
.
page
=
1
pagination
.
page
=
1
;
loadGroups
()
loadGroups
()
;
}
}
;
const
closeCreateModal
=
()
=>
{
const
closeCreateModal
=
()
=>
{
showCreateModal
.
value
=
false
showCreateModal
.
value
=
false
;
createModelRoutingRules
.
value
.
forEach
((
rule
)
=>
{
createModelRoutingRules
.
value
.
forEach
((
rule
)
=>
{
accountSearchRunner
.
clearKey
(
getCreateRuleSearchKey
(
rule
))
accountSearchRunner
.
clearKey
(
getCreateRuleSearchKey
(
rule
));
})
});
clearAllAccountSearchState
()
clearAllAccountSearchState
();
createForm
.
name
=
''
createForm
.
name
=
""
;
createForm
.
description
=
''
createForm
.
description
=
""
;
createForm
.
platform
=
'
anthropic
'
createForm
.
platform
=
"
anthropic
"
;
createForm
.
rate_multiplier
=
1.0
createForm
.
rate_multiplier
=
1.0
;
createForm
.
is_exclusive
=
false
createForm
.
is_exclusive
=
false
;
createForm
.
subscription_type
=
'
standard
'
createForm
.
subscription_type
=
"
standard
"
;
createForm
.
daily_limit_usd
=
null
createForm
.
daily_limit_usd
=
null
;
createForm
.
weekly_limit_usd
=
null
createForm
.
weekly_limit_usd
=
null
;
createForm
.
monthly_limit_usd
=
null
createForm
.
monthly_limit_usd
=
null
;
createForm
.
image_price_1k
=
null
createForm
.
image_price_1k
=
null
;
createForm
.
image_price_2k
=
null
createForm
.
image_price_2k
=
null
;
createForm
.
image_price_4k
=
null
createForm
.
image_price_4k
=
null
;
createForm
.
claude_code_only
=
false
createForm
.
claude_code_only
=
false
;
createForm
.
fallback_group_id
=
null
createForm
.
fallback_group_id
=
null
;
createForm
.
fallback_group_id_on_invalid_request
=
null
createForm
.
fallback_group_id_on_invalid_request
=
null
;
createForm
.
allow_messages_dispatch
=
false
resetMessagesDispatchFormState
(
createForm
);
createForm
.
require_oauth_only
=
false
createForm
.
require_oauth_only
=
false
;
createForm
.
require_privacy_set
=
false
createForm
.
require_privacy_set
=
false
;
createForm
.
default_mapped_model
=
'
gpt-5.4
'
createForm
.
supported_model_scopes
=
[
"
claude
"
,
"
gemini_text
"
,
"
gemini_image
"
];
createForm
.
supported_model_scopes
=
[
'
claude
'
,
'
gemini_text
'
,
'
gemini_image
'
]
createForm
.
mcp_xml_inject
=
true
;
createForm
.
mcp_xml_inject
=
true
createForm
.
copy_accounts_from_group_ids
=
[];
createForm
.
copy_accounts_from_group_ids
=
[]
createModelRoutingRules
.
value
=
[];
createModelRoutingRules
.
value
=
[]
};
}
const
normalizeOptionalLimit
=
(
const
normalizeOptionalLimit
=
(
value
:
number
|
string
|
null
|
undefined
):
number
|
null
=>
{
value
:
number
|
string
|
null
|
undefined
,
):
number
|
null
=>
{
if
(
value
===
null
||
value
===
undefined
)
{
if
(
value
===
null
||
value
===
undefined
)
{
return
null
return
null
;
}
}
if
(
typeof
value
===
'
string
'
)
{
if
(
typeof
value
===
"
string
"
)
{
const
trimmed
=
value
.
trim
()
const
trimmed
=
value
.
trim
()
;
if
(
!
trimmed
)
{
if
(
!
trimmed
)
{
return
null
return
null
;
}
}
const
parsed
=
Number
(
trimmed
)
const
parsed
=
Number
(
trimmed
)
;
return
Number
.
isFinite
(
parsed
)
&&
parsed
>
0
?
parsed
:
null
return
Number
.
isFinite
(
parsed
)
&&
parsed
>
0
?
parsed
:
null
;
}
}
return
Number
.
isFinite
(
value
)
&&
value
>
0
?
value
:
null
return
Number
.
isFinite
(
value
)
&&
value
>
0
?
value
:
null
;
}
}
;
const
handleCreateGroup
=
async
()
=>
{
const
handleCreateGroup
=
async
()
=>
{
if
(
!
createForm
.
name
.
trim
())
{
if
(
!
createForm
.
name
.
trim
())
{
appStore
.
showError
(
t
(
'
admin.groups.nameRequired
'
))
appStore
.
showError
(
t
(
"
admin.groups.nameRequired
"
))
;
return
return
;
}
}
submitting
.
value
=
true
submitting
.
value
=
true
;
try
{
try
{
// 构建请求数据,包含模型路由配置
// 构建请求数据,包含模型路由配置
const
requestData
=
{
const
requestData
=
{
...
createForm
,
...
createForm
,
daily_limit_usd
:
normalizeOptionalLimit
(
createForm
.
daily_limit_usd
as
number
|
string
|
null
),
daily_limit_usd
:
normalizeOptionalLimit
(
weekly_limit_usd
:
normalizeOptionalLimit
(
createForm
.
weekly_limit_usd
as
number
|
string
|
null
),
createForm
.
daily_limit_usd
as
number
|
string
|
null
,
monthly_limit_usd
:
normalizeOptionalLimit
(
createForm
.
monthly_limit_usd
as
number
|
string
|
null
),
),
model_routing
:
convertRoutingRulesToApiFormat
(
createModelRoutingRules
.
value
)
weekly_limit_usd
:
normalizeOptionalLimit
(
}
createForm
.
weekly_limit_usd
as
number
|
string
|
null
,
),
monthly_limit_usd
:
normalizeOptionalLimit
(
createForm
.
monthly_limit_usd
as
number
|
string
|
null
,
),
model_routing
:
convertRoutingRulesToApiFormat
(
createModelRoutingRules
.
value
,
),
messages_dispatch_model_config
:
createForm
.
platform
===
"
openai
"
?
messagesDispatchFormStateToConfig
({
allow_messages_dispatch
:
createForm
.
allow_messages_dispatch
,
opus_mapped_model
:
createForm
.
opus_mapped_model
,
sonnet_mapped_model
:
createForm
.
sonnet_mapped_model
,
haiku_mapped_model
:
createForm
.
haiku_mapped_model
,
exact_model_mappings
:
createForm
.
exact_model_mappings
,
})
:
undefined
,
};
// v-model.number 清空输入框时产生 "",转为 null 让后端设为无限制
// v-model.number 清空输入框时产生 "",转为 null 让后端设为无限制
const
emptyToNull
=
(
v
:
any
)
=>
v
===
''
?
null
:
v
const
emptyToNull
=
(
v
:
any
)
=>
(
v
===
""
?
null
:
v
);
requestData
.
daily_limit_usd
=
emptyToNull
(
requestData
.
daily_limit_usd
)
requestData
.
daily_limit_usd
=
emptyToNull
(
requestData
.
daily_limit_usd
)
;
requestData
.
weekly_limit_usd
=
emptyToNull
(
requestData
.
weekly_limit_usd
)
requestData
.
weekly_limit_usd
=
emptyToNull
(
requestData
.
weekly_limit_usd
)
;
requestData
.
monthly_limit_usd
=
emptyToNull
(
requestData
.
monthly_limit_usd
)
requestData
.
monthly_limit_usd
=
emptyToNull
(
requestData
.
monthly_limit_usd
)
;
await
adminAPI
.
groups
.
create
(
requestData
)
await
adminAPI
.
groups
.
create
(
requestData
)
;
appStore
.
showSuccess
(
t
(
'
admin.groups.groupCreated
'
))
appStore
.
showSuccess
(
t
(
"
admin.groups.groupCreated
"
))
;
closeCreateModal
()
closeCreateModal
()
;
loadGroups
()
loadGroups
()
;
// Only advance tour if active, on submit step, and creation succeeded
// Only advance tour if active, on submit step, and creation succeeded
if
(
onboardingStore
.
isCurrentStep
(
'
[data-tour="group-form-submit"]
'
))
{
if
(
onboardingStore
.
isCurrentStep
(
'
[data-tour="group-form-submit"]
'
))
{
onboardingStore
.
nextStep
(
500
)
onboardingStore
.
nextStep
(
500
)
;
}
}
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
admin.groups.failedToCreate
'
))
appStore
.
showError
(
console
.
error
(
'
Error creating group:
'
,
error
)
error
.
response
?.
data
?.
detail
||
t
(
"
admin.groups.failedToCreate
"
),
);
console
.
error
(
"
Error creating group:
"
,
error
);
// Don't advance tour on error
// Don't advance tour on error
}
finally
{
}
finally
{
submitting
.
value
=
false
submitting
.
value
=
false
;
}
}
}
}
;
const
handleEdit
=
async
(
group
:
AdminGroup
)
=>
{
const
handleEdit
=
async
(
group
:
AdminGroup
)
=>
{
editingGroup
.
value
=
group
editingGroup
.
value
=
group
;
editForm
.
name
=
group
.
name
editForm
.
name
=
group
.
name
;
editForm
.
description
=
group
.
description
||
''
editForm
.
description
=
group
.
description
||
""
;
editForm
.
platform
=
group
.
platform
editForm
.
platform
=
group
.
platform
;
editForm
.
rate_multiplier
=
group
.
rate_multiplier
editForm
.
rate_multiplier
=
group
.
rate_multiplier
;
editForm
.
is_exclusive
=
group
.
is_exclusive
editForm
.
is_exclusive
=
group
.
is_exclusive
;
editForm
.
status
=
group
.
status
editForm
.
status
=
group
.
status
;
editForm
.
subscription_type
=
group
.
subscription_type
||
'
standard
'
editForm
.
subscription_type
=
group
.
subscription_type
||
"
standard
"
;
editForm
.
daily_limit_usd
=
group
.
daily_limit_usd
editForm
.
daily_limit_usd
=
group
.
daily_limit_usd
;
editForm
.
weekly_limit_usd
=
group
.
weekly_limit_usd
editForm
.
weekly_limit_usd
=
group
.
weekly_limit_usd
;
editForm
.
monthly_limit_usd
=
group
.
monthly_limit_usd
editForm
.
monthly_limit_usd
=
group
.
monthly_limit_usd
;
editForm
.
image_price_1k
=
group
.
image_price_1k
editForm
.
image_price_1k
=
group
.
image_price_1k
;
editForm
.
image_price_2k
=
group
.
image_price_2k
editForm
.
image_price_2k
=
group
.
image_price_2k
;
editForm
.
image_price_4k
=
group
.
image_price_4k
editForm
.
image_price_4k
=
group
.
image_price_4k
;
editForm
.
claude_code_only
=
group
.
claude_code_only
||
false
editForm
.
claude_code_only
=
group
.
claude_code_only
||
false
;
editForm
.
fallback_group_id
=
group
.
fallback_group_id
editForm
.
fallback_group_id
=
group
.
fallback_group_id
;
editForm
.
fallback_group_id_on_invalid_request
=
group
.
fallback_group_id_on_invalid_request
editForm
.
fallback_group_id_on_invalid_request
=
editForm
.
allow_messages_dispatch
=
group
.
allow_messages_dispatch
||
false
group
.
fallback_group_id_on_invalid_request
;
editForm
.
require_oauth_only
=
group
.
require_oauth_only
??
false
const
messagesDispatchFormState
=
messagesDispatchConfigToFormState
(
editForm
.
require_privacy_set
=
group
.
require_privacy_set
??
false
group
.
messages_dispatch_model_config
,
editForm
.
default_mapped_model
=
group
.
default_mapped_model
||
''
);
editForm
.
model_routing_enabled
=
group
.
model_routing_enabled
||
false
editForm
.
allow_messages_dispatch
=
editForm
.
supported_model_scopes
=
group
.
supported_model_scopes
||
[
'
claude
'
,
'
gemini_text
'
,
'
gemini_image
'
]
group
.
allow_messages_dispatch
||
editForm
.
mcp_xml_inject
=
group
.
mcp_xml_inject
??
true
messagesDispatchFormState
.
allow_messages_dispatch
;
editForm
.
copy_accounts_from_group_ids
=
[]
// 复制账号字段每次编辑时重置为空
editForm
.
opus_mapped_model
=
messagesDispatchFormState
.
opus_mapped_model
;
editForm
.
sonnet_mapped_model
=
messagesDispatchFormState
.
sonnet_mapped_model
;
editForm
.
haiku_mapped_model
=
messagesDispatchFormState
.
haiku_mapped_model
;
editForm
.
exact_model_mappings
=
messagesDispatchFormState
.
exact_model_mappings
;
editForm
.
require_oauth_only
=
group
.
require_oauth_only
??
false
;
editForm
.
require_privacy_set
=
group
.
require_privacy_set
??
false
;
editForm
.
model_routing_enabled
=
group
.
model_routing_enabled
||
false
;
editForm
.
supported_model_scopes
=
group
.
supported_model_scopes
||
[
"
claude
"
,
"
gemini_text
"
,
"
gemini_image
"
,
];
editForm
.
mcp_xml_inject
=
group
.
mcp_xml_inject
??
true
;
editForm
.
copy_accounts_from_group_ids
=
[];
// 复制账号字段每次编辑时重置为空
// 加载模型路由规则(异步加载账号名称)
// 加载模型路由规则(异步加载账号名称)
editModelRoutingRules
.
value
=
await
convertApiFormatToRoutingRules
(
group
.
model_routing
)
editModelRoutingRules
.
value
=
await
convertApiFormatToRoutingRules
(
showEditModal
.
value
=
true
group
.
model_routing
,
}
);
showEditModal
.
value
=
true
;
};
const
closeEditModal
=
()
=>
{
const
closeEditModal
=
()
=>
{
editModelRoutingRules
.
value
.
forEach
((
rule
)
=>
{
editModelRoutingRules
.
value
.
forEach
((
rule
)
=>
{
accountSearchRunner
.
clearKey
(
getEditRuleSearchKey
(
rule
))
accountSearchRunner
.
clearKey
(
getEditRuleSearchKey
(
rule
));
})
});
clearAllAccountSearchState
()
clearAllAccountSearchState
();
showEditModal
.
value
=
false
showEditModal
.
value
=
false
;
editingGroup
.
value
=
null
editingGroup
.
value
=
null
;
editModelRoutingRules
.
value
=
[]
editModelRoutingRules
.
value
=
[];
editForm
.
copy_accounts_from_group_ids
=
[]
editForm
.
copy_accounts_from_group_ids
=
[];
}
resetMessagesDispatchFormState
(
editForm
);
};
const
handleUpdateGroup
=
async
()
=>
{
const
handleUpdateGroup
=
async
()
=>
{
if
(
!
editingGroup
.
value
)
return
if
(
!
editingGroup
.
value
)
return
;
if
(
!
editForm
.
name
.
trim
())
{
if
(
!
editForm
.
name
.
trim
())
{
appStore
.
showError
(
t
(
'
admin.groups.nameRequired
'
))
appStore
.
showError
(
t
(
"
admin.groups.nameRequired
"
))
;
return
return
;
}
}
submitting
.
value
=
true
submitting
.
value
=
true
;
try
{
try
{
// 转换 fallback_group_id: null -> 0 (后端使用 0 表示清除)
// 转换 fallback_group_id: null -> 0 (后端使用 0 表示清除)
const
payload
=
{
const
payload
=
{
...
editForm
,
...
editForm
,
daily_limit_usd
:
normalizeOptionalLimit
(
editForm
.
daily_limit_usd
as
number
|
string
|
null
),
daily_limit_usd
:
normalizeOptionalLimit
(
weekly_limit_usd
:
normalizeOptionalLimit
(
editForm
.
weekly_limit_usd
as
number
|
string
|
null
),
editForm
.
daily_limit_usd
as
number
|
string
|
null
,
monthly_limit_usd
:
normalizeOptionalLimit
(
editForm
.
monthly_limit_usd
as
number
|
string
|
null
),
),
fallback_group_id
:
editForm
.
fallback_group_id
===
null
?
0
:
editForm
.
fallback_group_id
,
weekly_limit_usd
:
normalizeOptionalLimit
(
editForm
.
weekly_limit_usd
as
number
|
string
|
null
,
),
monthly_limit_usd
:
normalizeOptionalLimit
(
editForm
.
monthly_limit_usd
as
number
|
string
|
null
,
),
fallback_group_id
:
editForm
.
fallback_group_id
===
null
?
0
:
editForm
.
fallback_group_id
,
fallback_group_id_on_invalid_request
:
fallback_group_id_on_invalid_request
:
editForm
.
fallback_group_id_on_invalid_request
===
null
editForm
.
fallback_group_id_on_invalid_request
===
null
?
0
?
0
:
editForm
.
fallback_group_id_on_invalid_request
,
:
editForm
.
fallback_group_id_on_invalid_request
,
model_routing
:
convertRoutingRulesToApiFormat
(
editModelRoutingRules
.
value
)
model_routing
:
convertRoutingRulesToApiFormat
(
}
editModelRoutingRules
.
value
,
),
messages_dispatch_model_config
:
editForm
.
platform
===
"
openai
"
?
messagesDispatchFormStateToConfig
({
allow_messages_dispatch
:
editForm
.
allow_messages_dispatch
,
opus_mapped_model
:
editForm
.
opus_mapped_model
,
sonnet_mapped_model
:
editForm
.
sonnet_mapped_model
,
haiku_mapped_model
:
editForm
.
haiku_mapped_model
,
exact_model_mappings
:
editForm
.
exact_model_mappings
,
})
:
undefined
,
};
// v-model.number 清空输入框时产生 "",转为 null 让后端设为无限制
// v-model.number 清空输入框时产生 "",转为 null 让后端设为无限制
const
emptyToNull
=
(
v
:
any
)
=>
v
===
''
?
null
:
v
const
emptyToNull
=
(
v
:
any
)
=>
(
v
===
""
?
null
:
v
);
payload
.
daily_limit_usd
=
emptyToNull
(
payload
.
daily_limit_usd
)
payload
.
daily_limit_usd
=
emptyToNull
(
payload
.
daily_limit_usd
)
;
payload
.
weekly_limit_usd
=
emptyToNull
(
payload
.
weekly_limit_usd
)
payload
.
weekly_limit_usd
=
emptyToNull
(
payload
.
weekly_limit_usd
)
;
payload
.
monthly_limit_usd
=
emptyToNull
(
payload
.
monthly_limit_usd
)
payload
.
monthly_limit_usd
=
emptyToNull
(
payload
.
monthly_limit_usd
)
;
await
adminAPI
.
groups
.
update
(
editingGroup
.
value
.
id
,
payload
)
await
adminAPI
.
groups
.
update
(
editingGroup
.
value
.
id
,
payload
)
;
appStore
.
showSuccess
(
t
(
'
admin.groups.groupUpdated
'
))
appStore
.
showSuccess
(
t
(
"
admin.groups.groupUpdated
"
))
;
closeEditModal
()
closeEditModal
()
;
loadGroups
()
loadGroups
()
;
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
admin.groups.failedToUpdate
'
))
appStore
.
showError
(
console
.
error
(
'
Error updating group:
'
,
error
)
error
.
response
?.
data
?.
detail
||
t
(
"
admin.groups.failedToUpdate
"
),
);
console
.
error
(
"
Error updating group:
"
,
error
);
}
finally
{
}
finally
{
submitting
.
value
=
false
submitting
.
value
=
false
;
}
}
}
};
const
addCreateMessagesDispatchMapping
=
()
=>
{
createForm
.
exact_model_mappings
.
push
({
claude_model
:
""
,
target_model
:
""
});
};
const
removeCreateMessagesDispatchMapping
=
(
row
:
MessagesDispatchMappingRow
,
)
=>
{
const
index
=
createForm
.
exact_model_mappings
.
indexOf
(
row
);
if
(
index
!==
-
1
)
{
createForm
.
exact_model_mappings
.
splice
(
index
,
1
);
}
};
const
addEditMessagesDispatchMapping
=
()
=>
{
editForm
.
exact_model_mappings
.
push
({
claude_model
:
""
,
target_model
:
""
});
};
const
removeEditMessagesDispatchMapping
=
(
row
:
MessagesDispatchMappingRow
)
=>
{
const
index
=
editForm
.
exact_model_mappings
.
indexOf
(
row
);
if
(
index
!==
-
1
)
{
editForm
.
exact_model_mappings
.
splice
(
index
,
1
);
}
};
const
handleRateMultipliers
=
(
group
:
AdminGroup
)
=>
{
const
handleRateMultipliers
=
(
group
:
AdminGroup
)
=>
{
rateMultipliersGroup
.
value
=
group
rateMultipliersGroup
.
value
=
group
;
showRateMultipliersModal
.
value
=
true
showRateMultipliersModal
.
value
=
true
;
}
}
;
const
handleDelete
=
(
group
:
AdminGroup
)
=>
{
const
handleDelete
=
(
group
:
AdminGroup
)
=>
{
deletingGroup
.
value
=
group
deletingGroup
.
value
=
group
;
showDeleteDialog
.
value
=
true
showDeleteDialog
.
value
=
true
;
}
}
;
const
confirmDelete
=
async
()
=>
{
const
confirmDelete
=
async
()
=>
{
if
(
!
deletingGroup
.
value
)
return
if
(
!
deletingGroup
.
value
)
return
;
try
{
try
{
await
adminAPI
.
groups
.
delete
(
deletingGroup
.
value
.
id
)
await
adminAPI
.
groups
.
delete
(
deletingGroup
.
value
.
id
)
;
appStore
.
showSuccess
(
t
(
'
admin.groups.groupDeleted
'
))
appStore
.
showSuccess
(
t
(
"
admin.groups.groupDeleted
"
))
;
showDeleteDialog
.
value
=
false
showDeleteDialog
.
value
=
false
;
deletingGroup
.
value
=
null
deletingGroup
.
value
=
null
;
loadGroups
()
loadGroups
()
;
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
admin.groups.failedToDelete
'
))
appStore
.
showError
(
console
.
error
(
'
Error deleting group:
'
,
error
)
error
.
response
?.
data
?.
detail
||
t
(
"
admin.groups.failedToDelete
"
),
);
console
.
error
(
"
Error deleting group:
"
,
error
);
}
}
}
}
;
// 监听 subscription_type 变化,订阅模式时 is_exclusive 默认为 true
// 监听 subscription_type 变化,订阅模式时 is_exclusive 默认为 true
watch
(
watch
(
()
=>
createForm
.
subscription_type
,
()
=>
createForm
.
subscription_type
,
(
newVal
)
=>
{
(
newVal
)
=>
{
if
(
newVal
===
'
subscription
'
)
{
if
(
newVal
===
"
subscription
"
)
{
createForm
.
is_exclusive
=
true
createForm
.
is_exclusive
=
true
;
createForm
.
fallback_group_id_on_invalid_request
=
null
createForm
.
fallback_group_id_on_invalid_request
=
null
;
}
}
}
}
,
)
)
;
watch
(
watch
(
()
=>
createForm
.
platform
,
()
=>
createForm
.
platform
,
(
newVal
)
=>
{
(
newVal
)
=>
{
if
(
!
[
'
anthropic
'
,
'
antigravity
'
].
includes
(
newVal
))
{
if
(
!
[
"
anthropic
"
,
"
antigravity
"
].
includes
(
newVal
))
{
createForm
.
fallback_group_id_on_invalid_request
=
null
createForm
.
fallback_group_id_on_invalid_request
=
null
;
}
}
if
(
newVal
!==
'
openai
'
)
{
if
(
newVal
!==
"
openai
"
)
{
createForm
.
allow_messages_dispatch
=
false
resetMessagesDispatchFormState
(
createForm
);
createForm
.
default_mapped_model
=
''
}
}
if
(
!
[
'
openai
'
,
'
antigravity
'
,
'
anthropic
'
,
'
gemini
'
].
includes
(
newVal
))
{
if
(
!
[
"
openai
"
,
"
antigravity
"
,
"
anthropic
"
,
"
gemini
"
].
includes
(
newVal
))
{
createForm
.
require_oauth_only
=
false
createForm
.
require_oauth_only
=
false
;
createForm
.
require_privacy_set
=
false
createForm
.
require_privacy_set
=
false
;
}
}
}
},
)
);
watch
(
()
=>
editForm
.
platform
,
(
newVal
)
=>
{
if
(
!
[
"
anthropic
"
,
"
antigravity
"
].
includes
(
newVal
))
{
editForm
.
fallback_group_id_on_invalid_request
=
null
;
}
if
(
newVal
!==
"
openai
"
)
{
resetMessagesDispatchFormState
(
editForm
);
}
if
(
!
[
"
openai
"
,
"
antigravity
"
,
"
anthropic
"
,
"
gemini
"
].
includes
(
newVal
))
{
editForm
.
require_oauth_only
=
false
;
editForm
.
require_privacy_set
=
false
;
}
},
);
// 点击外部关闭账号搜索下拉框
// 点击外部关闭账号搜索下拉框
const
handleClickOutside
=
(
event
:
MouseEvent
)
=>
{
const
handleClickOutside
=
(
event
:
MouseEvent
)
=>
{
const
target
=
event
.
target
as
HTMLElement
const
target
=
event
.
target
as
HTMLElement
;
// 检查是否点击在下拉框或输入框内
// 检查是否点击在下拉框或输入框内
if
(
!
target
.
closest
(
'
.account-search-container
'
))
{
if
(
!
target
.
closest
(
"
.account-search-container
"
))
{
Object
.
keys
(
showAccountDropdown
.
value
).
forEach
(
key
=>
{
Object
.
keys
(
showAccountDropdown
.
value
).
forEach
(
(
key
)
=>
{
showAccountDropdown
.
value
[
key
]
=
false
showAccountDropdown
.
value
[
key
]
=
false
;
})
})
;
}
}
}
}
;
// 打开排序弹窗
// 打开排序弹窗
const
openSortModal
=
async
()
=>
{
const
openSortModal
=
async
()
=>
{
try
{
try
{
// 获取所有分组(不分页)
// 获取所有分组(不分页)
const
allGroups
=
await
adminAPI
.
groups
.
getAll
()
const
allGroups
=
await
adminAPI
.
groups
.
getAll
()
;
// 按 sort_order 排序
// 按 sort_order 排序
sortableGroups
.
value
=
[...
allGroups
].
sort
((
a
,
b
)
=>
a
.
sort_order
-
b
.
sort_order
)
sortableGroups
.
value
=
[...
allGroups
].
sort
(
showSortModal
.
value
=
true
(
a
,
b
)
=>
a
.
sort_order
-
b
.
sort_order
,
);
showSortModal
.
value
=
true
;
}
catch
(
error
)
{
}
catch
(
error
)
{
appStore
.
showError
(
t
(
'
admin.groups.failedToLoad
'
))
appStore
.
showError
(
t
(
"
admin.groups.failedToLoad
"
))
;
console
.
error
(
'
Error loading groups for sorting:
'
,
error
)
console
.
error
(
"
Error loading groups for sorting:
"
,
error
)
;
}
}
}
}
;
// 关闭排序弹窗
// 关闭排序弹窗
const
closeSortModal
=
()
=>
{
const
closeSortModal
=
()
=>
{
showSortModal
.
value
=
false
showSortModal
.
value
=
false
;
sortableGroups
.
value
=
[]
sortableGroups
.
value
=
[]
;
}
}
;
// 保存排序
// 保存排序
const
saveSortOrder
=
async
()
=>
{
const
saveSortOrder
=
async
()
=>
{
sortSubmitting
.
value
=
true
sortSubmitting
.
value
=
true
;
try
{
try
{
const
updates
=
sortableGroups
.
value
.
map
((
g
,
index
)
=>
({
const
updates
=
sortableGroups
.
value
.
map
((
g
,
index
)
=>
({
id
:
g
.
id
,
id
:
g
.
id
,
sort_order
:
index
*
10
sort_order
:
index
*
10
,
}))
}))
;
await
adminAPI
.
groups
.
updateSortOrder
(
updates
)
await
adminAPI
.
groups
.
updateSortOrder
(
updates
)
;
appStore
.
showSuccess
(
t
(
'
admin.groups.sortOrderUpdated
'
))
appStore
.
showSuccess
(
t
(
"
admin.groups.sortOrderUpdated
"
))
;
closeSortModal
()
closeSortModal
()
;
loadGroups
()
loadGroups
()
;
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
admin.groups.failedToUpdateSortOrder
'
))
appStore
.
showError
(
console
.
error
(
'
Error updating sort order:
'
,
error
)
error
.
response
?.
data
?.
detail
||
t
(
"
admin.groups.failedToUpdateSortOrder
"
),
);
console
.
error
(
"
Error updating sort order:
"
,
error
);
}
finally
{
}
finally
{
sortSubmitting
.
value
=
false
sortSubmitting
.
value
=
false
;
}
}
}
}
;
onMounted
(()
=>
{
onMounted
(()
=>
{
loadGroups
()
loadGroups
()
;
document
.
addEventListener
(
'
click
'
,
handleClickOutside
)
document
.
addEventListener
(
"
click
"
,
handleClickOutside
)
;
})
})
;
onUnmounted
(()
=>
{
onUnmounted
(()
=>
{
document
.
removeEventListener
(
'
click
'
,
handleClickOutside
)
document
.
removeEventListener
(
"
click
"
,
handleClickOutside
)
;
accountSearchRunner
.
clearAll
()
accountSearchRunner
.
clearAll
()
;
clearAllAccountSearchState
()
clearAllAccountSearchState
()
;
})
})
;
</
script
>
</
script
>
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