Files
pingxiangcard/docs/superpowers/plans/2026-06-24-homepage-redesign.md
T

34 KiB
Raw Blame History

爱上萍乡小程序改版 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Redesign the mini-program from "湖南12301文旅专区" to "爱上萍乡" — a tourism card sales platform with 3-tab navigation, visual-impact homepage, and QR code entry page.

Architecture: The homepage (pages/index/index.vue) is a single-file Vue 3 SFC rebuilt with 4 sections: hero header, announcement swiper, card promotions, and scenic spots list. The QR code page (pages/qrcode/index.vue) is a new tab-level page with card swiper, expandable info panel, and feature buttons. Static scenic spot data lives in src/data/scenic-spots.ts. All existing API types, request utilities, and store patterns remain unchanged.

Tech Stack: Vue 3 + TypeScript + uni-app (WeChat mini-program) + Pinia + SCSS

Global Constraints

  • App name in all navigation bars: "爱上萍乡"
  • TabBar: exactly 3 items (首页, 入园码, 我的)
  • Homepage card prices: 赣湘全域旅游年卡 ¥300, 超级玩家卡 ¥198
  • ~48 scenic spots (merged deduplicated from both cards), hardcoded in Phase 1
  • QR code page: framework/placeholder only, no real QR generation logic
  • All images use gradient/placeholder until real assets are available
  • Announcements use mock data with API interface reserved
  • uni-app <script setup lang="ts"> + <style scoped> pattern throughout

Task 1: Configuration & Routing

Files:

  • Modify: src/pages.json
  • Create: src/pages/qrcode/index.vue (skeleton)
  • Create: src/static/tabbar/qrcode.png
  • Create: src/static/tabbar/qrcode-active.png
  • Modify: src/pages/profile/index.vue:9

Interfaces:

  • Consumes: nothing

  • Produces: qrcode page route registered at pages/qrcode/index, accessible via uni.switchTab({ url: '/pages/qrcode/index' })

  • Step 1: Copy tabbar icon placeholders

cp src/static/tabbar/home.png src/static/tabbar/qrcode.png
cp src/static/tabbar/home-active.png src/static/tabbar/qrcode-active.png
  • Step 2: Update pages.json

Replace the entire src/pages.json with:

{
  "easycom": {
    "autoscan": true,
    "custom": {
      "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
    }
  },
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "爱上萍乡"
      }
    },
    {
      "path": "pages/qrcode/index",
      "style": {
        "navigationBarTitleText": "入园码"
      }
    },
    {
      "path": "pages/login/login",
      "style": {
        "navigationBarTitleText": "登录"
      }
    },
    {
      "path": "pages/profile/index",
      "style": {
        "navigationBarTitleText": "个人中心"
      }
    }
  ],
  "subPackages": [
    {
      "root": "packages/user",
      "pages": [
        {
          "path": "pages/card/list",
          "style": {
            "navigationBarTitleText": "年卡列表"
          }
        },
        {
          "path": "pages/card/detail",
          "style": {
            "navigationBarTitleText": "年卡详情"
          }
        },
        {
          "path": "pages/card/buy",
          "style": {
            "navigationBarTitleText": "确认购买"
          }
        },
        {
          "path": "pages/card/activate",
          "style": {
            "navigationBarTitleText": "激活年卡"
          }
        },
        {
          "path": "pages/card/qrcode",
          "style": {
            "navigationBarTitleText": "入园码"
          }
        },
        {
          "path": "pages/card/bind",
          "style": {
            "navigationBarTitleText": "绑定年卡"
          }
        }
      ]
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "爱上萍乡",
    "navigationBarBackgroundColor": "#FFFFFF",
    "backgroundColor": "#F5F5F5"
  },
  "tabBar": {
    "color": "#7A7E83",
    "selectedColor": "#4A90D9",
    "borderStyle": "white",
    "backgroundColor": "#ffffff",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "static/tabbar/home.png",
        "selectedIconPath": "static/tabbar/home-active.png"
      },
      {
        "pagePath": "pages/qrcode/index",
        "text": "入园码",
        "iconPath": "static/tabbar/qrcode.png",
        "selectedIconPath": "static/tabbar/qrcode-active.png"
      },
      {
        "pagePath": "pages/profile/index",
        "text": "我的",
        "iconPath": "static/tabbar/profile.png",
        "selectedIconPath": "static/tabbar/profile-active.png"
      }
    ]
  }
}
  • Step 3: Create qrcode page skeleton

Create src/pages/qrcode/index.vue:

<template>
  <view class="page">
    <text>入园码页面(开发中)</text>
  </view>
</template>

<script setup lang="ts">
</script>

<style scoped>
.page { min-height: 100vh; background: #f5f5f5; display: flex; align-items: center; justify-content: center; }
</style>
  • Step 4: Update profile page text

In src/pages/profile/index.vue, change line 9 from:

<text class="user-desc" v-if="userStore.isLogin">欢迎使用湖南12301文旅专区</text>

to:

<text class="user-desc" v-if="userStore.isLogin">欢迎使用爱上萍乡</text>
  • Step 5: Verify build succeeds

Run: cd /Users/jacobxu/Documents/Project/12301-dscp/zhy-mp-test && pnpm run build:mp-weixin Expected: BUILD SUCCESS with no errors. Verify dist/build/mp-weixin/pages/qrcode/index exists in output.

  • Step 6: Commit
git add -A
git commit -m "feat: update tabBar to 3 items, add qrcode page skeleton, rename app"

Task 2: Scenic Spots Data Module

Files:

  • Create: src/data/scenic-spots.ts

Interfaces:

  • Consumes: nothing

  • Produces: ScenicSpotItem interface and scenicSpots array (48 items), imported by homepage as import { scenicSpots } from '../../data/scenic-spots'

  • Step 1: Create scenic spots data file

Create src/data/scenic-spots.ts:

/** 景点数据类型 */
export interface ScenicSpotItem {
  id: number
  name: string
  region: '萍乡' | '长沙' | '株洲' | '湘潭'
  price: number
  image: string
}

/**
 * 合并去重后的景点列表(48个)
 * 来源:赣湘全域旅游年卡(22家) + 超级玩家卡(40家),去重后
 * TODO: Phase 2 改为后端 API 获取
 */
export const scenicSpots: ScenicSpotItem[] = [
  // === 萍乡 (21家) ===
  { id: 1, name: '武功山金顶景区', region: '萍乡', price: 70, image: '' },
  { id: 2, name: '杨岐山孽龙洞景区', region: '萍乡', price: 68, image: '' },
  { id: 3, name: '仙凤三宝景区', region: '萍乡', price: 50, image: '' },
  { id: 4, name: '武功山君澜温泉', region: '萍乡', price: 228, image: '' },
  { id: 5, name: '凯光新天地生态旅游区', region: '萍乡', price: 90, image: '' },
  { id: 6, name: '山猫王国', region: '萍乡', price: 68, image: '' },
  { id: 7, name: '和平农场·哎马乐园', region: '萍乡', price: 79, image: '' },
  { id: 8, name: '花涧里景区', region: '萍乡', price: 80, image: '' },
  { id: 9, name: '玉皇谷景区', region: '萍乡', price: 50, image: '' },
  { id: 10, name: '龙溪秘境', region: '萍乡', price: 30, image: '' },
  { id: 11, name: '南正街唐朝诡事录', region: '萍乡', price: 158, image: '' },
  { id: 12, name: '孽龙洞水上乐园', region: '萍乡', price: 180, image: '' },
  { id: 13, name: '桐花谷', region: '萍乡', price: 50, image: '' },
  { id: 14, name: '赣西野生动物王国', region: '萍乡', price: 160, image: '' },
  { id: 15, name: '武功山外骨骼登山', region: '萍乡', price: 200, image: '' },
  { id: 16, name: '发光登山杖', region: '萍乡', price: 88, image: '' },
  { id: 17, name: '三只猫青年旅社', region: '萍乡', price: 60, image: '' },
  { id: 18, name: '武功山奖牌', region: '萍乡', price: 19.9, image: '' },
  { id: 19, name: 'AI中医', region: '萍乡', price: 125, image: '' },
  // === 长沙 (16家) ===
  { id: 20, name: '长沙方特东方神画', region: '长沙', price: 280, image: '' },
  { id: 21, name: '麦咭乐园红星运动空间站', region: '长沙', price: 278, image: '' },
  { id: 22, name: '宁乡野趣亲子乐园', region: '长沙', price: 88, image: '' },
  { id: 23, name: '长沙乐田派绣乡亲子乐园', region: '长沙', price: 68, image: '' },
  { id: 24, name: '湘东漂流', region: '长沙', price: 168, image: '' },
  { id: 25, name: '长沙黑麋峰森林公园', region: '长沙', price: 27, image: '' },
  { id: 26, name: '湘江游轮', region: '长沙', price: 120, image: '' },
  { id: 27, name: '天心阁', region: '长沙', price: 58, image: '' },
  { id: 28, name: '皇龙峡生态旅游区', region: '长沙', price: 138, image: '' },
  { id: 29, name: '靖港古镇', region: '长沙', price: 80, image: '' },
  { id: 30, name: '西溪磐石大峡谷', region: '长沙', price: 48, image: '' },
  { id: 31, name: '湖南省木偶皮影剧场', region: '长沙', price: 68, image: '' },
  { id: 32, name: '金坑溯溪', region: '长沙', price: 99, image: '' },
  { id: 33, name: '关山古镇', region: '长沙', price: 50, image: '' },
  { id: 34, name: '浏河第一湾', region: '长沙', price: 88, image: '' },
  // === 株洲 (6家) ===
  { id: 35, name: '炎帝陵', region: '株洲', price: 62, image: '' },
  { id: 36, name: '酷洛潮漫(株洲馆)', region: '株洲', price: 118, image: '' },
  { id: 37, name: '神农谷风景旅游区', region: '株洲', price: 62, image: '' },
  { id: 38, name: '云上大院', region: '株洲', price: 50, image: '' },
  { id: 39, name: '云阳山', region: '株洲', price: 45, image: '' },
  { id: 40, name: '酒仙湖', region: '株洲', price: 65, image: '' },
  // === 湘潭 (7家) ===
  { id: 41, name: '盘龙大观园', region: '湘潭', price: 128, image: '' },
  { id: 42, name: '米莱知识宇宙(湘潭万达馆)', region: '湘潭', price: 326, image: '' },
  { id: 43, name: '滴水洞', region: '湘潭', price: 40, image: '' },
  { id: 44, name: '毛泽东纪念园', region: '湘潭', price: 60, image: '' },
  { id: 45, name: '韶峰景区(含缆车)', region: '湘潭', price: 60, image: '' },
  { id: 46, name: '彭德怀纪念馆', region: '湘潭', price: 98, image: '' },
  { id: 47, name: '《最忆韶山冲》演出', region: '湘潭', price: 298, image: '' },
]
  • Step 2: Verify build succeeds

Run: pnpm run build:mp-weixin Expected: BUILD SUCCESS (data file has no side effects, just confirms it compiles).

  • Step 3: Commit
git add src/data/scenic-spots.ts
git commit -m "feat: add scenic spots data module (48 spots, hardcoded)"

Task 3: Homepage Redesign

Files:

  • Modify: src/pages/index/index.vue (complete rewrite)

Interfaces:

  • Consumes: ScenicSpotItem and scenicSpots from ../../data/scenic-spots

  • Produces: nothing (page component, no exports)

  • Step 1: Write the complete homepage

Replace the entire contents of src/pages/index/index.vue with:

<template>
  <view class="page">
    <!-- 头部区域:背景图 + 渐变遮罩 + 标题 + 搜索栏 -->
    <view class="hero">
      <view class="hero-overlay" />
      <view class="hero-content">
        <text class="hero-title">爱上萍乡</text>
        <text class="hero-subtitle">赣湘之间 · 发现旅途美好</text>
        <view class="search-bar" @click="goSearch">
          <text class="search-icon">🔍</text>
          <text class="search-placeholder">搜索景区、活动...</text>
        </view>
      </view>
    </view>

    <scroll-view scroll-y class="scroll-content">
      <!-- 公告栏 -->
      <view class="notice-bar" v-if="announcements.length">
        <view class="notice-label">
          <text class="notice-icon">📢</text>
          <text class="notice-text">公告</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-content">{{ item.content }}</text>
          </swiper-item>
        </swiper>
      </view>

      <!-- 卡片销售宣传 -->
      <view class="card-section">
        <view class="promo-card promo-card-gx" @click="goCardDetail('gx')">
          <view class="promo-card-badge">工会会员专属</view>
          <text class="promo-card-name">赣湘全域旅游年卡</text>
          <view class="promo-card-price-row">
            <text class="promo-card-currency">¥</text>
            <text class="promo-card-price">300</text>
            <text class="promo-card-unit">/套卡(2张)</text>
          </view>
          <view class="promo-card-features">
            <text class="promo-card-feature">✦ 22家景区免门票</text>
            <text class="promo-card-feature">✦ 武功山·方特·君澜温泉</text>
            <text class="promo-card-feature">✦ 覆盖萍乡/长沙/株洲/湘潭</text>
          </view>
          <view class="promo-card-btn">
            <text class="promo-card-btn-text">立即抢购 →</text>
          </view>
        </view>

        <view class="promo-card promo-card-super" @click="goCardDetail('super')">
          <view class="promo-card-badge">大学生·市民专属</view>
          <text class="promo-card-name">超级玩家卡</text>
          <view class="promo-card-price-row">
            <text class="promo-card-currency">¥</text>
            <text class="promo-card-price">198</text>
          </view>
          <view class="promo-card-features">
            <text class="promo-card-feature">✦ 40家景区免门票</text>
            <text class="promo-card-feature">✦ 武功山·方特·韶山·滴水洞</text>
            <text class="promo-card-feature">✦ 含外骨骼登山·青年旅社等</text>
          </view>
          <view class="promo-card-btn">
            <text class="promo-card-btn-text">立即抢购 →</text>
          </view>
        </view>
      </view>

      <!-- 热门景点列表 -->
      <view class="spots-section">
        <view class="spots-header">
          <text class="spots-title">热门景点</text>
          <text class="spots-more">查看全部 ></text>
        </view>
        <view class="spot-card" v-for="spot in scenicSpots" :key="spot.id" @click="goSpot(spot)">
          <view class="spot-image" :style="{ background: getGradient(spot.region) }">
            <view class="spot-region-tag">{{ spot.region }}</view>
          </view>
          <view class="spot-info">
            <text class="spot-name">{{ spot.name }}</text>
            <view class="spot-meta">
              <text class="spot-price">¥{{ spot.price }}</text>
              <view class="spot-free-tag">持卡免门票</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 regionGradients: Record<string, string> = {
  '萍乡': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
  '长沙': 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
  '株洲': 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
  '湘潭': 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
}

function getGradient(region: string): string {
  return regionGradients[region] || regionGradients['萍乡']
}

/** 跳转搜索(暂不实现) */
function goSearch() {
  uni.showToast({ title: '搜索功能开发中', icon: 'none' })
}

/** 跳转卡片详情 */
function goCardDetail(type: 'gx' | 'super') {
  uni.navigateTo({ url: '/packages/user/pages/card/list' })
}

/** 跳转景点详情 */
function goSpot(spot: ScenicSpotItem) {
  uni.showToast({ title: `${spot.name} 详情开发中`, icon: 'none' })
}
</script>

<style scoped>
.page { display: flex; flex-direction: column; min-height: 100vh; background: #f5f5f5; }

/* === 头部区域 === */
.hero { position: relative; height: 360rpx; overflow: hidden; }
.hero-overlay {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  background: linear-gradient(180deg, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.6) 100%);
  z-index: 1;
}
.hero::before {
  content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  background: linear-gradient(135deg, #1a237e 0%, #4a148c 50%, #e65100 100%);
}
.hero-content {
  position: relative; z-index: 2;
  padding: 80rpx 30rpx 30rpx;
  display: flex; flex-direction: column;
}
.hero-title { font-size: 42rpx; font-weight: bold; color: #fff; margin-bottom: 8rpx; }
.hero-subtitle { font-size: 24rpx; color: rgba(255,255,255,0.8); margin-bottom: 24rpx; }
.search-bar {
  display: flex; align-items: center;
  background: rgba(255,255,255,0.95); border-radius: 40rpx;
  padding: 18rpx 24rpx;
}
.search-icon { font-size: 28rpx; margin-right: 12rpx; }
.search-placeholder { font-size: 26rpx; color: #999; }

/* === 公告栏 === */
.notice-bar {
  display: flex; align-items: center;
  background: #fff; padding: 20rpx 24rpx; margin: 0;
  border-bottom: 1rpx solid #f0f0f0;
}
.notice-label { display: flex; align-items: center; margin-right: 16rpx; flex-shrink: 0; }
.notice-icon { font-size: 28rpx; margin-right: 6rpx; }
.notice-text { font-size: 24rpx; color: #E74C3C; font-weight: 500; }
.notice-swiper { flex: 1; height: 40rpx; }
.notice-content { font-size: 24rpx; color: #666; line-height: 40rpx; }

/* === 卡片销售宣传 === */
.card-section { padding: 24rpx; display: flex; flex-direction: column; gap: 24rpx; }

.promo-card {
  position: relative; border-radius: 24rpx; padding: 40rpx 32rpx;
  overflow: hidden;
}
.promo-card-gx {
  background: linear-gradient(135deg, #1a237e 0%, #283593 40%, #ffd54f 100%);
}
.promo-card-super {
  background: linear-gradient(135deg, #e65100 0%, #bf360c 40%, #7b1fa2 100%);
}
.promo-card-badge {
  display: inline-block;
  background: rgba(255,255,255,0.2); border-radius: 20rpx;
  padding: 6rpx 16rpx; margin-bottom: 16rpx;
  font-size: 22rpx; color: rgba(255,255,255,0.9);
}
.promo-card-name { font-size: 34rpx; font-weight: bold; color: #fff; display: block; margin-bottom: 12rpx; }
.promo-card-price-row { display: flex; align-items: baseline; margin-bottom: 20rpx; }
.promo-card-currency { font-size: 32rpx; color: #fff; font-weight: bold; }
.promo-card-price { font-size: 64rpx; color: #fff; font-weight: bold; line-height: 1; }
.promo-card-unit { font-size: 24rpx; color: rgba(255,255,255,0.8); margin-left: 8rpx; }
.promo-card-features { display: flex; flex-direction: column; gap: 8rpx; margin-bottom: 24rpx; }
.promo-card-feature { font-size: 24rpx; color: rgba(255,255,255,0.9); }
.promo-card-btn {
  display: flex; align-items: center; justify-content: center;
  background: #fff; border-radius: 40rpx;
  padding: 16rpx 0;
}
.promo-card-btn-text { font-size: 28rpx; font-weight: bold; color: #333; }

/* === 热门景点列表 === */
.spots-section { padding: 24rpx; }
.spots-header {
  display: flex; justify-content: space-between; align-items: center;
  margin-bottom: 20rpx;
}
.spots-title { font-size: 34rpx; font-weight: bold; color: #333; }
.spots-more { font-size: 24rpx; color: #999; }

.spot-card {
  background: #fff; border-radius: 20rpx; overflow: hidden;
  margin-bottom: 20rpx;
}
.spot-image {
  width: 100%; height: 320rpx; position: relative;
}
.spot-region-tag {
  position: absolute; top: 16rpx; left: 16rpx;
  background: rgba(0,0,0,0.5); border-radius: 16rpx;
  padding: 4rpx 16rpx;
  font-size: 22rpx; color: #fff;
}
.spot-info { padding: 20rpx 24rpx; }
.spot-name { font-size: 30rpx; font-weight: 600; color: #333; margin-bottom: 12rpx; display: block; }
.spot-meta { display: flex; justify-content: space-between; align-items: center; }
.spot-price { font-size: 32rpx; color: #E74C3C; font-weight: bold; }
.spot-free-tag {
  background: #E8F5E9; border-radius: 16rpx;
  padding: 4rpx 16rpx;
  font-size: 22rpx; color: #2E7D32; font-weight: 500;
}

/* === 滚动区域 === */
.scroll-content { flex: 1; }
</style>
  • Step 2: Verify build succeeds

Run: pnpm run build:mp-weixin Expected: BUILD SUCCESS.

  • Step 3: Run type check

Run: pnpm run type-check Expected: No type errors.

  • Step 4: Visual verification (WeChat devtools)

Run: pnpm run dev:mp-weixin

Open WeChat developer tools and import dist/dev/mp-weixin. Verify:

  1. Hero header shows gradient background, "爱上萍乡" title, subtitle, search bar
  2. Announcement bar scrolls vertically through 3 mock items
  3. Two promo cards display with correct gradients, prices (¥300, ¥198), and feature text
  4. Scenic spots list shows all 48 spots with gradient images, region tags, prices, and "持卡免门票" tags
  5. TabBar shows 3 items: 首页, 入园码, 我的
  • Step 5: Commit
git add src/pages/index/index.vue
git commit -m "feat: redesign homepage with hero, announcements, card promos, scenic spots"

Task 4: QR Code Entry Page

Files:

  • Modify: src/pages/qrcode/index.vue (replace skeleton with full implementation)

Interfaces:

  • Consumes: useUserStore from ../../store/user

  • Produces: nothing (page component, no exports)

  • Step 1: Write the complete QR code page

Replace the entire contents of src/pages/qrcode/index.vue with:

<template>
  <view class="page">
    <!-- 未持卡状态 -->
    <view class="empty-state" v-if="!hasCard">
      <text class="empty-icon">🎫</text>
      <text class="empty-text">您还没有旅游卡</text>
      <text class="empty-desc">购买旅游卡后即可使用入园码</text>
      <view class="empty-btn" @click="goBuyCard">
        <text class="empty-btn-text">去购买</text>
      </view>
    </view>

    <!-- 持卡状态 -->
    <scroll-view scroll-y class="scroll-content" v-else>
      <!-- 卡面展示区域(左右滑动切换) -->
      <view class="card-swiper-wrap">
        <swiper
          class="card-swiper"
          :current="currentCardIndex"
          @change="onCardChange"
          previous-margin="20rpx"
          next-margin="20rpx"
        >
          <swiper-item v-for="(card, index) in userCards" :key="card.id">
            <view class="card-face" :style="{ background: card.gradient }">
              <view class="card-face-top">
                <text class="card-face-brand">爱上萍乡</text>
                <image class="card-face-logo" src="/static/logo.png" mode="aspectFit" />
              </view>
              <text class="card-face-name">{{ card.cardName }}</text>
              <view class="card-face-user">
                <image
                  class="card-face-avatar"
                  :src="userStore.userInfo?.avatarUrl || ''"
                  mode="aspectFill"
                />
                <view class="card-face-user-info">
                  <text class="card-face-username">{{ card.userName }}</text>
                  <text class="card-face-expiry">有效期至 {{ card.expireDate }}</text>
                </view>
              </view>
            </view>
          </swiper-item>
        </swiper>
        <!-- 指示点 -->
        <view class="card-dots" v-if="userCards.length > 1">
          <view
            v-for="(card, index) in userCards"
            :key="index"
            class="card-dot"
            :class="{ active: index === currentCardIndex }"
          />
        </view>
      </view>

      <!-- 持卡信息(可展开/收起) -->
      <view class="info-panel" @click="toggleInfo">
        <view class="info-panel-header">
          <text class="info-panel-icon">📋</text>
          <text class="info-panel-title">持卡信息</text>
          <text class="info-panel-arrow">{{ infoExpanded ? '▲' : '▼' }}</text>
        </view>
        <view class="info-panel-body" v-if="infoExpanded">
          <view class="info-row">
            <text class="info-label">证件类型</text>
            <text class="info-value">{{ currentCard.idType }}</text>
          </view>
          <view class="info-row">
            <text class="info-label">证件号码</text>
            <text class="info-value">{{ currentCard.idNumber }}</text>
          </view>
          <view class="info-row">
            <text class="info-label">手机号码</text>
            <text class="info-value">{{ currentCard.phone }}</text>
          </view>
        </view>
      </view>

      <!-- 二维码区域 -->
      <view class="qrcode-section">
        <view class="qrcode-box">
          <view class="qrcode-placeholder">
            <text class="qrcode-placeholder-text">入园码</text>
            <text class="qrcode-placeholder-sub">(二维码占位)</text>
          </view>
        </view>
        <text class="qrcode-countdown">⏱ {{ countdown }}秒后自动刷新</text>
      </view>

      <!-- 功能入口 -->
      <view class="feature-grid">
        <view class="feature-item" @click="onFeature('reserve')">
          <view class="feature-icon-wrap feature-icon-reserve">
            <text class="feature-icon">📅</text>
          </view>
          <text class="feature-label">景区预约</text>
        </view>
        <view class="feature-item" @click="onFeature('history')">
          <view class="feature-icon-wrap feature-icon-history">
            <text class="feature-icon">📋</text>
          </view>
          <text class="feature-label">使用记录</text>
        </view>
        <view class="feature-item" @click="onFeature('spots')">
          <view class="feature-icon-wrap feature-icon-spots">
            <text class="feature-icon">🏔️</text>
          </view>
          <text class="feature-label">权益景区</text>
        </view>
      </view>
    </scroll-view>
  </view>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useUserStore } from '../../store/user'

const userStore = useUserStore()

/** 用户卡片 mock 数据(后续接 API) */
interface UserCardData {
  id: string
  cardName: string
  userName: string
  expireDate: string
  gradient: string
  idType: string
  idNumber: string
  phone: string
}

const userCards = ref<UserCardData[]>([
  {
    id: '1',
    cardName: '赣湘全域旅游年卡',
    userName: '张三',
    expireDate: '2027.06.24',
    gradient: 'linear-gradient(135deg, #1a237e 0%, #283593 40%, #ffd54f 100%)',
    idType: '身份证',
    idNumber: '360***1234',
    phone: '138****5678',
  },
  {
    id: '2',
    cardName: '超级玩家卡',
    userName: '张三',
    expireDate: '2027.06.24',
    gradient: 'linear-gradient(135deg, #e65100 0%, #bf360c 40%, #7b1fa2 100%)',
    idType: '身份证',
    idNumber: '360***1234',
    phone: '138****5678',
  },
])

/** 是否有卡(mock:默认有卡) */
const hasCard = computed(() => userCards.value.length > 0)

/** 当前选中的卡片索引 */
const currentCardIndex = ref(0)

/** 当前卡片数据 */
const currentCard = computed(() => userCards.value[currentCardIndex.value])

/** 持卡信息展开状态 */
const infoExpanded = ref(false)

/** 倒计时(60秒) */
const countdown = ref(60)
let timer: ReturnType<typeof setInterval> | null = null

function onCardChange(e: any) {
  currentCardIndex.value = e.detail.current
  infoExpanded.value = false
  resetCountdown()
}

function toggleInfo() {
  infoExpanded.value = !infoExpanded.value
}

function resetCountdown() {
  countdown.value = 60
}

function startTimer() {
  timer = setInterval(() => {
    countdown.value--
    if (countdown.value <= 0) {
      resetCountdown()
      // TODO: 刷新二维码(Phase 2 接入 API)
    }
  }, 1000)
}

function goBuyCard() {
  uni.navigateTo({ url: '/packages/user/pages/card/list' })
}

function onFeature(type: 'reserve' | 'history' | 'spots') {
  uni.showToast({ title: '功能开发中', icon: 'none' })
}

onMounted(() => {
  startTimer()
})

onUnmounted(() => {
  if (timer) clearInterval(timer)
})
</script>

<style scoped>
.page { min-height: 100vh; background: #f5f5f5; }
.scroll-content { height: 100vh; }

/* === 空状态 === */
.empty-state {
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; min-height: 100vh; padding: 40rpx;
}
.empty-icon { font-size: 120rpx; margin-bottom: 24rpx; }
.empty-text { font-size: 32rpx; font-weight: bold; color: #333; margin-bottom: 12rpx; }
.empty-desc { font-size: 26rpx; color: #999; margin-bottom: 40rpx; }
.empty-btn {
  background: linear-gradient(135deg, #4A90D9, #357ABD);
  border-radius: 44rpx; padding: 20rpx 80rpx;
}
.empty-btn-text { font-size: 30rpx; color: #fff; font-weight: bold; }

/* === 卡面展示区域 === */
.card-swiper-wrap { padding: 30rpx 0 16rpx; }
.card-swiper { height: 420rpx; }

.card-face {
  margin: 0 20rpx; height: 380rpx; border-radius: 24rpx;
  padding: 32rpx; position: relative; overflow: hidden;
  display: flex; flex-direction: column; justify-content: space-between;
}
.card-face-top {
  display: flex; justify-content: space-between; align-items: center;
}
.card-face-brand { font-size: 24rpx; color: rgba(255,255,255,0.8); }
.card-face-logo { width: 48rpx; height: 48rpx; }
.card-face-name { font-size: 32rpx; font-weight: bold; color: #fff; }
.card-face-user { display: flex; align-items: center; }
.card-face-avatar {
  width: 80rpx; height: 80rpx; border-radius: 50%;
  border: 4rpx solid rgba(255,255,255,0.5);
  background: rgba(255,255,255,0.2); margin-right: 16rpx;
}
.card-face-user-info { display: flex; flex-direction: column; }
.card-face-username { font-size: 28rpx; font-weight: bold; color: #fff; }
.card-face-expiry { font-size: 22rpx; color: rgba(255,255,255,0.7); margin-top: 4rpx; }

/* 指示点 */
.card-dots { display: flex; justify-content: center; gap: 12rpx; padding: 12rpx 0; }
.card-dot {
  width: 12rpx; height: 12rpx; border-radius: 50%;
  background: #ddd; transition: all 0.3s;
}
.card-dot.active { background: #4A90D9; width: 24rpx; border-radius: 6rpx; }

/* === 持卡信息 === */
.info-panel {
  margin: 0 24rpx 20rpx; background: #fff;
  border-radius: 16rpx; overflow: hidden;
}
.info-panel-header {
  display: flex; align-items: center; padding: 24rpx;
}
.info-panel-icon { font-size: 28rpx; margin-right: 8rpx; }
.info-panel-title { flex: 1; font-size: 28rpx; color: #333; font-weight: 500; }
.info-panel-arrow { font-size: 24rpx; color: #999; }
.info-panel-body { padding: 0 24rpx 24rpx; }
.info-row {
  display: flex; justify-content: space-between; align-items: center;
  padding: 16rpx 0; border-top: 1rpx solid #f5f5f5;
}
.info-label { font-size: 26rpx; color: #999; }
.info-value { font-size: 26rpx; color: #333; }

/* === 二维码区域 === */
.qrcode-section {
  display: flex; flex-direction: column; align-items: center;
  padding: 20rpx 24rpx 40rpx;
}
.qrcode-box {
  width: 400rpx; height: 400rpx; background: #fff;
  border-radius: 20rpx; display: flex; align-items: center;
  justify-content: center; margin-bottom: 16rpx;
  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.08);
}
.qrcode-placeholder {
  display: flex; flex-direction: column; align-items: center;
}
.qrcode-placeholder-text { font-size: 32rpx; color: #333; font-weight: bold; }
.qrcode-placeholder-sub { font-size: 24rpx; color: #999; margin-top: 8rpx; }
.qrcode-countdown { font-size: 24rpx; color: #999; }

/* === 功能入口 === */
.feature-grid {
  display: flex; justify-content: space-around;
  padding: 20rpx 40rpx 40rpx;
}
.feature-item {
  display: flex; flex-direction: column; align-items: center;
}
.feature-icon-wrap {
  width: 100rpx; height: 100rpx; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  margin-bottom: 12rpx;
}
.feature-icon-reserve { background: #E3F2FD; }
.feature-icon-history { background: #FFF3E0; }
.feature-icon-spots { background: #E8F5E9; }
.feature-icon { font-size: 44rpx; }
.feature-label { font-size: 24rpx; color: #666; }
</style>
  • Step 2: Verify build succeeds

Run: pnpm run build:mp-weixin Expected: BUILD SUCCESS.

  • Step 3: Run type check

Run: pnpm run type-check Expected: No type errors.

  • Step 4: Visual verification (WeChat devtools)

Open WeChat developer tools, navigate to the "入园码" tab. Verify:

  1. Card swiper shows 2 cards with correct gradients, names, user info
  2. Left/right swipe switches between cards with dot indicator updating
  3. "持卡信息" panel toggles expand/collapse on tap
  4. Expanded panel shows: 身份证, 360***1234, 138****5678
  5. QR code placeholder area is centered with countdown text
  6. Countdown decrements from 60 to 0, then resets
  7. 3 feature buttons show with colored circle icons and labels
  8. Tapping any feature button shows "功能开发中" toast
  • Step 5: Commit
git add src/pages/qrcode/index.vue
git commit -m "feat: implement QR code entry page with card swiper, info panel, features"

Task 5: Final Verification & Polish

Files:

  • No new files. Verify all modified files.

  • Step 1: Full build

Run: pnpm run build:mp-weixin Expected: BUILD SUCCESS with zero errors.

  • Step 2: Type check

Run: pnpm run type-check Expected: No type errors.

  • Step 3: End-to-end visual verification

Open WeChat developer tools, import dist/dev/mp-weixin. Go through this checklist:

  1. TabBar: 3 tabs visible (首页, 入园码, 我的), all switchable
  2. 首页:
    • Hero: gradient background, "爱上萍乡" title, "赣湘之间 · 发现旅途美好" subtitle, search bar
    • Announcement: vertical scrolling, 3 items cycling
    • Card 1 (赣湘年卡): blue-gold gradient, ¥300, 3 features, "立即抢购" button
    • Card 2 (超级玩家卡): orange-purple gradient, ¥198, 3 features, "立即抢购" button
    • Scenic spots: 48 spots listed vertically, each with gradient image, region tag, name, price, "持卡免门票" tag
    • Scrolling through all spots is smooth
  3. 入园码:
    • 2 cards in swiper, switchable
    • Expandable info panel works
    • QR placeholder with countdown timer
    • 3 feature buttons display and show toast on tap
  4. 我的:
    • "欢迎使用爱上萍乡" text displayed (not "湖南12301")
    • All menu items still functional
  5. Login: still accessible and functional
  • Step 4: Final commit
git add -A
git commit -m "chore: final verification pass for homepage redesign"