feat(home): 替换首页轮播图实现为广告系统驱动

- 移除原有的硬编码轮播图组件和相关样式
- 新增 getAdByCode 方法用于获取广告数据
- 实现解析广告数据的工具函数 parseSlides 和 parsePx
- 集成 useAsyncData 获取 flash 广告数据
- 添加备用图片以确保加载失败时的显示
- 更新页面样式适配新的轮播组件结构
This commit is contained in:
2026-01-29 13:38:58 +08:00
parent 76534dd2de
commit 08134d4598
3 changed files with 197 additions and 163 deletions

View File

@@ -104,3 +104,13 @@ export async function getCmsAd(id: number) {
}
return Promise.reject(new Error(res.data.message));
}
export async function getAdByCode(code: string){
const res = await request.get<ApiResult<CmsAd>>(
MODULES_API_URL + '/cms/cms-ad/getByCode/' + code
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

112
app/components/Carousel.vue Normal file
View File

@@ -0,0 +1,112 @@
<template>
<section class="carousel-wrap">
<div class="carousel-panel" :style="{ height: heightCss }">
<ClientOnly>
<a-carousel
class="carousel"
:autoplay="autoplayEnabled"
:autoplay-speed="autoplaySpeed"
:dots="dotsEnabled"
:effect="effect"
>
<div v-for="it in normalizedItems" :key="it.src" class="carousel-item">
<NuxtLink
v-if="it.href && !isExternal(it.href)"
:to="it.href"
class="carousel-link"
aria-label="carousel-slide"
>
<div class="carousel-bg" :style="{ backgroundImage: `url(${it.src})` }"></div>
</NuxtLink>
<a
v-else-if="it.href"
class="carousel-link"
:href="it.href"
target="_blank"
rel="noopener noreferrer"
aria-label="carousel-slide"
>
<div class="carousel-bg" :style="{ backgroundImage: `url(${it.src})` }"></div>
</a>
<div v-else class="carousel-bg" :style="{ backgroundImage: `url(${it.src})` }"></div>
</div>
</a-carousel>
<template #fallback>
<div class="carousel-bg" :style="{ height: heightCss, backgroundImage: `url(${fallbackSrc})` }"></div>
</template>
</ClientOnly>
</div>
</section>
</template>
<script setup lang="ts">
type CarouselItem = {
src: string
href?: string
}
const props = withDefaults(
defineProps<{
items: CarouselItem[]
height?: number | string
autoplaySpeed?: number
effect?: 'scrollx' | 'fade'
}>(),
{
height: 360,
autoplaySpeed: 4500,
effect: 'fade'
}
)
const heightCss = computed(() => (typeof props.height === 'number' ? `${props.height}px` : props.height))
const normalizedItems = computed(() => (props.items || []).filter((it) => it && typeof it.src === 'string' && it.src))
const autoplayEnabled = computed(() => normalizedItems.value.length > 1)
const dotsEnabled = computed(() => normalizedItems.value.length > 1)
const fallbackSrc = computed(() => normalizedItems.value[0]?.src || '')
function isExternal(href: string) {
return /^https?:\/\//i.test(href)
}
</script>
<style scoped>
.carousel-panel {
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
}
.carousel {
height: 100%;
}
.carousel-item {
height: 100%;
}
.carousel-link {
display: block;
height: 100%;
}
.carousel-bg {
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.carousel :deep(.slick-list),
.carousel :deep(.slick-track),
.carousel :deep(.slick-slide),
.carousel :deep(.slick-slide > div) {
height: 100%;
}
</style>

View File

@@ -1,85 +1,6 @@
<template>
<main class="home">
<section class="mx-auto max-w-screen-xl px-4 py-8">
<div class="grid grid-cols-12 gap-6">
<div class="col-span-12">
<div class="panel hero">
<div class="hero-bg" aria-hidden="true">
<ClientOnly>
<a-carousel class="hero-carousel" autoplay effect="fade" :autoplaySpeed="4500" :dots="false">
<div
v-for="src in heroSlides"
:key="src"
class="hero-slide"
:style="{ backgroundImage: `url(${src})` }"
></div>
</a-carousel>
<template #fallback>
<div class="hero-carousel">
<div class="hero-slide" :style="{ backgroundImage: `url(${heroSlides[0]})` }"></div>
</div>
</template>
</ClientOnly>
</div>
<div class="hero-inner">
<!-- <div class="hero-badge">OFFICIAL</div>-->
<!-- <div class="hero-title">{{ COMPANY.projectName }}</div>-->
<!-- <div class="hero-sub">生物基材料技术研发 · 技术服务 · 食品与农产品流通</div>-->
<!-- <div class="mt-6 flex flex-wrap gap-3">-->
<!-- <a-button type="primary" size="large" @click="navigateTo('/contact')">-->
<!-- <template #icon><PhoneOutlined /></template>-->
<!-- 合作咨询-->
<!-- </a-button>-->
<!-- <a-button size="large" @click="scrollToCompany">-->
<!-- <template #icon><IdcardOutlined /></template>-->
<!-- 工商信息-->
<!-- </a-button>-->
<!-- </div>-->
<!-- <div class="mt-6 flex flex-wrap gap-2">-->
<!-- <a-tag v-for="t in COMPANY.tags" :key="t" color="green">{{ t }}</a-tag>-->
<!-- </div>-->
</div>
</div>
</div>
</div>
<!-- <div class="mt-6 grid grid-cols-12 gap-6">-->
<!-- <div class="col-span-12 lg:col-span-7">-->
<!-- <div class="panel">-->
<!-- <div class="section-pill">-->
<!-- <span class="pill-left">-->
<!-- <AppstoreOutlined />-->
<!-- 业务板块-->
<!-- </span>-->
<!-- <span class="pill-right">SERVICES</span>-->
<!-- </div>-->
<!-- <div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">-->
<!-- <a-card-->
<!-- v-for="s in services"-->
<!-- :key="s.title"-->
<!-- class="guide-card service-card"-->
<!-- :bordered="true"-->
<!-- hoverable-->
<!-- @click="navigateTo('/products')"-->
<!-- >-->
<!-- <a-space>-->
<!-- <a-avatar :size="44" class="guide-icon service-icon">-->
<!-- <component :is="s.icon" />-->
<!-- </a-avatar>-->
<!-- <div>-->
<!-- <div class="guide-title">{{ s.title }}</div>-->
<!-- <div class="guide-desc">{{ s.desc }}</div>-->
<!-- </div>-->
<!-- </a-space>-->
<!-- </a-card>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</section>
<Carousel class="px-4 py-8" :items="flashSlides" :height="flashHeight" />
<section class="banner">
<div class="mx-auto max-w-screen-xl px-4 py-10">
@@ -148,6 +69,8 @@ import {
ShopOutlined
} from '@ant-design/icons-vue'
import { usePageSeo } from '@/composables/usePageSeo'
import { getAdByCode } from '@/api/cms/cmsAd'
import type { CmsAd } from '@/api/cms/cmsAd/model'
import { COMPANY } from '@/config/company'
usePageSeo({
@@ -157,10 +80,78 @@ usePageSeo({
path: '/'
})
const heroSlides = [
'https://file-cloud.yst.com.cn/photo/website/2024/11/28/5a5cc07336224e54a84561c80899bcac.jpg',
'https://oss.wsdns.cn/20260115/75690dea8f064ceda03246b198a7d710.jpg'
]
function parsePx(value?: string) {
if (!value) return undefined
const m = String(value).match(/(\d+)/)
if (!m) return undefined
const n = Number(m[1])
return Number.isFinite(n) ? n : undefined
}
function parseSlides(ad?: CmsAd | null) {
const raw = ad?.imageList
const fallbackHref = ad?.path
function pickString(obj: Record<string, unknown>, keys: string[]) {
for (const k of keys) {
const v = obj[k]
if (typeof v === 'string' && v.trim()) return v
}
return undefined
}
function asArray(val: unknown): unknown[] {
if (!val) return []
if (Array.isArray(val)) return val
if (typeof val === 'string') {
const text = val.trim()
if (!text) return []
try {
const parsed = JSON.parse(text)
return Array.isArray(parsed) ? parsed : [parsed]
} catch {
return text.split(/[,;\n]+/g).map((s) => s.trim()).filter(Boolean)
}
}
if (typeof val === 'object') {
const obj = val as Record<string, unknown>
if (Array.isArray(obj.list)) return obj.list
if (Array.isArray(obj.imageList)) return obj.imageList
if (Array.isArray(obj.images)) return obj.images
return [obj]
}
return []
}
return asArray(raw)
.map((it) => {
if (typeof it === 'string') return { src: it, href: fallbackHref }
if (it && typeof it === 'object') {
const obj = it as Record<string, unknown>
const src = pickString(obj, ['src', 'url', 'image', 'imageUrl', 'img', 'imgUrl', 'pic', 'picUrl'])
const href = pickString(obj, ['href', 'link', 'path', 'to']) ?? fallbackHref
if (!src) return null
return { src, href }
}
return null
})
.filter(Boolean) as Array<{ src: string; href?: string }>
}
const { data: flashAd } = await useAsyncData('cms-ad-flash', () =>
getAdByCode('flash').catch(() => null)
)
console.log(flashAd.value?.imageList,'flashAdflashAdflashAd');
const flashSlides = computed(() => {
const slides = parseSlides(flashAd.value)
return slides.length
? slides
: [{ src: 'https://file-cloud.yst.com.cn/photo/website/2024/11/28/5a5cc07336224e54a84561c80899bcac.jpg' }]
})
const flashHeight = computed(() => parsePx(flashAd.value?.height) ?? 360)
function splitScope(text: string) {
return text
@@ -274,75 +265,6 @@ function scrollToCompany() {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
}
.hero {
position: relative;
min-height: 360px;
}
.hero-bg {
position: absolute;
inset: 0;
z-index: 0;
}
.hero-carousel {
height: 100%;
}
.hero-slide {
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.hero-carousel :deep(.slick-list),
.hero-carousel :deep(.slick-track),
.hero-carousel :deep(.slick-slide),
.hero-carousel :deep(.slick-slide > div) {
height: 100%;
}
.hero-inner {
position: relative;
z-index: 1;
height: 100%;
padding: 28px;
display: flex;
flex-direction: column;
justify-content: center;
}
.hero-badge {
width: fit-content;
padding: 4px 10px;
border-radius: 9999px;
background: rgba(22, 163, 74, 0.12);
color: rgba(21, 128, 61, 0.95);
font-weight: 800;
letter-spacing: 0.12em;
font-size: 12px;
}
.hero-title {
margin-top: 14px;
font-size: 42px;
line-height: 1.1;
font-weight: 900;
color: rgba(0, 0, 0, 0.9);
}
.hero-sub {
margin-top: 12px;
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
line-height: 1.7;
}
.company {
height: 100%;
}
.panel-head {
display: flex;
align-items: center;
@@ -522,14 +444,4 @@ function scrollToCompany() {
color: rgba(0, 0, 0, 0.55);
line-height: 1.6;
}
@media (max-width: 640px) {
.hero-inner {
padding: 22px;
}
.hero-title {
font-size: 34px;
}
}
</style>