Files
hfb_sys/frontend/src/features/listings/components/HomeBanner.vue
T
yml2213 8e3283d951 style(home): 全面重构PC首页UI视觉层级与商品卡片布局
- 重构商品卡片为双栏分翼展台,消灭宽屏中间大面积空白
- 去除大粗冗余标题行,以核心资产微卡(哈夫币/保险/币比)作为视觉中心
- 过滤规格胶囊中的重复项(如AWM),与右侧仓库物资展台明确职责分工
- 优化核心资产文字与数字垂直居中对齐,统一沉稳灰阶底色与品牌点睛色
- 调整轮播图Banner高度与业务入口条呼吸感,提升首屏空间利用率与可读性
2026-09-06 18:57:13 +08:00

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>