mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
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:
@@ -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 \
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user