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.
Switch the user-facing 'Available Channels' view from "one row per
platform" to "one channel row-group with N platform sections".
Backend: userAvailableChannel now holds Platforms []section instead
of being exploded. buildPlatformSections replaces
explodeChannelByPlatform with the same per-platform grouping logic.
Frontend: drop the DataTable wrapper for this view and write a
four-column grid table (渠道名 / 平台 / 分组 / 支持模型) where the
channel name only renders on the first platform row of each channel —
visual rowspan without hacking DataTable.
- api/channels.ts: UserChannelPlatformSection + platforms[]
- AvailableChannelsTable: rewritten as native grid (header + per-
channel section with hover row highlight)
- AvailableChannelsView: search now filters platforms sub-array;
channel-name / description hits still keep the whole channel
- i18n: add availableChannels.columns.platform (zh/en)
Backend: one source channel → N output rows, one per platform that
has user-visible groups. Each row carries a single platform, so the
frontend can color/icon an entire row without mixing sources.
- userAvailableChannel: add Platform field
- new explodeChannelByPlatform helper; drop now-redundant
collectGroupPlatforms
Frontend: use the row platform to drive theming and stop repeating
"ANTHROPIC" / "OPENAI" labels on every model chip.
- api/channels.ts: UserAvailableChannel.platform
- AvailableChannelsTable: name cell — PlatformBadge next to channel
name (replaces the two-line name/description block; description
moves to the badge's title tooltip); groups cell — each chip uses
platformBadgeLightClass + PlatformIcon; model list passes
show-platform=false + platform-hint to child chips
- SupportedModelChip: chip bg/border driven by platformBadgeClass,
leading PlatformIcon; platform-hint fallback when model.platform
missing
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729