- 描述独立成列:渠道名与描述各占一列,均用 rowspan 纵向合并 - 渠道名单元格 text-center + align-middle,合并后视觉居中 - table-fixed:给 name/description/platform 显式宽度,groups 和 supported_models 在剩余空间均分。支持模型列此前在 table-auto 下 不会换行导致横向溢出遮挡(反馈截图),加 table-fixed 后天然 flex-wrap - i18n 增加 availableChannels.columns.description(zh/en)
128 lines
4.5 KiB
Vue
128 lines
4.5 KiB
Vue
<template>
|
||
<AppLayout>
|
||
<TablePageLayout>
|
||
<template #filters>
|
||
<div class="flex flex-col justify-between gap-4 lg:flex-row lg:items-start">
|
||
<div class="flex flex-1 flex-wrap items-center gap-3">
|
||
<div class="relative w-full sm:w-80">
|
||
<Icon
|
||
name="search"
|
||
size="md"
|
||
class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500"
|
||
/>
|
||
<input
|
||
v-model="searchQuery"
|
||
type="text"
|
||
:placeholder="t('availableChannels.searchPlaceholder')"
|
||
class="input pl-10"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex w-full flex-shrink-0 flex-wrap items-center justify-end gap-3 lg:w-auto">
|
||
<button
|
||
@click="loadChannels"
|
||
:disabled="loading"
|
||
class="btn btn-secondary"
|
||
:title="t('common.refresh', 'Refresh')"
|
||
>
|
||
<Icon name="refresh" size="md" :class="loading ? 'animate-spin' : ''" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template #table>
|
||
<AvailableChannelsTable
|
||
:columns="columnLabels"
|
||
:rows="filteredChannels"
|
||
:loading="loading"
|
||
:user-group-rates="userGroupRates"
|
||
pricing-key-prefix="availableChannels.pricing"
|
||
:no-pricing-label="t('availableChannels.noPricing')"
|
||
:no-models-label="t('availableChannels.noModels')"
|
||
:empty-label="t('availableChannels.empty')"
|
||
/>
|
||
</template>
|
||
</TablePageLayout>
|
||
</AppLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||
import TablePageLayout from '@/components/layout/TablePageLayout.vue'
|
||
import Icon from '@/components/icons/Icon.vue'
|
||
import AvailableChannelsTable from '@/components/channels/AvailableChannelsTable.vue'
|
||
import userChannelsAPI, { type UserAvailableChannel } from '@/api/channels'
|
||
import userGroupsAPI from '@/api/groups'
|
||
import { useAppStore } from '@/stores/app'
|
||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||
|
||
const { t } = useI18n()
|
||
const appStore = useAppStore()
|
||
|
||
const channels = ref<UserAvailableChannel[]>([])
|
||
const userGroupRates = ref<Record<number, number>>({})
|
||
const loading = ref(false)
|
||
const searchQuery = ref('')
|
||
|
||
const columnLabels = computed(() => ({
|
||
name: t('availableChannels.columns.name'),
|
||
description: t('availableChannels.columns.description'),
|
||
platform: t('availableChannels.columns.platform'),
|
||
groups: t('availableChannels.columns.groups'),
|
||
supportedModels: t('availableChannels.columns.supportedModels'),
|
||
}))
|
||
|
||
/**
|
||
* 搜索过滤:
|
||
* - 命中渠道名/描述 → 整个渠道(所有 platforms)都保留
|
||
* - 否则按 platform/group/model 维度在 sections 里过滤,保留有匹配的 section
|
||
* - 所有 sections 都不匹配时,渠道本身被过滤掉
|
||
*/
|
||
const filteredChannels = computed(() => {
|
||
const q = searchQuery.value.trim().toLowerCase()
|
||
if (!q) return channels.value
|
||
return channels.value
|
||
.map((ch) => {
|
||
const nameHit = ch.name.toLowerCase().includes(q)
|
||
const descHit = (ch.description || '').toLowerCase().includes(q)
|
||
if (nameHit || descHit) return ch
|
||
const matchingSections = ch.platforms.filter(
|
||
(p) =>
|
||
p.platform.toLowerCase().includes(q) ||
|
||
p.groups.some((g) => g.name.toLowerCase().includes(q)) ||
|
||
p.supported_models.some((m) => m.name.toLowerCase().includes(q)),
|
||
)
|
||
if (matchingSections.length === 0) return null
|
||
return { ...ch, platforms: matchingSections }
|
||
})
|
||
.filter((ch): ch is UserAvailableChannel => ch !== null)
|
||
})
|
||
|
||
async function loadChannels() {
|
||
loading.value = true
|
||
try {
|
||
// 渠道列表和用户专属倍率并发拉取。专属倍率失败不阻塞渠道展示——
|
||
// 失败时只是无法渲染专属倍率角标,降级为仅显示默认倍率。
|
||
const [list, rates] = await Promise.all([
|
||
userChannelsAPI.getAvailable(),
|
||
userGroupsAPI.getUserGroupRates().catch((err: unknown) => {
|
||
console.error('Failed to load user group rates:', err)
|
||
return {} as Record<number, number>
|
||
}),
|
||
])
|
||
channels.value = list
|
||
userGroupRates.value = rates
|
||
} catch (err: unknown) {
|
||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(loadChannels)
|
||
</script>
|