fix(frontend): refine account quota layout

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-09-02 12:14:03 +08:00
co-authored by Codex
parent dfacb20e43
commit 08d518324a
6 changed files with 121 additions and 36 deletions
@@ -77,7 +77,7 @@ const profileOpen = shallowRef(false)
</div>
</div>
<div class="grid max-h-44 min-h-0 gap-3 overflow-y-auto pr-1">
<div class="grid min-h-0 gap-3">
<AccountQuotaPanelEntry
v-for="entry in quotaEntries"
:key="entry.key"
@@ -10,15 +10,19 @@ import AccountUsageWindow from '../AccountUsageWindow/index.vue'
import { resolveAccountUsageWindowPresentation } from '../AccountUsageWindow/presenter'
import AccountQuotaWindowGroup from './WindowGroup.vue'
const props = defineProps<{
const props = withDefaults(defineProps<{
label: string | null
windows: AccountQuotaWindow[]
}>()
showPercentage?: boolean
}>(), {
showPercentage: true,
})
const now = useUiClock()
const grouped = computed(() => props.windows.length > 1 && Boolean(props.label))
const detailHeading = computed(() => props.label)
const detailTitle = computed(() => detailHeading.value ?? props.windows[0]?.labelDisplay ?? '额度详情')
const summaryLabel = computed(() => props.label ?? props.windows[0]?.labelDisplay ?? '额度')
const hasQuotaWindow = computed(() => props.windows.some(window => typeof window.usedPercent === 'number'))
const detailItems = computed(() =>
props.windows.map((window) => {
const view = resolveAccountUsageWindowPresentation({
@@ -82,7 +86,12 @@ function quotaWindowCode(windowSeconds: number | null, role: AccountQuotaWindow[
:aria-expanded="open"
aria-haspopup="dialog"
>
<AccountQuotaWindowGroup v-if="grouped && label" :label="label" :windows="windows" />
<AccountQuotaWindowGroup
v-if="hasQuotaWindow"
:label="summaryLabel"
:windows="windows"
:show-percentage="showPercentage"
/>
<AccountUsageWindow
v-else
:window="windows[0]"
@@ -4,10 +4,13 @@ import type { AccountQuotaWindow } from '../../constants'
import { computed } from 'vue'
import { quotaWindowPresentation } from '../AccountUsageWindow/presenter'
const props = defineProps<{
const props = withDefaults(defineProps<{
label: string
windows: AccountQuotaWindow[]
}>()
showPercentage?: boolean
}>(), {
showPercentage: true,
})
const windowItems = computed(() => props.windows.map(window => ({
key: window.key,
@@ -16,6 +19,17 @@ const windowItems = computed(() => props.windows.map(window => ({
usedPercentDisplay: window.usedPercentDisplay,
presentation: quotaWindowPresentation(window, '2px'),
})))
const highestUsageWindow = computed(() => props.windows.reduce((highest, window) => {
if (typeof window.usedPercent !== 'number')
return highest
if (!highest || typeof highest.usedPercent !== 'number' || window.usedPercent > highest.usedPercent)
return window
return highest
}, undefined as AccountQuotaWindow | undefined))
const highestUsageDisplay = computed(() => highestUsageWindow.value?.usedPercentDisplay ?? '—')
const highestUsageTextClass = computed(() => highestUsageWindow.value
? quotaWindowPresentation(highestUsageWindow.value, '2px').percentTextClass
: 'text-cp-text-quaternary')
const trackGridStyle = computed(() => ({
gridTemplateColumns: `repeat(${Math.max(windowItems.value.length, 1)}, minmax(0, 1fr))`,
@@ -23,16 +37,25 @@ const trackGridStyle = computed(() => ({
</script>
<template>
<div class="flex min-w-0 flex-col">
<div class="mb-1 min-w-0 truncate text-cp-xs leading-3.5 font-bold text-cp-text-quaternary">
{{ label }}
<div class="grid min-w-0 gap-1.5">
<div class="flex min-w-0 items-baseline justify-between gap-1 text-[10px] leading-3 font-bold">
<span class="min-w-0 truncate text-cp-text-quaternary" :title="label">
{{ label }}
</span>
<strong
v-if="showPercentage"
class="shrink-0 font-mono font-heavy tabular-nums"
:class="highestUsageTextClass"
>
{{ highestUsageDisplay }}
</strong>
</div>
<div class="grid min-w-0 gap-1" :style="trackGridStyle">
<div
v-for="item in windowItems"
:key="item.key"
class="h-0.75 w-full overflow-hidden rounded-full bg-cp-border-secondary"
class="h-1 w-full overflow-hidden rounded-full bg-cp-border-secondary"
role="progressbar"
:aria-label="item.labelTooltip"
aria-valuemin="0"
@@ -41,7 +64,7 @@ const trackGridStyle = computed(() => ({
:aria-valuetext="item.usedPercentDisplay"
>
<div
class="h-full rounded-full transition-[width,background-color] duration-200"
class="h-full rounded-full transition-[width,background-color] duration-200 motion-reduce:transition-none"
:class="item.presentation.barClass"
:style="item.presentation.barStyle"
/>
@@ -4,6 +4,7 @@ import type { AccountRow } from '../../constants'
import { computed } from 'vue'
import { groupedAccountQuotaWindows, visibleSummaryQuotaWindows } from '../../constants'
import AccountUsageWindow from '../AccountUsageWindow/index.vue'
import { quotaWindowPresentation } from '../AccountUsageWindow/presenter'
import AccountQuotaSummaryEntry from './Entry.vue'
const props = defineProps<{
@@ -13,24 +14,76 @@ const props = defineProps<{
const quotaWindows = computed(() => props.account.quota.windows)
const visibleQuotaWindows = computed(() => visibleSummaryQuotaWindows(quotaWindows.value))
const summaryEntries = computed(() => groupedAccountQuotaWindows(visibleQuotaWindows.value))
const summaryClass = computed(
() =>
`box-border grid w-full min-w-0 gap-1 py-1.5 ${
summaryEntries.value.length <= 1
? 'min-h-13 content-center'
: 'min-h-16.5 content-center'
}`,
)
const hasUsage = computed(() => (props.account.usage.requestCount ?? 0) > 0)
const highestUsageWindow = computed(() => visibleQuotaWindows.value.reduce((highest, window) => {
if (typeof window.usedPercent !== 'number')
return highest
if (!highest || typeof highest.usedPercent !== 'number' || window.usedPercent > highest.usedPercent)
return window
return highest
}, undefined as (typeof visibleQuotaWindows.value)[number] | undefined))
const highestUsageDisplay = computed(() => highestUsageWindow.value?.usedPercentDisplay ?? '—')
const highestUsageTextClass = computed(() => highestUsageWindow.value
? quotaWindowPresentation(highestUsageWindow.value, '2px').percentTextClass
: 'text-cp-text-quaternary')
const highestUsageEntry = computed(() => {
const highestWindow = highestUsageWindow.value
if (!highestWindow)
return summaryEntries.value[0]
return summaryEntries.value.find(entry =>
entry.windows.some(window => window.key === highestWindow.key),
) ?? summaryEntries.value[0]
})
const additionalEntryCount = computed(() => Math.max(summaryEntries.value.length - 1, 0))
</script>
<template>
<div :class="summaryClass">
<AccountQuotaSummaryEntry
v-for="entry in summaryEntries"
:key="entry.key"
:label="entry.label"
:windows="entry.windows"
/>
<AccountUsageWindow v-if="summaryEntries.length === 0" variant="compact" />
<div class="box-border grid min-h-16.5 w-full min-w-0 content-center gap-1.5 py-1.5">
<template v-if="summaryEntries.length > 0">
<div
v-if="hasUsage"
class="flex min-w-0 items-baseline justify-between gap-2 leading-none"
>
<span
class="flex min-w-0 items-baseline gap-1 font-mono tabular-nums"
title="当前额度窗口总 Token"
>
<strong class="truncate text-cp-xs font-heavy text-cp-text">
{{ account.usage.totalTokensDisplay }}
</strong>
<span class="shrink-0 text-[9px] font-emphasis tracking-[0.02em] text-cp-text-quaternary">
Tokens
</span>
</span>
<span
class="flex shrink-0 items-baseline gap-1 text-[9px] font-emphasis text-cp-text-quaternary"
title="所有额度窗口中的最高已用比例"
>
<span>最高占用</span>
<strong class="font-mono font-heavy tabular-nums" :class="highestUsageTextClass">
{{ highestUsageDisplay }}
</strong>
</span>
</div>
<div v-if="highestUsageEntry" class="flex min-w-0 items-end gap-2">
<div class="min-w-0 flex-1">
<AccountQuotaSummaryEntry
:label="highestUsageEntry.label"
:windows="highestUsageEntry.windows"
:show-percentage="false"
/>
</div>
<span
v-if="additionalEntryCount > 0"
class="grid h-5 min-w-5 shrink-0 place-items-center rounded-cp bg-cp-fill-quaternary px-1.5 font-mono text-[9px] font-heavy tabular-nums text-cp-text-tertiary"
:title="`另有 ${additionalEntryCount} 个额度组,可展开账号查看`"
>
+{{ additionalEntryCount }}
</span>
</div>
</template>
<AccountUsageWindow v-else variant="compact" />
</div>
</template>
@@ -31,15 +31,15 @@ const modelUsageColumns = defineTableColumns<AccountModelUsage>([
</script>
<template>
<section class="grid gap-4 rounded-lg bg-cp-bg-container p-4 shadow-cp-tertiary xl:grid-cols-[0.52fr_1.48fr]">
<div>
<div class="mb-3 flex items-baseline justify-between gap-3">
<section class="grid gap-4 rounded-lg bg-cp-bg-container p-4 shadow-cp-tertiary xl:min-h-0 xl:grid-cols-[0.52fr_1.48fr]">
<div class="xl:flex xl:min-h-0 xl:flex-col">
<div class="mb-3 flex shrink-0 items-baseline justify-between gap-3">
<h3 class="m-0 text-cp-lg font-heavy text-cp-text">
Token 结构
</h3>
<span class="text-cp-xs font-emphasis text-cp-text-quaternary">当前额度窗口</span>
</div>
<div class="grid gap-2">
<div class="grid gap-2 xl:min-h-0 xl:flex-1 xl:grid-rows-5">
<div class="flex items-center justify-between rounded-lg bg-cp-green-bg px-3 py-2">
<span class="text-cp-sm font-bold text-cp-green-text-on-bg">输入 Tokens</span>
<strong class="font-mono text-cp text-cp-text">
@@ -73,8 +73,8 @@ const modelUsageColumns = defineTableColumns<AccountModelUsage>([
</div>
</div>
<div class="min-w-0 pt-4 xl:pt-0 xl:pl-4">
<div class="mb-3 flex flex-wrap items-center gap-x-4 gap-y-2">
<div class="min-w-0 pt-4 xl:flex xl:min-h-0 xl:flex-col xl:pt-0 xl:pl-4">
<div class="mb-3 flex shrink-0 flex-wrap items-center gap-x-4 gap-y-2">
<h3 class="m-0 shrink-0 text-cp-lg font-heavy text-cp-text">
模型使用排行
</h3>
@@ -100,7 +100,7 @@ const modelUsageColumns = defineTableColumns<AccountModelUsage>([
</div>
</div>
<div class="h-52 min-w-0">
<div class="h-56 min-w-0 xl:h-auto xl:min-h-0 xl:flex-1 xl:basis-0">
<BaseTable
:columns="modelUsageColumns"
:rows="account.usage.models"
+1 -1
View File
@@ -287,7 +287,7 @@ const {
</template>
<template #expanded="{ row }">
<div class="grid gap-3 p-4 lg:grid-cols-[1.05fr_2.45fr]">
<div class="grid items-stretch gap-3 p-4 lg:grid-cols-[1.05fr_2.45fr] xl:min-h-[19.25rem]">
<AccountQuotaPanel
:account="row"
:refreshing="refreshingQuotaAccountIds.has(row.id)"