731 lines
17 KiB
Vue
731 lines
17 KiB
Vue
<template>
|
||
<view class="page">
|
||
<!-- Hero: 运营Banner轮播 -->
|
||
<view class="hero animate-fade-in-up" @click="goUnionBuy()">
|
||
<swiper class="hero-swiper" autoplay circular :interval="5000">
|
||
<swiper-item>
|
||
<image
|
||
class="hero-bg"
|
||
src="https://aliyunoss.hntgov.net/image/2026/04/14/d2bf03167e484ba59f7c2d62879ad07f.jpg"
|
||
mode="aspectFill"
|
||
/>
|
||
</swiper-item>
|
||
</swiper>
|
||
</view>
|
||
|
||
<scroll-view scroll-y class="main-scroll">
|
||
<!-- 公告栏 -->
|
||
<view v-if="announcements.length" class="notice animate-fade-in-up stagger-1">
|
||
<view class="notice-tag">
|
||
<view class="notice-pip animate-pulse" :class="`type-${currentNoticeType}`" />
|
||
<text class="notice-label">公告</text>
|
||
</view>
|
||
<swiper
|
||
class="notice-swiper"
|
||
vertical
|
||
:autoplay="swiperAutoplay"
|
||
circular
|
||
:interval="3000"
|
||
:duration="500"
|
||
@change="onNoticeChange"
|
||
@touchstart="onNoticeTouchStart"
|
||
@touchend="onNoticeTouchEnd"
|
||
>
|
||
<swiper-item v-for="item in announcements" :key="item.id">
|
||
<view class="notice-item" @click="openNotice(item)">
|
||
<text class="notice-text">{{ item.content }}</text>
|
||
</view>
|
||
</swiper-item>
|
||
</swiper>
|
||
<view class="notice-more" @click="goMoreNotice">
|
||
<text class="notice-more-text">更多</text>
|
||
<view class="chevron" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 金刚区 -->
|
||
<view class="quick-grid animate-fade-in-up stagger-2">
|
||
<view
|
||
v-for="item in quickActions"
|
||
:key="item.label"
|
||
class="quick-item"
|
||
@click="onQuickAction(item)"
|
||
>
|
||
<image class="quick-icon-img" :src="item.icon" mode="aspectFit" />
|
||
<text class="quick-label">{{ item.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 旅游卡 — 大字报风格 -->
|
||
<view class="promo-wrap animate-fade-in-up stagger-2">
|
||
<view class="promo" @click="goCardDetail()">
|
||
<image
|
||
class="promo-bg"
|
||
src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?fm=jpg&q=80&w=750&h=400&fit=crop"
|
||
mode="aspectFill"
|
||
/>
|
||
<view class="promo-overlay" />
|
||
<view class="promo-content">
|
||
<view class="promo-badge">爱上萍乡小程序专属</view>
|
||
<text class="promo-name">超级玩家卡</text>
|
||
<view class="promo-price-row">
|
||
<view class="promo-price-block">
|
||
<text class="promo-currency">¥</text>
|
||
<text class="promo-price">98</text>
|
||
</view>
|
||
<view class="promo-value-block">
|
||
<text class="promo-value">¥3800+</text>
|
||
<text class="promo-save">立省3600+</text>
|
||
</view>
|
||
</view>
|
||
<text class="promo-desc">42家景区全年免首道门票</text>
|
||
<text class="promo-desc-sub">含武功山/君澜温泉/方特/韶山,法定节假日通用</text>
|
||
<text class="promo-highlight">单次最高可省228元 | 单景区全年可免6次</text>
|
||
<view class="promo-cta">
|
||
<text class="cta-label">立即抢购|专属特惠</text>
|
||
<view class="cta-arrow" />
|
||
</view>
|
||
<text class="promo-trust">已售1.2万+ | 萍乡文旅官方联名卡</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 热门景点 -->
|
||
<view class="spots-section">
|
||
<view class="spots-head animate-fade-in-up stagger-3">
|
||
<text class="spots-title">热门景点</text>
|
||
<view class="spots-more" @click="goMore">
|
||
<text class="spots-more-text">查看全部</text>
|
||
<view class="chevron" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="spots-grid">
|
||
<view
|
||
v-for="(spot, idx) in hotSpots"
|
||
:key="spot.id"
|
||
class="spot animate-fade-in-up"
|
||
:class="`stagger-${Math.min(idx + 4, 10)}`"
|
||
>
|
||
<view class="spot-inner" @click="goSpot(spot)">
|
||
<view class="spot-cover">
|
||
<image class="spot-img" :src="spot.image" mode="aspectFill" />
|
||
<view class="spot-region">
|
||
<view class="pin-dot" />
|
||
<text>{{ spot.region }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="spot-body">
|
||
<text class="spot-name">{{ spot.name }}</text>
|
||
<view class="spot-meta">
|
||
<text class="spot-price">¥{{ spot.price }}</text>
|
||
<view class="spot-benefit">
|
||
<text>{{ spot.benefit }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 公告弹窗 -->
|
||
<AnnouncementModal
|
||
v-model="showNoticeModal"
|
||
:title="noticeModalTitle"
|
||
:body="noticeModalBody"
|
||
/>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed } from 'vue'
|
||
import { mockScenicList } from '@/mock/scenic'
|
||
import { mockNotices } from '@/mock/notice'
|
||
import AnnouncementModal from '../../components/AnnouncementModal.vue'
|
||
|
||
/** 金刚区 */
|
||
const quickActions = [
|
||
{ label: '我要绑卡', icon: '/static/icon/card.png' },
|
||
{ label: '我要预约', icon: '/static/icon/reserve.png' },
|
||
{ label: '我要入园', icon: '/static/icon/check.png' },
|
||
{ label: '常见问题', icon: '/static/icon/faq.png' },
|
||
]
|
||
|
||
function onQuickAction(item: { label: string }) {
|
||
if (item.label === '我要绑卡') {
|
||
uni.navigateTo({ url: '/packages/user/pages/bind' })
|
||
return
|
||
}
|
||
if (item.label === '我要预约') {
|
||
uni.navigateTo({ url: '/packages/content/pages/scenic-list' })
|
||
return
|
||
}
|
||
if (item.label === '我要入园') {
|
||
uni.switchTab({ url: '/pages/qrcode/index' })
|
||
return
|
||
}
|
||
if (item.label === '常见问题') {
|
||
uni.navigateTo({ url: '/packages/content/pages/faq' })
|
||
return
|
||
}
|
||
uni.showToast({ title: `${item.label}功能开发中`, icon: 'none' })
|
||
}
|
||
|
||
/** 公告数据(mock,后续接 API) */
|
||
const noticeTypeMap: Record<number, { type: string; typeLabel: string }> = {
|
||
1: { type: 'important', typeLabel: '重要' },
|
||
2: { type: 'scenic', typeLabel: '景区' },
|
||
3: { type: 'promo', typeLabel: '福利' },
|
||
}
|
||
|
||
const announcements = ref(mockNotices.slice(0, 3).map(n => ({
|
||
...n,
|
||
type: noticeTypeMap[n.id]?.type || 'scenic',
|
||
typeLabel: noticeTypeMap[n.id]?.typeLabel || '公告',
|
||
})))
|
||
|
||
/** 公告轮播状态 */
|
||
const swiperAutoplay = ref(true)
|
||
const currentNoticeIndex = ref(0)
|
||
const currentNoticeType = computed(() => announcements.value[currentNoticeIndex.value]?.type || 'scenic')
|
||
|
||
function onNoticeChange(e: any) { currentNoticeIndex.value = e.detail.current }
|
||
function onNoticeTouchStart() { swiperAutoplay.value = false }
|
||
function onNoticeTouchEnd() { swiperAutoplay.value = true }
|
||
|
||
/** 公告弹窗 */
|
||
const showNoticeModal = ref(false)
|
||
const noticeModalTitle = ref('公告')
|
||
const noticeModalBody = ref('')
|
||
|
||
function openNotice(item: { typeLabel: string; content: string; body?: string }) {
|
||
noticeModalTitle.value = `${item.typeLabel}公告`
|
||
noticeModalBody.value = item.body || item.content
|
||
showNoticeModal.value = true
|
||
}
|
||
|
||
function goMoreNotice() {
|
||
uni.navigateTo({ url: '/packages/content/pages/notice-list' })
|
||
}
|
||
|
||
/** 热门景点数据 */
|
||
const hotSpots = mockScenicList.slice(0, 6).map(s => ({
|
||
id: s.id,
|
||
name: s.name,
|
||
price: s.price,
|
||
benefit: `免门票${s.freeCount}次`,
|
||
region: s.region,
|
||
image: s.image,
|
||
}))
|
||
|
||
function goCardDetail() {
|
||
uni.navigateTo({ url: '/packages/user/pages/buy' })
|
||
}
|
||
|
||
function goUnionBuy() {
|
||
uni.navigateTo({ url: '/packages/user/pages/union-buy' })
|
||
}
|
||
|
||
function goSpot(spot: { id: number }) {
|
||
uni.navigateTo({ url: `/packages/content/pages/scenic-detail?id=${spot.id}` })
|
||
}
|
||
|
||
function goMore() {
|
||
uni.navigateTo({ url: '/packages/content/pages/scenic-list' })
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* ============================================
|
||
Page
|
||
============================================ */
|
||
.page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 100vh;
|
||
background: $color-background;
|
||
}
|
||
|
||
/* ============================================
|
||
Hero — 运营Banner轮播
|
||
============================================ */
|
||
.hero {
|
||
position: relative;
|
||
height: 340rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.hero-swiper {
|
||
width: 100%;
|
||
height: 340rpx;
|
||
}
|
||
|
||
.hero-bg {
|
||
width: 100%;
|
||
height: 340rpx;
|
||
}
|
||
|
||
/* ============================================
|
||
CSS-only icons
|
||
============================================ */
|
||
.notice-pip {
|
||
width: 12rpx;
|
||
height: 12rpx;
|
||
min-height: 12rpx;
|
||
max-height: 12rpx;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
background: $color-accent;
|
||
|
||
&.type-important { background: $uv-error; }
|
||
&.type-scenic { background: #3c9cff; }
|
||
&.type-promo { background: #f9ae3d; }
|
||
}
|
||
|
||
.chevron {
|
||
width: 10rpx;
|
||
height: 10rpx;
|
||
border-top: 2rpx solid $color-muted-foreground;
|
||
border-right: 2rpx solid $color-muted-foreground;
|
||
transform: rotate(45deg);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.pin-dot {
|
||
width: 8rpx;
|
||
height: 8rpx;
|
||
border-radius: 50%;
|
||
background: #fff;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.free-dot {
|
||
width: 8rpx;
|
||
height: 8rpx;
|
||
border-radius: 50%;
|
||
background: $color-accent;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ============================================
|
||
Notice bar
|
||
============================================ */
|
||
.notice {
|
||
display: flex;
|
||
align-items: center;
|
||
background: $color-card;
|
||
padding: $spacing-2 $spacing-3;
|
||
margin: $spacing-2 $spacing-3 0;
|
||
border-radius: 24rpx;
|
||
box-shadow: $shadow-sm;
|
||
}
|
||
|
||
.notice-tag {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-right: $spacing-2;
|
||
flex-shrink: 0;
|
||
gap: $spacing-1;
|
||
height: 40rpx;
|
||
}
|
||
|
||
.notice-label {
|
||
font-size: $font-size-xs;
|
||
color: $color-accent;
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
|
||
.notice-swiper {
|
||
flex: 1;
|
||
height: 40rpx;
|
||
}
|
||
|
||
.notice-item {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 40rpx;
|
||
}
|
||
|
||
.notice-text {
|
||
font-size: $font-size-sm;
|
||
color: $color-muted-foreground;
|
||
line-height: 40rpx;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 1;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
flex: 1;
|
||
}
|
||
|
||
.notice-more {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $spacing-1;
|
||
flex-shrink: 0;
|
||
margin-left: $spacing-2;
|
||
}
|
||
|
||
.notice-more-text {
|
||
font-size: $font-size-xs;
|
||
color: $color-muted-foreground;
|
||
}
|
||
|
||
/* ============================================
|
||
Quick actions — 金刚区
|
||
============================================ */
|
||
.quick-grid {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin: $spacing-3;
|
||
padding: $spacing-3 0;
|
||
background: $color-card;
|
||
border-radius: 24rpx;
|
||
box-shadow: $shadow-sm;
|
||
}
|
||
|
||
.quick-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
transition: transform $transition-fast;
|
||
|
||
&:active {
|
||
transform: scale(0.92);
|
||
}
|
||
}
|
||
|
||
.quick-icon-img {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
}
|
||
|
||
.quick-label {
|
||
font-size: 24rpx;
|
||
color: $color-foreground;
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
|
||
/* ============================================
|
||
Promo card — landscape photo hero card
|
||
============================================ */
|
||
.promo-wrap {
|
||
padding: 0 $spacing-3 $spacing-3;
|
||
}
|
||
|
||
.promo {
|
||
position: relative;
|
||
border-radius: $radius-lg;
|
||
overflow: hidden;
|
||
min-height: 400rpx;
|
||
transition: transform $transition-fast;
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
}
|
||
}
|
||
|
||
.promo-bg {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.promo-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(
|
||
160deg,
|
||
rgba($color-foreground, 0.88) 0%,
|
||
rgba($color-foreground, 0.6) 55%,
|
||
rgba($color-foreground, 0.3) 100%
|
||
);
|
||
}
|
||
|
||
.promo-content {
|
||
position: relative;
|
||
z-index: 2;
|
||
padding: $spacing-5 $spacing-4;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.promo-badge {
|
||
display: inline-block;
|
||
align-self: flex-start;
|
||
background: rgba($color-accent, 0.2);
|
||
border: $border-width solid rgba($color-accent, 0.4);
|
||
border-radius: $radius-full;
|
||
padding: 6rpx $spacing-3;
|
||
font-size: $font-size-sm;
|
||
color: rgba(255, 255, 255, 0.95);
|
||
font-weight: 500;
|
||
margin-bottom: $spacing-3;
|
||
}
|
||
|
||
.promo-name {
|
||
font-family: $font-family-display;
|
||
font-size: 38rpx;
|
||
font-weight: $font-weight-bold;
|
||
color: #fff;
|
||
display: block;
|
||
margin-bottom: $spacing-3;
|
||
text-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.4);
|
||
letter-spacing: 2rpx;
|
||
}
|
||
|
||
.promo-price-row {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: $spacing-4;
|
||
margin-bottom: $spacing-3;
|
||
}
|
||
|
||
.promo-price-block {
|
||
display: flex;
|
||
align-items: baseline;
|
||
}
|
||
|
||
.promo-currency {
|
||
font-size: $font-size-2xl;
|
||
font-weight: $font-weight-bold;
|
||
color: $color-price-highlight;
|
||
}
|
||
|
||
.promo-price {
|
||
font-size: 144rpx;
|
||
font-weight: $font-weight-bold;
|
||
color: $color-price-highlight;
|
||
line-height: 0.85;
|
||
letter-spacing: -4rpx;
|
||
}
|
||
|
||
.promo-value-block {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding-bottom: $spacing-2;
|
||
}
|
||
|
||
.promo-value {
|
||
font-size: $font-size-xl;
|
||
color: rgba(255, 255, 255, 0.45);
|
||
text-decoration: line-through;
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
|
||
.promo-save {
|
||
font-size: $font-size-xs;
|
||
color: $color-price;
|
||
font-weight: $font-weight-bold;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
|
||
.promo-desc {
|
||
font-size: $font-size-base;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
display: block;
|
||
margin-bottom: $spacing-1;
|
||
line-height: $line-height-relaxed;
|
||
}
|
||
|
||
.promo-desc-sub {
|
||
font-size: $font-size-sm;
|
||
color: rgba(255, 255, 255, 0.55);
|
||
display: block;
|
||
margin-bottom: $spacing-2;
|
||
}
|
||
|
||
.promo-highlight {
|
||
font-size: $font-size-sm;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
display: block;
|
||
margin-bottom: $spacing-4;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
|
||
.promo-cta {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: $spacing-2;
|
||
align-self: flex-start;
|
||
padding: $spacing-2 $spacing-8;
|
||
border-radius: $radius-full;
|
||
background: $uv-primary;
|
||
box-shadow: 0 8rpx 24rpx rgba(60, 156, 255, 0.3);
|
||
transition: transform $transition-fast;
|
||
|
||
&:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.cta-label {
|
||
font-size: $font-size-base;
|
||
font-weight: $font-weight-bold;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.cta-arrow {
|
||
width: 10rpx;
|
||
height: 10rpx;
|
||
border-top: 3rpx solid #ffffff;
|
||
border-right: 3rpx solid #ffffff;
|
||
transform: rotate(45deg);
|
||
}
|
||
}
|
||
|
||
.promo-trust {
|
||
font-size: $font-size-xs;
|
||
color: rgba(255, 255, 255, 0.35);
|
||
display: block;
|
||
margin-top: $spacing-3;
|
||
}
|
||
|
||
/* ============================================
|
||
Spots section
|
||
============================================ */
|
||
.spots-section {
|
||
padding: $spacing-3;
|
||
}
|
||
|
||
.spots-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: $spacing-3;
|
||
}
|
||
|
||
.spots-title {
|
||
font-family: $font-family-display;
|
||
font-size: $font-size-xl;
|
||
font-weight: $font-weight-bold;
|
||
color: $color-foreground;
|
||
}
|
||
|
||
.spots-more {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $spacing-1;
|
||
}
|
||
|
||
.spots-more-text {
|
||
font-size: $font-size-sm;
|
||
color: $color-muted-foreground;
|
||
}
|
||
|
||
/* Spots grid — 2 column (避免 gap 兼容问题,用 padding 模拟间距) */
|
||
.spots-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
margin: 0 (-$spacing-1);
|
||
}
|
||
|
||
.spot {
|
||
width: 50%;
|
||
padding: $spacing-1;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.spot-inner {
|
||
background: $color-card;
|
||
border-radius: $radius-lg;
|
||
overflow: hidden;
|
||
box-shadow: $shadow-sm;
|
||
transition: transform $transition-fast, box-shadow $transition-base;
|
||
|
||
&:active {
|
||
transform: scale(0.97);
|
||
box-shadow: $shadow-base;
|
||
}
|
||
}
|
||
|
||
.spot-cover {
|
||
width: 100%;
|
||
height: 220rpx;
|
||
position: relative;
|
||
background: $color-muted;
|
||
}
|
||
|
||
.spot-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.spot-region {
|
||
position: absolute;
|
||
top: $spacing-2;
|
||
left: $spacing-2;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4rpx;
|
||
background: rgba(0, 0, 0, 0.4);
|
||
backdrop-filter: blur(8rpx);
|
||
border-radius: $radius-sm;
|
||
padding: 4rpx $spacing-2;
|
||
|
||
text {
|
||
font-size: $font-size-xs;
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
.spot-body {
|
||
padding: $spacing-2;
|
||
}
|
||
|
||
.spot-name {
|
||
font-size: $font-size-sm;
|
||
font-weight: 500;
|
||
color: $color-foreground;
|
||
display: block;
|
||
margin-bottom: $spacing-1;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.spot-meta {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.spot-price {
|
||
font-size: $font-size-base;
|
||
color: $color-price;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.spot-benefit {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: $uv-primary;
|
||
border-radius: $radius-sm;
|
||
padding: 8rpx 12rpx;
|
||
line-height: 1;
|
||
|
||
text {
|
||
font-size: $font-size-xs;
|
||
color: #fff;
|
||
font-weight: $font-weight-medium;
|
||
line-height: 1;
|
||
}
|
||
}
|
||
|
||
/* ============================================
|
||
Scroll area
|
||
============================================ */
|
||
.main-scroll {
|
||
flex: 1;
|
||
}
|
||
</style>
|