mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(admin): align xAI quota usage displays
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -2,9 +2,12 @@
|
||||
import type { AccountQuotaWindow } from '../constants'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { useUiClock } from '@/composables/useUiClock'
|
||||
import AccountRequestTimeline from './AccountUsageWindow/AccountRequestTimeline.vue'
|
||||
import {
|
||||
quotaWindowLocalUsageDisplay,
|
||||
quotaWindowPresentation,
|
||||
resolveAccountUsageWindowPresentation,
|
||||
} from './AccountUsageWindow/presenter'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -12,12 +15,25 @@ const props = defineProps<{
|
||||
windows: AccountQuotaWindow[]
|
||||
}>()
|
||||
|
||||
const now = useUiClock()
|
||||
const grouped = computed(() => props.windows.length > 1 && Boolean(props.label))
|
||||
const items = computed(() => props.windows.map((window) => {
|
||||
const presentation = quotaWindowPresentation(window, '4px')
|
||||
const view = resolveAccountUsageWindowPresentation({
|
||||
window,
|
||||
variant: 'detail',
|
||||
showLocalValue: true,
|
||||
now: now.value.getTime(),
|
||||
})
|
||||
|
||||
return {
|
||||
key: window.key,
|
||||
local: view.mode === 'local',
|
||||
localLabel: view.local.label,
|
||||
requestDisplay: view.local.requestDisplay,
|
||||
requestBars: view.local.requestBars,
|
||||
timelineTitle: view.local.timelineTitle,
|
||||
durationDisplay: view.local.durationDisplay,
|
||||
label: grouped.value
|
||||
? window.windowLabelDisplay
|
||||
: window.labelDisplay,
|
||||
@@ -48,49 +64,80 @@ const items = computed(() => props.windows.map((window) => {
|
||||
:class="grouped ? 'grid-cols-2' : 'grid-cols-1'"
|
||||
>
|
||||
<div v-for="item in items" :key="item.key" class="grid min-w-0 gap-1.5">
|
||||
<div class="flex min-w-0 items-baseline justify-between gap-2 text-[11px] leading-3.5">
|
||||
<span class="min-w-0 truncate font-bold text-cp-muted-text">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<span class="flex shrink-0 items-baseline gap-1.5 font-mono font-heavy tabular-nums">
|
||||
<template v-if="item.local">
|
||||
<div class="flex min-w-0 items-baseline justify-between gap-2 text-[11px] leading-3.5">
|
||||
<span class="min-w-0 truncate font-bold text-cp-muted-text">
|
||||
{{ item.localLabel }}
|
||||
</span>
|
||||
<strong
|
||||
v-if="item.localUsageDisplay"
|
||||
class="text-cp-muted-text"
|
||||
:title="`窗口消耗:${item.localUsageDisplay}`"
|
||||
class="shrink-0 font-mono font-heavy tabular-nums text-cp-primary"
|
||||
:title="`窗口请求:${item.requestDisplay} 次`"
|
||||
>
|
||||
{{ item.localUsageDisplay }}
|
||||
{{ item.requestDisplay }} 次
|
||||
</strong>
|
||||
<strong
|
||||
:class="item.percentTextClass"
|
||||
:title="`额度已使用:${item.usedPercentDisplay}`"
|
||||
>
|
||||
{{ item.usedPercentDisplay }}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="h-1.5 w-full overflow-hidden rounded-full bg-cp-default-border"
|
||||
role="progressbar"
|
||||
:aria-label="item.ariaLabel"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
:aria-valuenow="item.usedPercent ?? undefined"
|
||||
:aria-valuetext="item.usedPercentDisplay"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-full transition-[width,background-color] duration-200 motion-reduce:transition-none"
|
||||
:class="item.barClass"
|
||||
:style="item.barStyle"
|
||||
<AccountRequestTimeline
|
||||
class="h-1.5 w-full"
|
||||
:bars="item.requestBars"
|
||||
:label="item.timelineTitle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="m-0 flex min-w-0 items-center justify-between gap-2 text-[10px] leading-3.5 text-cp-tertiary">
|
||||
<span class="shrink-0 font-emphasis">重置</span>
|
||||
<span class="min-w-0 truncate font-mono font-emphasis tabular-nums" :title="item.resetAtDisplay">
|
||||
{{ item.resetAtDisplay }}
|
||||
</span>
|
||||
</p>
|
||||
<p class="m-0 flex min-w-0 items-center justify-between gap-2 text-[10px] leading-3.5 text-cp-tertiary">
|
||||
<span class="shrink-0 font-emphasis">统计范围</span>
|
||||
<span
|
||||
class="min-w-0 truncate font-mono font-emphasis tabular-nums"
|
||||
:title="`滚动 ${item.durationDisplay}`"
|
||||
>
|
||||
滚动 {{ item.durationDisplay }}
|
||||
</span>
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex min-w-0 items-baseline justify-between gap-2 text-[11px] leading-3.5">
|
||||
<span class="min-w-0 truncate font-bold text-cp-muted-text">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<span class="flex shrink-0 items-baseline gap-1.5 font-mono font-heavy tabular-nums">
|
||||
<strong
|
||||
v-if="item.localUsageDisplay"
|
||||
class="text-cp-muted-text"
|
||||
:title="`窗口消耗:${item.localUsageDisplay}`"
|
||||
>
|
||||
{{ item.localUsageDisplay }}
|
||||
</strong>
|
||||
<strong
|
||||
:class="item.percentTextClass"
|
||||
:title="`额度已使用:${item.usedPercentDisplay}`"
|
||||
>
|
||||
{{ item.usedPercentDisplay }}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="h-1.5 w-full overflow-hidden rounded-full bg-cp-default-border"
|
||||
role="progressbar"
|
||||
:aria-label="item.ariaLabel"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
:aria-valuenow="item.usedPercent ?? undefined"
|
||||
:aria-valuetext="item.usedPercentDisplay"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-full transition-[width,background-color] duration-200 motion-reduce:transition-none"
|
||||
:class="item.barClass"
|
||||
:style="item.barStyle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="m-0 flex min-w-0 items-center justify-between gap-2 text-[10px] leading-3.5 text-cp-tertiary">
|
||||
<span class="shrink-0 font-emphasis">重置</span>
|
||||
<span class="min-w-0 truncate font-mono font-emphasis tabular-nums" :title="item.resetAtDisplay">
|
||||
{{ item.resetAtDisplay }}
|
||||
</span>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { computed } from 'vue'
|
||||
import BasePopover from '@/components/base/BasePopover.vue'
|
||||
import { useUiClock } from '@/composables/useUiClock'
|
||||
import AccountQuotaWindowGroup from './AccountQuotaWindowGroup.vue'
|
||||
import AccountRequestTimeline from './AccountUsageWindow/AccountRequestTimeline.vue'
|
||||
import AccountUsageWindow from './AccountUsageWindow/index.vue'
|
||||
import { resolveAccountUsageWindowPresentation } from './AccountUsageWindow/presenter'
|
||||
|
||||
@@ -37,6 +38,12 @@ const detailItems = computed(() => props.windows.map((window) => {
|
||||
|
||||
return {
|
||||
key: window.key,
|
||||
local: view.mode === 'local',
|
||||
localLabel: view.local.label,
|
||||
requestDisplay: view.local.requestDisplay,
|
||||
requestBars: view.local.requestBars,
|
||||
timelineTitle: view.local.timelineTitle,
|
||||
durationDisplay: view.local.durationDisplay,
|
||||
code: quotaWindowCode(window.windowSeconds, window.role),
|
||||
label: window.windowLabelDisplay,
|
||||
usedPercent: window.usedPercent,
|
||||
@@ -141,45 +148,69 @@ function quotaWindowCode(
|
||||
{{ item.code }}
|
||||
</span>
|
||||
<h4 class="m-0 min-w-0 truncate text-[12px] leading-4 font-heavy text-cp-secondary">
|
||||
{{ item.label }}
|
||||
{{ item.local ? item.localLabel : item.label }}
|
||||
</h4>
|
||||
<strong
|
||||
class="font-mono text-[11px] leading-none font-heavy tabular-nums"
|
||||
:class="item.percentTextClass"
|
||||
:class="item.local ? 'text-cp-primary' : item.percentTextClass"
|
||||
>
|
||||
{{ item.usedPercentDisplay }}
|
||||
{{ item.local ? `${item.requestDisplay} 次` : item.usedPercentDisplay }}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-2 h-1 w-full overflow-hidden rounded-full bg-cp-default-border"
|
||||
role="progressbar"
|
||||
:aria-label="item.label"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
:aria-valuenow="item.usedPercent ?? undefined"
|
||||
:aria-valuetext="item.usedPercentDisplay"
|
||||
>
|
||||
<AccountRequestTimeline
|
||||
v-if="item.local"
|
||||
class="mt-2 h-1 w-full"
|
||||
:bars="item.requestBars"
|
||||
:label="item.timelineTitle"
|
||||
/>
|
||||
<template v-else>
|
||||
<div
|
||||
class="h-full rounded-full transition-[width,background-color] duration-200 motion-reduce:transition-none"
|
||||
:class="item.barClass"
|
||||
:style="item.barStyle"
|
||||
/>
|
||||
</div>
|
||||
class="mt-2 h-1 w-full overflow-hidden rounded-full bg-cp-default-border"
|
||||
role="progressbar"
|
||||
:aria-label="item.label"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
:aria-valuenow="item.usedPercent ?? undefined"
|
||||
:aria-valuetext="item.usedPercentDisplay"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-full transition-[width,background-color] duration-200 motion-reduce:transition-none"
|
||||
:class="item.barClass"
|
||||
:style="item.barStyle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<dl class="mt-2.5 grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-[10px] leading-4">
|
||||
<dt class="font-emphasis text-cp-tertiary">
|
||||
窗口消耗
|
||||
</dt>
|
||||
<dd class="m-0 text-right font-mono font-heavy tabular-nums text-cp-primary">
|
||||
{{ item.localUsageDisplay }}
|
||||
</dd>
|
||||
<dt class="font-emphasis text-cp-tertiary">
|
||||
重置时间
|
||||
</dt>
|
||||
<dd class="m-0 truncate text-right font-mono font-emphasis tabular-nums text-cp-secondary">
|
||||
{{ item.resetAtDisplay }}
|
||||
</dd>
|
||||
<template v-if="item.local">
|
||||
<dt class="font-emphasis text-cp-tertiary">
|
||||
统计方式
|
||||
</dt>
|
||||
<dd class="m-0 text-right font-mono font-heavy tabular-nums text-cp-primary">
|
||||
滚动窗口
|
||||
</dd>
|
||||
<dt class="font-emphasis text-cp-tertiary">
|
||||
统计范围
|
||||
</dt>
|
||||
<dd class="m-0 truncate text-right font-mono font-emphasis tabular-nums text-cp-secondary">
|
||||
{{ item.durationDisplay }}
|
||||
</dd>
|
||||
</template>
|
||||
<template v-else>
|
||||
<dt class="font-emphasis text-cp-tertiary">
|
||||
窗口消耗
|
||||
</dt>
|
||||
<dd class="m-0 text-right font-mono font-heavy tabular-nums text-cp-primary">
|
||||
{{ item.localUsageDisplay }}
|
||||
</dd>
|
||||
<dt class="font-emphasis text-cp-tertiary">
|
||||
重置时间
|
||||
</dt>
|
||||
<dd class="m-0 truncate text-right font-mono font-emphasis tabular-nums text-cp-secondary">
|
||||
{{ item.resetAtDisplay }}
|
||||
</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccountRequestBar } from './presenter'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
bars: AccountRequestBar[]
|
||||
label: string
|
||||
showNativeTooltip?: boolean
|
||||
}>(),
|
||||
{
|
||||
showNativeTooltip: true,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-stretch gap-px overflow-hidden rounded-full"
|
||||
role="img"
|
||||
:aria-label="label"
|
||||
:title="showNativeTooltip ? label : undefined"
|
||||
>
|
||||
<span
|
||||
v-for="bar in bars"
|
||||
:key="bar.key"
|
||||
class="relative min-w-0 flex-1 bg-cp-default-border"
|
||||
:title="showNativeTooltip ? bar.title : undefined"
|
||||
>
|
||||
<span
|
||||
class="absolute inset-x-0 bottom-0 bg-cp-success"
|
||||
:style="{ height: bar.height }"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,6 +3,7 @@ import type { AccountQuotaWindow } from '../../constants'
|
||||
import type { AccountUsageWindowVariant } from './presenter'
|
||||
import { computed } from 'vue'
|
||||
import { useUiClock } from '@/composables/useUiClock'
|
||||
import AccountRequestTimeline from './AccountRequestTimeline.vue'
|
||||
import { resolveAccountUsageWindowPresentation } from './presenter'
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -116,25 +117,12 @@ const view = computed(() => resolveAccountUsageWindowPresentation({
|
||||
{{ view.local.requestDisplay }}
|
||||
</strong>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-stretch gap-px"
|
||||
<AccountRequestTimeline
|
||||
:bars="view.local.requestBars"
|
||||
:label="view.local.timelineTitle"
|
||||
:show-native-tooltip="showNativeTooltip"
|
||||
:class="[view.classes.trackShape, view.classes.trackOffset]"
|
||||
role="img"
|
||||
:aria-label="view.local.timelineTitle"
|
||||
:title="view.local.timelineTitle"
|
||||
>
|
||||
<span
|
||||
v-for="bar in view.local.requestBars"
|
||||
:key="bar.key"
|
||||
class="relative min-w-0 flex-1 bg-cp-default-border"
|
||||
:title="bar.title"
|
||||
>
|
||||
<span
|
||||
class="absolute inset-x-0 bottom-0 bg-cp-success"
|
||||
:style="{ height: bar.height }"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div v-else :class="view.classes.header">
|
||||
|
||||
@@ -12,6 +12,13 @@ interface AccountRequestBucket {
|
||||
requestCount: number
|
||||
}
|
||||
|
||||
export interface AccountRequestBar {
|
||||
key: string
|
||||
requestCount: number
|
||||
height: string
|
||||
title: string
|
||||
}
|
||||
|
||||
interface AccountLocalUsage {
|
||||
requestCount?: number
|
||||
requestCountDisplay?: string
|
||||
@@ -160,6 +167,7 @@ export function resolveAccountUsageWindowPresentation(
|
||||
requestValueVisible: localRequestValueVisible,
|
||||
timelineTitle: `${localRequestLabel} ${localRequestDisplay} 次`,
|
||||
requestBars: requestTimeline(localUsage?.requestBuckets ?? [], input.now),
|
||||
durationDisplay: rollingWindowDurationDisplay(input.window?.windowSeconds),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -242,6 +250,21 @@ function requestCountDisplay(localUsage: AccountLocalUsage | null) {
|
||||
return typeof count === 'number' ? formatInteger(count) : '0'
|
||||
}
|
||||
|
||||
function rollingWindowDurationDisplay(windowSeconds: number | null | undefined) {
|
||||
if (typeof windowSeconds !== 'number' || !Number.isFinite(windowSeconds) || windowSeconds <= 0)
|
||||
return '—'
|
||||
|
||||
const hourSeconds = 60 * 60
|
||||
const daySeconds = 24 * hourSeconds
|
||||
if (windowSeconds <= daySeconds * 2 && windowSeconds % hourSeconds === 0)
|
||||
return `${formatInteger(windowSeconds / hourSeconds)} 小时`
|
||||
if (windowSeconds % daySeconds === 0)
|
||||
return `${formatInteger(windowSeconds / daySeconds)} 天`
|
||||
if (windowSeconds % hourSeconds === 0)
|
||||
return `${formatInteger(windowSeconds / hourSeconds)} 小时`
|
||||
return `${formatInteger(windowSeconds)} 秒`
|
||||
}
|
||||
|
||||
export function quotaWindowPresentation(window: AccountQuotaWindow, minimumWidth: string) {
|
||||
const percent = clamp(window.usedPercent ?? 0, 0, 100)
|
||||
const tone = quotaWindowTone(window.usedPercent)
|
||||
@@ -268,7 +291,7 @@ function quotaWindowTone(usedPercent: number | null): QuotaWindowTone {
|
||||
function requestTimeline(
|
||||
buckets: NonNullable<AccountLocalUsage['requestBuckets']>,
|
||||
now: number,
|
||||
) {
|
||||
): AccountRequestBar[] {
|
||||
const currentHour = Math.floor(now / hourMilliseconds) * hourMilliseconds
|
||||
const bucketCounts = new Map(
|
||||
buckets.map((bucket) => {
|
||||
|
||||
Reference in New Issue
Block a user