fix(ui): refine admin filters, quota colors and batch actions

Simplify group filters and keep date and platform controls aligned. Apply quota warning thresholds independently to each window and move account batch actions into a compact menu.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-10-27 16:20:15 +08:00
co-authored by Codex
parent 18c75132c3
commit f1d5b73255
10 changed files with 63 additions and 97 deletions
@@ -368,9 +368,10 @@ export function quotaWindowPresentation(window: AccountQuotaWindow, minimumWidth
function quotaWindowTone(usedPercent: number | null): QuotaWindowTone {
if (usedPercent === null)
return 'unknown'
if (usedPercent >= 95)
// 每个窗口按自身的已用比例分档,剩余不足 30% / 10% 时分别预警
if (usedPercent > 90)
return 'danger'
if (usedPercent >= 80)
if (usedPercent > 70)
return 'warning'
return 'success'
}
-1
View File
@@ -23,7 +23,6 @@
"filters": {
"searchAccountGroup": "Search account group",
"searchGroupName": "Search group name",
"filterByGroupStatus": "Filter by group status",
"deleteSelected": "Delete ({count})",
"createGroup": "Create group"
},
@@ -23,7 +23,6 @@
"filters": {
"searchAccountGroup": "搜索账号分组",
"searchGroupName": "搜索分组名称",
"filterByGroupStatus": "按分组状态筛选",
"deleteSelected": "删除 ({count})",
"createGroup": "创建分组"
},
@@ -23,7 +23,6 @@
"filters": {
"searchAccountGroup": "搜索賬號分組",
"searchGroupName": "搜索分組名稱",
"filterByGroupStatus": "按分組狀態篩選",
"deleteSelected": "刪除 ({count})",
"createGroup": "創建分組"
},
@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { AccountGroupRef } from '@/api'
import { ZButton, ZInput, ZSelect } from '@codex-proxy/ui'
import { Download, ListTodo, Pencil, Search, Trash2, Upload } from '@lucide/vue'
import { ZButton, ZIconButton, ZInput, ZMenuItem, ZPopover, ZSelect } from '@codex-proxy/ui'
import { Download, ListTodo, MoreHorizontal, Pencil, Search, Trash2, Upload } from '@lucide/vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -94,61 +94,62 @@ const groupOptions = computed(() => [
</div>
<div
class="grid w-full grid-cols-2 gap-2 xl:flex xl:w-auto xl:flex-wrap xl:shrink-0 xl:self-end xl:items-center xl:justify-end xl:ml-auto"
class="flex w-full min-w-0 flex-wrap items-center justify-end gap-2 xl:ml-auto xl:w-auto xl:shrink-0 xl:self-end"
>
<ZButton
v-if="selectedCount > 0"
type="default" variant="solid"
class="w-full whitespace-nowrap xl:w-auto"
@click="emit('editSelected')"
>
<Pencil class="size-4 text-cp-link" />
{{ t('accounts.filters.editSelected', { count: selectedCount }) }}
</ZButton>
<ZButton
v-if="selectedCount > 0"
type="danger" variant="plain"
class="w-full whitespace-nowrap xl:w-auto"
:disabled="batchDeleting"
@click="emit('deleteSelected')"
>
<Trash2 class="size-4" />
{{ t('accounts.filters.deleteSelected', { count: selectedCount }) }}
</ZButton>
<ZButton
v-if="selectedCount > 0"
type="default" variant="solid"
class="w-full whitespace-nowrap xl:w-auto"
:loading="exportingAccounts"
:disabled="Boolean(exportDisabledReason)"
:title="exportDisabledReason || undefined"
:aria-description="exportDisabledReason || undefined"
@click="emit('exportSelected')"
>
<template #icon>
<Download class="size-4" />
<ZPopover v-if="selectedCount > 0" placement="bottom-end">
<template #reference="{ open }">
<ZIconButton
variant="plain"
:aria-label="t('accounts.actions.moreActions')"
:pressed="open"
:aria-expanded="open"
>
<MoreHorizontal class="size-4.5" aria-hidden="true" />
</ZIconButton>
</template>
{{ t('accounts.filters.exportSelected', { count: selectedCount }) }}
<template #default="{ close }">
<div class="w-32 p-1.5">
<ZMenuItem @click="(close(), emit('editSelected'))">
<template #icon>
<Pencil class="size-3.5 text-cp-text-quaternary" />
</template>
{{ t('accounts.filters.editSelected', { count: selectedCount }) }}
</ZMenuItem>
<ZMenuItem
:loading="exportingAccounts"
:disabled="Boolean(exportDisabledReason)"
:title="exportDisabledReason || undefined"
:aria-description="exportDisabledReason || undefined"
@click="(close(), emit('exportSelected'))"
>
<template #icon>
<Download class="size-3.5 text-cp-text-quaternary" />
</template>
{{ t('accounts.filters.exportSelected', { count: selectedCount }) }}
</ZMenuItem>
<ZMenuItem
type="danger"
:loading="batchDeleting"
@click="(close(), emit('deleteSelected'))"
>
<template #icon>
<Trash2 class="size-3.5" />
</template>
{{ t('accounts.filters.deleteSelected', { count: selectedCount }) }}
</ZMenuItem>
</div>
</template>
</ZPopover>
<ZButton v-if="hasImportTasks" class="whitespace-nowrap" @click="emit('importTasks')">
<ListTodo class="size-4" />
{{ t('accounts.filters.importTasks') }} <span v-if="activeImportCount" class="font-mono text-cp-link">{{ activeImportCount }}</span>
</ZButton>
<slot name="actions" />
<ZButton type="primary" class="whitespace-nowrap" @click="emit('create')">
<Upload class="size-4" />
{{ t('accounts.filters.importAccount') }}
</ZButton>
<div
class="col-span-2 flex min-w-0 flex-wrap items-center justify-end gap-2"
:class="selectedCount > 0 ? 'w-full xl:w-auto' : 'justify-self-end'"
>
<ZButton v-if="hasImportTasks" class="whitespace-nowrap" @click="emit('importTasks')">
<ListTodo class="size-4" />
{{ t('accounts.filters.importTasks') }} <span v-if="activeImportCount" class="font-mono text-cp-link">{{ activeImportCount }}</span>
</ZButton>
<slot name="actions" />
<ZButton
type="primary"
class="whitespace-nowrap"
:class="selectedCount > 0 ? 'flex-1 xl:flex-none' : undefined"
@click="emit('create')"
>
<Upload class="size-4" />
{{ t('accounts.filters.importAccount') }}
</ZButton>
</div>
</div>
</div>
</template>
@@ -1,10 +1,9 @@
<script setup lang="ts">
import { ZButton, ZInput, ZSelect } from '@codex-proxy/ui'
import { ZButton, ZInput } from '@codex-proxy/ui'
import { Plus, Search, Trash2 } from '@lucide/vue'
import { useI18n } from 'vue-i18n'
import { accountGroupStatusOptions } from '../constants'
defineProps<{
batchDeleting: boolean
@@ -19,14 +18,13 @@ const emit = defineEmits<{
const { t } = useI18n({ useScope: 'global' })
const search = defineModel<string>('search', { required: true })
const status = defineModel<string>('status', { required: true })
</script>
<template>
<div class="flex w-full flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<ZInput
v-model="search"
class="min-w-0 sm:w-80 sm:flex-1 sm:basis-64"
class="min-w-0 sm:w-80 sm:flex-none"
:aria-label="t('groups.filters.searchAccountGroup')"
:placeholder="t('groups.filters.searchGroupName')"
>
@@ -34,12 +32,6 @@ const status = defineModel<string>('status', { required: true })
<Search class="size-4.5 text-cp-text-tertiary" />
</template>
</ZInput>
<ZSelect
v-model="status"
:options="accountGroupStatusOptions"
:aria-label="t('groups.filters.filterByGroupStatus')"
class="w-40"
/>
<div class="flex max-w-full shrink-0 flex-wrap items-center justify-end gap-2 sm:ml-auto">
<ZButton
v-if="selectedCount > 0"
@@ -30,7 +30,6 @@ export interface AccountGroupFormValue {
export function useAccountGroups() {
const searchQuery = shallowRef('')
const statusQuery = shallowRef('')
const showFormModal = shallowRef(false)
const showDeleteModal = shallowRef(false)
const showBatchDeleteModal = shallowRef(false)
@@ -56,9 +55,6 @@ export function useAccountGroups() {
page,
pageSize,
search: searchQuery.value.trim() || undefined,
enabled: statusQuery.value
? statusQuery.value === 'enabled'
: undefined,
}, options),
})
@@ -308,9 +304,6 @@ export function useAccountGroups() {
}, 250)
onCleanup(() => clearTimeout(timer))
})
watch(statusQuery, () => {
void query.reloadFromStart()
})
onMounted(() => {
void Promise.all([
query.execute(),
@@ -323,7 +316,6 @@ export function useAccountGroups() {
loading: query.loading,
pagination,
searchQuery,
statusQuery,
showFormModal,
showDeleteModal,
showBatchDeleteModal,
-15
View File
@@ -62,18 +62,3 @@ export const accountGroupColumns = computed(() => (defineTableColumns<AccountGro
size: 'lg',
},
])))
export const accountGroupStatusOptions = computed(() => ([
{
label: t('common.status.all'),
value: '',
},
{
label: t('common.status.enabled'),
value: 'enabled',
},
{
label: t('common.status.disabled'),
value: 'disabled',
},
]))
-2
View File
@@ -18,7 +18,6 @@ const {
loading,
pagination,
searchQuery,
statusQuery,
showFormModal,
showDeleteModal,
showBatchDeleteModal,
@@ -64,7 +63,6 @@ const { allSelected, indeterminate, selectedRowKeys, toggleSelection, toggleAll
<template #header>
<AccountGroupFilters
v-model:search="searchQuery"
v-model:status="statusQuery"
:batch-deleting="batchDeleting"
:selected-count="selectedIds.size"
@create="openCreate"
+3 -3
View File
@@ -78,9 +78,9 @@ watch(timeRange, () => {
<template>
<div class="@container w-full">
<ZPageHeader :title="t('navigation.usage')" :description="t('usage.page.description')">
<ZPageHeader class="justify-end" :title="t('navigation.usage')" :description="t('usage.page.description')">
<template #actions>
<div class="flex w-[100cqw] max-w-[100cqw] flex-wrap items-center justify-end gap-2 @min-[28rem]:w-auto">
<div class="flex w-[100cqw] max-w-[100cqw] flex-wrap items-center justify-end gap-2 @min-[21rem]:flex-nowrap @min-[28rem]:w-auto">
<ZDatePicker
v-model="timeRange"
type="daterange"
@@ -89,7 +89,7 @@ watch(timeRange, () => {
:disabled-date="disabledDate"
single-panel
:aria-label="t('usage.page.dateRangeLabel')"
class="min-w-0 w-full @min-[28rem]:w-60"
class="min-w-0 flex-1 basis-full @min-[21rem]:basis-auto @min-[28rem]:w-60 @min-[28rem]:flex-none @max-[28rem]:[&>div]:gap-1.5 @max-[28rem]:[&>div]:px-2"
/>
<ProviderFilter
v-model="providerQuery"