diff --git a/frontend/src/components/ProviderIconGroup.vue b/frontend/src/components/ProviderIconGroup.vue
index b34d2940c..9d84bc0bd 100644
--- a/frontend/src/components/ProviderIconGroup.vue
+++ b/frontend/src/components/ProviderIconGroup.vue
@@ -1,5 +1,5 @@
@@ -60,7 +62,8 @@ const authenticationLabel = computed(() => {
:class="iconContainerClass"
:title="authenticationLabel"
>
-
+
+
?
diff --git a/frontend/src/views/accounts/components/AccountQuotaSummaryCell/index.vue b/frontend/src/views/accounts/components/AccountQuotaSummaryCell/index.vue
index beb67419d..be144720f 100644
--- a/frontend/src/views/accounts/components/AccountQuotaSummaryCell/index.vue
+++ b/frontend/src/views/accounts/components/AccountQuotaSummaryCell/index.vue
@@ -6,6 +6,7 @@ import { groupedAccountQuotaWindows, visibleSummaryQuotaWindows } from '../../co
import AccountUsageWindow from '../AccountUsageWindow/index.vue'
import { quotaWindowPresentation } from '../AccountUsageWindow/presenter'
import AccountQuotaSummaryEntry from './Entry.vue'
+import { recentlyUsedQuotaEntry, representativeQuotaWindow } from './presenter'
const props = defineProps<{
account: AccountRow
@@ -15,26 +16,15 @@ const quotaWindows = computed(() => props.account.quota.windows)
const visibleQuotaWindows = computed(() => visibleSummaryQuotaWindows(quotaWindows.value))
const summaryEntries = computed(() => groupedAccountQuotaWindows(visibleQuotaWindows.value))
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
+const recentUsageEntry = computed(() => recentlyUsedQuotaEntry(
+ summaryEntries.value,
+ props.account.usage.models,
+))
+const currentUsageWindow = computed(() => representativeQuotaWindow(recentUsageEntry.value))
+const currentUsageDisplay = computed(() => currentUsageWindow.value?.usedPercentDisplay ?? '—')
+const currentUsageTextClass = computed(() => currentUsageWindow.value
+ ? quotaWindowPresentation(currentUsageWindow.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))
@@ -58,20 +48,20 @@ const additionalEntryCount = computed(() => Math.max(summaryEntries.value.length
- 最高占用
-
- {{ highestUsageDisplay }}
+ 消耗比例
+
+ {{ currentUsageDisplay }}
-
+
diff --git a/frontend/src/views/accounts/components/AccountQuotaSummaryCell/presenter.ts b/frontend/src/views/accounts/components/AccountQuotaSummaryCell/presenter.ts
new file mode 100644
index 000000000..e8a516eff
--- /dev/null
+++ b/frontend/src/views/accounts/components/AccountQuotaSummaryCell/presenter.ts
@@ -0,0 +1,74 @@
+import type { AccountQuotaWindow, AccountQuotaWindowEntry, AccountRow } from '../../constants'
+
+type AccountModelUsage = AccountRow['usage']['models'][number]
+
+/**
+ * 根据账号最近一次模型请求选择对应额度组。
+ * 没有专属额度的模型统一回退到 Codex 通用额度。
+ */
+export function recentlyUsedQuotaEntry(
+ entries: readonly AccountQuotaWindowEntry[],
+ models: readonly AccountModelUsage[],
+) {
+ const recentModel = latestModelUsage(models)
+ if (recentModel) {
+ const modelKey = quotaIdentity(recentModel.model)
+ const modelEntry = entries.find(entry => quotaEntryIdentities(entry).has(modelKey))
+ if (modelEntry)
+ return modelEntry
+ }
+
+ return entries.find(entry => entry.windows.some(window => window.limitId === 'codex'))
+ ?? entries[0]
+}
+
+/** 一个额度组可能包含多个滚动窗口,摘要使用其中当前占用最高的窗口。 */
+export function representativeQuotaWindow(entry: AccountQuotaWindowEntry | undefined) {
+ return entry?.windows.reduce
((representative, window) => {
+ if (typeof window.usedPercent !== 'number')
+ return representative
+ if (
+ !representative
+ || typeof representative.usedPercent !== 'number'
+ || window.usedPercent > representative.usedPercent
+ ) {
+ return window
+ }
+ return representative
+ }, undefined)
+}
+
+function latestModelUsage(models: readonly AccountModelUsage[]) {
+ let latest: AccountModelUsage | undefined
+ let latestTimestamp = Number.NEGATIVE_INFINITY
+
+ for (const model of models) {
+ const timestamp = Date.parse(model.lastUsedAt)
+ if (!Number.isFinite(timestamp) || timestamp <= latestTimestamp)
+ continue
+ latest = model
+ latestTimestamp = timestamp
+ }
+
+ return latest
+}
+
+function quotaEntryIdentities(entry: AccountQuotaWindowEntry) {
+ const identities = new Set()
+ addQuotaIdentity(identities, entry.label)
+ for (const window of entry.windows) {
+ addQuotaIdentity(identities, window.limitId)
+ addQuotaIdentity(identities, window.limitName)
+ }
+ return identities
+}
+
+function addQuotaIdentity(identities: Set, value: string | null) {
+ const identity = quotaIdentity(value)
+ if (identity)
+ identities.add(identity)
+}
+
+function quotaIdentity(value: string | null) {
+ return value?.trim().toLowerCase().replace(/[^a-z0-9]+/g, '') ?? ''
+}
diff --git a/frontend/src/views/dashboard/components/AccountOverviewCard.vue b/frontend/src/views/dashboard/components/AccountOverviewCard.vue
index c948d65eb..63475b29a 100644
--- a/frontend/src/views/dashboard/components/AccountOverviewCard.vue
+++ b/frontend/src/views/dashboard/components/AccountOverviewCard.vue
@@ -5,11 +5,10 @@ import { CircleCheck, RefreshCw, ShieldAlert, TriangleAlert } from '@lucide/vue'
import { computed } from 'vue'
import BaseCard from '@/components/base/BaseCard.vue'
import BaseEmpty from '@/components/base/BaseEmpty.vue'
-import ProviderIconGroup from '@/components/ProviderIconGroup.vue'
import { formatCompactNumber } from '@/utils/number'
-import AccountIdentityCell from '@/views/accounts/components/AccountIdentityCell.vue'
import AccountUsageWindow from '@/views/accounts/components/AccountUsageWindow/index.vue'
import { metricToneIconClasses, metricToneValueClasses } from '../constants'
+import DashboardAccountIdentity from './DashboardAccountIdentity.vue'
type DashboardSnapshot = ReturnType
@@ -233,24 +232,12 @@ const statusBars = computed(() => {
-
-
-
-
-
+ class="min-w-0 xl:pr-4"
+ />
+import type { dashboardSnapshotView } from '../composables/useDashboard'
+import { Key, LinkAlt, Openai, Xai } from '@boxicons/vue'
+import { computed } from 'vue'
+
+import { formatProviderLabel } from '@/utils/providers'
+import AccountPlanBadge from '@/views/accounts/components/AccountPlanBadge.vue'
+import { stablePresetVisualToneClass } from '@/views/accounts/utils/visualTone'
+
+type DashboardSnapshot = ReturnType
+type DashboardAccount = DashboardSnapshot['accountUsage'][number]
+
+const props = defineProps<{
+ account: DashboardAccount
+}>()
+
+const email = computed(() => props.account.email?.trim() || String(props.account.id))
+const displayTitle = computed(() => email.value.split('@')[0] || email.value)
+const normalizedProvider = computed(() => props.account.provider?.trim().toLowerCase() || '')
+const normalizedAuthenticationKind = computed(() =>
+ props.account.authenticationKind?.trim().toLowerCase() || '',
+)
+const providerLabel = computed(() => formatProviderLabel(props.account.provider, '未知平台'))
+
+const authenticationLabel = computed(() => {
+ if (normalizedAuthenticationKind.value === 'oauth')
+ return 'OAuth'
+ if (normalizedAuthenticationKind.value === 'api_key')
+ return 'API Key'
+ return props.account.authenticationKind?.trim() || '未知认证类型'
+})
+
+const avatarToneClass = computed(() =>
+ stablePresetVisualToneClass(props.account.id || props.account.email || email.value),
+)
+
+
+
+
+
+
+
+
+ ?
+ {{ providerLabel }}
+
+
+
+
+
+ ?
+ {{ authenticationLabel }}
+
+
+
+
+
+
+ {{ displayTitle }}
+
+
+
+
+ {{ email }}
+
+
+
+
diff --git a/frontend/src/views/usage/components/OpsErrorPanel.vue b/frontend/src/views/usage/components/OpsErrorPanel.vue
index 7d58611d7..1be6c86c8 100644
--- a/frontend/src/views/usage/components/OpsErrorPanel.vue
+++ b/frontend/src/views/usage/components/OpsErrorPanel.vue
@@ -12,7 +12,9 @@ import ProviderIconGroup from '@/components/ProviderIconGroup.vue'
import { useOpsErrorsTable } from '../composables/useOpsErrorsTable'
import { opsErrorColumns } from '../constants'
import { opsErrorSummary } from '../utils/opsErrorPresentation'
+import { usageUserAgent } from '../utils/records'
import OpsErrorDetailModal from './OpsErrorDetailModal.vue'
+import UsageClientIpCell from './UsageClientIpCell.vue'
const props = defineProps<{
timeRangeParams: UsageTimeRangeParams
@@ -157,6 +159,14 @@ function upstreamSendStateText(value: string | null | undefined) {
{{ modelText(row) }}
+
+
+
+
+
+ {{ usageUserAgent(row) }}
+
+
-import type { UsageDisplayRecord } from '../utils/records'
-
import { computed } from 'vue'
import { usageClientIp } from '../utils/records'
const props = defineProps<{
- record: UsageDisplayRecord
+ record: { clientIp?: string | null }
}>()
const clientIp = computed(() => usageClientIp(props.record))
diff --git a/frontend/src/views/usage/constants.ts b/frontend/src/views/usage/constants.ts
index 0b62a085a..783ef719f 100644
--- a/frontend/src/views/usage/constants.ts
+++ b/frontend/src/views/usage/constants.ts
@@ -42,8 +42,8 @@ export const opsErrorColumns = defineTableColumns([
{ key: 'route', label: '端点', kind: 'mono', size: 'xl', emptyText: '未记录' },
{ key: 'createdAtDisplay', label: '时间', kind: 'datetime' },
{ key: 'requestId', label: '请求 ID', kind: 'mono', size: '2xl', emptyText: '未记录' },
- { key: 'clientIp', label: 'IP', kind: 'mono', size: '3xl', emptyText: '未记录' },
- { key: 'userAgent', label: 'User-Agent', kind: 'mono', size: '4xl', emptyText: '未记录' },
+ { key: 'clientIp', label: 'IP', kind: 'custom', size: '3xl', emptyText: '未记录' },
+ { key: 'userAgent', label: 'User-Agent', kind: 'custom', size: '4xl', emptyText: '未记录' },
{ key: 'actions', label: '操作', kind: 'actions', size: 'sm' },
])
diff --git a/frontend/src/views/usage/utils/records.ts b/frontend/src/views/usage/utils/records.ts
index 88a2fb147..33760df90 100644
--- a/frontend/src/views/usage/utils/records.ts
+++ b/frontend/src/views/usage/utils/records.ts
@@ -178,11 +178,11 @@ export function usageAuthenticationKind(record: UsageCommonRecord) {
return typeof record.authenticationKind === 'string' ? record.authenticationKind : null
}
-export function usageClientIp(record: UsageCommonRecord) {
+export function usageClientIp(record: { clientIp?: string | null }) {
return record.clientIp || '—'
}
-export function usageUserAgent(record: UsageCommonRecord) {
+export function usageUserAgent(record: { userAgent?: string | null }) {
return record.userAgent || '—'
}