Files
hjc-web/app/templates/template-09/components/HeroSection.vue
T
gxwebsoft 2b69686795 feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
2026-09-08 12:13:44 +08:00

154 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<section class="bg-white pt-4 pb-2">
<div class="t9-container">
<div class="relative overflow-hidden group w-full h-[220px] sm:h-[300px] lg:h-[380px] bg-[var(--t9-primary-soft)]">
<!-- 轮播图模式 -->
<template v-if="bannerMode">
<div
v-for="(b, i) in banners"
:key="b.id || i"
class="absolute inset-0 transition-opacity duration-700 ease-in-out"
:class="i === currentBannerIndex ? 'opacity-100' : 'opacity-0 pointer-events-none'"
>
<a v-if="bannerLink(b)" :href="bannerLink(b)" target="_blank" rel="noopener" class="block w-full h-full">
<img :src="bannerImg(b)" :alt="b.title || siteName" class="w-full h-full object-cover">
</a>
<img v-else :src="bannerImg(b)" :alt="b.title || siteName" class="w-full h-full object-cover">
</div>
</template>
<!-- 无图兜底墨绿渐变 + 站点标语 -->
<template v-else>
<div
class="absolute inset-0"
style="background-image: linear-gradient(120deg, #008960 0%, #00a878 100%)"
></div>
<div class="absolute -top-16 -left-10 w-64 h-64 rounded-full bg-white/10 blur-3xl"></div>
<div class="absolute -bottom-20 -right-6 w-72 h-72 rounded-full bg-black/10 blur-3xl"></div>
<div class="relative z-10 h-full flex flex-col items-center justify-center text-center text-white px-6">
<h1 class="text-2xl sm:text-3xl lg:text-4xl font-bold tracking-wide mb-4">{{ heroTitle }}</h1>
<p class="text-sm sm:text-base max-w-2xl text-white/90 t9-clamp-2">{{ heroSubtitle }}</p>
</div>
</template>
<!-- 左右箭头 -->
<button
v-if="bannerMode && banners.length > 1"
type="button" aria-label="上一张"
class="absolute left-0 top-1/2 -translate-y-1/2 w-10 h-16 bg-black/40 hover:bg-black/60 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition z-20"
@click="prev"
>
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
</button>
<button
v-if="bannerMode && banners.length > 1"
type="button" aria-label="下一张"
class="absolute right-0 top-1/2 -translate-y-1/2 w-10 h-16 bg-black/40 hover:bg-black/60 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition z-20"
@click="next"
>
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
</button>
<!-- 指示点右下角对齐原站 .dot 样式 -->
<div
v-if="bannerMode && banners.length > 1"
class="absolute bottom-0 left-0 right-0 h-[30px] bg-[#6c7475]/80 flex items-center justify-end pr-4 gap-1.5 z-20"
>
<button
v-for="(b, i) in banners"
:key="'dot-' + (b.id || i)"
type="button"
:aria-label="'切换到第 ' + (i + 1) + ' '"
class="w-2.5 h-2.5 rounded-full border border-white transition"
:class="i === currentBannerIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/70'"
@click="goTo(i)"
/>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
/**
* 模板 9 - Hero 主视觉区(墨绿文化出版风)
*
* 对齐原站:1200px 定宽、380px 高的 Banner,右下角灰条 + 白色小圆点指示器。
* - 有轮播图:图片走马灯 + 左右箭头 + 底部指示条;
* - 无轮播图:墨绿渐变 + 站点标语兜底。
*/
import type { CmsBanner, ApiEnvelope, PageResult } from '~/types'
import { ensureFullUrl } from '~/utils/image'
import { getBannerLink } from '~/utils'
const { siteInfo, siteName, fetchSiteInfo } = useSite()
await fetchSiteInfo()
/** 轮播图数据(SSR 预取,首屏即有) */
const { data: bannerRes } = await useFetch<ApiEnvelope<PageResult<CmsBanner>> | PageResult<CmsBanner>>('/api/banner', {
query: { position: 'home_slider' }
})
const banners = computed<CmsBanner[]>(() => {
const d = bannerRes.value as (ApiEnvelope<PageResult<CmsBanner>> | PageResult<CmsBanner>) | null
if (!d) return []
const list = ((d as ApiEnvelope<PageResult<CmsBanner>>)?.data?.list
?? (d as PageResult<CmsBanner>)?.list
?? []) as CmsBanner[]
return list
.filter((b) => hasImage(b) && b.deleted !== 1 && b.status !== 2)
.sort((a, b) => (a.sortNum ?? 0) - (b.sortNum ?? 0))
})
/** 是否有轮播图 */
const bannerMode = computed(() => banners.value.length > 0)
/** 当前帧索引 */
const currentBannerIndex = ref(0)
let bannerTimer: ReturnType<typeof setInterval> | null = null
function startAuto() {
if (banners.value.length > 1) {
bannerTimer = setInterval(() => {
currentBannerIndex.value = (currentBannerIndex.value + 1) % banners.value.length
}, 5000)
}
}
function stopAuto() {
if (bannerTimer) { clearInterval(bannerTimer); bannerTimer = null }
}
onMounted(() => { startAuto() })
onBeforeUnmount(() => { stopAuto() })
function prev() {
const n = banners.value.length
currentBannerIndex.value = (currentBannerIndex.value - 1 + n) % n
}
function next() {
const n = banners.value.length
currentBannerIndex.value = (currentBannerIndex.value + 1) % n
}
function goTo(i: number) {
currentBannerIndex.value = i
}
/** 取轮播图地址(兼容 image/imageUrl/pic/url,并提升 OSS 压缩宽度) */
function bannerImg(b: CmsBanner): string {
const raw = ensureFullUrl(b.image || b.imageUrl || b.pic || b.url || '')
return raw.replace(/resize,w_\d+/, 'resize,w_1600')
}
/** 取轮播图跳转链接(统一走 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)
}
const heroTitle = computed(() => siteInfo.value?.slogan || siteName.value || '欢迎来到我们的官网')
const heroSubtitle = computed(() => siteInfo.value?.comments || '专注内容出版与文化传播,用好书连接读者与世界。')
</script>