feat(channels): themed model popover + group-badge with rate, subscription & exclusivity

Pricing popover:
- Teleport to body + fixed-position re-measuring on hover/scroll/resize so it
  escapes the card's overflow-hidden clip that was chopping off the lower
  half of the panel.
- Header + border adopt the platform theme via platformBorderClass /
  platformBadgeLightClass so each model card reads at a glance.

Accessible groups:
- Backend AvailableGroupRef / user DTO now expose subscription_type,
  rate_multiplier and is_exclusive. User-specific rates continue to come
  from /groups/rates (same pattern as the API keys page).
- Table renders groups through the shared GroupBadge, which already deepens
  subscription-type colors and shows default vs custom rate as
  <s>default</s> <b>custom</b>.
- Exclusive groups are labelled with a purple shield row, public groups
  with a grey globe row so admins and users can tell at a glance which
  groups they got via explicit grant vs. public access.

i18n keys for exclusive / public / their tooltips added to zh + en.
This commit is contained in:
erio
2026-04-21 21:44:34 +08:00
parent 84b03efa0b
commit ff4ef1b574
9 changed files with 316 additions and 119 deletions

View File

@@ -37,6 +37,7 @@
: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')"
@@ -55,6 +56,7 @@ 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'
@@ -62,6 +64,7 @@ const { t } = useI18n()
const appStore = useAppStore()
const channels = ref<UserAvailableChannel[]>([])
const userGroupRates = ref<Record<number, number>>({})
const loading = ref(false)
const searchQuery = ref('')
@@ -101,7 +104,17 @@ const filteredChannels = computed(() => {
async function loadChannels() {
loading.value = true
try {
channels.value = await userChannelsAPI.getAvailable()
// 渠道列表和用户专属倍率并发拉取。专属倍率失败不阻塞渠道展示——
// 失败时只是无法渲染专属倍率角标,降级为仅显示默认倍率。
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 {