mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-05-05 05:30:44 +08:00
Follow-up to the available-channels review pass. No behavior change for end users; tightens internals based on three independent code reviews. Backend - service/channel.go: collapse buildPricingLookup + pricedNamesFor into a single platformPricingIndex (byLower + originalCase + ordered names), built once per SupportedModels call. Fixes a casing- consistency bug where the same logical model appeared with mapping case in the exact branch but pricing case in the wildcard branch — pricing's original case now wins everywhere. - service/channel.go: doc that a mapping key of just "*" expands to every priced model on the platform (intentional "passthrough all"). - service/channel_available.go: normalize empty BillingModelSource to channel_mapped at construction time, removing the same fallback duplicated in the admin DTO mapper and the admin Vue template. - handler/admin/available_channel_handler.go: unexport availableChannelToAdminResponse (same-package usage only); mapper is now a pure passthrough. - handler/available_channel_handler.go: drop the middleware2 alias (no name collision in this file). Frontend - utils/pricing.ts: extract formatScaled, used by SupportedModelChip and PricingRow. - api/admin/channels.ts: re-export BillingMode from constants/channel; tighten Channel.status / billing_model_source to ChannelStatus / BillingModelSource (and same for AvailableChannel). - components/channels/AvailableChannelsTable.vue: drop dead withDefaults wrapper (loading is required, both call sites pass it). - views/admin/AvailableChannelsView.vue: drop the redundant || BILLING_MODEL_SOURCE_CHANNEL_MAPPED fallback (now applied in service layer); remove unused import. - i18n zh + en: delete unused tierLabel and tokenRange keys from both availableChannels.pricing and admin.availableChannels.pricing. Tests - New: SupportedModels_ExactKeyUsesPricedCaseWhenAvailable locks the pricing-case-wins rule. - New: SupportedModels_AsteriskOnlyMappingExpandsAllPriced documents the "*" expansion rule. - Admin handler: existing tests adjusted to pass an explicit BillingModelSource (default-fill is now exercised by service tests).
108 lines
3.1 KiB
Vue
108 lines
3.1 KiB
Vue
<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>
|
||
</template>
|
||
|
||
<template #cell-groups="{ row }">
|
||
<div v-if="row.groups.length === 0" class="text-xs text-gray-400">
|
||
<slot name="empty-groups">-</slot>
|
||
</div>
|
||
<div v-else class="flex flex-wrap gap-1">
|
||
<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"
|
||
>
|
||
{{ g.name }}
|
||
</span>
|
||
</div>
|
||
</template>
|
||
|
||
<template #cell-supported_models="{ row }">
|
||
<div v-if="row.supported_models.length === 0" class="text-xs text-gray-400">
|
||
{{ noModelsLabel }}
|
||
</div>
|
||
<div v-else class="flex max-w-[560px] flex-wrap gap-1">
|
||
<SupportedModelChip
|
||
v-for="m in row.supported_models"
|
||
:key="`${m.platform}-${m.name}`"
|
||
:model="m"
|
||
:pricing-key-prefix="pricingKeyPrefix"
|
||
:no-pricing-label="noPricingLabel"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 允许父组件为额外列提供自定义渲染(如 admin 的 status / billing_model_source)。 -->
|
||
<template v-for="slot in extraCellSlots" :key="slot" #[slot]="scope">
|
||
<slot :name="slot" v-bind="scope" />
|
||
</template>
|
||
|
||
<template #empty>
|
||
<slot name="empty">
|
||
<div class="flex flex-col items-center py-8">
|
||
<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>
|
||
</slot>
|
||
</template>
|
||
</DataTable>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, useSlots } from 'vue'
|
||
import DataTable from '@/components/common/DataTable.vue'
|
||
import Icon from '@/components/icons/Icon.vue'
|
||
import SupportedModelChip from './SupportedModelChip.vue'
|
||
|
||
interface GroupRef {
|
||
id: number
|
||
name: string
|
||
platform?: string
|
||
}
|
||
|
||
interface Row {
|
||
name: string
|
||
description?: string
|
||
groups: GroupRef[]
|
||
supported_models: Array<{
|
||
name: string
|
||
platform: string
|
||
pricing: unknown | null
|
||
}>
|
||
[key: string]: unknown
|
||
}
|
||
|
||
interface Column {
|
||
key: string
|
||
label: string
|
||
}
|
||
|
||
defineProps<{
|
||
columns: Column[]
|
||
rows: Row[]
|
||
loading: boolean
|
||
pricingKeyPrefix: string
|
||
noPricingLabel: string
|
||
noModelsLabel: string
|
||
emptyLabel: string
|
||
}>()
|
||
|
||
const slots = useSlots()
|
||
/**
|
||
* 透传父组件提供的 cell-* 插槽(除本组件内置的 name/groups/supported_models/empty-groups/empty
|
||
* 之外),让 admin 场景可以自定义 status / billing_model_source 等列。
|
||
*/
|
||
const extraCellSlots = computed(() => {
|
||
const reserved = new Set(['cell-name', 'cell-groups', 'cell-supported_models', 'empty-groups', 'empty'])
|
||
return Object.keys(slots).filter((name) => name.startsWith('cell-') && !reserved.has(name))
|
||
})
|
||
</script>
|