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

998 lines
34 KiB
Markdown
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.
# 爱上萍乡小程序改版 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**
```bash
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:
```json
{
"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`:
```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:
```html
<text class="user-desc" v-if="userStore.isLogin">欢迎使用湖南12301文旅专区</text>
```
to:
```html
<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**
```bash
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`:
```typescript
/** 景点数据类型 */
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**
```bash
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:
```vue
<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**
```bash
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:
```vue
<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**
```bash
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**
```bash
git add -A
git commit -m "chore: final verification pass for homepage redesign"
```