- 重构商品卡片为双栏分翼展台,消灭宽屏中间大面积空白 - 去除大粗冗余标题行,以核心资产微卡(哈夫币/保险/币比)作为视觉中心 - 过滤规格胶囊中的重复项(如AWM),与右侧仓库物资展台明确职责分工 - 优化核心资产文字与数字垂直居中对齐,统一沉稳灰阶底色与品牌点睛色 - 调整轮播图Banner高度与业务入口条呼吸感,提升首屏空间利用率与可读性
266 lines
5.2 KiB
Vue
266 lines
5.2 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
|
|
interface HomeBannerSlide {
|
|
title?: string
|
|
eyebrow?: string
|
|
pill?: string
|
|
badge?: string
|
|
tone?: string
|
|
image_url?: string
|
|
}
|
|
|
|
interface StatItem {
|
|
label: string
|
|
value: string
|
|
hint?: string
|
|
}
|
|
|
|
interface Props {
|
|
banners: HomeBannerSlide[]
|
|
stats?: StatItem[]
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
const previewVisible = ref(false)
|
|
const previewIndex = ref(0)
|
|
|
|
const previewUrls = computed(() =>
|
|
props.banners
|
|
.map(slide => slide.image_url?.trim() || '')
|
|
.filter((url): url is string => Boolean(url))
|
|
)
|
|
|
|
function openPreview(slide: HomeBannerSlide) {
|
|
const url = slide.image_url?.trim()
|
|
if (!url) return
|
|
const index = previewUrls.value.indexOf(url)
|
|
if (index < 0) return
|
|
previewIndex.value = index
|
|
previewVisible.value = true
|
|
}
|
|
|
|
function closePreview() {
|
|
previewVisible.value = false
|
|
}
|
|
|
|
function hasSlideCopy(slide: HomeBannerSlide) {
|
|
return Boolean(slide.eyebrow?.trim() || slide.title?.trim() || slide.pill?.trim())
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<section class="hero-board">
|
|
<el-carousel height="200px" :interval="3600">
|
|
<el-carousel-item v-for="slide in banners" :key="slide.title || slide.image_url">
|
|
<div
|
|
class="hero-slide"
|
|
:class="[
|
|
`tone-${slide.tone}`,
|
|
{
|
|
'has-image': slide.image_url,
|
|
'has-copy': hasSlideCopy(slide),
|
|
'is-previewable': !!slide.image_url,
|
|
},
|
|
]"
|
|
@click="openPreview(slide)"
|
|
>
|
|
<img
|
|
v-if="slide.image_url"
|
|
:src="slide.image_url"
|
|
:alt="slide.title || slide.eyebrow || '首页轮播'"
|
|
/>
|
|
<div class="hero-copy">
|
|
<span v-if="slide.eyebrow">{{ slide.eyebrow }}</span>
|
|
<h1 v-if="slide.title">{{ slide.title }}</h1>
|
|
<p v-if="slide.pill">{{ slide.pill }}</p>
|
|
</div>
|
|
<em v-if="slide.badge">{{ slide.badge }}</em>
|
|
</div>
|
|
</el-carousel-item>
|
|
</el-carousel>
|
|
|
|
<!-- 右下角半透明悬浮统计标签 -->
|
|
<div v-if="stats && stats.length" class="banner-stat-chips">
|
|
<div v-for="st in stats" :key="st.label" class="stat-pill" :title="st.hint">
|
|
<strong class="stat-pill-val">{{ st.value }}</strong>
|
|
<span class="stat-pill-lbl">{{ st.label }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<el-image-viewer
|
|
v-if="previewVisible"
|
|
:url-list="previewUrls"
|
|
:initial-index="previewIndex"
|
|
teleported
|
|
hide-on-click-modal
|
|
@close="closePreview"
|
|
/>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.hero-board {
|
|
position: relative;
|
|
min-width: 0;
|
|
width: 100%;
|
|
border-radius: 14px;
|
|
overflow: hidden;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.hero-board :deep(.el-carousel) {
|
|
min-width: 0;
|
|
}
|
|
|
|
.hero-board :deep(.el-carousel__container) {
|
|
height: 200px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.hero-board :deep(.el-carousel__item) {
|
|
height: 100%;
|
|
}
|
|
|
|
.hero-slide {
|
|
position: relative;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 24px 36px;
|
|
overflow: hidden;
|
|
color: #17233d;
|
|
background: linear-gradient(135deg, #eff6ff 0%, #ffffff 58%, #fff7ed 100%);
|
|
}
|
|
|
|
.hero-slide.is-previewable {
|
|
cursor: zoom-in;
|
|
}
|
|
|
|
.hero-slide.tone-blue {
|
|
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 56%, #fff7ed 100%);
|
|
}
|
|
|
|
.hero-slide.tone-orange {
|
|
background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%);
|
|
}
|
|
.hero-slide.tone-green {
|
|
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
|
|
}
|
|
|
|
.hero-slide img {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.hero-slide.has-image.has-copy::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(
|
|
90deg,
|
|
rgba(15, 23, 42, 0.74) 0%,
|
|
rgba(15, 23, 42, 0.24) 62%,
|
|
transparent 100%
|
|
);
|
|
}
|
|
|
|
.hero-copy {
|
|
position: relative;
|
|
z-index: 1;
|
|
max-width: 480px;
|
|
}
|
|
|
|
.hero-copy span {
|
|
color: #ff6a00;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.hero-slide.has-image .hero-copy {
|
|
color: #ffffff;
|
|
}
|
|
|
|
.hero-slide.has-image .hero-copy span {
|
|
color: #fed7aa;
|
|
}
|
|
|
|
.hero-copy h1 {
|
|
margin: 6px 0;
|
|
font-size: 22px;
|
|
font-weight: 800;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.hero-copy p {
|
|
display: inline-block;
|
|
margin: 0;
|
|
padding: 3px 10px;
|
|
border-radius: 999px;
|
|
color: #1e293b;
|
|
background: rgba(255, 255, 255, 0.72);
|
|
backdrop-filter: blur(4px);
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.hero-slide em {
|
|
position: absolute;
|
|
top: 14px;
|
|
right: 18px;
|
|
z-index: 1;
|
|
padding: 4px 9px;
|
|
border-radius: 999px;
|
|
color: #ffffff;
|
|
background: #ff6a00;
|
|
font-size: 11px;
|
|
font-style: normal;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* 悬浮微型统计标签栏 */
|
|
.banner-stat-chips {
|
|
position: absolute;
|
|
bottom: 12px;
|
|
right: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
z-index: 3;
|
|
}
|
|
|
|
.stat-pill {
|
|
display: inline-flex;
|
|
align-items: baseline;
|
|
gap: 5px;
|
|
padding: 4px 12px;
|
|
border-radius: 999px;
|
|
background: rgba(15, 23, 42, 0.65);
|
|
backdrop-filter: blur(6px);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
color: #ffffff;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.stat-pill-val {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: #fb923c;
|
|
}
|
|
|
|
.stat-pill-lbl {
|
|
font-size: 11px;
|
|
color: #cbd5e1;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.banner-stat-chips {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|