feat(channels): explode available channels by platform + apply platform theme

Backend: one source channel → N output rows, one per platform that
has user-visible groups. Each row carries a single platform, so the
frontend can color/icon an entire row without mixing sources.

- userAvailableChannel: add Platform field
- new explodeChannelByPlatform helper; drop now-redundant
  collectGroupPlatforms

Frontend: use the row platform to drive theming and stop repeating
"ANTHROPIC" / "OPENAI" labels on every model chip.

- api/channels.ts: UserAvailableChannel.platform
- AvailableChannelsTable: name cell — PlatformBadge next to channel
  name (replaces the two-line name/description block; description
  moves to the badge's title tooltip); groups cell — each chip uses
  platformBadgeLightClass + PlatformIcon; model list passes
  show-platform=false + platform-hint to child chips
- SupportedModelChip: chip bg/border driven by platformBadgeClass,
  leading PlatformIcon; platform-hint fallback when model.platform
  missing
This commit is contained in:
erio
2026-04-21 18:47:54 +08:00
parent 9ba42aa556
commit 800802b8aa
5 changed files with 98 additions and 38 deletions

View File

@@ -43,6 +43,11 @@ export interface UserSupportedModel {
export interface UserAvailableChannel {
name: string
description: string
/**
* 所属平台anthropic / openai / antigravity / gemini ...)。后端按平台把一个渠道
* 摊开成多条记录,因此此字段决定整行的配色与图标。
*/
platform: string
groups: UserAvailableGroup[]
supported_models: UserSupportedModel[]
}

View File

@@ -1,12 +1,19 @@
<template>
<DataTable :columns="columns" :data="rows" :loading="loading">
<template #cell-name="{ row }">
<div class="font-medium text-gray-900 dark:text-white">{{ row.name }}</div>
<div
v-if="row.description"
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
>
{{ row.description }}
<div class="flex items-center gap-2">
<span class="font-medium text-gray-900 dark:text-white">{{ row.name }}</span>
<span
v-if="row.platform"
:class="[
'inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[11px] font-medium uppercase',
platformBadgeClass(row.platform),
]"
:title="row.description || undefined"
>
<PlatformIcon :platform="row.platform as GroupPlatform" size="xs" />
{{ row.platform }}
</span>
</div>
</template>
@@ -18,8 +25,16 @@
<span
v-for="g in row.groups"
:key="g.id"
class="inline-flex items-center rounded bg-blue-50 px-2 py-0.5 text-xs font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-300"
:class="[
'inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs font-medium',
platformBadgeLightClass(g.platform || row.platform || ''),
]"
>
<PlatformIcon
v-if="g.platform || row.platform"
:platform="(g.platform || row.platform) as GroupPlatform"
size="xs"
/>
{{ g.name }}
</span>
</div>
@@ -36,6 +51,8 @@
:model="m"
:pricing-key-prefix="pricingKeyPrefix"
:no-pricing-label="noPricingLabel"
:show-platform="false"
:platform-hint="row.platform"
/>
</div>
</template>
@@ -60,9 +77,12 @@
import { computed, useSlots } from 'vue'
import DataTable from '@/components/common/DataTable.vue'
import Icon from '@/components/icons/Icon.vue'
import PlatformIcon from '@/components/common/PlatformIcon.vue'
import SupportedModelChip from './SupportedModelChip.vue'
import type { UserSupportedModel } from '@/api/channels'
import type { ChannelStatus, BillingModelSource } from '@/constants/channel'
import type { GroupPlatform } from '@/types'
import { platformBadgeClass, platformBadgeLightClass } from '@/utils/platformColors'
interface GroupRef {
id: number
@@ -73,6 +93,8 @@ interface GroupRef {
interface Row {
name: string
description?: string
/** 单条记录归属的平台后端按平台摊开后每行一个。admin 场景可能缺失,因此允许 optional。 */
platform?: string
groups: GroupRef[]
// 复用 user 侧最小 DTOadmin 侧 SupportedModel 结构上是其超集,可直接传入。
supported_models: UserSupportedModel[]

View File

@@ -1,11 +1,21 @@
<template>
<div class="group relative inline-block">
<span
class="inline-flex cursor-help items-center rounded-md border border-gray-200 bg-gray-50 px-2 py-0.5 text-xs font-medium text-gray-700 transition-colors hover:border-brand-400 hover:bg-brand-50 hover:text-brand-700 dark:border-dark-600 dark:bg-dark-800 dark:text-gray-300 dark:hover:border-brand-500 dark:hover:bg-brand-900/30 dark:hover:text-brand-300"
:class="[
'inline-flex cursor-help items-center gap-1 rounded-md border px-2 py-0.5 text-xs font-medium transition-colors',
effectivePlatform
? platformBadgeClass(effectivePlatform)
: 'border-gray-200 bg-gray-50 text-gray-700 dark:border-dark-600 dark:bg-dark-800 dark:text-gray-300',
]"
>
<PlatformIcon
v-if="effectivePlatform"
:platform="effectivePlatform as GroupPlatform"
size="xs"
/>
<span
v-if="showPlatform && model.platform"
class="mr-1 rounded bg-gray-200 px-1 text-[10px] uppercase text-gray-600 dark:bg-dark-700 dark:text-gray-400"
class="rounded bg-gray-200/60 px-1 text-[10px] uppercase text-gray-600 dark:bg-dark-700 dark:text-gray-400"
>
{{ model.platform }}
</span>
@@ -130,6 +140,9 @@ import {
// 复用 api/channels.ts 的用户侧最小形态 DTO
// admin ChannelModelPricing 字段更多但结构上是用户 DTO 的超集admin 视图传入可直接通过结构化子类型检查
import type { UserPricingInterval, UserSupportedModel } from '@/api/channels'
import PlatformIcon from '@/components/common/PlatformIcon.vue'
import type { GroupPlatform } from '@/types'
import { platformBadgeClass } from '@/utils/platformColors'
const props = withDefaults(
defineProps<{
@@ -138,14 +151,22 @@ const props = withDefaults(
pricingKeyPrefix?: string
noPricingLabel?: string
showPlatform?: boolean
/**
* model.platform 缺失 admin 聚合场景用父行的平台作为兜底着色
* 仅用于视觉不影响业务逻辑
*/
platformHint?: string
}>(),
{
pricingKeyPrefix: 'availableChannels.pricing',
noPricingLabel: '',
showPlatform: true
showPlatform: true,
platformHint: ''
}
)
const effectivePlatform = computed<string>(() => props.model.platform || props.platformHint || '')
const { t } = useI18n()
/** 按 token 定价展示时的换算单位:每百万 token。 */