mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
Merge remote-tracking branch 'entropy-xu/codex/user-groups-default-permissions' into aether-rust-pioneer
This commit is contained in:
@@ -15,6 +15,15 @@
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 新增用户按钮 -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="分组管理"
|
||||
@click="showUserGroupsDialog = true"
|
||||
>
|
||||
<FolderKanban class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -61,6 +70,25 @@
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
v-model="filterGroup"
|
||||
>
|
||||
<SelectTrigger class="w-24 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="分组" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
全部
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
v-for="group in userGroups"
|
||||
:key="group.id"
|
||||
:value="group.id"
|
||||
>
|
||||
{{ group.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
v-model="filterStatus"
|
||||
>
|
||||
@@ -149,9 +177,38 @@
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Select v-model="filterGroup">
|
||||
<SelectTrigger class="w-32 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部分组" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
全部分组
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
v-for="group in userGroups"
|
||||
:key="group.id"
|
||||
:value="group.id"
|
||||
>
|
||||
{{ group.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="h-4 w-px bg-border" />
|
||||
|
||||
<!-- 新增用户按钮 -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="分组管理"
|
||||
@click="showUserGroupsDialog = true"
|
||||
>
|
||||
<FolderKanban class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
|
||||
<!-- 新增用户按钮 -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -207,7 +264,7 @@
|
||||
<Button
|
||||
size="sm"
|
||||
class="h-7 px-3 text-[11px]"
|
||||
:disabled="selectedCount === 0 || usersStore.loading"
|
||||
:disabled="(selectedCount === 0 && userGroups.length === 0) || usersStore.loading"
|
||||
@click="openUserBatchDialog"
|
||||
>
|
||||
批量操作
|
||||
@@ -317,6 +374,19 @@
|
||||
>
|
||||
{{ user.email || '-' }}
|
||||
</div>
|
||||
<div
|
||||
v-if="user.groups?.length"
|
||||
class="mt-1 flex flex-wrap gap-1"
|
||||
>
|
||||
<Badge
|
||||
v-for="group in user.groups"
|
||||
:key="group.id"
|
||||
variant="outline"
|
||||
class="h-5 px-1.5 py-0 text-[10px]"
|
||||
>
|
||||
{{ group.name }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -550,9 +620,18 @@
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="h-5 px-1.5 py-0 text-[10px] font-medium"
|
||||
:title="formatUserEffectiveRateLimitSource(user)"
|
||||
>
|
||||
{{ formatRateLimitInheritable(user.rate_limit) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-for="group in user.groups || []"
|
||||
:key="group.id"
|
||||
variant="outline"
|
||||
class="h-5 px-1.5 py-0 text-[10px] font-medium"
|
||||
>
|
||||
{{ group.name }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-border/60 bg-muted/40 p-3.5">
|
||||
@@ -697,6 +776,7 @@
|
||||
ref="userFormDialogRef"
|
||||
:open="showUserFormDialog"
|
||||
:user="editingUser"
|
||||
:groups="userGroups"
|
||||
@close="closeUserFormDialog"
|
||||
@submit="handleUserFormSubmit"
|
||||
/>
|
||||
@@ -707,10 +787,18 @@
|
||||
:select-all-filtered="selectAllFiltered"
|
||||
:selected-count="selectedCount"
|
||||
:filters="batchSelectionFilters"
|
||||
:groups="userGroups"
|
||||
@close="showUserBatchDialog = false"
|
||||
@completed="handleUserBatchCompleted"
|
||||
/>
|
||||
|
||||
<UserGroupsDialog
|
||||
:open="showUserGroupsDialog"
|
||||
:users="usersStore.users"
|
||||
@close="showUserGroupsDialog = false"
|
||||
@changed="handleUserGroupsChanged"
|
||||
/>
|
||||
|
||||
<!-- API Keys 管理对话框 -->
|
||||
<Dialog
|
||||
v-model="showApiKeysDialog"
|
||||
@@ -1134,7 +1222,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useUsersStore } from '@/stores/users'
|
||||
import type { User, ApiKey, UserSession, UserBatchActionResponse, UserBatchSelectionFilters } from '@/api/users'
|
||||
import type { User, ApiKey, UserSession, UserBatchActionResponse, UserBatchSelectionFilters, UserGroup } from '@/api/users'
|
||||
import { formatSessionMeta } from '@/types/session'
|
||||
import { adminWalletApi, type AdminWallet } from '@/api/admin-wallets'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
@@ -1184,12 +1272,14 @@ import {
|
||||
CheckCircle,
|
||||
Lock,
|
||||
LockOpen,
|
||||
MonitorSmartphone
|
||||
MonitorSmartphone,
|
||||
FolderKanban,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
// 功能组件
|
||||
import UserFormDialog, { type UserFormData } from '@/features/users/components/UserFormDialog.vue'
|
||||
import UserBatchActionDialog from '@/features/users/components/UserBatchActionDialog.vue'
|
||||
import UserGroupsDialog from '@/features/users/components/UserGroupsDialog.vue'
|
||||
import WalletOpsDrawer from '@/features/wallet/components/WalletOpsDrawer.vue'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { formatTokens, formatRateLimitInheritable, formatRateLimitSimple, isRateLimitInherited, isRateLimitUnlimited } from '@/utils/format'
|
||||
@@ -1233,10 +1323,13 @@ const userWalletMap = ref<Record<string, AdminWallet>>({})
|
||||
const showWalletActionDialogState = ref(false)
|
||||
const walletActionTarget = ref<{ user: User; wallet: AdminWallet } | null>(null)
|
||||
const showUserBatchDialog = ref(false)
|
||||
const showUserGroupsDialog = ref(false)
|
||||
|
||||
const searchQuery = ref('')
|
||||
const filterRole = ref('all')
|
||||
const filterStatus = ref('all')
|
||||
const filterGroup = ref('all')
|
||||
const userGroups = ref<UserGroup[]>([])
|
||||
const userRoleFilterOptions = [
|
||||
{ value: 'all', label: '全部角色' },
|
||||
{ value: 'admin', label: '管理员' },
|
||||
@@ -1285,6 +1378,10 @@ const filteredUsers = computed(() => {
|
||||
)
|
||||
}
|
||||
|
||||
if (filterGroup.value !== 'all') {
|
||||
filtered = filtered.filter(u => (u.groups || []).some(group => group.id === filterGroup.value))
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
@@ -1322,11 +1419,12 @@ const batchSelectionFilters = computed<UserBatchSelectionFilters>(() => {
|
||||
if (filterRole.value === 'admin' || filterRole.value === 'user') filters.role = filterRole.value
|
||||
if (filterStatus.value === 'active') filters.is_active = true
|
||||
if (filterStatus.value === 'inactive') filters.is_active = false
|
||||
if (filterGroup.value !== 'all') filters.group_id = filterGroup.value
|
||||
return filters
|
||||
})
|
||||
|
||||
// Watch filter changes and reset to first page
|
||||
watch([searchQuery, filterRole, filterStatus], () => {
|
||||
watch([searchQuery, filterRole, filterStatus, filterGroup], () => {
|
||||
currentPage.value = 1
|
||||
resetBatchSelection()
|
||||
})
|
||||
@@ -1339,14 +1437,33 @@ onMounted(() => {
|
||||
|
||||
async function refreshUsers(options: { preferCache?: boolean } = {}) {
|
||||
const cacheTtlMs = options.preferCache ? USERS_PAGE_CACHE_TTL_MS : 0
|
||||
await usersStore.fetchUsers({ cacheTtlMs })
|
||||
await Promise.all([
|
||||
usersStore.fetchUsers({ cacheTtlMs }),
|
||||
loadUserGroups(),
|
||||
])
|
||||
void loadUserWallets({
|
||||
cacheTtlMs: options.preferCache ? USER_WALLETS_CACHE_TTL_MS : 0,
|
||||
})
|
||||
}
|
||||
|
||||
async function loadUserGroups(): Promise<void> {
|
||||
try {
|
||||
const response = await usersStore.listUserGroups()
|
||||
userGroups.value = response.items
|
||||
if (filterGroup.value !== 'all' && !userGroups.value.some((group) => group.id === filterGroup.value)) {
|
||||
filterGroup.value = 'all'
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('加载用户分组失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUserGroupsChanged(): Promise<void> {
|
||||
await refreshUsers()
|
||||
}
|
||||
|
||||
function openUserBatchDialog(): void {
|
||||
if (selectedCount.value === 0) return
|
||||
if (selectedCount.value === 0 && userGroups.value.length === 0) return
|
||||
showUserBatchDialog.value = true
|
||||
}
|
||||
|
||||
@@ -1429,6 +1546,18 @@ function formatConcurrentLimitSimple(concurrentLimit?: number | null): string {
|
||||
return `${concurrentLimit} 并发`
|
||||
}
|
||||
|
||||
function formatUserEffectiveRateLimitSource(user: User): string {
|
||||
const source = user.effective_policy?.rate_limit
|
||||
if (!source) return ''
|
||||
if (source.source === 'group' && source.group_name) {
|
||||
return `继承自分组:${source.group_name}`
|
||||
}
|
||||
if (source.source === 'user') {
|
||||
return '用户单独配置'
|
||||
}
|
||||
return '系统默认'
|
||||
}
|
||||
|
||||
function isNegativeWalletValue(value: number | null): boolean {
|
||||
return typeof value === 'number' && value < 0
|
||||
}
|
||||
@@ -1470,7 +1599,12 @@ function editUser(user: User) {
|
||||
allowed_providers: user.allowed_providers == null ? null : [...user.allowed_providers],
|
||||
allowed_api_formats: user.allowed_api_formats == null ? null : [...user.allowed_api_formats],
|
||||
allowed_models: user.allowed_models == null ? null : [...user.allowed_models],
|
||||
rate_limit: user.rate_limit ?? null
|
||||
rate_limit: user.rate_limit ?? null,
|
||||
allowed_providers_mode: user.allowed_providers_mode ?? (user.allowed_providers == null ? 'unrestricted' : 'specific'),
|
||||
allowed_api_formats_mode: user.allowed_api_formats_mode ?? (user.allowed_api_formats == null ? 'unrestricted' : 'specific'),
|
||||
allowed_models_mode: user.allowed_models_mode ?? (user.allowed_models == null ? 'unrestricted' : 'specific'),
|
||||
rate_limit_mode: user.rate_limit_mode ?? (user.rate_limit == null ? 'system' : 'custom'),
|
||||
group_ids: (user.groups || []).map(group => group.id),
|
||||
}
|
||||
showUserFormDialog.value = true
|
||||
}
|
||||
@@ -1491,9 +1625,14 @@ async function handleUserFormSubmit(data: UserFormData & { password?: string; un
|
||||
unlimited: data.unlimited,
|
||||
role: data.role,
|
||||
allowed_providers: data.allowed_providers,
|
||||
allowed_providers_mode: data.allowed_providers_mode,
|
||||
allowed_api_formats: data.allowed_api_formats,
|
||||
allowed_api_formats_mode: data.allowed_api_formats_mode,
|
||||
allowed_models: data.allowed_models,
|
||||
rate_limit: data.rate_limit ?? null
|
||||
allowed_models_mode: data.allowed_models_mode,
|
||||
rate_limit: data.rate_limit ?? null,
|
||||
rate_limit_mode: data.rate_limit_mode,
|
||||
group_ids: data.group_ids ?? [],
|
||||
}
|
||||
if (data.password) {
|
||||
updateData.password = data.password
|
||||
@@ -1511,9 +1650,14 @@ async function handleUserFormSubmit(data: UserFormData & { password?: string; un
|
||||
unlimited: data.unlimited,
|
||||
role: data.role,
|
||||
allowed_providers: data.allowed_providers,
|
||||
allowed_providers_mode: data.allowed_providers_mode,
|
||||
allowed_api_formats: data.allowed_api_formats,
|
||||
allowed_api_formats_mode: data.allowed_api_formats_mode,
|
||||
allowed_models: data.allowed_models,
|
||||
rate_limit: data.rate_limit ?? null
|
||||
allowed_models_mode: data.allowed_models_mode,
|
||||
rate_limit: data.rate_limit ?? null,
|
||||
rate_limit_mode: data.rate_limit_mode,
|
||||
group_ids: data.group_ids ?? [],
|
||||
})
|
||||
// 如果创建时指定为禁用,则更新状态
|
||||
if (data.is_active === false && newUser) {
|
||||
|
||||
Reference in New Issue
Block a user