Files
sub2api/frontend/src/components/channels/AvailableChannelsTable.vue
erio 3cdd5754df feat(channels): aggregate by channel with platform sections + rowspan table
Switch the user-facing 'Available Channels' view from "one row per
platform" to "one channel row-group with N platform sections".

Backend: userAvailableChannel now holds Platforms []section instead
of being exploded. buildPlatformSections replaces
explodeChannelByPlatform with the same per-platform grouping logic.

Frontend: drop the DataTable wrapper for this view and write a
four-column grid table (渠道名 / 平台 / 分组 / 支持模型) where the
channel name only renders on the first platform row of each channel —
visual rowspan without hacking DataTable.

- api/channels.ts: UserChannelPlatformSection + platforms[]
- AvailableChannelsTable: rewritten as native grid (header + per-
  channel section with hover row highlight)
- AvailableChannelsView: search now filters platforms sub-array;
  channel-name / description hits still keep the whole channel
- i18n: add availableChannels.columns.platform (zh/en)
2026-04-21 19:46:55 +08:00

125 lines
4.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="card overflow-hidden">
<!-- 表头 -->
<div
class="grid items-center border-b border-gray-100 bg-gray-50/50 px-4 py-3 text-xs font-medium uppercase tracking-wide text-gray-500 dark:border-dark-700 dark:bg-dark-800/50 dark:text-gray-400"
:style="gridStyle"
>
<div>{{ columns.name }}</div>
<div>{{ columns.platform }}</div>
<div>{{ columns.groups }}</div>
<div>{{ columns.supportedModels }}</div>
</div>
<div v-if="loading" class="flex justify-center py-10">
<Icon name="refresh" size="lg" class="animate-spin text-gray-400" />
</div>
<div v-else-if="rows.length === 0" class="flex flex-col items-center py-12">
<Icon name="inbox" size="xl" class="mb-3 h-12 w-12 text-gray-400" />
<p class="text-sm text-gray-500 dark:text-gray-400">{{ emptyLabel }}</p>
</div>
<!-- 渠道分组每个渠道一个 section内部按 platform 顺序铺开 -->
<div
v-else
v-for="(channel, chIdx) in rows"
:key="`${channel.name}-${chIdx}`"
class="border-b border-gray-100 last:border-b-0 dark:border-dark-700"
>
<div
v-for="(section, secIdx) in channel.platforms"
:key="`${channel.name}-${section.platform}`"
class="grid items-center px-4 py-3 transition-colors hover:bg-gray-50/40 dark:hover:bg-dark-800/40"
:class="{ 'border-t border-gray-100/70 dark:border-dark-700/50': secIdx > 0 }"
:style="gridStyle"
>
<!-- 渠道名仅第一行渲染后续行留空视觉上的 rowspan -->
<div>
<template v-if="secIdx === 0">
<div class="font-medium text-gray-900 dark:text-white">{{ channel.name }}</div>
<div
v-if="channel.description"
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
>
{{ channel.description }}
</div>
</template>
</div>
<!-- 平台徽章 -->
<div>
<span
:class="[
'inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] font-medium uppercase',
platformBadgeClass(section.platform),
]"
>
<PlatformIcon :platform="section.platform as GroupPlatform" size="xs" />
{{ section.platform }}
</span>
</div>
<!-- 分组 -->
<div class="flex flex-wrap gap-1">
<span
v-for="g in section.groups"
:key="g.id"
:class="[
'inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs font-medium',
platformBadgeLightClass(section.platform),
]"
>
<PlatformIcon :platform="section.platform as GroupPlatform" size="xs" />
{{ g.name }}
</span>
<span v-if="section.groups.length === 0" class="text-xs text-gray-400">-</span>
</div>
<!-- 支持模型 -->
<div class="flex flex-wrap gap-1">
<SupportedModelChip
v-for="m in section.supported_models"
:key="`${section.platform}-${m.name}`"
:model="m"
:pricing-key-prefix="pricingKeyPrefix"
:no-pricing-label="noPricingLabel"
:show-platform="false"
:platform-hint="section.platform"
/>
<span v-if="section.supported_models.length === 0" class="text-xs text-gray-400">
{{ noModelsLabel }}
</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Icon from '@/components/icons/Icon.vue'
import PlatformIcon from '@/components/common/PlatformIcon.vue'
import SupportedModelChip from './SupportedModelChip.vue'
import type { UserAvailableChannel } from '@/api/channels'
import type { GroupPlatform } from '@/types'
import { platformBadgeClass, platformBadgeLightClass } from '@/utils/platformColors'
/** 四列 grid 的 template-columns与表头、每个 section 行共享。 */
const gridStyle = 'grid-template-columns: 220px 140px minmax(200px, 1fr) minmax(280px, 2fr); display: grid;'
defineProps<{
columns: {
name: string
platform: string
groups: string
supportedModels: string
}
rows: UserAvailableChannel[]
loading: boolean
pricingKeyPrefix: string
noPricingLabel: string
noModelsLabel: string
emptyLabel: string
}>()
</script>