fix(admin): 补全平台筛选选项

This commit is contained in:
hansnow
2026-08-20 10:00:09 +08:00
parent 32a0d9ba2d
commit 1f2a87adb0
11 changed files with 148 additions and 62 deletions
@@ -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'
])
})
})
+28
View File
@@ -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>[]
+7 -28
View File
@@ -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(() => [