Files
pingxiangcard/src/pages/index/index.vue
T
jacobxu666andClaude 5f1ab737e9 feat: UI redesign with uv-ui color system and landscape imagery
- Replace all colors with uv-ui design tokens (primary #3c9cff, warning #f9ae3d, etc.)
- Hero section: Wugong Mountain landscape photo with dark overlay
- Super Player card: photo background, large typography, warning-color price/CTA, value comparison (¥198 vs ¥3800+)
- Spots section: landscape thumbnails instead of CSS gradients, warning-color prices
- Profile page: landscape hero header, CSS menu icons, floating card layout
- Card list page: landscape hero header, filter pills, empty state design
- TabBar: rename '入园码' to '旅游卡', Lucide-style PNG icons (home/card/profile)
- Typography: corrected font weights (semibold for prices, medium for labels)
- Remove broken font-loader (lxgw-wen-kai CDN 404)
- Skip wx.getPrivacySetting in devtools to avoid framework timeout
- Replace all inline SVG with CSS-only icons for mini-program compatibility
- Add design system files (styles/, components/, animations)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-25 01:07:21 +08:00

574 lines
13 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: mountain landscape photo -->
<view class="hero animate-fade-in-up">
<image
class="hero-bg"
src="https://images.pexels.com/photos/28594448/pexels-photo-28594448.jpeg?auto=compress&cs=tinysrgb&w=750"
mode="aspectFill"
/>
<view class="hero-overlay" />
<view class="hero-inner">
<text class="hero-title">爱上萍乡</text>
<text class="hero-sub">赣湘之间 · 发现旅途美好</text>
</view>
</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" />
<text class="notice-label">公告</text>
</view>
<swiper class="notice-swiper" vertical autoplay circular :interval="3000" :duration="500">
<swiper-item v-for="item in announcements" :key="item.id">
<text class="notice-text">{{ item.content }}</text>
</swiper-item>
</swiper>
</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">198</text>
</view>
<view class="promo-value-block">
<text class="promo-value-label">景区总价值</text>
<text class="promo-value">¥3800+</text>
</view>
</view>
<text class="promo-desc">40家景区免门票 · 武功山·方特·韶山·滴水洞</text>
<view class="promo-cta">
<text class="cta-label">立即抢购</text>
<view class="cta-arrow" />
</view>
</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 scenicSpots.slice(0, 6)"
:key="spot.id"
class="spot animate-fade-in-up"
:class="`stagger-${Math.min(idx + 4, 10)}`"
@click="goSpot(spot)"
>
<view class="spot-cover">
<image class="spot-img" :src="getSpotImage(spot.id)" 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-free">
<view class="free-dot" />
<text>免门票</text>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { scenicSpots } from '../../data/scenic-spots'
import type { ScenicSpotItem } from '../../data/scenic-spots'
/** 公告数据(mock,后续接 API) */
const announcements = ref([
{ id: 1, content: '赣湘全域旅游年卡限时优惠,工会会员专享¥300/套卡!' },
{ id: 2, content: '超级玩家卡全新上线,40家景区免门票,仅¥198!' },
{ id: 3, content: '武功山金顶景区已开放,持卡可免费入园1次。' },
])
/** 景点山水风景图 */
const spotImages: Record<number, string> = {
1: 'https://images.unsplash.com/photo-1513415564515-763d91423bdd?fm=jpg&q=80&w=400&h=250&fit=crop',
2: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?fm=jpg&q=80&w=400&h=250&fit=crop',
3: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?fm=jpg&q=80&w=400&h=250&fit=crop',
4: 'https://images.unsplash.com/photo-1433086966358-54859d0ed716?fm=jpg&q=80&w=400&h=250&fit=crop',
5: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?fm=jpg&q=80&w=400&h=250&fit=crop',
6: 'https://images.unsplash.com/photo-1501854140801-50d01698950b?fm=jpg&q=80&w=400&h=250&fit=crop',
}
function getSpotImage(id: number): string {
return spotImages[id] || `https://images.unsplash.com/photo-1506905925346-21bda4d32df4?fm=jpg&q=80&w=400&h=250&fit=crop`
}
function goCardDetail() {
uni.navigateTo({ url: '/packages/user/pages/card/list' })
}
function goSpot(spot: ScenicSpotItem) {
uni.showToast({ title: `${spot.name} 详情开发中`, icon: 'none' })
}
function goMore() {
uni.showToast({ title: '景点列表开发中', icon: 'none' })
}
</script>
<style lang="scss" scoped>
/* ============================================
Page
============================================ */
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
background: $color-background;
}
/* ============================================
Hero — mountain landscape photo
============================================ */
.hero {
position: relative;
height: 340rpx;
overflow: hidden;
}
.hero-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.hero-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.45) 0%,
rgba(0, 0, 0, 0.3) 40%,
rgba(0, 0, 0, 0.65) 100%
);
}
.hero-inner {
position: relative;
z-index: 2;
padding: $spacing-16 $spacing-4 $spacing-4;
display: flex;
flex-direction: column;
}
.hero-title {
font-family: $font-family-display;
font-size: $font-size-3xl;
font-weight: $font-weight-bold;
color: #fff;
line-height: $line-height-tight;
margin-bottom: $spacing-1;
text-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.4);
}
.hero-sub {
font-size: $font-size-sm;
color: rgba(255, 255, 255, 0.9);
letter-spacing: 4rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
/* ============================================
CSS-only icons
============================================ */
.notice-pip {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: $color-accent;
flex-shrink: 0;
}
.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;
border-bottom: $border-width solid $color-border;
}
.notice-tag {
display: flex;
align-items: center;
margin-right: $spacing-2;
flex-shrink: 0;
gap: $spacing-1;
}
.notice-label {
font-size: $font-size-xs;
color: $color-accent;
font-weight: $font-weight-medium;
}
.notice-swiper {
flex: 1;
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;
}
/* ============================================
Promo card — landscape photo hero card
============================================ */
.promo-wrap {
padding: $spacing-3;
}
.promo {
position: relative;
border-radius: $radius-xl;
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: $font-weight-semibold;
margin-bottom: $spacing-3;
}
.promo-name {
font-family: $font-family-display;
font-size: 56rpx;
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: $uv-warning;
}
.promo-price {
font-size: 96rpx;
font-weight: $font-weight-bold;
color: $uv-warning;
line-height: 0.85;
letter-spacing: -4rpx;
}
.promo-value-block {
display: flex;
flex-direction: column;
padding-bottom: $spacing-2;
}
.promo-value-label {
font-size: $font-size-xs;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 4rpx;
}
.promo-value {
font-size: $font-size-xl;
color: rgba(255, 255, 255, 0.45);
text-decoration: line-through;
font-weight: $font-weight-medium;
}
.promo-desc {
font-size: $font-size-base;
color: rgba(255, 255, 255, 0.8);
display: block;
margin-bottom: $spacing-4;
line-height: $line-height-relaxed;
}
.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-warning;
transition: transform $transition-fast;
&:active {
transform: scale(0.96);
}
.cta-label {
font-size: $font-size-base;
font-weight: $font-weight-bold;
color: $uv-main-color;
}
.cta-arrow {
width: 10rpx;
height: 10rpx;
border-top: 3rpx solid $uv-main-color;
border-right: 3rpx solid $uv-main-color;
transform: rotate(45deg);
}
}
/* ============================================
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 */
.spots-grid {
display: flex;
flex-wrap: wrap;
gap: $spacing-2;
}
.spot {
width: calc(50% - #{$spacing-1});
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: $font-weight-semibold;
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: $uv-warning;
font-weight: $font-weight-semibold;
}
.spot-free {
display: flex;
align-items: center;
gap: 4rpx;
background: rgba($color-accent, 0.08);
border-radius: $radius-sm;
padding: 4rpx $spacing-1;
text {
font-size: $font-size-xs;
color: $color-accent;
font-weight: $font-weight-medium;
}
}
/* ============================================
Scroll area
============================================ */
.main-scroll {
flex: 1;
}
</style>