Files
sub2api/frontend/src/views/user/AvailableChannelsView.vue
erio 25a5035503 fix(available-channels): description as own column, fixed table layout
- 描述独立成列:渠道名与描述各占一列,均用 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)
2026-04-22 19:47:03 +08:00

128 lines
4.5 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>
<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>