feat(users): refine batch action dialog

This commit is contained in:
RWDai
2026-05-07 22:22:55 +08:00
parent 1e2ff26e86
commit fcefa4d198
2 changed files with 356 additions and 148 deletions

View File

@@ -2,11 +2,13 @@ import apiClient from './client'
import { cachedRequest } from '@/utils/cache' import { cachedRequest } from '@/utils/cache'
import type { UserSession as SessionRecord } from '@/types/session' import type { UserSession as SessionRecord } from '@/types/session'
export type UserRole = 'admin' | 'user'
export interface User { export interface User {
id: string // UUID id: string // UUID
username: string username: string
email: string email: string
role: 'admin' | 'user' role: UserRole
is_active: boolean is_active: boolean
unlimited: boolean unlimited: boolean
allowed_providers: string[] | null // 允许使用的提供商 ID 列表 allowed_providers: string[] | null // 允许使用的提供商 ID 列表
@@ -24,7 +26,7 @@ export interface CreateUserRequest {
username: string username: string
password: string password: string
email: string email: string
role?: 'admin' | 'user' role?: UserRole
initial_gift_usd?: number | null initial_gift_usd?: number | null
unlimited?: boolean unlimited?: boolean
allowed_providers?: string[] | null allowed_providers?: string[] | null
@@ -36,7 +38,7 @@ export interface CreateUserRequest {
export interface UpdateUserRequest { export interface UpdateUserRequest {
email?: string email?: string
is_active?: boolean is_active?: boolean
role?: 'admin' | 'user' role?: UserRole
unlimited?: boolean unlimited?: boolean
password?: string password?: string
allowed_providers?: string[] | null allowed_providers?: string[] | null
@@ -47,7 +49,7 @@ export interface UpdateUserRequest {
export interface UserBatchSelectionFilters { export interface UserBatchSelectionFilters {
search?: string search?: string
role?: 'admin' | 'user' role?: UserRole
is_active?: boolean is_active?: boolean
} }
@@ -60,7 +62,7 @@ export interface UserBatchSelectionItem {
user_id: string user_id: string
username: string username: string
email?: string | null email?: string | null
role: 'admin' | 'user' role: UserRole
is_active: boolean is_active: boolean
} }
@@ -74,16 +76,40 @@ export interface UserBatchAccessControlPayload {
allowed_api_formats?: string[] | null allowed_api_formats?: string[] | null
allowed_models?: string[] | null allowed_models?: string[] | null
rate_limit?: number | null rate_limit?: number | null
unlimited?: boolean
} }
export type UserBatchAction = 'enable' | 'disable' | 'update_access_control' export interface UserBatchRolePayload {
role: UserRole
}
export interface UserBatchActionRequest { export type UserBatchAction = 'enable' | 'disable' | 'update_access_control' | 'update_role'
export type UserBatchActionPayload = UserBatchAccessControlPayload | UserBatchRolePayload
export interface UserBatchToggleActionRequest {
selection: UserBatchSelection selection: UserBatchSelection
action: UserBatchAction action: 'enable' | 'disable'
payload?: UserBatchAccessControlPayload | null payload?: null
} }
export interface UserBatchAccessControlActionRequest {
selection: UserBatchSelection
action: 'update_access_control'
payload: UserBatchAccessControlPayload
}
export interface UserBatchRoleActionRequest {
selection: UserBatchSelection
action: 'update_role'
payload: UserBatchRolePayload
}
export type UserBatchActionRequest =
| UserBatchToggleActionRequest
| UserBatchAccessControlActionRequest
| UserBatchRoleActionRequest
export interface UserBatchActionFailure { export interface UserBatchActionFailure {
user_id: string user_id: string
reason: string reason: string

View File

@@ -2,41 +2,43 @@
<Dialog <Dialog
:model-value="open" :model-value="open"
title="用户批量操作" title="用户批量操作"
description="按当前选择批量启用、禁用或更新访问控制" description="按当前选择批量调整用户状态、角色、访问控制和额度"
size="2xl" size="2xl"
persistent persistent
@update:model-value="handleDialogUpdate" @update:model-value="handleDialogUpdate"
> >
<div class="space-y-4"> <div class="space-y-5">
<div class="rounded-lg border bg-muted/30 px-3 py-3 text-sm"> <div class="rounded-2xl border border-primary/15 bg-gradient-to-br from-primary/10 via-background to-muted/40 p-4 shadow-sm">
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-start justify-between gap-3">
<div> <div class="min-w-0 space-y-1">
<div class="font-medium text-foreground"> <div class="flex items-center gap-2 text-sm font-semibold text-foreground">
影响用户{{ impactCount }} <UsersRound class="h-4 w-4 text-primary" />
<span>影响用户{{ impactCount }} </span>
</div> </div>
<div class="mt-1 text-xs text-muted-foreground"> <p class="text-xs leading-relaxed text-muted-foreground">
{{ selectAllFiltered ? '目标为当前筛选条件匹配的全部用户,执行前后端会重新解析。' : '目标为当前已勾选的用户,重复 ID 会自动去重。' }} {{ selectAllFiltered ? '目标为当前筛选条件匹配的全部用户,执行前后端会重新解析。' : '目标为当前已勾选的用户,重复 ID 会自动去重。' }}
</div> </p>
</div> </div>
<Badge variant="secondary"> <Badge variant="secondary" class="shrink-0">
{{ selectAllFiltered ? '全选筛选结果' : '手动选择' }} {{ selectAllFiltered ? '全选筛选结果' : '手动选择' }}
</Badge> </Badge>
</div> </div>
<div <div
v-if="previewLoading" v-if="previewLoading"
class="mt-3 text-xs text-muted-foreground" class="mt-3 rounded-xl border border-border/60 bg-background/65 px-3 py-2 text-xs text-muted-foreground"
> >
正在解析影响范围... 正在解析影响范围...
</div> </div>
<div <div
v-else-if="previewItems.length > 0" v-else-if="previewItems.length > 0"
class="mt-3 flex flex-wrap gap-1.5" class="mt-3 flex flex-wrap items-center gap-1.5"
> >
<Badge <Badge
v-for="item in previewItems" v-for="item in previewItems"
:key="item.user_id" :key="item.user_id"
variant="outline" variant="outline"
class="text-[11px]" class="bg-background/70 text-[11px]"
> >
{{ item.username }} {{ item.username }}
</Badge> </Badge>
@@ -49,136 +51,230 @@
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2.5">
<Label class="text-sm font-medium">批量动作</Label> <div class="flex items-center justify-between gap-3">
<Select v-model="selectedAction"> <Label class="text-sm font-medium">选择批量动作</Label>
<SelectTrigger class="h-10 w-full"> <span class="text-[11px] text-muted-foreground">只会提交当前动作对应的字段</span>
<SelectValue /> </div>
</SelectTrigger> <div class="grid gap-2 md:grid-cols-4">
<SelectContent> <button
<SelectItem value="enable"> v-for="action in actionOptions"
批量启用用户 :key="action.value"
</SelectItem> type="button"
<SelectItem value="disable"> :class="actionCardClass(action.value)"
批量禁用用户 @click="selectedAction = action.value"
</SelectItem> >
<SelectItem value="update_access_control"> <span class="flex items-center gap-2">
批量设置访问控制 <span :class="actionIconClass(action.value)">
</SelectItem> <component :is="action.icon" class="h-4 w-4" />
</SelectContent> </span>
</Select> <span class="font-medium text-foreground">{{ action.label }}</span>
</span>
<span class="mt-1 block text-[11px] leading-relaxed text-muted-foreground">
{{ action.description }}
</span>
</button>
</div>
</div>
<div
v-if="selectedAction === 'update_role'"
class="space-y-4 rounded-2xl border bg-background p-4 shadow-sm"
>
<div class="flex items-start gap-3">
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
<UserCog class="h-4 w-4" />
</div>
<div class="min-w-0 space-y-1">
<h4 class="text-sm font-semibold text-foreground">批量修改用户角色</h4>
<p class="text-xs leading-relaxed text-muted-foreground">
将所选用户统一调整为同一个角色管理员角色拥有后台管理权限请确认选择范围
</p>
</div>
</div>
<div class="grid gap-3 rounded-xl border border-border/70 bg-muted/25 p-3 sm:grid-cols-[10rem_minmax(0,1fr)] sm:items-center">
<div>
<Label class="text-sm font-medium">目标角色</Label>
<p class="mt-1 text-[11px] text-muted-foreground">对所有目标用户生效</p>
</div>
<Select v-model="targetRole">
<SelectTrigger class="h-10 w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">普通用户</SelectItem>
<SelectItem value="admin">管理员</SelectItem>
</SelectContent>
</Select>
</div>
<div class="rounded-xl border border-amber-200/70 bg-amber-50/70 px-3 py-2.5 text-xs leading-relaxed text-amber-800 dark:border-amber-900/50 dark:bg-amber-950/30 dark:text-amber-200">
{{ targetRole === 'admin' ? '提示:设置为管理员会授予用户后台管理能力。' : '提示:设置为普通用户会移除目标用户的管理员权限。' }}
</div>
</div> </div>
<div <div
v-if="selectedAction === 'update_access_control'" v-if="selectedAction === 'update_access_control'"
class="space-y-3 rounded-lg border bg-background px-3 py-3" class="space-y-4 rounded-2xl border bg-background p-4 shadow-sm"
> >
<div class="text-xs text-muted-foreground"> <div class="flex items-start gap-3">
每个字段都可独立选择不修改 / 不限制 / 指定列表指定列表为空表示全部禁用 <div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
</div> <ShieldCheck class="h-4 w-4" />
</div>
<div class="space-y-2"> <div class="min-w-0 space-y-1">
<Label class="text-sm font-medium">允许的提供商</Label> <h4 class="text-sm font-semibold text-foreground">批量设置访问控制与额度</h4>
<div class="grid gap-2 md:grid-cols-[10rem_minmax(0,1fr)]"> <p class="text-xs leading-relaxed text-muted-foreground">
<Select v-model="providerMode"> 每个字段都可独立选择不修改 / 不限制 / 指定列表指定列表为空表示全部禁用
<SelectTrigger class="h-9"> </p>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="skip">不修改</SelectItem>
<SelectItem value="unrestricted">不限制</SelectItem>
<SelectItem value="specific">指定列表</SelectItem>
</SelectContent>
</Select>
<MultiSelect
v-model="allowedProviders"
:options="providerOptions"
:disabled="providerMode !== 'specific'"
:search-threshold="0"
placeholder="未选择时表示全部禁用"
empty-text="暂无可用提供商"
/>
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="grid gap-3">
<Label class="text-sm font-medium">允许的端点</Label> <div class="rounded-xl border border-border/70 bg-muted/20 p-3">
<div class="grid gap-2 md:grid-cols-[10rem_minmax(0,1fr)]"> <div class="grid gap-3 lg:grid-cols-[9rem_minmax(0,1fr)] lg:items-start">
<Select v-model="apiFormatMode"> <div>
<SelectTrigger class="h-9"> <Label class="text-sm font-medium">允许的提供商</Label>
<SelectValue /> <p class="mt-1 text-[11px] text-muted-foreground">控制可使用的供应商</p>
</SelectTrigger> </div>
<SelectContent> <div class="grid gap-2 sm:grid-cols-[9rem_minmax(0,1fr)]">
<SelectItem value="skip">不修改</SelectItem> <Select v-model="providerMode">
<SelectItem value="unrestricted">不限制</SelectItem> <SelectTrigger class="h-9">
<SelectItem value="specific">指定列表</SelectItem> <SelectValue />
</SelectContent> </SelectTrigger>
</Select> <SelectContent>
<MultiSelect <SelectItem value="skip">不修改</SelectItem>
v-model="allowedApiFormats" <SelectItem value="unrestricted">不限制</SelectItem>
:options="apiFormatOptions" <SelectItem value="specific">指定列表</SelectItem>
:disabled="apiFormatMode !== 'specific'" </SelectContent>
:search-threshold="0" </Select>
placeholder="未选择时表示全部禁用" <MultiSelect
empty-text="暂无可用端点" v-model="allowedProviders"
/> :options="providerOptions"
:disabled="providerMode !== 'specific'"
:search-threshold="0"
placeholder="未选择时表示全部禁用"
empty-text="暂无可用提供商"
/>
</div>
</div>
</div> </div>
</div>
<div class="space-y-2"> <div class="rounded-xl border border-border/70 bg-muted/20 p-3">
<Label class="text-sm font-medium">允许的模型</Label> <div class="grid gap-3 lg:grid-cols-[9rem_minmax(0,1fr)] lg:items-start">
<div class="grid gap-2 md:grid-cols-[10rem_minmax(0,1fr)]"> <div>
<Select v-model="modelMode"> <Label class="text-sm font-medium">允许的端点</Label>
<SelectTrigger class="h-9"> <p class="mt-1 text-[11px] text-muted-foreground">控制 API 格式入口</p>
<SelectValue /> </div>
</SelectTrigger> <div class="grid gap-2 sm:grid-cols-[9rem_minmax(0,1fr)]">
<SelectContent> <Select v-model="apiFormatMode">
<SelectItem value="skip">不修改</SelectItem> <SelectTrigger class="h-9">
<SelectItem value="unrestricted">不限制</SelectItem> <SelectValue />
<SelectItem value="specific">指定列表</SelectItem> </SelectTrigger>
</SelectContent> <SelectContent>
</Select> <SelectItem value="skip">不修改</SelectItem>
<MultiSelect <SelectItem value="unrestricted">不限制</SelectItem>
v-model="allowedModels" <SelectItem value="specific">指定列表</SelectItem>
:options="modelOptions" </SelectContent>
:disabled="modelMode !== 'specific'" </Select>
:search-threshold="0" <MultiSelect
placeholder="未选择时表示全部禁用" v-model="allowedApiFormats"
empty-text="暂无可用模型" :options="apiFormatOptions"
/> :disabled="apiFormatMode !== 'specific'"
:search-threshold="0"
placeholder="未选择时表示全部禁用"
empty-text="暂无可用端点"
/>
</div>
</div>
</div> </div>
</div>
<div class="space-y-2"> <div class="rounded-xl border border-border/70 bg-muted/20 p-3">
<Label class="text-sm font-medium">速率限制</Label> <div class="grid gap-3 lg:grid-cols-[9rem_minmax(0,1fr)] lg:items-start">
<div class="grid gap-2 md:grid-cols-[10rem_minmax(0,1fr)]"> <div>
<Select v-model="rateLimitMode"> <Label class="text-sm font-medium">允许的模型</Label>
<SelectTrigger class="h-9"> <p class="mt-1 text-[11px] text-muted-foreground">控制模型白名单</p>
<SelectValue /> </div>
</SelectTrigger> <div class="grid gap-2 sm:grid-cols-[9rem_minmax(0,1fr)]">
<SelectContent> <Select v-model="modelMode">
<SelectItem value="skip">不修改</SelectItem> <SelectTrigger class="h-9">
<SelectItem value="inherit">跟随默认</SelectItem> <SelectValue />
<SelectItem value="custom">指定数值</SelectItem> </SelectTrigger>
</SelectContent> <SelectContent>
</Select> <SelectItem value="skip">不修改</SelectItem>
<Input <SelectItem value="unrestricted">不限制</SelectItem>
:model-value="rateLimit ?? ''" <SelectItem value="specific">指定列表</SelectItem>
type="number" </SelectContent>
min="0" </Select>
max="10000" <MultiSelect
class="h-9" v-model="allowedModels"
:disabled="rateLimitMode !== 'custom'" :options="modelOptions"
placeholder="0 = 不限速" :disabled="modelMode !== 'specific'"
@update:model-value="(value) => rateLimit = parseNumberInput(value, { min: 0, max: 10000 })" :search-threshold="0"
/> placeholder="未选择时表示全部禁用"
empty-text="暂无可用模型"
/>
</div>
</div>
</div>
<div class="grid gap-3 md:grid-cols-2">
<div class="rounded-xl border border-border/70 bg-muted/20 p-3">
<div class="space-y-2">
<div>
<Label class="text-sm font-medium">速率限制</Label>
<p class="mt-1 text-[11px] text-muted-foreground">请求/分钟0 表示不限速</p>
</div>
<div class="grid gap-2 sm:grid-cols-[9rem_minmax(0,1fr)] md:grid-cols-1 xl:grid-cols-[9rem_minmax(0,1fr)]">
<Select v-model="rateLimitMode">
<SelectTrigger class="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="skip">不修改</SelectItem>
<SelectItem value="inherit">跟随默认</SelectItem>
<SelectItem value="custom">指定数值</SelectItem>
</SelectContent>
</Select>
<Input
:model-value="rateLimit ?? ''"
type="number"
min="0"
max="10000"
class="h-9"
:disabled="rateLimitMode !== 'custom'"
placeholder="0 = 不限速"
@update:model-value="(value) => rateLimit = parseNumberInput(value, { min: 0, max: 10000 })"
/>
</div>
</div>
</div>
<div class="rounded-xl border border-border/70 bg-muted/20 p-3">
<div class="space-y-2">
<div>
<Label class="text-sm font-medium">额度</Label>
<p class="mt-1 text-[11px] text-muted-foreground">与单用户编辑保持一致</p>
</div>
<Select v-model="quotaMode">
<SelectTrigger class="h-9 w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="skip">不修改</SelectItem>
<SelectItem value="wallet">按钱包余额限制</SelectItem>
<SelectItem value="unlimited">无限额度</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div <div
v-if="lastResult" v-if="lastResult"
class="rounded-lg border bg-muted/20 px-3 py-2 text-xs text-muted-foreground" class="rounded-xl border bg-muted/20 px-3 py-2 text-xs text-muted-foreground"
> >
成功 {{ lastResult.success }} 个,失败 {{ lastResult.failed }} 个 成功 {{ lastResult.success }} 个,失败 {{ lastResult.failed }} 个
<span v-if="lastResult.failures.length > 0"> <span v-if="lastResult.failures.length > 0">
@@ -199,14 +295,21 @@
:disabled="!canExecute" :disabled="!canExecute"
@click="executeBatchAction" @click="executeBatchAction"
> >
{{ executing ? '执行中...' : `确认执行(${impactCount}` }} {{ executing ? '执行中...' : executeButtonLabel }}
</Button> </Button>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue' import { computed, ref, watch, type Component } from 'vue'
import {
Ban,
CheckCircle2,
ShieldCheck,
UserCog,
UsersRound,
} from 'lucide-vue-next'
import { import {
Dialog, Dialog,
Button, Button,
@@ -224,18 +327,30 @@ import { useUsersStore } from '@/stores/users'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
import { parseApiError } from '@/utils/errorParser' import { parseApiError } from '@/utils/errorParser'
import { parseNumberInput } from '@/utils/form' import { parseNumberInput } from '@/utils/form'
import { cn } from '@/lib/utils'
import { useUserAccessControlOptions } from '@/features/users/composables/useUserAccessControlOptions' import { useUserAccessControlOptions } from '@/features/users/composables/useUserAccessControlOptions'
import type { import type {
UserBatchAccessControlPayload, UserBatchAccessControlPayload,
UserBatchAction, UserBatchAction,
UserBatchActionRequest,
UserBatchActionResponse, UserBatchActionResponse,
UserBatchRolePayload,
UserBatchSelection, UserBatchSelection,
UserBatchSelectionFilters, UserBatchSelectionFilters,
UserBatchSelectionItem, UserBatchSelectionItem,
UserRole,
} from '@/api/users' } from '@/api/users'
type AccessFieldMode = 'skip' | 'unrestricted' | 'specific' type AccessFieldMode = 'skip' | 'unrestricted' | 'specific'
type RateLimitMode = 'skip' | 'inherit' | 'custom' type RateLimitMode = 'skip' | 'inherit' | 'custom'
type QuotaMode = 'skip' | 'wallet' | 'unlimited'
interface ActionOption {
value: UserBatchAction
label: string
description: string
icon: Component
}
const props = defineProps<{ const props = defineProps<{
open: boolean open: boolean
@@ -259,11 +374,40 @@ const {
loadAccessControlOptions, loadAccessControlOptions,
} = useUserAccessControlOptions() } = useUserAccessControlOptions()
const actionOptions: ActionOption[] = [
{
value: 'enable',
label: '启用',
description: '恢复用户登录与调用',
icon: CheckCircle2,
},
{
value: 'disable',
label: '禁用',
description: '暂停用户访问权限',
icon: Ban,
},
{
value: 'update_access_control',
label: '访问控制',
description: '提供商、端点、模型、限速和额度',
icon: ShieldCheck,
},
{
value: 'update_role',
label: '修改角色',
description: '批量设为普通用户或管理员',
icon: UserCog,
},
]
const selectedAction = ref<UserBatchAction>('enable') const selectedAction = ref<UserBatchAction>('enable')
const targetRole = ref<UserRole>('user')
const providerMode = ref<AccessFieldMode>('skip') const providerMode = ref<AccessFieldMode>('skip')
const apiFormatMode = ref<AccessFieldMode>('skip') const apiFormatMode = ref<AccessFieldMode>('skip')
const modelMode = ref<AccessFieldMode>('skip') const modelMode = ref<AccessFieldMode>('skip')
const rateLimitMode = ref<RateLimitMode>('skip') const rateLimitMode = ref<RateLimitMode>('skip')
const quotaMode = ref<QuotaMode>('skip')
const allowedProviders = ref<string[]>([]) const allowedProviders = ref<string[]>([])
const allowedApiFormats = ref<string[]>([]) const allowedApiFormats = ref<string[]>([])
const allowedModels = ref<string[]>([]) const allowedModels = ref<string[]>([])
@@ -276,6 +420,10 @@ const lastResult = ref<UserBatchActionResponse | null>(null)
const impactCount = computed(() => resolvedTotal.value ?? props.selectedCount) const impactCount = computed(() => resolvedTotal.value ?? props.selectedCount)
const canExecute = computed(() => props.selectedCount > 0 && !previewLoading.value && !executing.value) const canExecute = computed(() => props.selectedCount > 0 && !previewLoading.value && !executing.value)
const selectedActionLabel = computed(() => (
actionOptions.find((action) => action.value === selectedAction.value)?.label ?? '批量操作'
))
const executeButtonLabel = computed(() => `确认${selectedActionLabel.value}${impactCount.value}`)
watch( watch(
() => props.open, () => props.open,
@@ -302,10 +450,12 @@ function handleDialogUpdate(value: boolean): void {
function resetLocalState(): void { function resetLocalState(): void {
selectedAction.value = 'enable' selectedAction.value = 'enable'
targetRole.value = 'user'
providerMode.value = 'skip' providerMode.value = 'skip'
apiFormatMode.value = 'skip' apiFormatMode.value = 'skip'
modelMode.value = 'skip' modelMode.value = 'skip'
rateLimitMode.value = 'skip' rateLimitMode.value = 'skip'
quotaMode.value = 'skip'
allowedProviders.value = [] allowedProviders.value = []
allowedApiFormats.value = [] allowedApiFormats.value = []
allowedModels.value = [] allowedModels.value = []
@@ -313,6 +463,24 @@ function resetLocalState(): void {
lastResult.value = null lastResult.value = null
} }
function actionCardClass(action: UserBatchAction): string {
return cn(
'rounded-xl border p-3 text-left transition-all hover:-translate-y-0.5 hover:border-primary/35 hover:bg-primary/5 hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/30',
selectedAction.value === action
? 'border-primary/60 bg-primary/10 shadow-sm ring-1 ring-primary/20'
: 'border-border/70 bg-background',
)
}
function actionIconClass(action: UserBatchAction): string {
return cn(
'flex h-7 w-7 items-center justify-center rounded-lg transition-colors',
selectedAction.value === action
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground',
)
}
function buildSelection(): UserBatchSelection { function buildSelection(): UserBatchSelection {
if (props.selectAllFiltered) { if (props.selectAllFiltered) {
return { filters: props.filters } return { filters: props.filters }
@@ -350,32 +518,46 @@ function buildAccessControlPayload(): UserBatchAccessControlPayload | null {
if (modelMode.value === 'specific') payload.allowed_models = [...allowedModels.value] if (modelMode.value === 'specific') payload.allowed_models = [...allowedModels.value]
if (rateLimitMode.value === 'inherit') payload.rate_limit = null if (rateLimitMode.value === 'inherit') payload.rate_limit = null
if (rateLimitMode.value === 'custom' && rateLimit.value != null) payload.rate_limit = rateLimit.value if (rateLimitMode.value === 'custom' && rateLimit.value != null) payload.rate_limit = rateLimit.value
if (quotaMode.value === 'wallet') payload.unlimited = false
if (quotaMode.value === 'unlimited') payload.unlimited = true
return Object.keys(payload).length > 0 ? payload : null return Object.keys(payload).length > 0 ? payload : null
} }
function buildRolePayload(): UserBatchRolePayload {
return { role: targetRole.value }
}
async function executeBatchAction(): Promise<void> { async function executeBatchAction(): Promise<void> {
if (!canExecute.value) return if (!canExecute.value) return
if (selectedAction.value === 'update_access_control' && rateLimitMode.value === 'custom' && rateLimit.value == null) { if (selectedAction.value === 'update_access_control' && rateLimitMode.value === 'custom' && rateLimit.value == null) {
warning('请输入速率限制数值0 表示不限速') warning('请输入速率限制数值0 表示不限速')
return return
} }
const payload = selectedAction.value === 'update_access_control' const selection = buildSelection()
? buildAccessControlPayload() let request: UserBatchActionRequest
: null if (selectedAction.value === 'update_access_control') {
if (selectedAction.value === 'update_access_control' && payload === null) { const payload = buildAccessControlPayload()
warning('请至少选择一个要修改的访问控制字段') if (payload === null) {
return warning('请至少选择一个要修改的访问控制或额度字段')
return
}
request = { selection, action: 'update_access_control', payload }
} else if (selectedAction.value === 'update_role') {
request = { selection, action: 'update_role', payload: buildRolePayload() }
} else {
request = { selection, action: selectedAction.value }
} }
executing.value = true executing.value = true
try { try {
const result = await usersStore.batchAction({ const result = await usersStore.batchAction(request)
selection: buildSelection(),
action: selectedAction.value,
payload,
})
lastResult.value = result lastResult.value = result
success(`批量操作完成:成功 ${result.success} 个,失败 ${result.failed}`) const message = `批量操作完成:成功 ${result.success} 个,失败 ${result.failed}`
if (result.failed > 0) {
warning(message)
} else {
success(message)
}
emit('completed', result) emit('completed', result)
} catch (err) { } catch (err) {
error(parseApiError(err, '批量操作失败')) error(parseApiError(err, '批量操作失败'))