998 lines
34 KiB
Markdown
998 lines
34 KiB
Markdown
# 爱上萍乡小程序改版 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"
|
||
```
|