Files
hjc-web/app/templates/template-04/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

271 lines
10 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="relative overflow-hidden"
:class="bannerMode ? 'group text-white' : 'bg-[#0f0f0f] text-white'"
>
<!-- 模式 B有轮播图 全宽轮播背景 + 文案叠加 -->
<template v-if="bannerMode">
<div class="relative w-full min-h-[520px] lg:min-h-[600px] flex items-center">
<!-- 轮播图背景 -->
<div class="absolute inset-0">
<div
v-for="(b, i) in banners"
:key="b.id || i"
class="absolute inset-0 w-full h-full transition-opacity duration-700 ease-in-out"
:class="i === currentBannerIndex ? 'opacity-100 z-10' : 'opacity-0 z-0'"
>
<img
:src="bannerImg(b)"
:alt="b.title || ''"
class="w-full h-full object-cover"
>
<!-- 黑色遮罩保证文案可读 -->
<div class="absolute inset-0 bg-gradient-to-r from-black/85 via-black/60 to-black/30" />
</div>
</div>
<!-- 文案内容 -->
<div class="relative z-20 w-full">
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-20 lg:py-28">
<div class="max-w-2xl">
<h1
class="text-3xl sm:text-4xl lg:text-5xl font-bold leading-tight mb-6"
data-reveal
>
{{ heroTitle }}
</h1>
<p
class="text-base sm:text-lg text-gray-200 mb-8 leading-relaxed"
data-reveal
data-reveal-delay="100"
>
{{ heroSubtitle }}
</p>
<div class="flex flex-wrap gap-4" data-reveal data-reveal-delay="200">
<button
type="button"
class="inline-flex items-center justify-center px-6 py-3 bg-[#d4af37] text-[#1a1a1a] font-semibold rounded hover:bg-[#e6c35a] transition-colors"
@click="openConsult()"
>
立即咨询
</button>
<NuxtLink
v-if="newsNav"
:to="newsNav.path || '/article'"
class="inline-flex items-center justify-center px-6 py-3 border border-[#d4af37] text-[#d4af37] font-semibold rounded hover:bg-[#d4af37]/10 transition-colors"
>
了解更多
</NuxtLink>
</div>
</div>
</div>
</div>
<!-- 左右箭头 -->
<button
v-if="banners.length > 1"
type="button"
aria-label="上一张"
class="absolute left-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-black/40 hover:bg-black/60 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition z-30"
@click="prev"
>
<svg class="w-5 h-5" 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="banners.length > 1"
type="button"
aria-label="下一张"
class="absolute right-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-black/40 hover:bg-black/60 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition z-30"
@click="next"
>
<svg class="w-5 h-5" 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>
<!-- 指示点 -->
<div
v-if="banners.length > 1"
class="absolute bottom-6 left-1/2 -translate-x-1/2 flex gap-2 z-30"
>
<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 transition"
:class="i === currentBannerIndex ? 'bg-[#d4af37] scale-110' : 'bg-white/50 hover:bg-white/80'"
@click="goTo(i)"
/>
</div>
</div>
</template>
<!-- 模式 A无轮播图 黑金背景 + 文案 + 右侧特性卡片 -->
<template v-else>
<!-- 装饰光晕 -->
<div class="absolute top-0 right-0 w-[500px] h-[500px] bg-[#d4af37]/10 rounded-full blur-[120px] -translate-y-1/3 translate-x-1/4" />
<div class="absolute bottom-0 left-0 w-[400px] h-[400px] bg-[#d4af37]/5 rounded-full blur-[100px] translate-y-1/3 -translate-x-1/4" />
<div class="relative z-10 container mx-auto px-4 sm:px-6 lg:px-8 py-20 lg:py-28">
<div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
<!-- 左侧文案 -->
<div class="max-w-2xl">
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-[#d4af37]/30 bg-[#d4af37]/10 text-[#d4af37] text-sm mb-6" data-reveal>
<span class="w-1.5 h-1.5 rounded-full bg-[#d4af37]" />
专业 · 高效 · 可信赖
</div>
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold leading-tight mb-6" data-reveal data-reveal-delay="100">
{{ heroTitle }}
</h1>
<p class="text-base sm:text-lg text-gray-300 mb-8 leading-relaxed" data-reveal data-reveal-delay="200">
{{ heroSubtitle }}
</p>
<div class="flex flex-wrap gap-4" data-reveal data-reveal-delay="300">
<button
type="button"
class="inline-flex items-center justify-center px-6 py-3 bg-[#d4af37] text-[#1a1a1a] font-semibold rounded hover:bg-[#e6c35a] transition-colors"
@click="openConsult()"
>
立即咨询
</button>
<NuxtLink
v-if="newsNav"
:to="newsNav.path || '/article'"
class="inline-flex items-center justify-center px-6 py-3 border border-[#d4af37] text-[#d4af37] font-semibold rounded hover:bg-[#d4af37]/10 transition-colors"
>
了解更多
</NuxtLink>
</div>
</div>
<!-- 右侧特性卡片 -->
<div class="grid sm:grid-cols-2 gap-4" data-reveal data-reveal-delay="400">
<div
v-for="(f, i) in displayFeatures"
:key="i"
class="bg-[#1a1a1a]/80 border border-[#333] rounded-lg p-5 hover:border-[#d4af37]/50 transition-colors"
:data-reveal-delay="400 + i * 100"
>
<div class="w-10 h-10 rounded-lg bg-[#d4af37]/10 flex items-center justify-center mb-3">
<svg class="w-5 h-5 text-[#d4af37]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<p class="text-white font-medium">{{ f }}</p>
</div>
</div>
</div>
</div>
</template>
</section>
</template>
<script setup lang="ts">
/**
* 模板 4 - Hero 主视觉区(黑金高端风)
*
* 双模式:
* - 有轮播图(/api/banner 返回 ≥1 张):全宽轮播背景 + 文案叠加 + 左右箭头/指示点;
* - 无轮播图:黑金背景 + 左侧文案/CTA + 右侧 4 个特性卡片。
*/
import type { CmsNavigation, CmsBanner, ApiEnvelope, PageResult } from '~/types'
import { ensureFullUrl } from '~/utils/image'
import { getBannerLink } from '~/utils'
const { siteInfo, siteName, navigations, fetchSiteInfo } = useSite()
const { openConsult } = useConsult()
await fetchSiteInfo()
/** 轮播图数据 */
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
}
function bannerImg(b: CmsBanner): string {
const raw = ensureFullUrl(b.image || b.imageUrl || b.pic || b.url || '')
return raw.replace(/resize,w_\d+/, 'resize,w_1920')
}
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(() => {
return siteName.value ? `${siteName.value} · 让企业人才管理更简单` : '专业人力资源服务伙伴'
})
const heroSubtitle = computed(() => {
return siteInfo.value?.comments
|| '提供人才招聘、劳务派遣、人事外包、薪酬社保等一站式人力资源解决方案,助力企业降本增效、专注核心业务。'
})
const newsNav = computed<CmsNavigation | undefined>(() => {
const navs = navigations.value || []
return navs.find((n) => n.model === 'article')
})
const defaultFeatures = [
'人才招聘与配置',
'劳务派遣外包',
'薪酬社保代缴',
'企业培训咨询'
]
const { heroFeatures } = useFeatures()
const displayFeatures = computed<string[]>(() => {
const cfg = heroFeatures.value
if (cfg && cfg.length > 0) return cfg.slice(0, 4)
return defaultFeatures
})
</script>