mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-08-31 01:13:06 +08:00
fix(admin): 补全平台筛选选项
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
// Package model 定义服务层使用的数据模型。
|
||||
package model
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
)
|
||||
|
||||
// ErrorPassthroughRule 全局错误透传规则
|
||||
// 用于控制上游错误如何返回给客户端
|
||||
@@ -32,16 +36,28 @@ const MatchModeAll = "all"
|
||||
|
||||
// 支持的平台常量
|
||||
const (
|
||||
PlatformAnthropic = "anthropic"
|
||||
PlatformOpenAI = "openai"
|
||||
PlatformGemini = "gemini"
|
||||
PlatformAntigravity = "antigravity"
|
||||
PlatformGrok = "grok"
|
||||
PlatformAnthropic = domain.PlatformAnthropic
|
||||
PlatformOpenAI = domain.PlatformOpenAI
|
||||
PlatformGemini = domain.PlatformGemini
|
||||
PlatformAntigravity = domain.PlatformAntigravity
|
||||
PlatformGrok = domain.PlatformGrok
|
||||
PlatformKimi = domain.PlatformKimi
|
||||
PlatformZhipu = domain.PlatformZhipu
|
||||
PlatformDeepseek = domain.PlatformDeepseek
|
||||
)
|
||||
|
||||
// AllPlatforms 返回所有支持的平台列表
|
||||
func AllPlatforms() []string {
|
||||
return []string{PlatformAnthropic, PlatformOpenAI, PlatformGemini, PlatformAntigravity, PlatformGrok}
|
||||
return []string{
|
||||
PlatformAnthropic,
|
||||
PlatformOpenAI,
|
||||
PlatformGemini,
|
||||
PlatformAntigravity,
|
||||
PlatformGrok,
|
||||
PlatformKimi,
|
||||
PlatformZhipu,
|
||||
PlatformDeepseek,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 验证规则配置的有效性
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAllPlatformsIncludesEveryConcretePlatform(t *testing.T) {
|
||||
require.ElementsMatch(t, []string{
|
||||
"anthropic",
|
||||
"openai",
|
||||
"gemini",
|
||||
"antigravity",
|
||||
"grok",
|
||||
"kimi",
|
||||
"zhipu",
|
||||
"deepseek",
|
||||
}, AllPlatforms())
|
||||
}
|
||||
@@ -433,6 +433,7 @@
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { CONCRETE_PLATFORM_OPTIONS } from '@/constants/platforms'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { ErrorPassthroughRule } from '@/api/admin/errorPassthrough'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
@@ -485,13 +486,7 @@ const matchModeOptions = computed(() => [
|
||||
{ value: 'all', label: t('admin.errorPassthrough.matchMode.all'), description: t('admin.errorPassthrough.matchMode.allHint') }
|
||||
])
|
||||
|
||||
const platformOptions = [
|
||||
{ value: 'anthropic', label: 'Anthropic' },
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
{ value: 'gemini', label: 'Gemini' },
|
||||
{ value: 'antigravity', label: 'Antigravity' },
|
||||
{ value: 'grok', label: 'Grok' }
|
||||
]
|
||||
const platformOptions = CONCRETE_PLATFORM_OPTIONS
|
||||
|
||||
// Load rules when dialog opens
|
||||
watch(() => props.show, (newVal) => {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import Select from '@/components/common/Select.vue'; import SearchInput from '@/components/common/SearchInput.vue'
|
||||
import type { AdminGroup } from '@/types'
|
||||
import { CONCRETE_PLATFORM_OPTIONS } from '@/constants/platforms'
|
||||
const props = defineProps<{ searchQuery: string; filters: Record<string, any>; groups?: AdminGroup[] }>()
|
||||
const emit = defineEmits(['update:searchQuery', 'update:filters', 'change']); const { t } = useI18n()
|
||||
const updatePlatform = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, platform: value }) }
|
||||
@@ -25,7 +26,7 @@ const updateType = (value: string | number | boolean | null) => { emit('update:f
|
||||
const updateStatus = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, status: value }) }
|
||||
const updatePrivacyMode = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, privacy_mode: value }) }
|
||||
const updateGroup = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, group: value }) }
|
||||
const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }, { value: 'grok', label: 'Grok' }])
|
||||
const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, ...CONCRETE_PLATFORM_OPTIONS])
|
||||
const tOpts = computed(() => [{ value: '', label: t('admin.accounts.allTypes') }, { value: 'oauth', label: t('admin.accounts.oauthType') }, { value: 'setup-token', label: t('admin.accounts.setupToken') }, { value: 'apikey', label: t('admin.accounts.apiKey') }, { value: 'bedrock', label: 'AWS Bedrock' }])
|
||||
const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }, { value: 'rate_limited', label: t('admin.accounts.status.rateLimited') }, { value: 'temp_unschedulable', label: t('admin.accounts.status.tempUnschedulable') }, { value: 'unschedulable', label: t('admin.accounts.status.unschedulable') }])
|
||||
const privacyOpts = computed(() => [
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { CONCRETE_PLATFORM_OPTIONS, GROUP_PLATFORM_OPTIONS } from '@/constants/platforms'
|
||||
|
||||
const concretePlatforms = [
|
||||
'anthropic',
|
||||
'openai',
|
||||
'gemini',
|
||||
'antigravity',
|
||||
'grok',
|
||||
'kimi',
|
||||
'zhipu',
|
||||
'deepseek'
|
||||
]
|
||||
|
||||
describe('platform option catalogs', () => {
|
||||
it('exposes every concrete account platform', () => {
|
||||
expect(CONCRETE_PLATFORM_OPTIONS.map((option) => option.value)).toEqual(concretePlatforms)
|
||||
})
|
||||
|
||||
it('adds composite for group-backed filters', () => {
|
||||
expect(GROUP_PLATFORM_OPTIONS.map((option) => option.value)).toEqual([
|
||||
...concretePlatforms,
|
||||
'composite'
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { AccountPlatform, GroupPlatform } from '@/types'
|
||||
|
||||
export interface PlatformOption<T extends string = string> {
|
||||
value: T
|
||||
label: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Concrete upstream platforms supported by accounts and request routing.
|
||||
* Keep platform selectors derived from this catalog so newly added providers
|
||||
* do not silently disappear from list filters.
|
||||
*/
|
||||
export const CONCRETE_PLATFORM_OPTIONS = [
|
||||
{ value: 'anthropic', label: 'Anthropic' },
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
{ value: 'gemini', label: 'Gemini' },
|
||||
{ value: 'antigravity', label: 'Antigravity' },
|
||||
{ value: 'grok', label: 'Grok' },
|
||||
{ value: 'kimi', label: 'Kimi' },
|
||||
{ value: 'zhipu', label: 'Zhipu GLM' },
|
||||
{ value: 'deepseek', label: 'DeepSeek' }
|
||||
] as const satisfies readonly PlatformOption<AccountPlatform>[]
|
||||
|
||||
/** Platforms that can own a group. */
|
||||
export const GROUP_PLATFORM_OPTIONS = [
|
||||
...CONCRETE_PLATFORM_OPTIONS,
|
||||
{ value: 'composite', label: 'Composite' }
|
||||
] as const satisfies readonly PlatformOption<GroupPlatform>[]
|
||||
@@ -4422,6 +4422,10 @@ import type {
|
||||
GroupPlatform,
|
||||
SubscriptionType,
|
||||
} from "@/types";
|
||||
import {
|
||||
CONCRETE_PLATFORM_OPTIONS,
|
||||
GROUP_PLATFORM_OPTIONS,
|
||||
} from "@/constants/platforms";
|
||||
import type { Column } from "@/components/common/types";
|
||||
import AppLayout from "@/components/layout/AppLayout.vue";
|
||||
import TablePageLayout from "@/components/layout/TablePageLayout.vue";
|
||||
@@ -4740,40 +4744,15 @@ const exclusiveOptions = computed(() => [
|
||||
{ value: "false", label: t("admin.groups.nonExclusive") },
|
||||
]);
|
||||
|
||||
const platformOptions = computed(() => [
|
||||
{ value: "anthropic", label: "Anthropic" },
|
||||
{ value: "openai", label: "OpenAI" },
|
||||
{ value: "gemini", label: "Gemini" },
|
||||
{ value: "antigravity", label: "Antigravity" },
|
||||
{ value: "grok", label: "Grok" },
|
||||
{ value: "kimi", label: "Kimi" },
|
||||
{ value: "zhipu", label: "Zhipu GLM" },
|
||||
{ value: "deepseek", label: "DeepSeek" },
|
||||
{ value: "composite", label: "Composite" },
|
||||
]);
|
||||
const platformOptions = computed(() => [...GROUP_PLATFORM_OPTIONS]);
|
||||
|
||||
const platformFilterOptions = computed(() => [
|
||||
{ value: "", label: t("admin.groups.allPlatforms") },
|
||||
{ value: "anthropic", label: "Anthropic" },
|
||||
{ value: "openai", label: "OpenAI" },
|
||||
{ value: "gemini", label: "Gemini" },
|
||||
{ value: "antigravity", label: "Antigravity" },
|
||||
{ value: "grok", label: "Grok" },
|
||||
{ value: "kimi", label: "Kimi" },
|
||||
{ value: "zhipu", label: "Zhipu GLM" },
|
||||
{ value: "deepseek", label: "DeepSeek" },
|
||||
{ value: "composite", label: "Composite" },
|
||||
...GROUP_PLATFORM_OPTIONS,
|
||||
]);
|
||||
|
||||
const compositeRoutePlatformOptions = computed(() => [
|
||||
{ value: "anthropic", label: "Anthropic" },
|
||||
{ value: "openai", label: "OpenAI" },
|
||||
{ value: "gemini", label: "Gemini" },
|
||||
{ value: "antigravity", label: "Antigravity" },
|
||||
{ value: "grok", label: "Grok" },
|
||||
{ value: "kimi", label: "Kimi" },
|
||||
{ value: "zhipu", label: "Zhipu GLM" },
|
||||
{ value: "deepseek", label: "DeepSeek" },
|
||||
...CONCRETE_PLATFORM_OPTIONS,
|
||||
]);
|
||||
|
||||
const compositeRouteEndpointOptions = computed(() => [
|
||||
|
||||
@@ -788,6 +788,7 @@ import {
|
||||
isOneTimeDailyQuota,
|
||||
type RemainingDurationParts
|
||||
} from '@/utils/subscriptionQuota'
|
||||
import { GROUP_PLATFORM_OPTIONS } from '@/constants/platforms'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
@@ -996,10 +997,7 @@ const groupOptions = computed(() => [
|
||||
|
||||
const platformFilterOptions = computed(() => [
|
||||
{ value: '', label: t('admin.subscriptions.allPlatforms') },
|
||||
{ value: 'anthropic', label: 'Anthropic' },
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
{ value: 'gemini', label: 'Gemini' },
|
||||
{ value: 'antigravity', label: 'Antigravity' }
|
||||
...GROUP_PLATFORM_OPTIONS
|
||||
])
|
||||
|
||||
// Group options for assign (only subscription type groups)
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { CONCRETE_PLATFORM_OPTIONS } from '@/constants/platforms'
|
||||
|
||||
describe('GroupsView Composite route options', () => {
|
||||
it('offers Kimi, Zhipu GLM, and DeepSeek as route targets', () => {
|
||||
const source = readFileSync(resolve('src/views/admin/GroupsView.vue'), 'utf8')
|
||||
const options = source.slice(
|
||||
source.indexOf('const compositeRoutePlatformOptions'),
|
||||
source.indexOf('const compositeRouteEndpointOptions')
|
||||
expect(CONCRETE_PLATFORM_OPTIONS.map((option) => option.value)).toEqual(
|
||||
expect.arrayContaining(['kimi', 'zhipu', 'deepseek'])
|
||||
)
|
||||
|
||||
expect(options).toContain('{ value: "kimi", label: "Kimi" }')
|
||||
expect(options).toContain('{ value: "zhipu", label: "Zhipu GLM" }')
|
||||
expect(options).toContain('{ value: "deepseek", label: "DeepSeek" }')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
function readSource(path: string): string {
|
||||
return readFileSync(resolve(path), 'utf8')
|
||||
}
|
||||
|
||||
describe('admin platform filters', () => {
|
||||
it('uses the group platform catalog on the subscriptions page', () => {
|
||||
const source = readSource('src/views/admin/SubscriptionsView.vue')
|
||||
expect(source).toContain("import { GROUP_PLATFORM_OPTIONS } from '@/constants/platforms'")
|
||||
expect(source).toMatch(/const platformFilterOptions[\s\S]*?\.\.\.GROUP_PLATFORM_OPTIONS/)
|
||||
})
|
||||
|
||||
it('uses the shared catalogs on the groups page', () => {
|
||||
const source = readSource('src/views/admin/GroupsView.vue')
|
||||
expect(source).toContain('...GROUP_PLATFORM_OPTIONS')
|
||||
expect(source).toContain('...CONCRETE_PLATFORM_OPTIONS')
|
||||
})
|
||||
|
||||
it('uses the concrete platform catalog wherever concrete platforms are selected', () => {
|
||||
for (const path of [
|
||||
'src/components/admin/account/AccountTableFilters.vue',
|
||||
'src/components/admin/ErrorPassthroughRulesModal.vue',
|
||||
'src/views/admin/ops/components/OpsDashboardHeader.vue'
|
||||
]) {
|
||||
const source = readSource(path)
|
||||
expect(source).toContain("import { CONCRETE_PLATFORM_OPTIONS } from '@/constants/platforms'")
|
||||
expect(source).toMatch(/platformOptions\s*=.*CONCRETE_PLATFORM_OPTIONS|pOpts.*\.\.\.CONCRETE_PLATFORM_OPTIONS/s)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import { CONCRETE_PLATFORM_OPTIONS } from '@/constants/platforms'
|
||||
import HelpTooltip from '@/components/common/HelpTooltip.vue'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
@@ -109,11 +110,7 @@ const groups = ref<Array<{ id: number; name: string; platform: string }>>([])
|
||||
|
||||
const platformOptions = computed(() => [
|
||||
{ value: '', label: t('common.all') },
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
{ value: 'anthropic', label: 'Anthropic' },
|
||||
{ value: 'gemini', label: 'Gemini' },
|
||||
{ value: 'antigravity', label: 'Antigravity' },
|
||||
{ value: 'grok', label: 'Grok' }
|
||||
...CONCRETE_PLATFORM_OPTIONS
|
||||
])
|
||||
|
||||
const timeRangeOptions = computed(() => [
|
||||
|
||||
Reference in New Issue
Block a user