mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
fix(frontend): 优化移动端弹窗并完善提供商配额刷新
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user