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)
This commit is contained in:
@@ -1,133 +1,147 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="card overflow-hidden">
|
||||||
<!-- 表头 -->
|
<table class="w-full table-fixed border-collapse text-sm">
|
||||||
<div
|
<thead>
|
||||||
class="grid items-center rounded-t-lg 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"
|
<tr class="border-b border-gray-100 bg-gray-50/50 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"
|
<th class="w-[180px] px-4 py-3 text-center">{{ columns.name }}</th>
|
||||||
>
|
<th class="w-[200px] px-4 py-3 text-left">{{ columns.description }}</th>
|
||||||
<div>{{ columns.name }}</div>
|
<th class="w-[140px] px-4 py-3 text-left">{{ columns.platform }}</th>
|
||||||
<div>{{ columns.platform }}</div>
|
<th class="px-4 py-3 text-left">{{ columns.groups }}</th>
|
||||||
<div>{{ columns.groups }}</div>
|
<th class="px-4 py-3 text-left">{{ columns.supportedModels }}</th>
|
||||||
<div>{{ columns.supportedModels }}</div>
|
</tr>
|
||||||
</div>
|
</thead>
|
||||||
|
<tbody v-if="loading">
|
||||||
<div v-if="loading" class="flex justify-center py-10">
|
<tr>
|
||||||
<Icon name="refresh" size="lg" class="animate-spin text-gray-400" />
|
<td colspan="5" class="py-10 text-center">
|
||||||
</div>
|
<Icon name="refresh" size="lg" class="inline-block animate-spin text-gray-400" />
|
||||||
|
</td>
|
||||||
<div v-else-if="rows.length === 0" class="flex flex-col items-center py-12">
|
</tr>
|
||||||
<Icon name="inbox" size="xl" class="mb-3 h-12 w-12 text-gray-400" />
|
</tbody>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ emptyLabel }}</p>
|
<tbody v-else-if="rows.length === 0">
|
||||||
</div>
|
<tr>
|
||||||
|
<td colspan="5" class="py-12 text-center">
|
||||||
<!-- 渠道分组:每个渠道一个 section,内部按 platform 顺序铺开。
|
<Icon name="inbox" size="xl" class="mx-auto mb-3 h-12 w-12 text-gray-400" />
|
||||||
外层无 overflow-hidden,避免裁掉 SupportedModelChip 的价格浮层。 -->
|
<p class="text-sm text-gray-500 dark:text-gray-400">{{ emptyLabel }}</p>
|
||||||
<div
|
</td>
|
||||||
v-else
|
</tr>
|
||||||
v-for="(channel, chIdx) in rows"
|
</tbody>
|
||||||
:key="`${channel.name}-${chIdx}`"
|
<!-- 每个渠道一个 tbody:首行 td rowspan 渠道名,后续行只渲染其余三列。
|
||||||
class="border-b border-gray-100 last:rounded-b-lg last:border-b-0 dark:border-dark-700"
|
tbody 之间强分隔线表达"渠道边界",tbody 内部用淡分隔线区分平台。 -->
|
||||||
>
|
<tbody
|
||||||
<div
|
v-else
|
||||||
v-for="(section, secIdx) in channel.platforms"
|
v-for="(channel, chIdx) in rows"
|
||||||
:key="`${channel.name}-${section.platform}`"
|
:key="`${channel.name}-${chIdx}`"
|
||||||
class="grid items-center px-4 py-3 transition-colors hover:bg-gray-50/40 dark:hover:bg-dark-800/40"
|
class="border-b-2 border-gray-200 last:border-b-0 dark:border-dark-600"
|
||||||
:class="{ 'border-t border-gray-100/70 dark:border-dark-700/50': secIdx > 0 }"
|
|
||||||
:style="gridStyle"
|
|
||||||
>
|
>
|
||||||
<!-- 渠道名:仅第一行渲染,后续行留空(视觉上的 rowspan) -->
|
<tr
|
||||||
<div>
|
v-for="(section, secIdx) in channel.platforms"
|
||||||
<template v-if="secIdx === 0">
|
:key="`${channel.name}-${section.platform}`"
|
||||||
<div class="font-medium text-gray-900 dark:text-white">{{ channel.name }}</div>
|
class="transition-colors hover:bg-gray-50/40 dark:hover:bg-dark-800/40"
|
||||||
<div
|
:class="{ 'border-t border-gray-100/70 dark:border-dark-700/50': secIdx > 0 }"
|
||||||
v-if="channel.description"
|
>
|
||||||
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
|
<!-- 渠道名:只在第一行渲染并用 rowspan 纵向合并 -->
|
||||||
|
<td
|
||||||
|
v-if="secIdx === 0"
|
||||||
|
:rowspan="channel.platforms.length"
|
||||||
|
class="px-4 py-3 text-center align-middle font-medium text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
{{ channel.name }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- 描述:独立一列,同样用 rowspan 纵向合并 -->
|
||||||
|
<td
|
||||||
|
v-if="secIdx === 0"
|
||||||
|
:rowspan="channel.platforms.length"
|
||||||
|
class="px-4 py-3 align-middle text-xs text-gray-500 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
<template v-if="channel.description">{{ channel.description }}</template>
|
||||||
|
<span v-else class="text-gray-400">-</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- 平台徽章 -->
|
||||||
|
<td class="align-top px-4 py-3">
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] font-medium uppercase',
|
||||||
|
platformBadgeClass(section.platform),
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
{{ channel.description }}
|
<PlatformIcon :platform="section.platform as GroupPlatform" size="xs" />
|
||||||
|
{{ section.platform }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- 分组:专属分组在前(紫色 shield 行),公开分组在后(灰色 globe 行)。 -->
|
||||||
|
<td class="align-top px-4 py-3">
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<div
|
||||||
|
v-if="exclusiveGroups(section).length > 0"
|
||||||
|
class="flex flex-wrap items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-0.5 text-[10px] font-medium uppercase text-purple-600 dark:text-purple-400"
|
||||||
|
:title="t('availableChannels.exclusiveTooltip')"
|
||||||
|
>
|
||||||
|
<Icon name="shield" size="xs" class="h-3 w-3" />
|
||||||
|
{{ t('availableChannels.exclusive') }}
|
||||||
|
</span>
|
||||||
|
<GroupBadge
|
||||||
|
v-for="g in exclusiveGroups(section)"
|
||||||
|
:key="`ex-${g.id}`"
|
||||||
|
:name="g.name"
|
||||||
|
:platform="g.platform as GroupPlatform"
|
||||||
|
:subscription-type="(g.subscription_type || 'standard') as SubscriptionType"
|
||||||
|
:rate-multiplier="g.rate_multiplier"
|
||||||
|
:user-rate-multiplier="userGroupRates[g.id] ?? null"
|
||||||
|
always-show-rate
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="publicGroups(section).length > 0"
|
||||||
|
class="flex flex-wrap items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-0.5 text-[10px] font-medium uppercase text-gray-500 dark:text-gray-400"
|
||||||
|
:title="t('availableChannels.publicTooltip')"
|
||||||
|
>
|
||||||
|
<Icon name="globe" size="xs" class="h-3 w-3" />
|
||||||
|
{{ t('availableChannels.public') }}
|
||||||
|
</span>
|
||||||
|
<GroupBadge
|
||||||
|
v-for="g in publicGroups(section)"
|
||||||
|
:key="`pub-${g.id}`"
|
||||||
|
:name="g.name"
|
||||||
|
:platform="g.platform as GroupPlatform"
|
||||||
|
:subscription-type="(g.subscription_type || 'standard') as SubscriptionType"
|
||||||
|
:rate-multiplier="g.rate_multiplier"
|
||||||
|
:user-rate-multiplier="userGroupRates[g.id] ?? null"
|
||||||
|
always-show-rate
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span v-if="section.groups.length === 0" class="text-xs text-gray-400">-</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</td>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 平台徽章 -->
|
<!-- 支持模型 -->
|
||||||
<div>
|
<td class="align-top px-4 py-3">
|
||||||
<span
|
<div class="flex flex-wrap gap-1">
|
||||||
:class="[
|
<SupportedModelChip
|
||||||
'inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] font-medium uppercase',
|
v-for="m in section.supported_models"
|
||||||
platformBadgeClass(section.platform),
|
:key="`${section.platform}-${m.name}`"
|
||||||
]"
|
:model="m"
|
||||||
>
|
:pricing-key-prefix="pricingKeyPrefix"
|
||||||
<PlatformIcon :platform="section.platform as GroupPlatform" size="xs" />
|
:no-pricing-label="noPricingLabel"
|
||||||
{{ section.platform }}
|
:show-platform="false"
|
||||||
</span>
|
:platform-hint="section.platform"
|
||||||
</div>
|
/>
|
||||||
|
<span v-if="section.supported_models.length === 0" class="text-xs text-gray-400">
|
||||||
<!-- 分组:专属分组在前(紫色 shield 行),公开分组在后(灰色 globe 行)。
|
{{ noModelsLabel }}
|
||||||
订阅分组由 GroupBadge 内部按 subscription_type 自动加深背景。 -->
|
</span>
|
||||||
<div class="flex flex-col gap-1.5">
|
</div>
|
||||||
<div
|
</td>
|
||||||
v-if="exclusiveGroups(section).length > 0"
|
</tr>
|
||||||
class="flex flex-wrap items-center gap-1.5"
|
</tbody>
|
||||||
>
|
</table>
|
||||||
<span
|
|
||||||
class="inline-flex items-center gap-0.5 text-[10px] font-medium uppercase text-purple-600 dark:text-purple-400"
|
|
||||||
:title="t('availableChannels.exclusiveTooltip')"
|
|
||||||
>
|
|
||||||
<Icon name="shield" size="xs" class="h-3 w-3" />
|
|
||||||
{{ t('availableChannels.exclusive') }}
|
|
||||||
</span>
|
|
||||||
<GroupBadge
|
|
||||||
v-for="g in exclusiveGroups(section)"
|
|
||||||
:key="`ex-${g.id}`"
|
|
||||||
:name="g.name"
|
|
||||||
:platform="g.platform as GroupPlatform"
|
|
||||||
:subscription-type="(g.subscription_type || 'standard') as SubscriptionType"
|
|
||||||
:rate-multiplier="g.rate_multiplier"
|
|
||||||
:user-rate-multiplier="userGroupRates[g.id] ?? null"
|
|
||||||
always-show-rate
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="publicGroups(section).length > 0"
|
|
||||||
class="flex flex-wrap items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center gap-0.5 text-[10px] font-medium uppercase text-gray-500 dark:text-gray-400"
|
|
||||||
:title="t('availableChannels.publicTooltip')"
|
|
||||||
>
|
|
||||||
<Icon name="globe" size="xs" class="h-3 w-3" />
|
|
||||||
{{ t('availableChannels.public') }}
|
|
||||||
</span>
|
|
||||||
<GroupBadge
|
|
||||||
v-for="g in publicGroups(section)"
|
|
||||||
:key="`pub-${g.id}`"
|
|
||||||
:name="g.name"
|
|
||||||
:platform="g.platform as GroupPlatform"
|
|
||||||
:subscription-type="(g.subscription_type || 'standard') as SubscriptionType"
|
|
||||||
:rate-multiplier="g.rate_multiplier"
|
|
||||||
:user-rate-multiplier="userGroupRates[g.id] ?? null"
|
|
||||||
always-show-rate
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -141,12 +155,10 @@ import type { UserAvailableChannel, UserAvailableGroup, UserChannelPlatformSecti
|
|||||||
import type { GroupPlatform, SubscriptionType } from '@/types'
|
import type { GroupPlatform, SubscriptionType } from '@/types'
|
||||||
import { platformBadgeClass } from '@/utils/platformColors'
|
import { platformBadgeClass } from '@/utils/platformColors'
|
||||||
|
|
||||||
/** 四列 grid 的 template-columns;与表头、每个 section 行共享。 */
|
|
||||||
const gridStyle = 'grid-template-columns: 220px 140px minmax(240px, 1fr) minmax(280px, 2fr); display: grid;'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
columns: {
|
columns: {
|
||||||
name: string
|
name: string
|
||||||
|
description: string
|
||||||
platform: string
|
platform: string
|
||||||
groups: string
|
groups: string
|
||||||
supportedModels: string
|
supportedModels: string
|
||||||
|
|||||||
@@ -944,6 +944,7 @@ export default {
|
|||||||
publicTooltip: 'Groups open to all users',
|
publicTooltip: 'Groups open to all users',
|
||||||
columns: {
|
columns: {
|
||||||
name: 'Channel',
|
name: 'Channel',
|
||||||
|
description: 'Description',
|
||||||
platform: 'Platform',
|
platform: 'Platform',
|
||||||
groups: 'Your Accessible Groups',
|
groups: 'Your Accessible Groups',
|
||||||
supportedModels: 'Supported Models'
|
supportedModels: 'Supported Models'
|
||||||
|
|||||||
@@ -948,6 +948,7 @@ export default {
|
|||||||
publicTooltip: '对所有用户公开的分组',
|
publicTooltip: '对所有用户公开的分组',
|
||||||
columns: {
|
columns: {
|
||||||
name: '渠道名',
|
name: '渠道名',
|
||||||
|
description: '描述',
|
||||||
platform: '平台',
|
platform: '平台',
|
||||||
groups: '我可访问的分组',
|
groups: '我可访问的分组',
|
||||||
supportedModels: '支持模型'
|
supportedModels: '支持模型'
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const searchQuery = ref('')
|
|||||||
|
|
||||||
const columnLabels = computed(() => ({
|
const columnLabels = computed(() => ({
|
||||||
name: t('availableChannels.columns.name'),
|
name: t('availableChannels.columns.name'),
|
||||||
|
description: t('availableChannels.columns.description'),
|
||||||
platform: t('availableChannels.columns.platform'),
|
platform: t('availableChannels.columns.platform'),
|
||||||
groups: t('availableChannels.columns.groups'),
|
groups: t('availableChannels.columns.groups'),
|
||||||
supportedModels: t('availableChannels.columns.supportedModels'),
|
supportedModels: t('availableChannels.columns.supportedModels'),
|
||||||
|
|||||||
Reference in New Issue
Block a user