feat: show used quota in groups list

This commit is contained in:
wucm667
2026-07-09 11:45:59 +08:00
parent 6f43986c37
commit 54859022aa
4 changed files with 84 additions and 23 deletions
@@ -107,7 +107,7 @@ describe('BulkEditAccountModal', () => {
expect(mappingTab).toBeTruthy()
await mappingTab!.trigger('click')
expect(wrapper.text()).toContain('3.1-Flash-Image passthrough')
expect(wrapper.text()).toContain('3.1-Flash-Image透传')
expect(wrapper.text()).toContain('3-Pro-Image→3.1')
expect(wrapper.text()).not.toContain('GPT-5.3 Codex Spark')
})
+75 -20
View File
@@ -168,20 +168,40 @@
<!-- Subscription Limits - compact single line -->
<div
v-if="row.subscription_type === 'subscription'"
class="text-xs text-gray-500 dark:text-gray-400"
class="space-y-0.5 text-xs text-gray-500 dark:text-gray-400"
>
<template
<div
v-if="
row.daily_limit_usd ||
row.weekly_limit_usd ||
row.monthly_limit_usd
"
class="flex flex-wrap items-center gap-x-1 gap-y-0.5"
>
<span v-if="row.daily_limit_usd"
>${{ row.daily_limit_usd }}/{{
t("admin.groups.limitDay")
}}</span
>
<span v-if="row.daily_limit_usd" class="whitespace-nowrap">
<span
v-if="usageLoading"
class="font-medium text-gray-400 dark:text-gray-500"
>—</span
>
<span
v-else
:class="
getQuotaUsageClass(
usageMap.get(row.id)?.today_cost ?? 0,
row.daily_limit_usd
)
"
>{{
formatUsd(usageMap.get(row.id)?.today_cost ?? 0)
}}</span
>
<span class="text-gray-400 dark:text-gray-500">
/ {{ formatUsd(row.daily_limit_usd) }}/{{
t("admin.groups.limitDay")
}}</span
>
</span>
<span
v-if="
row.daily_limit_usd &&
@@ -190,8 +210,8 @@
class="mx-1 text-gray-300 dark:text-gray-600"
>·</span
>
<span v-if="row.weekly_limit_usd"
>${{ row.weekly_limit_usd }}/{{
<span v-if="row.weekly_limit_usd" class="whitespace-nowrap"
>{{ formatUsd(row.weekly_limit_usd) }}/{{
t("admin.groups.limitWeek")
}}</span
>
@@ -200,15 +220,25 @@
class="mx-1 text-gray-300 dark:text-gray-600"
>·</span
>
<span v-if="row.monthly_limit_usd"
>${{ row.monthly_limit_usd }}/{{
<span v-if="row.monthly_limit_usd" class="whitespace-nowrap"
>{{ formatUsd(row.monthly_limit_usd) }}/{{
t("admin.groups.limitMonth")
}}</span
>
</template>
</div>
<span v-else class="text-gray-400 dark:text-gray-500">{{
t("admin.groups.subscription.noLimit")
}}</span>
<div class="text-gray-400 dark:text-gray-500">
{{ t("admin.groups.usageTotal") }}
<span class="ml-1 font-medium text-gray-600 dark:text-gray-300"
>{{
usageLoading
? "—"
: formatUsd(usageMap.get(row.id)?.total_cost ?? 0)
}}</span
>
</div>
</div>
</div>
</template>
@@ -3396,7 +3426,9 @@ const saveColumnsToStorage = () => {
};
const isColumnVisible = (key: string) => !hiddenColumns.has(key);
const hasVisibleUsageColumn = computed(() => isColumnVisible("usage"));
const hasVisibleUsageSummaryConsumer = computed(
() => isColumnVisible("usage") || isColumnVisible("billing_type"),
);
const hasVisibleCapacityColumn = computed(() => isColumnVisible("capacity"));
const toggleColumn = (key: string) => {
@@ -3411,7 +3443,7 @@ const toggleColumn = (key: string) => {
}
saveColumnsToStorage();
if (wasHidden && key === "usage") {
if (wasHidden && (key === "usage" || key === "billing_type")) {
loadUsageSummary();
}
if (wasHidden && key === "capacity") {
@@ -3571,9 +3603,12 @@ const copyAccountsGroupOptionsForEdit = computed(() => {
const groups = ref<AdminGroup[]>([]);
const loading = ref(false);
const usageMap = ref<Map<number, { today_cost: number; total_cost: number }>>(
new Map(),
);
type GroupUsageSummary = {
today_cost: number;
total_cost: number;
};
const usageMap = ref<Map<number, GroupUsageSummary>>(new Map());
const usageLoading = ref(false);
const capacityMap = ref<
Map<
@@ -4146,7 +4181,7 @@ const loadGroups = async () => {
groups.value = response.items;
pagination.total = response.total;
pagination.pages = response.pages;
if (hasVisibleUsageColumn.value) {
if (hasVisibleUsageSummaryConsumer.value) {
loadUsageSummary();
} else {
usageLoading.value = false;
@@ -4177,8 +4212,28 @@ const formatCost = (cost: number): string => {
return cost.toFixed(2);
};
const formatUsd = (cost: number | null | undefined): string =>
`$${formatCost(cost ?? 0)}`;
const getQuotaUsageClass = (
used: number,
limit: number | null | undefined,
): string => {
if (!limit || limit <= 0) {
return "font-medium text-gray-700 dark:text-gray-300";
}
const ratio = used / limit;
if (ratio >= 1) {
return "font-semibold text-red-600 dark:text-red-400";
}
if (ratio >= 0.8) {
return "font-semibold text-amber-600 dark:text-amber-400";
}
return "font-medium text-gray-700 dark:text-gray-300";
};
const loadUsageSummary = async () => {
if (!hasVisibleUsageColumn.value) {
if (!hasVisibleUsageSummaryConsumer.value) {
usageLoading.value = false;
return;
}
@@ -4186,7 +4241,7 @@ const loadUsageSummary = async () => {
try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
const data = await adminAPI.groups.getUsageSummary(tz);
const map = new Map<number, { today_cost: number; total_cost: number }>();
const map = new Map<number, GroupUsageSummary>();
for (const item of data) {
map.set(item.group_id, {
today_cost: item.today_cost,
@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import type { DashboardStats } from '@/types'
import DashboardView from '../DashboardView.vue'
@@ -87,6 +88,8 @@ const createDashboardStats = (): DashboardStats => ({
describe('admin DashboardView', () => {
beforeEach(() => {
setActivePinia(createPinia())
getSnapshotV2.mockReset()
getUserUsageTrend.mockReset()
getUserSpendingRanking.mockReset()
@@ -308,8 +308,11 @@ describe('admin GroupsView column settings', () => {
expect(localStorage.getItem('group-hidden-columns')).toBe(JSON.stringify(['usage']))
})
it('skips hidden usage and capacity fetches until those columns are shown', async () => {
localStorage.setItem('group-hidden-columns', JSON.stringify(['usage', 'capacity']))
it('skips usage and capacity fetches until consuming columns are shown', async () => {
localStorage.setItem(
'group-hidden-columns',
JSON.stringify(['billing_type', 'usage', 'capacity']),
)
const wrapper = await mountView()