Merge pull request #7289 from wucm667/fix/issue-7281-platform-quota-options

fix(admin): align platform quota editor with supported platforms
This commit is contained in:
Wesley Liddick
2026-09-22 20:29:26 +08:00
committed by GitHub
4 changed files with 55 additions and 18 deletions
+1
View File
@@ -9,6 +9,7 @@ FRONTEND_CRITICAL_VITEST := \
src/components/account/__tests__/OpenAIReferralCell.transport.spec.ts \
src/components/account/__tests__/OpenAIQuotaResetCell.spark_shadow.spec.ts \
src/components/keys/__tests__/BulkEditKeysModal.spec.ts \
src/components/admin/user/__tests__/UserPlatformQuotaModal.spec.ts \
src/views/user/__tests__/KeysView.spec.ts \
src/api/__tests__/channelMonitorV2.spec.ts \
src/views/auth/__tests__/LinuxDoCallbackView.spec.ts \
+6 -1
View File
@@ -330,7 +330,12 @@ export async function bindUserAuthIdentity(
/**
* Platform quota types
*/
export type PlatformQuotaPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'grok'
// Keep aligned with backend/internal/service/domain_constants.go AllowedQuotaPlatforms.
export const PLATFORM_QUOTA_PLATFORMS = [
'anthropic', 'openai', 'gemini', 'antigravity', 'grok',
'kimi', 'zhipu', 'deepseek', 'minimax', 'opencode_go',
] as const
export type PlatformQuotaPlatform = typeof PLATFORM_QUOTA_PLATFORMS[number]
export type PlatformQuotaWindow = 'daily' | 'weekly' | 'monthly'
export interface PlatformQuotaItem {
@@ -119,6 +119,7 @@ import { ref, reactive, watch, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin'
import { PLATFORM_QUOTA_PLATFORMS } from '@/api/admin/users'
import type { AdminUser, PlatformQuotaItem, PlatformQuotaPlatform, PlatformQuotaWindow } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue'
@@ -128,8 +129,6 @@ const emit = defineEmits(['close', 'success'])
const { t } = useI18n()
const appStore = useAppStore()
const PLATFORMS: PlatformQuotaPlatform[] = ['anthropic', 'openai', 'gemini', 'antigravity', 'grok']
interface QuotaRow {
platform: PlatformQuotaPlatform
daily_limit_usd: number | null
@@ -176,7 +175,7 @@ function emptyRow(p: PlatformQuotaPlatform): QuotaRow {
function normalize(items: PlatformQuotaItem[]): QuotaRow[] {
const byPlatform = new Map<PlatformQuotaPlatform, PlatformQuotaItem>()
for (const it of items) byPlatform.set(it.platform, it)
return PLATFORMS.map((p) => {
return PLATFORM_QUOTA_PLATFORMS.map((p) => {
const it = byPlatform.get(p)
if (!it) return emptyRow(p)
return {
@@ -205,7 +204,7 @@ async function load() {
savedConfigured.value = configuredPlatforms(data.platform_quotas || [])
} catch {
appStore.showError(t('admin.users.platformQuota.loadFailed'))
quotas.value = PLATFORMS.map(emptyRow)
quotas.value = PLATFORM_QUOTA_PLATFORMS.map(emptyRow)
savedConfigured.value = new Set()
} finally {
loading.value = false
@@ -50,7 +50,7 @@ vi.mock('@/components/common/BaseDialog.vue', () => ({
}))
import UserPlatformQuotaModal from '../UserPlatformQuotaModal.vue'
import type { UserSubscription } from '@/types'
import type { PlatformQuotaUpdateItem, UserSubscription } from '@/types'
function makeUser(overrides: { subscriptions?: UserSubscription[] } = {}) {
return { id: 99, email: 'u@example.com', ...overrides } as any
@@ -101,16 +101,48 @@ describe('UserPlatformQuotaModal', () => {
expect(apiMocks.getPlatformQuotas).toHaveBeenCalledWith(99)
})
it('空数据渲染 5 个 platform 行', async () => {
it('renders all ten supported platforms with empty limits', async () => {
const w = await mountAndOpen()
const html = w.html()
expect(html).toContain('anthropic')
expect(html).toContain('openai')
expect(html).toContain('gemini')
expect(html).toContain('antigravity')
expect(html).toContain('grok')
const rows = w.findAll('tbody tr')
expect(rows.map(row => row.find('td').text())).toEqual([
'anthropic', 'openai', 'gemini', 'antigravity', 'grok',
'kimi', 'zhipu', 'deepseek', 'minimax', 'opencode_go',
])
for (const row of rows) {
const inputs = row.findAll('input[type=number]')
expect(inputs).toHaveLength(3)
expect(inputs.map(input => input.element.value)).toEqual(['', '', ''])
}
w.unmount()
})
it.each(['kimi', 'zhipu', 'deepseek', 'minimax', 'opencode_go'] as const)(
'saves edits to %s without erasing existing platform limits', async (platform) => {
const existing: PlatformQuotaUpdateItem[] = [
{ platform: 'openai', daily_limit_usd: 10, weekly_limit_usd: 20, monthly_limit_usd: 100 },
...(['kimi', 'zhipu', 'deepseek', 'minimax', 'opencode_go'] as const).map(p => ({
platform: p, daily_limit_usd: 0, weekly_limit_usd: null, monthly_limit_usd: 50,
})),
]
apiMocks.getPlatformQuotas.mockResolvedValueOnce({ platform_quotas: existing })
const w = await mountAndOpen()
const row = w.findAll('tbody tr').find(r => r.find('td').text() === platform)!
const inputs = row.findAll('input[type=number]')
expect(inputs.map(input => input.element.value)).toEqual(['0', '', '50'])
await inputs[1].setValue('12.5')
await w.findAll('button').find(b => b.text() === 'admin.users.platformQuota.save')!.trigger('click')
await flushPromises()
const expected = existing.map(item => item.platform === platform
? { ...item, weekly_limit_usd: 12.5 }
: item)
expect(apiMocks.updatePlatformQuotas).toHaveBeenCalledTimes(1)
expect(apiMocks.updatePlatformQuotas).toHaveBeenCalledWith(99, expect.arrayContaining(expected))
expect(apiMocks.updatePlatformQuotas.mock.calls[0][1]).toHaveLength(10)
expect(w.emitted('success')).toHaveLength(1)
w.unmount()
},
)
it('已有数据正确填充 limit input', async () => {
apiMocks.getPlatformQuotas.mockResolvedValueOnce({
platform_quotas: [
@@ -120,13 +152,13 @@ describe('UserPlatformQuotaModal', () => {
})
const w = await mountAndOpen()
const inputs = w.findAll('input[type=number]')
// 5 platforms × 3 windows = 15 inputs
expect(inputs.length).toBe(15)
// 10 platforms × 3 windows = 30 inputs
expect(inputs.length).toBe(30)
// 第一个 input 是 anthropic.daily = 10
expect((inputs[0].element as HTMLInputElement).value).toBe('10')
})
it('保存提交完整 5 platform payload', async () => {
it('保存提交完整 10 platform payload', async () => {
apiMocks.getPlatformQuotas.mockResolvedValueOnce({
platform_quotas: [
{ platform: 'openai', daily_limit_usd: null, weekly_limit_usd: 20, monthly_limit_usd: null,
@@ -143,7 +175,7 @@ describe('UserPlatformQuotaModal', () => {
expect(apiMocks.updatePlatformQuotas).toHaveBeenCalledTimes(1)
const [uid, payload] = apiMocks.updatePlatformQuotas.mock.calls[0]
expect(uid).toBe(99)
expect(payload).toHaveLength(5) // 5 platforms always submitted
expect(payload).toHaveLength(10) // 10 platforms always submitted
const openai = payload.find((p: any) => p.platform === 'openai')
expect(openai.weekly_limit_usd).toBe(20)
})
@@ -208,7 +240,7 @@ describe('UserPlatformQuotaModal', () => {
it('未配置限额的平台重置按钮禁用并提示不可用', async () => {
const w = await mountAndOpen()
const resetBtns = w.findAll('button').filter((b) => b.text() === '↻')
expect(resetBtns.length).toBe(15) // 5 平台 × 3 窗口
expect(resetBtns.length).toBe(30) // 10 平台 × 3 窗口
for (const b of resetBtns) {
expect((b.element as HTMLButtonElement).disabled).toBe(true)
expect(b.attributes('title')).toBe('admin.users.platformQuota.reset.unavailable')