mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-09 00:05:47 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
941c4d57fa | ||
|
|
cf046ad9a4 | ||
|
|
7699a954ac | ||
|
|
2c31708d1b | ||
|
|
80f338ce01 |
@@ -729,21 +729,19 @@ pub(crate) fn dashboard_view(
|
||||
last_used: relative_time(credential.last_used_at, range.end),
|
||||
});
|
||||
}
|
||||
let unavailable_accounts = provider_accounts
|
||||
.total
|
||||
.saturating_sub(provider_accounts.normal);
|
||||
|
||||
DashboardDataView {
|
||||
cards: DashboardCardsView {
|
||||
credentials: DashboardCredentialsCardView {
|
||||
total: format_compact_number(provider_accounts.total),
|
||||
total: provider_accounts.total.to_string(),
|
||||
total_value: provider_accounts.total,
|
||||
available: format_compact_number(provider_accounts.normal),
|
||||
available: provider_accounts.normal.to_string(),
|
||||
available_value: provider_accounts.normal,
|
||||
unavailable: format_compact_number(
|
||||
provider_accounts
|
||||
.total
|
||||
.saturating_sub(provider_accounts.normal),
|
||||
),
|
||||
unavailable_value: provider_accounts
|
||||
.total
|
||||
.saturating_sub(provider_accounts.normal),
|
||||
unavailable: unavailable_accounts.to_string(),
|
||||
unavailable_value: unavailable_accounts,
|
||||
},
|
||||
traffic: DashboardTrafficCardView {
|
||||
today_requests: format_compact_number(today.request_count),
|
||||
|
||||
@@ -221,9 +221,9 @@ async fn dashboard_summary_should_include_quota_exhaustion_in_unavailable_headli
|
||||
attempts: AttemptMetrics::default(),
|
||||
totals: Default::default(),
|
||||
provider_accounts: AccountPoolMetrics {
|
||||
total: 807,
|
||||
normal: 15,
|
||||
quota_exhausted: 785,
|
||||
total: 1002,
|
||||
normal: 943,
|
||||
quota_exhausted: 52,
|
||||
rate_limited: 0,
|
||||
disabled: 3,
|
||||
error: 4,
|
||||
@@ -259,9 +259,21 @@ async fn dashboard_summary_should_include_quota_exhaustion_in_unavailable_headli
|
||||
&value["data"]["cards"]["credentials"]["unavailableValue"],
|
||||
),
|
||||
(
|
||||
&serde_json::json!(807),
|
||||
&serde_json::json!(15),
|
||||
&serde_json::json!(792)
|
||||
&serde_json::json!(1002),
|
||||
&serde_json::json!(943),
|
||||
&serde_json::json!(59)
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
&value["data"]["cards"]["credentials"]["total"],
|
||||
&value["data"]["cards"]["credentials"]["available"],
|
||||
&value["data"]["cards"]["credentials"]["unavailable"],
|
||||
),
|
||||
(
|
||||
&serde_json::json!("1002"),
|
||||
&serde_json::json!("943"),
|
||||
&serde_json::json!("59")
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -203,10 +203,13 @@ onBeforeUnmount(() => {
|
||||
tabindex="-1"
|
||||
>
|
||||
<header
|
||||
class="grid shrink-0 items-start gap-3 p-4 pb-0 sm:gap-4 sm:p-6 sm:pb-0"
|
||||
:class="tone === 'neutral'
|
||||
? 'grid-cols-[minmax(0,1fr)_28px]'
|
||||
: 'grid-cols-[auto_minmax(0,1fr)_28px]'"
|
||||
class="grid shrink-0 gap-3 p-4 pb-0 sm:gap-4 sm:p-6 sm:pb-0"
|
||||
:class="[
|
||||
tone === 'neutral'
|
||||
? 'grid-cols-[minmax(0,1fr)_28px]'
|
||||
: 'grid-cols-[auto_minmax(0,1fr)_28px]',
|
||||
description ? 'items-start' : 'items-center',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
v-if="tone !== 'neutral'"
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: string
|
||||
name: string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
showLabel?: boolean
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
showLabel: false,
|
||||
},
|
||||
)
|
||||
|
||||
defineSlots<{
|
||||
default?: () => unknown
|
||||
}>()
|
||||
|
||||
const model = defineModel<string>({ required: true })
|
||||
const checked = computed(() => model.value === props.value)
|
||||
const indicatorClasses = computed(() => [
|
||||
'relative inline-grid size-4 shrink-0 place-items-center rounded-full transition-colors duration-150 motion-reduce:transition-none',
|
||||
props.disabled
|
||||
? 'bg-cp-disabled text-cp-disabled-icon'
|
||||
: checked.value
|
||||
? 'bg-cp-accent text-cp-accent-on'
|
||||
: 'bg-cp-muted text-transparent group-hover:bg-cp-default-active',
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- The native radio is nested so the full label remains one accessible click target. -->
|
||||
<!-- eslint-disable-next-line vue-a11y/label-has-for -->
|
||||
<label
|
||||
class="group relative inline-flex min-h-4 min-w-4 items-center gap-2.5"
|
||||
:class="disabled ? 'cursor-not-allowed opacity-55' : 'cursor-pointer'"
|
||||
>
|
||||
<input
|
||||
v-model="model"
|
||||
type="radio"
|
||||
class="peer sr-only"
|
||||
:name="name"
|
||||
:value="value"
|
||||
:aria-label="label"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<span
|
||||
:class="indicatorClasses"
|
||||
class="peer-focus-visible:ring-2 peer-focus-visible:ring-cp-accent-border peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-cp-surface"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span class="size-1.5 rounded-full bg-current" />
|
||||
</span>
|
||||
<span v-if="$slots.default" class="min-w-0 flex-1">
|
||||
<slot />
|
||||
</span>
|
||||
<span v-else-if="showLabel" class="text-[13px] leading-none font-emphasis">
|
||||
{{ label }}
|
||||
</span>
|
||||
</label>
|
||||
</template>
|
||||
@@ -1,13 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { Account } from '@/api'
|
||||
import { AlertTriangle, RefreshCw, RotateCcw, TicketCheck } from '@lucide/vue'
|
||||
import { AlertTriangle, RefreshCw, TicketCheck } from '@lucide/vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, shallowRef, watch } from 'vue'
|
||||
import { computed, shallowRef, useId, watch } from 'vue'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import BaseEmpty from '@/components/base/BaseEmpty.vue'
|
||||
import BaseIconButton from '@/components/base/BaseIconButton.vue'
|
||||
import BaseModal from '@/components/base/BaseModal/index.vue'
|
||||
import BaseRadio from '@/components/base/BaseRadio.vue'
|
||||
import { useAccountResetCredits } from '../composables/useAccountResetCredits'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -21,6 +22,9 @@ const emit = defineEmits<{
|
||||
const {
|
||||
availableCredits,
|
||||
availableCount,
|
||||
selectedCreditId,
|
||||
consumptionCredit,
|
||||
canRequestConsume,
|
||||
hasSnapshot,
|
||||
loading,
|
||||
consuming,
|
||||
@@ -28,6 +32,7 @@ const {
|
||||
ambiguous,
|
||||
showConfirm,
|
||||
loadCredits,
|
||||
selectCredit,
|
||||
requestConsume,
|
||||
cancelConsume,
|
||||
confirmConsume,
|
||||
@@ -37,14 +42,15 @@ const {
|
||||
})
|
||||
|
||||
const panelOpen = shallowRef(false)
|
||||
const confirmTitle = computed(() => ambiguous.value ? '确认上次重置结果?' : '立即重置额度?')
|
||||
const confirmDescription = computed(() => ambiguous.value
|
||||
? '上次请求的结果不确定。本次会复用同一个请求标识,只用于确认或完成原操作。'
|
||||
: '这会消费一张上游主动重置卡。操作完成后将立即重新读取额度窗口。')
|
||||
const modalTitle = computed(() => showConfirm.value ? confirmTitle.value : '主动重置额度')
|
||||
const modalDescription = computed(() => showConfirm.value
|
||||
? confirmDescription.value
|
||||
: '查看可用重置卡,并按需提前开启新的 Codex 额度窗口。')
|
||||
const creditRadioName = `account-reset-credit-${useId()}`
|
||||
const modalTitle = computed(() => {
|
||||
if (!showConfirm.value)
|
||||
return '主动重置额度'
|
||||
return ambiguous.value ? '确认上次重置' : '确认重置额度'
|
||||
})
|
||||
const modalDescription = computed(() =>
|
||||
showConfirm.value ? undefined : '选择一张重置卡后继续',
|
||||
)
|
||||
const triggerLabel = computed(() => {
|
||||
if (ambiguous.value)
|
||||
return '查看主动重置卡,有一项操作待确认'
|
||||
@@ -59,7 +65,7 @@ const triggerLabel = computed(() => {
|
||||
})
|
||||
const showTriggerCount = computed(() => hasSnapshot.value && availableCount.value > 0)
|
||||
const confirmCreditTitle = computed(() =>
|
||||
availableCredits.value[0]?.title || 'Codex 主动重置卡',
|
||||
consumptionCredit.value?.title || 'Codex 主动重置卡',
|
||||
)
|
||||
|
||||
watch(panelOpen, (isOpen) => {
|
||||
@@ -77,6 +83,24 @@ function expiryLabel(value: string | null) {
|
||||
const expiry = dayjs(value)
|
||||
return expiry.isValid() ? `${expiry.format('YYYY-MM-DD HH:mm')} 到期` : '到期时间未知'
|
||||
}
|
||||
|
||||
function creditOptionClasses(creditId: string) {
|
||||
return [
|
||||
'w-full min-w-0 rounded-cp-control px-4 py-3 outline-none transition-colors duration-150 motion-reduce:transition-none',
|
||||
selectedCreditId.value === creditId
|
||||
? 'bg-cp-info-bg'
|
||||
: 'bg-cp-subtle hover:bg-cp-default-hover',
|
||||
]
|
||||
}
|
||||
|
||||
function creditOptionLabel(credit: { title: string | null, expiresAt: string | null }) {
|
||||
return `${credit.title || 'Codex 主动重置卡'},${expiryLabel(credit.expiresAt)}`
|
||||
}
|
||||
|
||||
async function handleConfirmConsume() {
|
||||
if (await confirmConsume())
|
||||
panelOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -115,8 +139,11 @@ function expiryLabel(value: string | null) {
|
||||
<p class="mt-1.5 mb-0 text-[14px] leading-snug font-heavy text-cp-primary">
|
||||
{{ confirmCreditTitle }}
|
||||
</p>
|
||||
<p class="mt-2 mb-0 text-[12px] leading-[1.55] font-emphasis text-cp-secondary">
|
||||
只消费上游重置卡;完成后系统会回读官方额度,不会直接改写本地重置时间。
|
||||
<p
|
||||
v-if="consumptionCredit"
|
||||
class="mt-1 mb-0 font-mono text-[10px] leading-normal font-emphasis text-cp-muted-text"
|
||||
>
|
||||
{{ expiryLabel(consumptionCredit.expiresAt) }}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
@@ -133,7 +160,7 @@ function expiryLabel(value: string | null) {
|
||||
上次操作结果待确认
|
||||
</p>
|
||||
<p class="mt-1 mb-0 text-[11px] leading-normal font-emphasis text-cp-secondary">
|
||||
再次确认会复用原请求标识,不会创建新的消费操作。
|
||||
再确认一次即可。
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -162,7 +189,7 @@ function expiryLabel(value: string | null) {
|
||||
<section class="grid gap-2.5">
|
||||
<div class="flex min-h-8 items-center justify-between gap-3">
|
||||
<h3 class="m-0 text-[12px] font-heavy text-cp-muted-text">
|
||||
卡片明细
|
||||
选择重置卡
|
||||
</h3>
|
||||
<BaseIconButton
|
||||
variant="ghost"
|
||||
@@ -187,22 +214,32 @@ function expiryLabel(value: string | null) {
|
||||
{{ loadError }},请刷新重试。
|
||||
</p>
|
||||
|
||||
<div v-else-if="availableCredits.length" class="grid gap-2">
|
||||
<article
|
||||
<div
|
||||
v-else-if="availableCredits.length"
|
||||
class="grid gap-2"
|
||||
role="radiogroup"
|
||||
aria-label="选择要使用的重置卡"
|
||||
>
|
||||
<BaseRadio
|
||||
v-for="credit in availableCredits"
|
||||
:key="credit.id"
|
||||
class="flex min-w-0 items-center gap-3 rounded-cp-control bg-cp-subtle px-4 py-3"
|
||||
:model-value="selectedCreditId"
|
||||
:value="credit.id"
|
||||
:name="creditRadioName"
|
||||
:label="creditOptionLabel(credit)"
|
||||
:disabled="ambiguous || consuming"
|
||||
:class="creditOptionClasses(credit.id)"
|
||||
@update:model-value="selectCredit"
|
||||
>
|
||||
<span class="size-1.5 shrink-0 rounded-full bg-cp-info" aria-hidden="true" />
|
||||
<div class="min-w-0">
|
||||
<p class="m-0 truncate text-[12px] font-heavy text-cp-primary">
|
||||
<span class="block min-w-0">
|
||||
<span class="block truncate text-[12px] font-heavy text-cp-primary">
|
||||
{{ credit.title || 'Codex 主动重置卡' }}
|
||||
</p>
|
||||
<p class="mt-1 mb-0 truncate font-mono text-[10px] font-emphasis text-cp-muted-text">
|
||||
</span>
|
||||
<span class="mt-1 block truncate font-mono text-[10px] font-emphasis text-cp-muted-text">
|
||||
{{ expiryLabel(credit.expiresAt) }}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</span>
|
||||
</span>
|
||||
</BaseRadio>
|
||||
</div>
|
||||
|
||||
<div v-else class="overflow-hidden rounded-cp-control bg-cp-subtle">
|
||||
@@ -222,8 +259,8 @@ function expiryLabel(value: string | null) {
|
||||
<BaseButton variant="ghost" :disabled="consuming" @click="cancelConsume">
|
||||
返回
|
||||
</BaseButton>
|
||||
<BaseButton variant="primary" :loading="consuming" @click="confirmConsume">
|
||||
{{ ambiguous ? '确认原操作' : '消费并重置' }}
|
||||
<BaseButton variant="primary" :loading="consuming" @click="handleConfirmConsume">
|
||||
{{ ambiguous ? '再次确认' : '确认重置' }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -232,13 +269,10 @@ function expiryLabel(value: string | null) {
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
variant="primary"
|
||||
:disabled="loading || consuming || (!ambiguous && availableCount <= 0)"
|
||||
:disabled="loading || consuming || !canRequestConsume"
|
||||
@click="requestConsume"
|
||||
>
|
||||
<template #icon>
|
||||
<RotateCcw class="size-3.5" />
|
||||
</template>
|
||||
{{ ambiguous ? '确认原操作' : '立即重置' }}
|
||||
{{ ambiguous ? '继续确认' : '下一步' }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,8 @@ import { errorMessage } from '@/utils/async'
|
||||
|
||||
interface PendingResetCreditOperation {
|
||||
accountId: string
|
||||
creditId?: string
|
||||
creditId: string
|
||||
credit: AccountResetCredit
|
||||
redeemRequestId: string
|
||||
hasTransportFailure: boolean
|
||||
}
|
||||
@@ -36,13 +37,37 @@ export function useAccountResetCredits(options: {
|
||||
const consuming = shallowRef(false)
|
||||
const loadError = shallowRef('')
|
||||
const showConfirm = shallowRef(false)
|
||||
const selectedCreditId = shallowRef('')
|
||||
const pendingOperation = shallowRef<PendingResetCreditOperation | null>(null)
|
||||
let loadSequence = 0
|
||||
|
||||
const availableCredits = computed(() =>
|
||||
credits.value.filter(credit => credit.status === 'available'),
|
||||
)
|
||||
const selectedCredit = computed(() =>
|
||||
availableCredits.value.find(credit => credit.id === selectedCreditId.value),
|
||||
)
|
||||
const consumptionCredit = computed(() =>
|
||||
pendingOperation.value?.credit ?? selectedCredit.value,
|
||||
)
|
||||
const ambiguous = computed(() => pendingOperation.value?.hasTransportFailure === true)
|
||||
const canRequestConsume = computed(() =>
|
||||
ambiguous.value || (availableCount.value > 0 && selectedCredit.value !== undefined),
|
||||
)
|
||||
|
||||
function reconcileSelectedCredit(accountId: string) {
|
||||
if (accountId !== options.accountId())
|
||||
return
|
||||
|
||||
const operation = pendingOperation.value
|
||||
if (operation?.accountId === accountId) {
|
||||
selectedCreditId.value = operation.creditId
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedCredit.value)
|
||||
selectedCreditId.value = ''
|
||||
}
|
||||
|
||||
function applySnapshot(accountId: string, snapshot: ResetCreditsSnapshot) {
|
||||
snapshotsByAccountId.set(accountId, snapshot)
|
||||
@@ -51,6 +76,7 @@ export function useAccountResetCredits(options: {
|
||||
credits.value = snapshot.credits
|
||||
availableCount.value = snapshot.availableCount
|
||||
hasSnapshot.value = true
|
||||
reconcileSelectedCredit(accountId)
|
||||
}
|
||||
|
||||
function restoreSnapshot(accountId: string) {
|
||||
@@ -58,15 +84,22 @@ export function useAccountResetCredits(options: {
|
||||
credits.value = snapshot?.credits ?? []
|
||||
availableCount.value = snapshot?.availableCount ?? 0
|
||||
hasSnapshot.value = snapshot !== undefined
|
||||
reconcileSelectedCredit(accountId)
|
||||
}
|
||||
|
||||
function selectCredit(creditId: string) {
|
||||
if (pendingOperation.value || consuming.value)
|
||||
return
|
||||
if (!availableCredits.value.some(credit => credit.id === creditId))
|
||||
return
|
||||
selectedCreditId.value = creditId
|
||||
}
|
||||
|
||||
function applyConfirmedConsumption(operation: PendingResetCreditOperation) {
|
||||
const snapshot = snapshotsByAccountId.get(operation.accountId)
|
||||
if (!snapshot)
|
||||
return
|
||||
const creditIndex = operation.creditId
|
||||
? snapshot.credits.findIndex(credit => credit.id === operation.creditId)
|
||||
: snapshot.credits.findIndex(credit => credit.status === 'available')
|
||||
const creditIndex = snapshot.credits.findIndex(credit => credit.id === operation.creditId)
|
||||
const nextCredits = creditIndex < 0
|
||||
? snapshot.credits
|
||||
: snapshot.credits.filter((_, index) => index !== creditIndex)
|
||||
@@ -107,9 +140,13 @@ export function useAccountResetCredits(options: {
|
||||
if (!existing || existing.accountId !== accountId) {
|
||||
if (availableCount.value <= 0)
|
||||
return
|
||||
const credit = selectedCredit.value
|
||||
if (!credit)
|
||||
return
|
||||
pendingOperation.value = {
|
||||
accountId,
|
||||
creditId: availableCredits.value[0]?.id,
|
||||
creditId: credit.id,
|
||||
credit,
|
||||
redeemRequestId: globalThis.crypto.randomUUID(),
|
||||
hasTransportFailure: false,
|
||||
}
|
||||
@@ -123,10 +160,10 @@ export function useAccountResetCredits(options: {
|
||||
pendingOperation.value = null
|
||||
}
|
||||
|
||||
async function confirmConsume() {
|
||||
async function confirmConsume(): Promise<boolean> {
|
||||
const operation = pendingOperation.value
|
||||
if (!operation || consuming.value)
|
||||
return
|
||||
return false
|
||||
|
||||
consuming.value = true
|
||||
try {
|
||||
@@ -142,27 +179,27 @@ export function useAccountResetCredits(options: {
|
||||
if (!confirmed) {
|
||||
toast.error(resetResultMessage(result.code))
|
||||
await loadCredits()
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
toast.success(
|
||||
result.code === 'already_redeemed'
|
||||
? '已确认上次操作完成'
|
||||
: '额度已重置',
|
||||
)
|
||||
const successMessage = result.code === 'already_redeemed'
|
||||
? '上次重置已完成'
|
||||
: '额度已重置'
|
||||
applyConfirmedConsumption(operation)
|
||||
await loadCredits()
|
||||
try {
|
||||
const quota = await refreshAccountQuota({ accountId: operation.accountId })
|
||||
if (operation.accountId === options.accountId())
|
||||
options.onAccountUpdated(quota.account)
|
||||
toast.success(successMessage)
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.warning(
|
||||
`重置卡已消费,额度快照刷新失败:${errorMessage(error, '请手动刷新额度')}`,
|
||||
`${successMessage},但最新额度加载失败:${errorMessage(error, '请手动刷新额度')}`,
|
||||
{ duration: 5000 },
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
catch (error: unknown) {
|
||||
showConfirm.value = false
|
||||
@@ -178,6 +215,7 @@ export function useAccountResetCredits(options: {
|
||||
toast.error(errorMessage(error, '额度重置失败'))
|
||||
await loadCredits()
|
||||
}
|
||||
return false
|
||||
}
|
||||
finally {
|
||||
consuming.value = false
|
||||
@@ -188,10 +226,11 @@ export function useAccountResetCredits(options: {
|
||||
options.accountId,
|
||||
(accountId) => {
|
||||
loadSequence += 1
|
||||
pendingOperation.value = null
|
||||
selectedCreditId.value = ''
|
||||
restoreSnapshot(accountId)
|
||||
loading.value = false
|
||||
loadError.value = ''
|
||||
pendingOperation.value = null
|
||||
showConfirm.value = false
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -201,6 +240,9 @@ export function useAccountResetCredits(options: {
|
||||
credits,
|
||||
availableCredits,
|
||||
availableCount,
|
||||
selectedCreditId,
|
||||
consumptionCredit,
|
||||
canRequestConsume,
|
||||
hasSnapshot,
|
||||
loading,
|
||||
consuming,
|
||||
@@ -208,6 +250,7 @@ export function useAccountResetCredits(options: {
|
||||
ambiguous,
|
||||
showConfirm,
|
||||
loadCredits,
|
||||
selectCredit,
|
||||
requestConsume,
|
||||
cancelConsume,
|
||||
confirmConsume,
|
||||
|
||||
@@ -194,7 +194,7 @@ function windowRoleOrder(role: AccountQuotaWindow['role']) {
|
||||
|
||||
function quotaLimitLabel(window: AccountQuotaWindow) {
|
||||
if (window.limitId === 'codex')
|
||||
return '主额度'
|
||||
return '通用额度'
|
||||
return window.limitName
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { computed, onMounted, shallowRef } from 'vue'
|
||||
import { getDashboardSummary, getDashboardTrend } from '@/api'
|
||||
import { withMinimumDuration } from '@/utils/async'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { formatCompactNumber } from '@/utils/number'
|
||||
import { formatCompactNumber, formatInteger } from '@/utils/number'
|
||||
import { normalizeUsageRecord } from '@/views/usage/utils/records'
|
||||
|
||||
export function useDashboard() {
|
||||
@@ -263,7 +263,7 @@ function metricCards(
|
||||
title: '账号',
|
||||
value: credentials.total,
|
||||
valueRaw: credentials.totalValue,
|
||||
valueFormatter: formatCompactNumber,
|
||||
valueFormatter: formatAccountCount,
|
||||
icon: Users,
|
||||
tone: 'normal',
|
||||
details: [
|
||||
@@ -337,6 +337,10 @@ function metricCards(
|
||||
]
|
||||
}
|
||||
|
||||
function formatAccountCount(value: number) {
|
||||
return formatInteger(Math.max(0, Math.round(value)))
|
||||
}
|
||||
|
||||
function sparkline(values: number[], tone: MetricTone) {
|
||||
return values.some(value => value > 0) ? { values, tone } : undefined
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
version: "3.2.15"
|
||||
version: "3.2.16"
|
||||
|
||||
Reference in New Issue
Block a user