fix(frontend): 优化移动端弹窗并完善提供商配额刷新

This commit is contained in:
ZheFox
2026-07-16 15:21:11 +08:00
parent 71fcb9c168
commit 1333efdad5
13 changed files with 200 additions and 152 deletions
@@ -1,6 +1,6 @@
<template>
<div class="space-y-4 border-t border-border/60 pt-5">
<div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-1 border-b border-border/60 pb-2">
<div class="flex items-center justify-between gap-2 border-b border-border/60 pb-2">
<span class="text-sm font-medium">{{ legacyT('组权限') }}</span>
<span class="flex items-center gap-1 text-[11px] text-muted-foreground">
{{ legacyT('组权限叠加,Key 可再收窄') }}
@@ -9,11 +9,11 @@
<TooltipTrigger as-child>
<button
type="button"
class="inline-flex h-4 w-4 items-center justify-center rounded-full border border-border/70 bg-muted/40 text-muted-foreground outline-none transition-colors hover:border-primary/50 hover:text-primary focus-visible:border-primary/60 focus-visible:text-primary"
class="inline-flex h-10 w-10 items-center justify-center rounded-full text-muted-foreground outline-none transition-colors hover:bg-muted/60 hover:text-primary focus-visible:ring-2 focus-visible:ring-ring"
:title="helpText"
:aria-label="legacyT('查看组权限合并规则')"
>
<Info class="h-3 w-3" />
<Info class="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent class="max-w-72 text-xs leading-5">
@@ -27,11 +27,14 @@
<div class="space-y-2">
<Label class="text-sm font-medium">{{ legacyT('允许的提供商') }}</Label>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
<div class="flex w-full items-center sm:w-auto sm:shrink-0">
<div class="flex min-h-10 w-full items-center gap-2 sm:w-auto sm:shrink-0">
<Switch
:model-value="form.allowed_providers_mode === 'unrestricted'"
@update:model-value="setProvidersUnrestricted"
/>
<span class="text-xs text-muted-foreground sm:sr-only">
{{ legacyT(form.allowed_providers_mode === 'unrestricted' ? '不限制' : '选择提供商') }}
</span>
</div>
<div class="min-w-0 flex-1">
<MultiSelect
@@ -50,11 +53,14 @@
<div class="space-y-2">
<Label class="text-sm font-medium">{{ legacyT('允许的端点') }}</Label>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
<div class="flex w-full items-center sm:w-auto sm:shrink-0">
<div class="flex min-h-10 w-full items-center gap-2 sm:w-auto sm:shrink-0">
<Switch
:model-value="form.allowed_api_formats_mode === 'unrestricted'"
@update:model-value="setApiFormatsUnrestricted"
/>
<span class="text-xs text-muted-foreground sm:sr-only">
{{ legacyT(form.allowed_api_formats_mode === 'unrestricted' ? '不限制' : '选择端点') }}
</span>
</div>
<div class="min-w-0 flex-1">
<MultiSelect
@@ -73,11 +79,14 @@
<div class="space-y-2">
<Label class="text-sm font-medium">{{ legacyT('允许的模型') }}</Label>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
<div class="flex w-full items-center sm:w-auto sm:shrink-0">
<div class="flex min-h-10 w-full items-center gap-2 sm:w-auto sm:shrink-0">
<Switch
:model-value="form.allowed_models_mode === 'unrestricted'"
@update:model-value="setModelsUnrestricted"
/>
<span class="text-xs text-muted-foreground sm:sr-only">
{{ legacyT(form.allowed_models_mode === 'unrestricted' ? '不限制' : '选择模型') }}
</span>
</div>
<div class="min-w-0 flex-1">
<MultiSelect
@@ -96,11 +105,14 @@
<div class="space-y-2">
<Label class="text-sm font-medium">{{ legacyT('速率限制 (请求/分钟)') }}</Label>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
<div class="flex w-full items-center sm:w-auto sm:shrink-0">
<div class="flex min-h-10 w-full items-center gap-2 sm:w-auto sm:shrink-0">
<Switch
:model-value="form.rate_limit_mode === 'system'"
@update:model-value="setSystemRateLimit"
/>
<span class="text-xs text-muted-foreground sm:sr-only">
{{ legacyT(form.rate_limit_mode === 'system' ? '系统默认' : '自定义') }}
</span>
</div>
<div class="min-w-0 flex-1">
<Input
@@ -1,7 +1,7 @@
<template>
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
<div class="mb-4 flex items-start justify-between gap-3">
<div class="min-w-0">
<h4 class="truncate text-base font-semibold text-foreground">
<h4 class="text-balance break-words text-base font-semibold text-foreground">
{{ legacyT(editing ? '编辑分组' : '新建分组') }}
</h4>
<p class="text-xs text-muted-foreground">
@@ -15,7 +15,7 @@
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
class="h-10 w-10"
:class="isDefault ? 'text-emerald-500 hover:text-emerald-500' : ''"
:disabled="saving || isDefault"
:title="legacyT(isDefault ? '默认注册组' : '设为默认注册组')"
@@ -26,7 +26,7 @@
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
class="h-10 w-10"
:disabled="saving || isDefault"
:title="legacyT('删除分组')"
@click="$emit('delete')"
@@ -1,11 +1,11 @@
<template>
<div class="rounded-xl border border-border/70 bg-muted/20 p-3">
<div class="mb-3 flex items-center justify-between gap-2">
<div class="rounded-lg border border-border/70 bg-muted/20 p-2.5 sm:rounded-xl sm:p-3">
<div class="mb-2 flex items-center justify-between gap-2 sm:mb-3">
<Label class="text-sm font-semibold">{{ legacyT('分组') }}</Label>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
class="h-10 w-10"
:title="legacyT('新建分组')"
@click="$emit('create')"
>
@@ -27,7 +27,7 @@
</div>
<div
v-else
class="max-h-60 space-y-1.5 overflow-y-auto lg:max-h-none lg:overflow-visible"
class="flex snap-x snap-mandatory gap-2 overflow-x-auto overscroll-x-contain pb-1 lg:block lg:max-h-none lg:space-y-1.5 lg:overflow-visible lg:pb-0"
>
<button
v-for="group in groups"
@@ -48,7 +48,7 @@
</Badge>
</span>
</span>
<ChevronRight class="h-4 w-4 shrink-0 text-muted-foreground" />
<ChevronRight class="hidden h-4 w-4 shrink-0 text-muted-foreground lg:block" />
</button>
</div>
</div>
@@ -76,7 +76,7 @@ const { legacyT } = useI18n()
function groupButtonClass(groupId: string): string {
return cn(
'flex w-full items-center gap-2 rounded-lg border px-3 py-2 transition-colors',
'flex min-h-10 w-auto min-w-[8.5rem] shrink-0 snap-start items-center gap-2 rounded-lg border px-3 py-2 transition-colors lg:w-full lg:min-w-0',
props.selectedGroupId === groupId
? 'border-primary/50 bg-primary/10'
: 'border-transparent hover:border-border hover:bg-background',
@@ -7,7 +7,7 @@
persistent
@update:model-value="handleDialogUpdate"
>
<div class="grid gap-4 lg:min-h-[560px] lg:grid-cols-[17rem_minmax(0,1fr)]">
<div class="grid gap-3 lg:min-h-[560px] lg:grid-cols-[17rem_minmax(0,1fr)] lg:gap-4">
<UserGroupListPanel
:loading="loading"
:groups="groups"
@@ -16,7 +16,7 @@
@select="selectGroup"
/>
<div class="min-w-0 rounded-xl border border-border/70 bg-background p-3 sm:p-4">
<div class="min-w-0 bg-background sm:rounded-xl sm:border sm:border-border/70 sm:p-4">
<UserGroupEditorHeader
:editing="Boolean(editingGroupId)"
:is-default="Boolean(selectedGroup?.is_default)"
@@ -0,0 +1,39 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
function readSource(path: string): string {
return readFileSync(resolve(process.cwd(), path), 'utf8')
}
describe('mobile dialog layout', () => {
it('uses a full-height mobile sheet with stacked full-width actions', () => {
const source = readSource('src/components/ui/dialog/Dialog.vue')
expect(source).toContain('max-h-[100dvh]')
expect(source).toContain('flex-col-reverse')
expect(source).toContain('[&>button]:w-full')
expect(source).toContain('sm:flex-row-reverse')
expect(source).toContain('sm:[&>button]:w-auto')
})
it('keeps group navigation compact before the editor on mobile', () => {
const dialogSource = readSource('src/features/users/components/UserGroupsDialog.vue')
const listSource = readSource('src/features/users/components/UserGroupListPanel.vue')
expect(dialogSource).toContain('lg:grid-cols-[17rem_minmax(0,1fr)]')
expect(listSource).toContain('snap-x snap-mandatory')
expect(listSource).toContain('overflow-x-auto')
expect(listSource).toContain('min-h-10')
expect(listSource).toContain('lg:w-full')
})
it('provides mobile state labels and 40px icon targets', () => {
const accessSource = readSource('src/features/users/components/UserGroupAccessControlFields.vue')
const headerSource = readSource('src/features/users/components/UserGroupEditorHeader.vue')
expect(accessSource).toContain('sm:sr-only')
expect(accessSource).toContain('h-10 w-10')
expect(headerSource.match(/h-10 w-10/g)).toHaveLength(2)
})
})