diff --git a/frontend/src/views/accounts/components/AccountQuotaPanelEntry.vue b/frontend/src/views/accounts/components/AccountQuotaPanelEntry.vue
index 4818f9ac0..4368cd381 100644
--- a/frontend/src/views/accounts/components/AccountQuotaPanelEntry.vue
+++ b/frontend/src/views/accounts/components/AccountQuotaPanelEntry.vue
@@ -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'"
>
-
-
- {{ item.label }}
-
-
+
+
+
+ {{ item.localLabel }}
+
- {{ item.localUsageDisplay }}
+ {{ item.requestDisplay }} 次
-
- {{ item.usedPercentDisplay }}
-
-
-
+
-
-
- 重置
-
- {{ item.resetAtDisplay }}
-
-
+
+ 统计范围
+
+ 滚动 {{ item.durationDisplay }}
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+ {{ item.localUsageDisplay }}
+
+
+ {{ item.usedPercentDisplay }}
+
+
+
+
+
+
+
+ 重置
+
+ {{ item.resetAtDisplay }}
+
+
+
diff --git a/frontend/src/views/accounts/components/AccountQuotaSummaryEntry.vue b/frontend/src/views/accounts/components/AccountQuotaSummaryEntry.vue
index b16be2150..c9177e4ec 100644
--- a/frontend/src/views/accounts/components/AccountQuotaSummaryEntry.vue
+++ b/frontend/src/views/accounts/components/AccountQuotaSummaryEntry.vue
@@ -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 }}
- {{ item.label }}
+ {{ item.local ? item.localLabel : item.label }}
- {{ item.usedPercentDisplay }}
+ {{ item.local ? `${item.requestDisplay} 次` : item.usedPercentDisplay }}
-
+ 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"
+ >
+
+
+
- -
- 窗口消耗
-
- -
- {{ item.localUsageDisplay }}
-
- -
- 重置时间
-
- -
- {{ item.resetAtDisplay }}
-
+
+ -
+ 统计方式
+
+ -
+ 滚动窗口
+
+ -
+ 统计范围
+
+ -
+ {{ item.durationDisplay }}
+
+
+
+ -
+ 窗口消耗
+
+ -
+ {{ item.localUsageDisplay }}
+
+ -
+ 重置时间
+
+ -
+ {{ item.resetAtDisplay }}
+
+
diff --git a/frontend/src/views/accounts/components/AccountUsageWindow/AccountRequestTimeline.vue b/frontend/src/views/accounts/components/AccountUsageWindow/AccountRequestTimeline.vue
new file mode 100644
index 000000000..bbc734726
--- /dev/null
+++ b/frontend/src/views/accounts/components/AccountUsageWindow/AccountRequestTimeline.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/accounts/components/AccountUsageWindow/index.vue b/frontend/src/views/accounts/components/AccountUsageWindow/index.vue
index 8b50ec436..b1e5253e9 100644
--- a/frontend/src/views/accounts/components/AccountUsageWindow/index.vue
+++ b/frontend/src/views/accounts/components/AccountUsageWindow/index.vue
@@ -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 }}
-
-
-
-
-
+ />
diff --git a/frontend/src/views/accounts/components/AccountUsageWindow/presenter.ts b/frontend/src/views/accounts/components/AccountUsageWindow/presenter.ts
index afa0f9270..fddfff107 100644
--- a/frontend/src/views/accounts/components/AccountUsageWindow/presenter.ts
+++ b/frontend/src/views/accounts/components/AccountUsageWindow/presenter.ts
@@ -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
,
now: number,
-) {
+): AccountRequestBar[] {
const currentHour = Math.floor(now / hourMilliseconds) * hourMilliseconds
const bucketCounts = new Map(
buckets.map((bucket) => {