feat(distribution-banner): 分销站点轮播支持 PC/移动端分图与热区配置
This commit is contained in:
@@ -58,4 +58,71 @@ describe('mapDistributionHomeBanners', () => {
|
||||
expect(slide?.pc.action.type).toBe('none')
|
||||
expect(slide?.pc.hotspots).toEqual([])
|
||||
})
|
||||
|
||||
it('supports pc and mobile images, copy and actions independently', () => {
|
||||
const [slide] = mapDistributionHomeBanners(
|
||||
[
|
||||
{
|
||||
eyebrow: '本店活动',
|
||||
title: '周末特惠',
|
||||
badge: 'NEW',
|
||||
pill: '限时立减',
|
||||
tone: 'orange',
|
||||
enabled: true,
|
||||
pc: {
|
||||
image_url: '/pc-poster.webp',
|
||||
alt: 'PC 海报',
|
||||
action: { type: 'route', target: '/listings', open_new_tab: false },
|
||||
hotspots: [
|
||||
{
|
||||
id: 'hotspot-pc',
|
||||
label: '去选号',
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 20,
|
||||
height: 30,
|
||||
image_url: '/button.png',
|
||||
hover_scale: 1.06,
|
||||
action: { type: 'link', target: 'https://example.com/act', open_new_tab: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
mobile: {
|
||||
image_url: '/mobile-poster.webp',
|
||||
action: { type: 'preview', target: '', open_new_tab: false },
|
||||
},
|
||||
},
|
||||
],
|
||||
'天域商行'
|
||||
)
|
||||
|
||||
expect(slide?.pc.image_url).toBe('/pc-poster.webp')
|
||||
expect(slide?.mobile.image_url).toBe('/mobile-poster.webp')
|
||||
expect(slide?.eyebrow).toBe('本店活动')
|
||||
expect(slide?.title).toBe('周末特惠')
|
||||
expect(slide?.tone).toBe('orange')
|
||||
expect(slide?.pc.hotspots[0]?.label).toBe('去选号')
|
||||
expect(slide?.mobile.hotspots).toEqual([])
|
||||
})
|
||||
|
||||
it('still accepts the legacy link field used by the old distribution admin', () => {
|
||||
const [slide] = mapDistributionHomeBanners(
|
||||
[{ image_url: '/poster.png', link: '/m/announcements' }],
|
||||
'天域商行'
|
||||
)
|
||||
expect(slide?.pc.action).toEqual({
|
||||
type: 'route',
|
||||
target: '/m/announcements',
|
||||
open_new_tab: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('allows one side to stay empty when the two ends are configured separately', () => {
|
||||
const [slide] = mapDistributionHomeBanners(
|
||||
[{ title: '本店精选', pc: { image_url: '/pc.webp' }, mobile: {} }],
|
||||
'天域商行'
|
||||
)
|
||||
expect(slide?.pc.image_url).toBe('/pc.webp')
|
||||
expect(slide?.mobile.image_url).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,49 +10,104 @@ import {
|
||||
import {
|
||||
createEmptyBannerAction,
|
||||
createEmptyBannerVisual,
|
||||
normalizeBannerVisual,
|
||||
type BannerAction,
|
||||
type BannerVisual,
|
||||
type HomeBannerSlide,
|
||||
} from '@/features/listings/api/homeConfig'
|
||||
|
||||
/**
|
||||
* 分站轮播数据。
|
||||
* 新结构复用主站的 pc/mobile 视觉对象;image_url/link_url/link 仅用于兼容历史扁平配置。
|
||||
*/
|
||||
export interface DistributionBanner {
|
||||
image_url: string
|
||||
image_url?: string
|
||||
link_url?: string
|
||||
/** 早期分销后台保存的字段名,与前端读取的 link_url 不一致,这里一并兼容。 */
|
||||
link?: string
|
||||
title?: string
|
||||
eyebrow?: string
|
||||
badge?: string
|
||||
pill?: string
|
||||
tone?: string
|
||||
enabled?: boolean
|
||||
pc?: Partial<BannerVisual>
|
||||
mobile?: Partial<BannerVisual>
|
||||
}
|
||||
|
||||
/** 分站首页轮播:有图时不再叠标题,避免整幅压暗。 */
|
||||
const DISTRIBUTION_BANNER_TONES = ['blue', 'green', 'orange', 'gold']
|
||||
|
||||
/** 分站首页轮播:新结构按两端分别出图,旧结构继续沿用原来的海报/文字卡片表现。 */
|
||||
export function mapDistributionHomeBanners(
|
||||
banners: DistributionBanner[],
|
||||
siteName: string
|
||||
): HomeBannerSlide[] {
|
||||
return banners.map(banner => {
|
||||
const imageUrl = banner.image_url?.trim() || ''
|
||||
const action = resolveDistributionBannerAction(banner.link_url)
|
||||
const visual: BannerVisual = { ...createEmptyBannerVisual(), image_url: imageUrl, action }
|
||||
const legacyImageUrl = text(banner.image_url)
|
||||
const legacyAction = resolveDistributionBannerAction(banner.link_url || banner.link)
|
||||
const structured = Boolean(banner.pc || banner.mobile)
|
||||
const fallbackVisual: BannerVisual = {
|
||||
...createEmptyBannerVisual(),
|
||||
image_url: legacyImageUrl,
|
||||
action: legacyAction,
|
||||
}
|
||||
const pc = structured
|
||||
? normalizeBannerVisual(banner.pc, legacyImageUrl)
|
||||
: { ...fallbackVisual, hotspots: [] }
|
||||
const mobile = structured
|
||||
? normalizeBannerVisual(banner.mobile, legacyImageUrl)
|
||||
: { ...fallbackVisual, hotspots: [] }
|
||||
const legacyImage = Boolean(legacyImageUrl)
|
||||
const hasImage = Boolean(pc.image_url || mobile.image_url)
|
||||
const copy = structured
|
||||
? {
|
||||
eyebrow: text(banner.eyebrow),
|
||||
title: text(banner.title),
|
||||
pill: text(banner.pill),
|
||||
}
|
||||
: {
|
||||
eyebrow: legacyImage ? '' : siteName,
|
||||
title: legacyImage ? '' : text(banner.title) || '精选游戏账号 · 随租随玩',
|
||||
pill: legacyImage ? '' : '官方保障',
|
||||
}
|
||||
// 双端都没配图和文案时仍保留一张可读的文字卡片,避免前台出现空白轮播。
|
||||
if (structured && !hasImage && !copy.eyebrow && !copy.title && !copy.pill) {
|
||||
copy.eyebrow = siteName
|
||||
copy.title = '精选游戏账号 · 随租随玩'
|
||||
copy.pill = '官方保障'
|
||||
}
|
||||
|
||||
return {
|
||||
eyebrow: imageUrl ? '' : siteName,
|
||||
title: imageUrl ? '' : banner.title?.trim() || '精选游戏账号 · 随租随玩',
|
||||
badge: '本店精选',
|
||||
pill: imageUrl ? '' : '官方保障',
|
||||
tone: 'gold',
|
||||
enabled: true,
|
||||
// 分站装修只配一张图,两端共用,避免移动端丢图
|
||||
pc: { ...visual, hotspots: [] },
|
||||
mobile: { ...visual, hotspots: [] },
|
||||
eyebrow: copy.eyebrow,
|
||||
title: copy.title,
|
||||
badge: text(banner.badge) || (structured ? '' : '本店精选'),
|
||||
pill: copy.pill,
|
||||
tone: structured ? normalizeBannerTone(banner.tone) : 'gold',
|
||||
enabled: banner.enabled !== false,
|
||||
pc,
|
||||
mobile,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 分站轮播的 link_url:绝对地址走外链新窗口,站内路径走路由。 */
|
||||
function resolveDistributionBannerAction(linkUrl?: string): BannerAction {
|
||||
const target = linkUrl?.trim() || ''
|
||||
const target = text(linkUrl)
|
||||
if (!target) return createEmptyBannerAction()
|
||||
if (/^https?:\/\//i.test(target)) return { type: 'link', target, open_new_tab: true }
|
||||
if (target.startsWith('/')) return { type: 'route', target, open_new_tab: false }
|
||||
return createEmptyBannerAction()
|
||||
}
|
||||
|
||||
function text(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : ''
|
||||
}
|
||||
|
||||
function normalizeBannerTone(value: unknown): string {
|
||||
const tone = text(value)
|
||||
return DISTRIBUTION_BANNER_TONES.includes(tone) ? tone : 'gold'
|
||||
}
|
||||
|
||||
/** 分站首页主标题:站名与主站联名。主站或站名已含主站名时保持原样。 */
|
||||
export function formatDistributionCoBrandName(siteName: string, isDistribution: boolean): string {
|
||||
const name = siteName.trim()
|
||||
|
||||
Reference in New Issue
Block a user