diff --git a/app/templates/template-07/components/HeroSection.vue b/app/templates/template-07/components/HeroSection.vue index 2127ee8..e25b441 100644 --- a/app/templates/template-07/components/HeroSection.vue +++ b/app/templates/template-07/components/HeroSection.vue @@ -5,28 +5,42 @@
+ + + + + +
@@ -156,10 +170,16 @@ * * 文案优先级:当前 banner 的 title/subtitle → siteInfo.slogan/comments → siteName 拼接 → 硬编码兜底。 * 即使某张 banner 未配文案也不会出现空白。 + * + * 点击行为:点击轮播图按后台「轮播图管理」配置的跳转类型跳转 + * (linkType:1 外链、2 文章详情、3 产品详情,目标由 linkUrl / linkTargetId 决定): + * - 站内目标(文章 / 产品详情)走 NuxtLink 当前页跳转; + * - 外链新标签页打开; + * - 未配置跳转的图片保持纯图不可点(不渲染空的可点区域)。 */ import type { CmsNavigation, CmsBanner, ApiEnvelope, PageResult } from '~/types' import { ensureFullUrl } from '~/utils/image' -import { getNavLink, getBannerLink } from '~/utils' +import { getNavLink, getBannerTarget } from '~/utils' interface Props { /** 是否启用 Banner 轮播背景;false 时即使有轮播图也回退到蓝渐变 */ @@ -196,6 +216,15 @@ const banners = computed(() => { /** 是否进入轮播图模式:有可用轮播图且后台开启 Banner 轮播开关 */ const bannerMode = computed(() => props.showBanner && banners.value.length > 0) +/** + * 轮播项 + 解析好的点击目标(后台「轮播图管理」配置的跳转)。 + * target 为 null 表示该图未配置跳转,此时渲染纯图不可点。 + */ +const bannerItems = computed(() => banners.value.map((b) => ({ + banner: b, + target: getBannerTarget(b) +}))) + /** 当前帧索引 */ const currentBannerIndex = ref(0) @@ -246,11 +275,6 @@ function bannerImg(b: CmsBanner): string { return raw.replace(/resize,w_\d+/, 'resize,w_1920') } -/** 取轮播图跳转链接(统一走 utils.getBannerLink,兼容 link/linkUrl/link_url 多字段名) */ -function bannerLink(b: CmsBanner): string { - return getBannerLink(b) -} - /** 是否有可用图片 */ function hasImage(b: CmsBanner): boolean { return !!(b.image || b.imageUrl || b.pic || b.url) diff --git a/app/types/index.ts b/app/types/index.ts index 0a59a8a..0eaf9d9 100644 --- a/app/types/index.ts +++ b/app/types/index.ts @@ -47,8 +47,18 @@ export interface CmsBanner { /** 标题 / 副标题(可选,部分模板会叠加在图上) */ title?: string subtitle?: string - /** 点击跳转链接(可选) */ + /** 点击跳转链接(可选,标准字段名) */ link?: string + /** 点击跳转链接(CMS 原始字段名,Java 把 link_url 序列化为 linkUrl) */ + linkUrl?: string + /** + * 跳转类型(后台「轮播图管理」配置): + * 0=无跳转、1=外链、2=文章详情、3=产品详情。 + * 类型 2/3 时目标页由 linkTargetId 决定(linkUrl 为空)。 + */ + linkType?: number + /** 跳转目标 ID:类型 2 为文章 ID,类型 3 为产品 ID */ + linkTargetId?: number | null /** 排序(升序)—— 上游字段名为 sortNum */ sortNum?: number /** 排序(升序,兼容旧字段名) */ diff --git a/app/utils/index.ts b/app/utils/index.ts index 611e81c..a524d24 100644 --- a/app/utils/index.ts +++ b/app/utils/index.ts @@ -166,25 +166,67 @@ export function getNavLink(nav?: CmsNavigation | null): string { } /** - * 取轮播图(banner)点击跳转链接。 + * 判断链接是否需要「站外」打开(新标签页)。 * - * 兼容上游多种字段命名,避免「后台配了 link_url 但前端读不到」这类错位: - * - link:本仓库 server/api/banner.get.ts 映射出的标准字段; - * - linkUrl:CMS 原始返回字段(Java 把数据库列 link_url 序列化为驼峰 linkUrl); - * - link_url:个别上游/老接口可能直接透传的蛇形字段名。 - * 仅当为非空字符串时才返回,否则返回 ''(调用方据此不渲染 ,避免空白可点区域)。 + * 站外判定:绝对地址(http://、https://)、协议相对地址(//host) + * 以及 mailto: / tel: 等非路由协议;其余(含以 / 开头的站内路由)视为站内。 + */ +export function isExternalUrl(url?: string | null): boolean { + if (!url) return false + const v = url.trim() + if (!v) return false + return /^([a-z][a-z0-9+.-]*:|\/\/)/i.test(v) +} + +/** 轮播图点击目标 */ +export interface BannerTarget { + /** 跳转地址 */ + href: string + /** true=站外(新标签页打开);false=站内路由(当前页跳转) */ + external: boolean +} + +/** + * 解析轮播图(banner)点击目标。 + * + * 对齐后台「轮播图管理」的跳转配置(linkType + linkTargetId/linkUrl), + * 同时兼容上游多种字段命名,避免「后台配了却点不动」: + * - linkType=1(外链):取 linkUrl; + * - linkType=2(文章详情):/article/{linkTargetId}; + * - linkType=3(产品详情):/product/{linkTargetId}; + * - linkType 缺省或为 0 但 linkUrl 非空:按外链兜底(兼容只存了地址的老数据)。 + * + * 字段名兼容:link(本仓库 server/api/banner.get.ts 映射出的标准字段)、 + * linkUrl(Java 把 link_url 序列化为驼峰)、link_url(个别上游透传的蛇形字段)。 + * + * @returns 跳转目标;返回 null 表示未配置跳转(调用方据此不渲染可点区域,避免空白可点) + */ +export function getBannerTarget(banner?: Partial | null): BannerTarget | null { + if (!banner) return null + + const type = Number(banner.linkType ?? 0) || 0 + const targetId = Number(banner.linkTargetId ?? 0) || 0 + if (type === 2 && targetId > 0) return { href: `/article/${targetId}`, external: false } + if (type === 3 && targetId > 0) return { href: `/product/${targetId}`, external: false } + + const raw = banner.link + ?? banner.linkUrl + ?? (banner as Record).link_url + ?? '' + const href = typeof raw === 'string' ? raw.trim() : '' + if (!href) return null + return { href, external: isExternalUrl(href) } +} + +/** + * 取轮播图(banner)点击跳转链接(需要字符串地址的场景使用)。 + * 仅当后台配置了有效跳转(外链 / 文章 / 产品)时才返回非空字符串。 * * @param banner 轮播图数据 * @returns 跳转链接字符串;无效时返回 '' */ export function getBannerLink(banner?: Partial | null): string { - if (!banner) return '' - const raw = - (banner as Record).link - ?? (banner as Record).linkUrl - ?? (banner as Record).link_url - ?? '' - return typeof raw === 'string' && raw.trim() ? raw.trim() : '' + return getBannerTarget(banner)?.href ?? '' } /**