Files
pingxiangcard/src/pages/index/index.vue
T

731 lines
17 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>
<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>