mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(frontend): refine account quota layout
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
Reference in New Issue
Block a user