mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
fix(admin): align quota metadata and usage layout
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -570,13 +570,34 @@ pub enum QuotaLocalUsageAttribution {
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
/// Provider quota bucket 中窗口的官方位置语义。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProviderQuotaWindowRole {
|
||||
Primary,
|
||||
Secondary,
|
||||
Monthly,
|
||||
}
|
||||
|
||||
impl ProviderQuotaWindowRole {
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Primary => "primary",
|
||||
Self::Secondary => "secondary",
|
||||
Self::Monthly => "monthly",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个 Provider quota 窗口的公共投影。
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ProviderQuotaWindow {
|
||||
pub key: String,
|
||||
pub group: String,
|
||||
pub label: String,
|
||||
pub source: Option<String>,
|
||||
pub limit_id: Option<String>,
|
||||
pub limit_name: Option<String>,
|
||||
pub role: Option<ProviderQuotaWindowRole>,
|
||||
pub local_usage_attribution: QuotaLocalUsageAttribution,
|
||||
pub window_seconds: Option<u64>,
|
||||
pub used_percent: Option<f64>,
|
||||
|
||||
@@ -165,7 +165,9 @@ fn representative_window_usage_should_accept_account_wide_rolling_window_without
|
||||
key: "free-rolling-24h".to_owned(),
|
||||
group: "shortTerm".to_owned(),
|
||||
label: "日限额".to_owned(),
|
||||
source: None,
|
||||
limit_id: None,
|
||||
limit_name: None,
|
||||
role: None,
|
||||
local_usage_attribution: QuotaLocalUsageAttribution::AccountWide,
|
||||
window_seconds: Some(86_400),
|
||||
used_percent: None,
|
||||
@@ -234,7 +236,9 @@ fn quota_window(
|
||||
key: group.to_owned(),
|
||||
group: group.to_owned(),
|
||||
label: group.to_owned(),
|
||||
source: None,
|
||||
limit_id: None,
|
||||
limit_name: None,
|
||||
role: None,
|
||||
local_usage_attribution: QuotaLocalUsageAttribution::AccountWide,
|
||||
window_seconds,
|
||||
used_percent,
|
||||
|
||||
@@ -1262,7 +1262,9 @@ async fn accounts_list_should_degrade_quota_failure_to_empty_window_without_drop
|
||||
key: "primary".to_owned(),
|
||||
group: "shortTerm".to_owned(),
|
||||
label: "5小时限额".to_owned(),
|
||||
source: None,
|
||||
limit_id: None,
|
||||
limit_name: None,
|
||||
role: None,
|
||||
local_usage_attribution: QuotaLocalUsageAttribution::AccountWide,
|
||||
window_seconds: Some(5 * 60 * 60),
|
||||
used_percent: Some(97.0),
|
||||
@@ -1391,7 +1393,9 @@ async fn accounts_list_should_not_derive_rate_limited_from_provider_quota_view()
|
||||
key: "primary".to_owned(),
|
||||
group: "shortTerm".to_owned(),
|
||||
label: "5小时限额".to_owned(),
|
||||
source: None,
|
||||
limit_id: None,
|
||||
limit_name: None,
|
||||
role: None,
|
||||
local_usage_attribution: QuotaLocalUsageAttribution::AccountWide,
|
||||
window_seconds: Some(5 * 60 * 60),
|
||||
used_percent: Some(100.0),
|
||||
@@ -1436,7 +1440,9 @@ async fn accounts_list_should_not_derive_exhaustion_from_provider_quota_view() {
|
||||
key: "primary".to_owned(),
|
||||
group: "shortTerm".to_owned(),
|
||||
label: "5小时限额".to_owned(),
|
||||
source: None,
|
||||
limit_id: None,
|
||||
limit_name: None,
|
||||
role: None,
|
||||
local_usage_attribution: QuotaLocalUsageAttribution::AccountWide,
|
||||
window_seconds: Some(5 * 60 * 60),
|
||||
used_percent: Some(100.0),
|
||||
@@ -1482,7 +1488,9 @@ async fn accounts_list_should_attach_local_usage_to_quota_windows() {
|
||||
key: "primary".to_owned(),
|
||||
group: "shortTerm".to_owned(),
|
||||
label: "5小时限额".to_owned(),
|
||||
source: None,
|
||||
limit_id: None,
|
||||
limit_name: None,
|
||||
role: None,
|
||||
local_usage_attribution: QuotaLocalUsageAttribution::AccountWide,
|
||||
window_seconds: Some(5 * 60 * 60),
|
||||
used_percent: Some(97.0),
|
||||
@@ -1547,7 +1555,9 @@ async fn accounts_list_should_not_attach_account_usage_to_model_specific_quota_w
|
||||
key: "core-primary".to_owned(),
|
||||
group: "weekly".to_owned(),
|
||||
label: "周额度".to_owned(),
|
||||
source: Some("codex".to_owned()),
|
||||
limit_id: Some("codex".to_owned()),
|
||||
limit_name: None,
|
||||
role: None,
|
||||
local_usage_attribution: QuotaLocalUsageAttribution::AccountWide,
|
||||
window_seconds: Some(7 * 24 * 60 * 60),
|
||||
used_percent: Some(1.0),
|
||||
@@ -1559,8 +1569,10 @@ async fn accounts_list_should_not_attach_account_usage_to_model_specific_quota_w
|
||||
ProviderQuotaWindow {
|
||||
key: "codex-bengalfox-primary".to_owned(),
|
||||
group: "weekly".to_owned(),
|
||||
label: "GPT-5.3-Codex-Spark · 周额度".to_owned(),
|
||||
source: Some("codex_bengalfox".to_owned()),
|
||||
label: "周额度".to_owned(),
|
||||
limit_id: Some("codex_bengalfox".to_owned()),
|
||||
limit_name: Some("GPT-5.3-Codex-Spark".to_owned()),
|
||||
role: None,
|
||||
local_usage_attribution: QuotaLocalUsageAttribution::Unavailable,
|
||||
window_seconds: Some(7 * 24 * 60 * 60),
|
||||
used_percent: Some(0.0),
|
||||
|
||||
@@ -305,8 +305,12 @@ pub struct AccountQuotaView {
|
||||
pub struct AccountQuotaWindowView {
|
||||
pub key: String,
|
||||
pub group: String,
|
||||
pub limit_id: Option<String>,
|
||||
pub limit_name: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub window_seconds: Option<u64>,
|
||||
pub label_display: String,
|
||||
pub window_label_display: String,
|
||||
pub used_percent: Option<f64>,
|
||||
pub used_percent_display: String,
|
||||
pub limit_reached: bool,
|
||||
@@ -1603,7 +1607,9 @@ fn quota_window_view(window: ProviderQuotaWindow) -> AccountQuotaWindowView {
|
||||
key,
|
||||
group,
|
||||
label,
|
||||
source: _,
|
||||
limit_id,
|
||||
limit_name,
|
||||
role,
|
||||
local_usage_attribution: _,
|
||||
window_seconds,
|
||||
used_percent,
|
||||
@@ -1612,10 +1618,17 @@ fn quota_window_view(window: ProviderQuotaWindow) -> AccountQuotaWindowView {
|
||||
local_usage,
|
||||
provider_data: _,
|
||||
} = window;
|
||||
let label_display = limit_name
|
||||
.as_ref()
|
||||
.map_or_else(|| label.clone(), |name| format!("{name} · {label}"));
|
||||
AccountQuotaWindowView {
|
||||
label_display: label,
|
||||
label_display,
|
||||
window_label_display: label,
|
||||
key,
|
||||
group,
|
||||
limit_id,
|
||||
limit_name,
|
||||
role: role.map(|value| value.as_str().to_owned()),
|
||||
window_seconds,
|
||||
used_percent,
|
||||
used_percent_display: used_percent
|
||||
|
||||
@@ -19,7 +19,7 @@ use gateway_admin::model::provider_credentials::{
|
||||
PreparedCredentialCreate, PreparedCredentialImport, PreparedCredentialRotation,
|
||||
PreparedCredentialRotationFacts, ProviderDocument, ProviderExport,
|
||||
ProviderExportCredentialInput, ProviderModel, ProviderModels, ProviderQuota,
|
||||
ProviderQuotaRequest, ProviderQuotaWindow, QuotaLocalUsageAttribution,
|
||||
ProviderQuotaRequest, ProviderQuotaWindow, ProviderQuotaWindowRole, QuotaLocalUsageAttribution,
|
||||
};
|
||||
use gateway_admin::model::{MutationActor, MutationContext, Revision};
|
||||
use gateway_admin::ports::provider::{ProviderAdmin, ProviderAdminError, ProviderAdminErrorKind};
|
||||
@@ -734,39 +734,24 @@ fn project_quota_snapshot(snapshot: CodexAccountQuotaSnapshot) -> ProviderQuota
|
||||
.windows()
|
||||
.iter()
|
||||
.filter(|window| should_project_quota_window(window))
|
||||
.map(|window| {
|
||||
let mut data = Map::new();
|
||||
data.insert(
|
||||
"source".to_owned(),
|
||||
Value::String(window.source().to_owned()),
|
||||
);
|
||||
data.insert(
|
||||
"role".to_owned(),
|
||||
Value::String(quota_role(window.role()).to_owned()),
|
||||
);
|
||||
ProviderQuotaWindow {
|
||||
key: window.key().to_owned(),
|
||||
group: quota_group(window.kind()).to_owned(),
|
||||
label: codex_quota_window_label(
|
||||
window.limit_name(),
|
||||
window.kind(),
|
||||
window.role(),
|
||||
window.source(),
|
||||
window.window_seconds(),
|
||||
),
|
||||
source: Some(window.source().to_owned()),
|
||||
local_usage_attribution: if window.is_account_wide() {
|
||||
QuotaLocalUsageAttribution::AccountWide
|
||||
} else {
|
||||
QuotaLocalUsageAttribution::Unavailable
|
||||
},
|
||||
window_seconds: window.window_seconds(),
|
||||
used_percent: window.used_percent(),
|
||||
reset_at: window.reset_at(),
|
||||
limit_reached: window.limit_reached(),
|
||||
local_usage: None,
|
||||
provider_data: Some(ProviderDocument::new(OpaqueProviderData::new(data))),
|
||||
}
|
||||
.map(|window| ProviderQuotaWindow {
|
||||
key: window.key().to_owned(),
|
||||
group: quota_group(window.kind()).to_owned(),
|
||||
label: codex_quota_window_label(window.kind(), window.role(), window.window_seconds()),
|
||||
limit_id: Some(window.source().to_owned()),
|
||||
limit_name: window.limit_name().map(str::to_owned),
|
||||
role: Some(quota_role(window.role())),
|
||||
local_usage_attribution: if window.is_account_wide() {
|
||||
QuotaLocalUsageAttribution::AccountWide
|
||||
} else {
|
||||
QuotaLocalUsageAttribution::Unavailable
|
||||
},
|
||||
window_seconds: window.window_seconds(),
|
||||
used_percent: window.used_percent(),
|
||||
reset_at: window.reset_at(),
|
||||
limit_reached: window.limit_reached(),
|
||||
local_usage: None,
|
||||
provider_data: None,
|
||||
})
|
||||
.collect();
|
||||
// 快照级 limit_reached 只看滚动后的窗口触顶:顶层标记是观测事实,不能
|
||||
@@ -806,23 +791,21 @@ const fn quota_group(kind: CodexQuotaWindowKind) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
const fn quota_role(role: CodexQuotaWindowRole) -> &'static str {
|
||||
const fn quota_role(role: CodexQuotaWindowRole) -> ProviderQuotaWindowRole {
|
||||
match role {
|
||||
CodexQuotaWindowRole::Primary => "primary",
|
||||
CodexQuotaWindowRole::Secondary => "secondary",
|
||||
CodexQuotaWindowRole::Monthly => "monthly",
|
||||
CodexQuotaWindowRole::Primary => ProviderQuotaWindowRole::Primary,
|
||||
CodexQuotaWindowRole::Secondary => ProviderQuotaWindowRole::Secondary,
|
||||
CodexQuotaWindowRole::Monthly => ProviderQuotaWindowRole::Monthly,
|
||||
}
|
||||
}
|
||||
|
||||
/// 按窗口时长显示汉化额度名;非核心桶补上上游桶名称作为前缀。
|
||||
/// 按窗口时长显示汉化额度名;额度桶名称通过独立字段投影。
|
||||
fn codex_quota_window_label(
|
||||
limit_name: Option<&str>,
|
||||
kind: CodexQuotaWindowKind,
|
||||
role: CodexQuotaWindowRole,
|
||||
source: &str,
|
||||
window_seconds: Option<u64>,
|
||||
) -> String {
|
||||
let base = match kind {
|
||||
match kind {
|
||||
CodexQuotaWindowKind::Monthly => "月额度".to_owned(),
|
||||
CodexQuotaWindowKind::Weekly => "周额度".to_owned(),
|
||||
CodexQuotaWindowKind::ShortTerm => {
|
||||
@@ -833,29 +816,7 @@ fn codex_quota_window_label(
|
||||
}
|
||||
}
|
||||
CodexQuotaWindowKind::Other => custom_quota_window_label(window_seconds, role),
|
||||
};
|
||||
if let Some(prefix) = codex_quota_window_prefix(limit_name, source) {
|
||||
return format!("{prefix} · {base}");
|
||||
}
|
||||
base
|
||||
}
|
||||
|
||||
fn codex_quota_window_prefix(limit_name: Option<&str>, source: &str) -> Option<String> {
|
||||
let source_identifier = quota_identifier(source);
|
||||
if matches!(source_identifier.as_str(), "core" | "codex") {
|
||||
return None;
|
||||
}
|
||||
let label = limit_name.unwrap_or(source);
|
||||
match quota_identifier(label).as_str() {
|
||||
"premium" => Some("Premium".to_owned()),
|
||||
"code_review" => Some("代码审查".to_owned()),
|
||||
"spend_control" => Some("消费控制".to_owned()),
|
||||
_ => Some(label.replace('_', " ")),
|
||||
}
|
||||
}
|
||||
|
||||
fn quota_identifier(value: &str) -> String {
|
||||
value.trim().to_ascii_lowercase().replace(['-', ' '], "_")
|
||||
}
|
||||
|
||||
fn custom_quota_window_label(window_seconds: Option<u64>, role: CodexQuotaWindowRole) -> String {
|
||||
|
||||
@@ -14,7 +14,7 @@ use gateway_admin::model::provider_credentials::{
|
||||
AuthorizationMutationTarget, AuthorizationOwnerBinding, CompleteAuthorization,
|
||||
PendingAuthorizationMutation, PrepareCredentialImport, PrepareCredentialRefresh,
|
||||
PrepareCredentialRotation, ProviderDocument, ProviderExportCredentialInput,
|
||||
ProviderQuotaRequest, QuotaLocalUsageAttribution,
|
||||
ProviderQuotaRequest, ProviderQuotaWindowRole, QuotaLocalUsageAttribution,
|
||||
};
|
||||
use gateway_admin::model::{MutationActor, MutationContext, Revision};
|
||||
use gateway_admin::ports::provider::ProviderAdminErrorKind;
|
||||
@@ -566,7 +566,7 @@ async fn openai_admin_provider_projects_official_codex_quota_and_independent_buc
|
||||
assert_eq!(monthly.len(), 1);
|
||||
assert!(monthly.iter().any(|window| {
|
||||
window.label == "月额度"
|
||||
&& window.source.as_deref() == Some("codex")
|
||||
&& window.limit_id.as_deref() == Some("codex")
|
||||
&& window.used_percent == Some(91.0)
|
||||
}));
|
||||
assert!(
|
||||
@@ -586,13 +586,15 @@ async fn openai_admin_provider_projects_official_codex_quota_and_independent_buc
|
||||
let review = quota
|
||||
.windows
|
||||
.iter()
|
||||
.find(|window| window.source.as_deref() == Some("code_review"))
|
||||
.find(|window| window.limit_id.as_deref() == Some("code_review"))
|
||||
.expect("code review quota");
|
||||
assert_eq!(review.label, "代码审查 · 周额度");
|
||||
assert_eq!(review.label, "周额度");
|
||||
assert_eq!(review.limit_name.as_deref(), Some("code_review"));
|
||||
assert_eq!(review.role, Some(ProviderQuotaWindowRole::Primary));
|
||||
let spark = quota
|
||||
.windows
|
||||
.iter()
|
||||
.find(|window| window.source.as_deref() == Some("codex_bengalfox"))
|
||||
.find(|window| window.limit_id.as_deref() == Some("codex_bengalfox"))
|
||||
.expect("Spark quota");
|
||||
assert_eq!(
|
||||
(
|
||||
|
||||
@@ -1071,7 +1071,9 @@ fn project_quota(
|
||||
key: billing.period_type().unwrap_or("billing").to_owned(),
|
||||
group: quota_group(period_kind).to_owned(),
|
||||
label: xai_quota_window_label(period_kind, window_seconds),
|
||||
source: None,
|
||||
limit_id: None,
|
||||
limit_name: None,
|
||||
role: None,
|
||||
local_usage_attribution: QuotaLocalUsageAttribution::AccountWide,
|
||||
window_seconds,
|
||||
used_percent: billing.used_percent(),
|
||||
@@ -1087,7 +1089,9 @@ fn project_quota(
|
||||
key: "free-rolling-24h".to_owned(),
|
||||
group: "shortTerm".to_owned(),
|
||||
label: "日限额".to_owned(),
|
||||
source: None,
|
||||
limit_id: None,
|
||||
limit_name: None,
|
||||
role: None,
|
||||
local_usage_attribution: QuotaLocalUsageAttribution::AccountWide,
|
||||
window_seconds: Some(crate::GROK_FREE_ROLLING_WINDOW_SECONDS),
|
||||
used_percent: None,
|
||||
|
||||
@@ -14,8 +14,12 @@ export type AccountErrorReason
|
||||
export interface AccountQuotaWindow {
|
||||
key: string
|
||||
group: string
|
||||
limitId: string | null
|
||||
limitName: string | null
|
||||
role: 'primary' | 'secondary' | 'monthly' | null
|
||||
windowSeconds: number | null
|
||||
labelDisplay: string
|
||||
windowLabelDisplay: string
|
||||
usedPercent: number | null
|
||||
usedPercentDisplay: string
|
||||
limitReached: boolean
|
||||
|
||||
@@ -4,9 +4,9 @@ import { RefreshCw } from '@lucide/vue'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import BaseIconButton from '@/components/base/BaseIconButton.vue'
|
||||
import { orderedPanelQuotaWindows } from '../constants'
|
||||
import { groupedAccountQuotaWindows, orderedPanelQuotaWindows } from '../constants'
|
||||
import AccountPlanBadge from './AccountPlanBadge.vue'
|
||||
import AccountUsageWindow from './AccountUsageWindow/index.vue'
|
||||
import AccountQuotaPanelEntry from './AccountQuotaPanelEntry.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
account: AccountRow
|
||||
@@ -17,23 +17,24 @@ const emit = defineEmits<{
|
||||
refreshQuota: [accountId: string]
|
||||
}>()
|
||||
|
||||
const quotaWindows = computed(() => orderedPanelQuotaWindows(props.account.quota.windows))
|
||||
const quotaEntries = computed(() => groupedAccountQuotaWindows(
|
||||
orderedPanelQuotaWindows(props.account.quota.windows),
|
||||
))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="rounded-lg bg-cp-surface p-4 shadow-cp-control">
|
||||
<div class="mb-3 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<section class="flex min-h-0 flex-col rounded-lg bg-cp-surface p-4 shadow-cp-control">
|
||||
<div class="mb-3 flex shrink-0 items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<h3 class="m-0 text-[14px] font-heavy text-cp-primary">
|
||||
账号额度
|
||||
</h3>
|
||||
<p
|
||||
class="m-0 mt-1 flex flex-wrap items-center gap-x-1.5 gap-y-1 text-[12px] font-emphasis text-cp-secondary"
|
||||
class="m-0 mt-1 flex min-w-0 items-center gap-1.5 text-[11px] font-emphasis text-cp-secondary"
|
||||
>
|
||||
<span>{{ account.provider === 'xai' ? 'xAI 用量窗口' : 'Codex 额度' }}</span>
|
||||
<template v-if="account.provider === 'openai'">
|
||||
<span>·</span>
|
||||
<span>套餐:</span>
|
||||
<AccountPlanBadge :plan-type="account.planType" size="sm" />
|
||||
</template>
|
||||
<span>·</span>
|
||||
@@ -55,9 +56,16 @@ const quotaWindows = computed(() => orderedPanelQuotaWindows(props.account.quota
|
||||
</BaseIconButton>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3">
|
||||
<AccountUsageWindow v-for="window in quotaWindows" :key="window.key" :window="window" />
|
||||
<AccountUsageWindow v-if="quotaWindows.length === 0" />
|
||||
<div class="grid max-h-48 min-h-0 gap-3 overflow-y-auto pr-1">
|
||||
<AccountQuotaPanelEntry
|
||||
v-for="entry in quotaEntries"
|
||||
:key="entry.key"
|
||||
:label="entry.label"
|
||||
:windows="entry.windows"
|
||||
/>
|
||||
<p v-if="quotaEntries.length === 0" class="m-0 text-[12px] font-emphasis text-cp-secondary">
|
||||
额度待观测
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccountQuotaWindow } from '../constants'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
quotaWindowLocalUsageDisplay,
|
||||
quotaWindowPresentation,
|
||||
} from './AccountUsageWindow/presenter'
|
||||
|
||||
const props = defineProps<{
|
||||
label: string | null
|
||||
windows: AccountQuotaWindow[]
|
||||
}>()
|
||||
|
||||
const grouped = computed(() => props.windows.length > 1 && Boolean(props.label))
|
||||
const items = computed(() => props.windows.map((window) => {
|
||||
const presentation = quotaWindowPresentation(window, '4px')
|
||||
|
||||
return {
|
||||
key: window.key,
|
||||
label: grouped.value
|
||||
? window.windowLabelDisplay
|
||||
: window.labelDisplay,
|
||||
ariaLabel: window.labelDisplay,
|
||||
usedPercent: window.usedPercent,
|
||||
usedPercentDisplay: window.usedPercentDisplay,
|
||||
localUsageDisplay: quotaWindowLocalUsageDisplay(window),
|
||||
resetAtDisplay: window.resetAtDisplay,
|
||||
percentTextClass: presentation.percentTextClass,
|
||||
barClass: presentation.barClass,
|
||||
barStyle: presentation.barStyle,
|
||||
}
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="grid min-w-0 gap-2">
|
||||
<h4
|
||||
v-if="grouped && label"
|
||||
class="m-0 truncate text-[11px] leading-4 font-heavy text-cp-secondary"
|
||||
:title="label"
|
||||
>
|
||||
{{ label }}
|
||||
</h4>
|
||||
|
||||
<div
|
||||
class="grid min-w-0 gap-3"
|
||||
: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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -2,7 +2,8 @@
|
||||
import type { AccountRow } from '../constants'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { visibleSummaryQuotaWindows } from '../constants'
|
||||
import { groupedAccountQuotaWindows, visibleSummaryQuotaWindows } from '../constants'
|
||||
import AccountQuotaSummaryEntry from './AccountQuotaSummaryEntry.vue'
|
||||
import AccountUsageWindow from './AccountUsageWindow/index.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -11,10 +12,11 @@ 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(
|
||||
() =>
|
||||
`grid w-full min-w-0 gap-1.5 py-0.5 ${
|
||||
visibleQuotaWindows.value.length <= 1
|
||||
`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'
|
||||
}`,
|
||||
@@ -23,12 +25,12 @@ const summaryClass = computed(
|
||||
|
||||
<template>
|
||||
<div :class="summaryClass">
|
||||
<AccountUsageWindow
|
||||
v-for="window in visibleQuotaWindows"
|
||||
:key="window.key"
|
||||
:window="window"
|
||||
variant="compact"
|
||||
<AccountQuotaSummaryEntry
|
||||
v-for="entry in summaryEntries"
|
||||
:key="entry.key"
|
||||
:label="entry.label"
|
||||
:windows="entry.windows"
|
||||
/>
|
||||
<AccountUsageWindow v-if="quotaWindows.length === 0" variant="compact" />
|
||||
<AccountUsageWindow v-if="summaryEntries.length === 0" variant="compact" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccountQuotaWindow } from '../constants'
|
||||
|
||||
import { computed } from 'vue'
|
||||
|
||||
import BasePopover from '@/components/base/BasePopover.vue'
|
||||
import { useUiClock } from '@/composables/useUiClock'
|
||||
import AccountQuotaWindowGroup from './AccountQuotaWindowGroup.vue'
|
||||
import AccountUsageWindow from './AccountUsageWindow/index.vue'
|
||||
import { resolveAccountUsageWindowPresentation } from './AccountUsageWindow/presenter'
|
||||
|
||||
const props = defineProps<{
|
||||
label: string | null
|
||||
windows: AccountQuotaWindow[]
|
||||
}>()
|
||||
|
||||
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 detailItems = computed(() => props.windows.map((window) => {
|
||||
const view = resolveAccountUsageWindowPresentation({
|
||||
window,
|
||||
variant: 'detail',
|
||||
showLocalValue: true,
|
||||
now: now.value.getTime(),
|
||||
})
|
||||
|
||||
return {
|
||||
key: window.key,
|
||||
code: quotaWindowCode(window.windowSeconds, window.role),
|
||||
label: window.windowLabelDisplay,
|
||||
usedPercent: window.usedPercent,
|
||||
usedPercentDisplay: window.usedPercentDisplay,
|
||||
resetAtDisplay: window.resetAtDisplay,
|
||||
localUsageDisplay: view.quota.localUsageVisible
|
||||
? view.quota.localUsageDisplay
|
||||
: '—',
|
||||
percentTextClass: view.quota.percentTextClass,
|
||||
barClass: view.quota.barClass,
|
||||
barStyle: view.quota.barStyle,
|
||||
}
|
||||
}))
|
||||
function quotaWindowCode(
|
||||
windowSeconds: number | null,
|
||||
role: AccountQuotaWindow['role'],
|
||||
) {
|
||||
if (typeof windowSeconds === 'number' && Number.isFinite(windowSeconds)) {
|
||||
const daySeconds = 24 * 60 * 60
|
||||
const hourSeconds = 60 * 60
|
||||
|
||||
if (windowSeconds >= daySeconds && windowSeconds % daySeconds === 0)
|
||||
return `${windowSeconds / daySeconds}D`
|
||||
if (windowSeconds >= hourSeconds && windowSeconds % hourSeconds === 0)
|
||||
return `${windowSeconds / hourSeconds}H`
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case 'primary':
|
||||
return 'P'
|
||||
case 'secondary':
|
||||
return 'S'
|
||||
case 'monthly':
|
||||
return 'M'
|
||||
default:
|
||||
return 'Q'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasePopover
|
||||
class="w-full"
|
||||
trigger="hover-click"
|
||||
placement="right"
|
||||
:hover-delay="240"
|
||||
>
|
||||
<template #trigger="{ open }">
|
||||
<button
|
||||
type="button"
|
||||
class="block w-full cursor-help rounded-sm border-0 bg-transparent p-0 text-left outline-none focus-visible:ring-2 focus-visible:ring-cp-info-border"
|
||||
:aria-label="`查看${detailTitle}详情`"
|
||||
:aria-expanded="open"
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
<AccountQuotaWindowGroup
|
||||
v-if="grouped && label"
|
||||
:label="label"
|
||||
:windows="windows"
|
||||
/>
|
||||
<AccountUsageWindow
|
||||
v-else
|
||||
:window="windows[0]"
|
||||
variant="compact"
|
||||
:show-local-value="true"
|
||||
:show-percentage="false"
|
||||
:show-native-tooltip="false"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<section
|
||||
class="w-84 overflow-hidden rounded-cp-overlay"
|
||||
role="dialog"
|
||||
:aria-label="`${detailTitle}详情`"
|
||||
>
|
||||
<header
|
||||
v-if="detailHeading"
|
||||
class="bg-cp-muted p-3"
|
||||
>
|
||||
<h3
|
||||
class="m-0 truncate text-[13px] leading-5 font-heavy text-cp-primary"
|
||||
:title="detailHeading"
|
||||
>
|
||||
{{ detailHeading }}
|
||||
</h3>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="grid gap-4"
|
||||
:class="detailHeading ? 'p-3' : 'p-4'"
|
||||
>
|
||||
<article
|
||||
v-for="item in detailItems"
|
||||
:key="item.key"
|
||||
>
|
||||
<div class="grid grid-cols-[32px_minmax(0,1fr)_auto] items-center gap-2.5">
|
||||
<span
|
||||
class="grid h-5 w-8 place-items-center rounded-sm bg-cp-muted font-mono text-[9px] leading-none font-heavy tracking-[0.04em] text-cp-info-text"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{{ item.code }}
|
||||
</span>
|
||||
<h4 class="m-0 min-w-0 truncate text-[12px] leading-4 font-heavy text-cp-secondary">
|
||||
{{ item.label }}
|
||||
</h4>
|
||||
<strong
|
||||
class="font-mono text-[11px] leading-none font-heavy tabular-nums"
|
||||
:class="item.percentTextClass"
|
||||
>
|
||||
{{ 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"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-full transition-[width,background-color] duration-200 motion-reduce:transition-none"
|
||||
:class="item.barClass"
|
||||
:style="item.barStyle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</dl>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</BasePopover>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccountQuotaWindow } from '../constants'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { quotaWindowPresentation } from './AccountUsageWindow/presenter'
|
||||
|
||||
const props = defineProps<{
|
||||
label: string
|
||||
windows: AccountQuotaWindow[]
|
||||
}>()
|
||||
|
||||
const windowItems = computed(() => props.windows.map(window => ({
|
||||
key: window.key,
|
||||
labelTooltip: window.labelDisplay,
|
||||
usedPercent: window.usedPercent,
|
||||
usedPercentDisplay: window.usedPercentDisplay,
|
||||
presentation: quotaWindowPresentation(window, '2px'),
|
||||
})))
|
||||
|
||||
const trackGridStyle = computed(() => ({
|
||||
gridTemplateColumns: `repeat(${Math.max(windowItems.value.length, 1)}, minmax(0, 1fr))`,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<div class="mb-1 min-w-0 truncate text-[11px] leading-3.5 font-bold text-cp-muted-text">
|
||||
{{ label }}
|
||||
</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-default-border"
|
||||
role="progressbar"
|
||||
:aria-label="item.labelTooltip"
|
||||
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"
|
||||
:class="item.presentation.barClass"
|
||||
:style="item.presentation.barStyle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -69,7 +69,7 @@ const modelUsageColumns = defineTableColumns<AccountModelUsage>([
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="min-w-0 pt-4 shadow-[inset_0_1px_0_var(--cp-divider-subtle)] xl:pt-0 xl:pl-4 xl:shadow-[inset_1px_0_0_var(--cp-divider-subtle)]"
|
||||
class="min-w-0 pt-4 xl:pt-0 xl:pl-4"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h3 class="m-0 text-[14px] font-heavy text-cp-primary">
|
||||
|
||||
@@ -10,10 +10,14 @@ const props = withDefaults(
|
||||
window?: AccountQuotaWindow
|
||||
variant?: AccountUsageWindowVariant
|
||||
showLocalValue?: boolean
|
||||
showPercentage?: boolean
|
||||
showNativeTooltip?: boolean
|
||||
}>(),
|
||||
{
|
||||
variant: 'detail',
|
||||
showLocalValue: true,
|
||||
showPercentage: true,
|
||||
showNativeTooltip: true,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -33,7 +37,7 @@ const view = computed(() => resolveAccountUsageWindowPresentation({
|
||||
<span
|
||||
class="min-w-0"
|
||||
:class="view.classes.label"
|
||||
:title="view.labelTooltip"
|
||||
:title="showNativeTooltip ? view.labelTooltip : undefined"
|
||||
>
|
||||
{{ window.labelDisplay }}
|
||||
</span>
|
||||
@@ -45,15 +49,15 @@ const view = computed(() => resolveAccountUsageWindowPresentation({
|
||||
v-if="view.quota.localUsageVisible"
|
||||
class="text-right text-cp-muted-text"
|
||||
:class="view.classes.value"
|
||||
:title="`本地 Token 用量:${view.quota.localUsageDisplay}`"
|
||||
:title="showNativeTooltip ? `窗口消耗:${view.quota.localUsageDisplay}` : undefined"
|
||||
>
|
||||
{{ view.quota.localUsageDisplay }}
|
||||
</span>
|
||||
<span
|
||||
v-if="view.quota.valueVisible"
|
||||
v-if="showPercentage && view.quota.valueVisible"
|
||||
class="text-right"
|
||||
:class="[view.classes.value, view.quota.percentTextClass]"
|
||||
:title="`额度已使用:${window.usedPercentDisplay}`"
|
||||
:title="showNativeTooltip ? `额度已使用:${window.usedPercentDisplay}` : undefined"
|
||||
>
|
||||
{{ window.usedPercentDisplay }}
|
||||
</span>
|
||||
@@ -70,7 +74,7 @@ const view = computed(() => resolveAccountUsageWindowPresentation({
|
||||
{{ view.quota.localUsageDisplay }}
|
||||
</span>
|
||||
<span
|
||||
v-if="view.quota.valueVisible"
|
||||
v-if="showPercentage && view.quota.valueVisible"
|
||||
:class="[view.classes.value, view.quota.percentTextClass]"
|
||||
>
|
||||
{{ window.usedPercentDisplay }}
|
||||
|
||||
@@ -119,9 +119,7 @@ export function resolveAccountUsageWindowPresentation(
|
||||
const localUsage = accountLocalUsage(input.window?.localUsage)
|
||||
const mode = usageWindowMode(input.window, localUsage)
|
||||
const quotaLocalUsageDisplay = localTokenDisplay(localUsage)
|
||||
const quotaLocalUsageVisible = input.showLocalValue
|
||||
&& typeof localUsage?.totalTokens === 'number'
|
||||
&& localUsage.totalTokens > 0
|
||||
const quotaLocalUsageVisible = input.showLocalValue && Boolean(quotaLocalUsageDisplay)
|
||||
const localRequestDisplay = requestCountDisplay(localUsage)
|
||||
const localRequestValueVisible = input.showLocalValue
|
||||
&& typeof localUsage?.requestCount === 'number'
|
||||
@@ -146,7 +144,7 @@ export function resolveAccountUsageWindowPresentation(
|
||||
},
|
||||
quota: {
|
||||
valueVisible: typeof input.window?.usedPercent === 'number'
|
||||
&& input.window.usedPercent > 0,
|
||||
&& (input.variant !== 'compact' || input.window.usedPercent > 0),
|
||||
localUsageDisplay: quotaLocalUsageDisplay,
|
||||
localUsageVisible: quotaLocalUsageVisible,
|
||||
barStyle: quota?.barStyle,
|
||||
@@ -221,11 +219,19 @@ function usageWindowMode(
|
||||
}
|
||||
|
||||
function localTokenDisplay(localUsage: AccountLocalUsage | null) {
|
||||
const total = localUsage?.totalTokens
|
||||
if (typeof total !== 'number' || total <= 0)
|
||||
return ''
|
||||
|
||||
const display = localUsage?.totalTokensDisplay
|
||||
if (typeof display === 'string' && display.trim())
|
||||
return display.trim()
|
||||
const total = localUsage?.totalTokens
|
||||
return typeof total === 'number' && total > 0 ? formatInteger(total) : ''
|
||||
return formatInteger(total)
|
||||
}
|
||||
|
||||
export function quotaWindowLocalUsageDisplay(window: AccountQuotaWindow) {
|
||||
const localUsage = accountLocalUsage(window.localUsage)
|
||||
return localTokenDisplay(localUsage) || null
|
||||
}
|
||||
|
||||
function requestCountDisplay(localUsage: AccountLocalUsage | null) {
|
||||
@@ -236,7 +242,7 @@ function requestCountDisplay(localUsage: AccountLocalUsage | null) {
|
||||
return typeof count === 'number' ? formatInteger(count) : '0'
|
||||
}
|
||||
|
||||
function quotaWindowPresentation(window: AccountQuotaWindow, minimumWidth: string) {
|
||||
export function quotaWindowPresentation(window: AccountQuotaWindow, minimumWidth: string) {
|
||||
const percent = clamp(window.usedPercent ?? 0, 0, 100)
|
||||
const tone = quotaWindowTone(window.usedPercent)
|
||||
return {
|
||||
|
||||
@@ -5,6 +5,12 @@ import { formatProviderLabel } from '@/utils/providers'
|
||||
export type AccountRow = Awaited<ReturnType<typeof getAccounts>>['items'][number]
|
||||
export type AccountQuotaWindow = AccountRow['quota']['windows'][number]
|
||||
|
||||
export interface AccountQuotaWindowEntry {
|
||||
key: string
|
||||
label: string | null
|
||||
windows: AccountQuotaWindow[]
|
||||
}
|
||||
|
||||
const quotaGroupOrder = new Map([
|
||||
['shortTerm', 0],
|
||||
['monthly', 1],
|
||||
@@ -104,6 +110,37 @@ export function orderedPanelQuotaWindows(windows: AccountQuotaWindow[]) {
|
||||
return [...windows].sort(compareQuotaWindows)
|
||||
}
|
||||
|
||||
export function groupedAccountQuotaWindows(windows: AccountQuotaWindow[]) {
|
||||
const entries: AccountQuotaWindowEntry[] = []
|
||||
const limitEntryIndexes = new Map<string, number>()
|
||||
|
||||
for (const window of windows) {
|
||||
const limitLabel = quotaLimitLabel(window)
|
||||
if (!window.limitId || !limitLabel) {
|
||||
entries.push({ key: window.key, label: null, windows: [window] })
|
||||
continue
|
||||
}
|
||||
|
||||
const existingIndex = limitEntryIndexes.get(window.limitId)
|
||||
if (existingIndex !== undefined) {
|
||||
entries[existingIndex]?.windows.push(window)
|
||||
continue
|
||||
}
|
||||
|
||||
limitEntryIndexes.set(window.limitId, entries.length)
|
||||
entries.push({
|
||||
key: `limit:${window.limitId}`,
|
||||
label: limitLabel,
|
||||
windows: [window],
|
||||
})
|
||||
}
|
||||
|
||||
for (const entry of entries)
|
||||
entry.windows.sort(compareQuotaLimitWindows)
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
export function modelSuccessRateTextClass(successRate: number | null) {
|
||||
if (successRate === null)
|
||||
return 'text-cp-muted-text'
|
||||
@@ -121,12 +158,7 @@ function compareQuotaWindows(left: AccountQuotaWindow, right: AccountQuotaWindow
|
||||
if (groupDifference !== 0)
|
||||
return groupDifference
|
||||
|
||||
const secondsDifference = (left.windowSeconds ?? Number.MAX_SAFE_INTEGER)
|
||||
- (right.windowSeconds ?? Number.MAX_SAFE_INTEGER)
|
||||
if (secondsDifference !== 0)
|
||||
return secondsDifference
|
||||
|
||||
// 同组同周期保留 Provider 的投影顺序,避免内部 key 把 additional 排到 core 前面。
|
||||
// 同组保留 Provider 的投影顺序,避免按窗口时长打散 core 与模型专属额度。
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -134,6 +166,38 @@ function groupOrder(window: AccountQuotaWindow) {
|
||||
return quotaGroupOrder.get(window.group) ?? quotaGroupOrder.size
|
||||
}
|
||||
|
||||
function compareQuotaLimitWindows(left: AccountQuotaWindow, right: AccountQuotaWindow) {
|
||||
const durationDifference = windowDurationOrder(left.windowSeconds)
|
||||
- windowDurationOrder(right.windowSeconds)
|
||||
if (durationDifference !== 0)
|
||||
return durationDifference
|
||||
|
||||
return windowRoleOrder(left.role) - windowRoleOrder(right.role)
|
||||
}
|
||||
|
||||
function windowDurationOrder(windowSeconds: number | null) {
|
||||
return windowSeconds ?? Number.POSITIVE_INFINITY
|
||||
}
|
||||
|
||||
function windowRoleOrder(role: AccountQuotaWindow['role']) {
|
||||
switch (role) {
|
||||
case 'primary':
|
||||
return 0
|
||||
case 'secondary':
|
||||
return 1
|
||||
case 'monthly':
|
||||
return 2
|
||||
default:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
function quotaLimitLabel(window: AccountQuotaWindow) {
|
||||
if (window.limitId === 'codex')
|
||||
return '核心额度'
|
||||
return window.limitName
|
||||
}
|
||||
|
||||
function accountProviderLabel(value?: string | null) {
|
||||
return formatProviderLabel(value)
|
||||
}
|
||||
|
||||
@@ -462,7 +462,11 @@ function dashboardUsageWindow(item: DashboardSummary['accountUsage'][number]) {
|
||||
return {
|
||||
key: 'quota',
|
||||
group: 'other',
|
||||
limitId: null,
|
||||
limitName: null,
|
||||
role: null,
|
||||
labelDisplay: '额度',
|
||||
windowLabelDisplay: '额度',
|
||||
usedPercent,
|
||||
usedPercentDisplay: usedPercent === null ? '—' : `${usedPercent}%`,
|
||||
limitReached: (usedPercent ?? 0) >= 100,
|
||||
@@ -484,7 +488,11 @@ function dashboardUsageWindow(item: DashboardSummary['accountUsage'][number]) {
|
||||
return {
|
||||
key: 'dailyRequests',
|
||||
group: 'other',
|
||||
limitId: null,
|
||||
limitName: null,
|
||||
role: null,
|
||||
labelDisplay: '日请求',
|
||||
windowLabelDisplay: '日请求',
|
||||
usedPercent: null,
|
||||
usedPercentDisplay: '—',
|
||||
limitReached: false,
|
||||
|
||||
@@ -15,7 +15,7 @@ const latencyDetails = computed(() => usageLatencyDetails(props.record))
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-1.5">
|
||||
<div
|
||||
class="grid grid-cols-[auto_auto] items-center justify-end gap-x-2 gap-y-1.5 font-mono text-[12px] leading-none font-heavy tabular-nums"
|
||||
class="grid grid-cols-[auto_auto] items-center justify-end gap-x-2 gap-y-1.5 whitespace-nowrap font-mono text-[12px] leading-none font-heavy tabular-nums"
|
||||
>
|
||||
<span class="text-[11px] text-cp-muted-text">{{ latencyDetails.firstOutputLabel }}</span>
|
||||
<span class="text-cp-secondary">{{ latencyDetails.firstOutputDisplay }}</span>
|
||||
|
||||
@@ -19,15 +19,15 @@ export const usageRecordColumns = defineTableColumns<UsageDisplayRecord>([
|
||||
size: 'sm',
|
||||
format: (value: unknown) => formatProvider(typeof value === 'string' ? value : null),
|
||||
},
|
||||
{ key: 'model', label: '模型', kind: 'custom' },
|
||||
{ key: 'model', label: '模型', kind: 'custom', size: 'xl' },
|
||||
{ key: 'reasoningEffort', label: '推理强度', kind: 'status' },
|
||||
{ key: 'route', label: '端点', kind: 'mono' },
|
||||
{ key: 'recordType', label: '类型', kind: 'status', size: 'sm' },
|
||||
{ key: 'tokenDetails', label: 'TOKEN', kind: 'numeric', size: 'xl' },
|
||||
{ key: 'billing', label: '费用', kind: 'numeric', size: 'lg' },
|
||||
{ key: 'latency', label: '延迟', kind: 'numeric', size: 'lg' },
|
||||
{ key: 'latency', label: '延迟', kind: 'numeric', size: 'xl' },
|
||||
{ key: 'createdAtDisplay', label: '时间', kind: 'datetime' },
|
||||
{ key: 'clientIp', label: 'IP', kind: 'custom', size: '2xl' },
|
||||
{ key: 'clientIp', label: 'IP', kind: 'custom', size: '3xl' },
|
||||
{ key: 'userAgent', label: 'User-Agent', kind: 'custom', size: '4xl' },
|
||||
{ key: 'actions', label: '操作', kind: 'actions', size: 'sm' },
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user