feat(template-07): 首页轮播图按后台配置的跳转类型跳转

轮播图原先只读 linkUrl 渲染 <a>,而后台「轮播图管理」的跳转是
linkType + linkUrl/linkTargetId 三元组:配「文章详情 / 产品详情」时
linkUrl 为空,前端拿不到地址,图片就渲染成不可点的纯 <img>,
运营配了也点不动。

- utils 新增 getBannerTarget():linkType 2 → /article/{id}、
  3 → /product/{id}、1 → linkUrl;类型缺省但地址非空时按外链兜底
  (兼容只存了地址的老数据);字段名兼容 link / linkUrl / link_url
- 新增 isExternalUrl():http(s)、//host、mailto 等视为站外
- getBannerLink() 保留为 getBannerTarget() 的薄封装,对外行为不变
- HeroSection:图片按目标渲染 NuxtLink(站内目标当前页跳转)/
  <a target="_blank">(外链)/ 纯 <img>(未配置跳转,保持不可点)
- CmsBanner 补 linkType / linkUrl / linkTargetId 字段声明

首页 4 张图(政府采购代理 / 投融资服务 / AI数智化 / 工程代理)
已在后台配置为「产品详情」跳转。
This commit is contained in:
2026-10-10 19:56:45 +08:00
parent 9d0599ec90
commit 4491613902
3 changed files with 104 additions and 28 deletions
@@ -5,28 +5,42 @@
<div class="absolute inset-0 z-0">
<!-- 轮播图(绝对定位叠放,靠 opacity 交叉淡入) -->
<div
v-for="(b, i) in banners"
:key="b.id || i"
v-for="(item, i) in bannerItems"
:key="item.banner.id || i"
class="absolute inset-0 transition-opacity duration-1000 ease-in-out"
:class="i === currentBannerIndex ? 'opacity-100' : 'opacity-0 pointer-events-none'"
>
<!-- 后台配了站内目标(文章详情 / 产品详情)→ 当前页跳转 -->
<NuxtLink
v-if="item.target && !item.target.external"
:to="item.target.href"
class="block w-full h-full"
>
<img
:src="bannerImg(item.banner)"
:alt="item.banner.title || ''"
class="w-full h-full object-cover"
>
</NuxtLink>
<!-- 后台配了外链 → 新标签页打开 -->
<a
v-if="bannerLink(b)"
:href="bannerLink(b)"
v-else-if="item.target"
:href="item.target.href"
target="_blank"
rel="noopener"
class="block w-full h-full"
>
<img
:src="bannerImg(b)"
:alt="b.title || ''"
:src="bannerImg(item.banner)"
:alt="item.banner.title || ''"
class="w-full h-full object-cover"
>
</a>
<!-- 后台未配置跳转 → 纯图,不可点 -->
<img
v-else
:src="bannerImg(b)"
:alt="b.title || ''"
:src="bannerImg(item.banner)"
:alt="item.banner.title || ''"
class="w-full h-full object-cover"
>
</div>
@@ -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<CmsBanner[]>(() => {
/** 是否进入轮播图模式:有可用轮播图且后台开启 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)
+11 -1
View File
@@ -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
/** 排序(升序,兼容旧字段名) */
+55 -13
View File
@@ -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:个别上游/老接口可能直接透传的蛇形字段名。
* 仅当为非空字符串时才返回,否则返回 ''(调用方据此不渲染 <a>,避免空白可点区域)。
* 站外判定:绝对地址(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<CmsBanner> | 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<string, unknown>).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<CmsBanner> | null): string {
if (!banner) return ''
const raw =
(banner as Record<string, unknown>).link
?? (banner as Record<string, unknown>).linkUrl
?? (banner as Record<string, unknown>).link_url
?? ''
return typeof raw === 'string' && raw.trim() ? raw.trim() : ''
return getBannerTarget(banner)?.href ?? ''
}
/**