hookehuyr

feat(ActivitiesCover): 集成 map_activity detail API

- 集成 detailAPI 获取活动详情数据
- 添加数据转换函数 transformApiDataToActivityData
- 移除单独的 fetchActivityStatus,统一通过 fetchActivityDetail 获取
- 积分规则改为动态渲染(v-for)
- 支持开发环境使用 mock 数据测试
- 创建测试指南文档

影响文件:
- src/pages/ActivitiesCover/index.vue
- docs/ActivitiesCover-测试指南.md
- .gitignore (添加 .tmp/ 目录)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -40,3 +40,4 @@ coverage/
.claude/
CLAUDE.md
**/CLAUDE.md
.tmp/
......
# ActivitiesCover 页面测试指南
**测试日期**: 2026-02-09
**测试目标**: 验证 map_activity detail API 是否能覆盖 ActivitiesCover 页面的数据需求
---
## 📊 数据覆盖分析
### ✅ API 完全覆盖的字段
| 页面需求 | API 字段 | 数据类型 | 状态 |
|---------|---------|---------|------|
| 活动标题 | `tittle` | string | ✅ 完全匹配 |
| 封面图 | `cover` | string | ✅ 完全匹配 |
| 开始时间 | `begin_date` | string | ✅ 完全匹配 |
| 结束时间 | `end_date` | string | ✅ 完全匹配 |
| 活动是否开始 | `is_begin` | boolean | ✅ 完全匹配 |
| 活动是否结束 | `is_ended` | boolean | ✅ 完全匹配 |
| 首次打卡积分 | `first_checkin_points` | integer | ✅ 完全匹配 |
| 完成打卡积分 | `complete_points` | integer | ✅ 完全匹配 |
| 需要打卡次数 | `required_checkin_count` | integer | ✅ 完全匹配 |
### ⚠️ 需要转换的字段
| 页面需求 | 数据来源 | 转换逻辑 | 状态 |
|---------|---------|---------|------|
| 副标题 (`subtitle`) | 硬编码 | 固定文案 | ✅ 已处理 |
| 日期范围 (`dateRange`) | `begin_date` + `end_date` | 字符串拼接 | ✅ 已处理 |
| 活动描述 (`description`) | API 数据 | 模板生成 | ✅ 已处理 |
| 活动规则 (`rules`) | 积分相关字段 | 数组生成 | ✅ 已处理 |
| 奖励列表 (`rewards`) | 积分相关字段 | 数组生成 | ✅ 已处理 |
---
## 🔧 代码修改说明
### 1. 新增导入
```javascript
import { detailAPI } from '@/api/map_activity'
import { mockMapActivityDetailAPI } from '@/utils/mockData'
```
### 2. 添加环境变量
```javascript
// 开发环境使用 mock 数据,生产环境使用真实 API
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
```
### 3. 数据转换函数
```javascript
/**
* 将 API 数据转换为页面需要的 activityData 格式
* @param {Object} apiData - API 返回的活动详情数据
* @returns {Object} 页面活动数据对象
*/
const transformApiDataToActivityData = (apiData) => {
if (!apiData) return null
// 生成日期范围字符串
const dateRange = `${apiData.begin_date} - ${apiData.end_date}`
// 根据积分规则生成规则描述
const rules = [
`打卡任意1关,视为参与,奖励${apiData.first_checkin_points}积分`,
`打卡任意${apiData.required_checkin_count}关,视为完成,奖励${apiData.complete_points}积分`,
'不需要区分打卡点的先后次序'
]
// 生成奖励描述
const rewards = [
`首次打卡获得${apiData.first_checkin_points}积分`,
`完成${apiData.required_checkin_count}个打卡点获得${apiData.complete_points}积分`,
apiData.discount_title || '打卡点专属优惠'
]
return {
title: apiData.tittle || '活动标题',
subtitle: '探索城市魅力,感受时尚脉搏',
dateRange: dateRange,
posterUrl: apiData.cover || defaultPoster.value,
description: `欢迎参加${apiData.tittle}活动!`,
rules: rules,
rewards: rewards
}
}
```
### 4. 获取活动详情
```javascript
/**
* 获取活动详情
*/
const fetchActivityDetail = async () => {
try {
if (!activityId.value) {
console.warn('[ActivitiesCover] 未提供活动ID,跳过详情获取')
return
}
console.log('[ActivitiesCover] 开始获取活动详情, ID:', activityId.value)
// 根据环境选择真实 API 或 mock API
const response = USE_MOCK_DATA
? await mockMapActivityDetailAPI({ id: activityId.value })
: await detailAPI({ id: activityId.value })
if (response.code === 1 && response.data) {
console.log('[ActivitiesCover] 活动详情获取成功:', response.data)
// 转换 API 数据为页面格式
const transformedData = transformApiDataToActivityData(response.data)
if (transformedData) {
activityData.value = transformedData
// 更新默认海报图
if (response.data.cover) {
defaultPoster.value = response.data.cover
}
// 更新活动状态
activityStatus.value.is_begin = Boolean(response.data.is_begin)
activityStatus.value.is_ended = Boolean(response.data.is_ended)
}
}
} catch (error) {
console.error('[ActivitiesCover] 获取活动详情异常:', error)
}
}
```
### 5. 移除旧逻辑
- ❌ 移除了 `fetchActivityStatus` 函数(不再需要单独获取活动状态)
- ❌ 移除了 `getActivityStatusAPI` 导入
- ✅ 活动状态现在通过 `fetchActivityDetail` 统一获取
### 6. 动态积分规则显示
```vue
<!-- 积分规则说明 - 使用 v-for 动态渲染 -->
<view v-if="activityData.rules && activityData.rules.length" class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4 opacity-90">
<text class="text-blue-500 text-base font-medium block mb-2">积分规则说明:</text>
<text
v-for="(rule, index) in activityData.rules"
:key="index"
class="text-blue-500 text-sm leading-relaxed block mb-1"
style="padding-left: 20rpx; text-indent: -20rpx;"
>
• {{ rule }}
</text>
</view>
```
---
## 🧪 测试步骤
### 方式 1: Mock 数据测试(开发环境)
```bash
# 1. 启动开发服务器
pnpm run dev:weapp
# 2. 打开微信开发者工具,导入 dist 目录
# 3. 访问页面(带活动 ID 参数)
/pages/ActivitiesCover/index?id=1
# 或
/pages/ActivitiesCover/index?activity_id=1
# 4. 检查控制台日志,应该看到:
# [ActivitiesCover] 页面加载, 参数: {id: "1"}
# [ActivitiesCover] 开始获取活动详情, ID: 1
# [Mock] detailAPI - 活动详情,ID:1
# [ActivitiesCover] 活动详情获取成功: {...}
```
**预期结果**:
- ✅ 页面显示活动标题、封面图
- ✅ 日期范围正确显示
- ✅ 积分规则动态显示(不是硬编码)
- ✅ "立即参加" 按钮状态正确(根据活动状态)
### 方式 2: 真实 API 测试(生产环境)
```bash
# 1. 修改环境变量(确保不是开发环境)
# NODE_ENV=production
# 2. 重新构建
pnpm run build:weapp
# 3. 在微信开发者工具中测试
```
**预期结果**:
- ✅ 真实 API 数据正确显示
- ✅ 所有字段正确映射
---
## 📝 Mock 数据示例
### 输入参数
```javascript
{
id: "1"
}
```
### Mock 返回数据
```javascript
{
code: 1,
msg: "success",
data: {
url: "https://example.com/map",
id: "1",
cover: "https://picsum.photos/400/300?random=1",
tittle: "公园晨跑打卡",
begin_date: "2025.01.15",
end_date: "2025.02.28",
is_ended: false,
is_begin: true,
first_checkin_points: 10,
required_checkin_count: 5,
complete_points: 50,
discount_title: "打卡点优惠信息"
}
}
```
### 转换后的页面数据
```javascript
{
title: "公园晨跑打卡",
subtitle: "探索城市魅力,感受时尚脉搏",
dateRange: "2025.01.15 - 2025.02.28",
posterUrl: "https://picsum.photos/400/300?random=1",
description: "欢迎参加公园晨跑打卡活动!",
rules: [
"打卡任意1关,视为参与,奖励10积分",
"打卡任意5关,视为完成,奖励50积分",
"不需要区分打卡点的先后次序"
],
rewards: [
"首次打卡获得10积分",
"完成5个打卡点获得50积分",
"打卡点优惠信息"
]
}
```
---
## ✅ 验收标准
### 功能验收
- [ ] 页面能正确加载活动详情
- [ ] 活动标题正确显示
- [ ] 封面图正确显示
- [ ] 日期范围格式正确
- [ ] 积分规则动态生成并显示
- [ ] 活动状态(已开始/已结束)正确反映在按钮状态上
### 技术验收
- [ ] 开发环境使用 mock 数据
- [ ] 生产环境使用真实 API
- [ ] 数据转换逻辑正确
- [ ] 错误处理完善
- [ ] 控制台日志清晰
---
## 🐛 已知问题
### 1. API 字段拼写问题
**问题描述**: API 返回的字段是 `tittle`(拼写错误),不是 `title`
**解决方案**: 代码中使用 `apiData.tittle`,与 API 保持一致
### 2. 活动 ID 参数
**问题描述**: 可能使用 `id``activity_id` 作为参数名
**解决方案**:
```javascript
// 支持两种参数名
if (options.id) {
activityId.value = options.id
} else if (options.activity_id) {
activityId.value = options.activity_id
} else {
// 默认使用 ID: 1
activityId.value = '1'
}
```
---
## 📚 相关文件
- **页面代码**: `src/pages/ActivitiesCover/index.vue`
- **API 定义**: `src/api/map_activity.js`
- **Mock 数据**: `src/utils/mockData.js`
- **API 文档**: `docs/api-specs/map_activity/detail.md`
---
## 🎯 下一步
1. ✅ 完成功能开发
2. ⏳ 进行真机测试
3. ⏳ 测试不同活动 ID
4. ⏳ 测试边界情况(无网络、API 错误等)
5. ⏳ 优化用户体验
---
**测试人员**: Claude Code
**测试日期**: 2026-02-09
**测试状态**: ✅ 开发完成,等待测试
......@@ -8,11 +8,7 @@
<template>
<view class="activities-cover-container">
<!-- 背景图片 -->
<image
:src="defaultPoster"
class="background-image"
:mode="imageDisplayMode"
/>
<image :src="defaultPoster" class="background-image" :mode="imageDisplayMode" />
<!-- 分享按钮组件 -->
<ShareButton
......@@ -26,14 +22,26 @@
<!-- 底部按钮区域 -->
<view class="bottom-section">
<!-- 积分规则说明 -->
<view class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4 opacity-90">
<view
v-if="activityData.rules && activityData.rules.length"
class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4 opacity-90"
>
<text class="text-blue-500 text-base font-medium block mb-2">积分规则说明:</text>
<text class="text-blue-500 text-sm leading-relaxed block mb-1" style="padding-left: 20rpx; text-indent: -20rpx;">• 打卡任意1关,视为参与,奖励1000积分</text>
<text class="text-blue-500 text-sm leading-relaxed block mb-1" style="padding-left: 20rpx; text-indent: -20rpx;">• 打卡任意7关,视为完成,奖励5000积分</text>
<text class="text-blue-500 text-sm leading-relaxed block mb-1" style="padding-left: 20rpx; text-indent: -20rpx;">• 不需要区分打卡点的先后次序</text>
<text
v-for="(rule, index) in activityData.rules"
:key="index"
class="text-blue-500 text-sm leading-relaxed block mb-1"
style="padding-left: 20rpx; text-indent: -20rpx"
>
• {{ rule }}
</text>
</view>
<!-- 未授权定位提示 - 仅在用户点击参加活动且未授权时显示 -->
<view v-if="showLocationPrompt && !hasLocationAuth && !locationError" class="location-tip" @click="retryGetLocation">
<view
v-if="showLocationPrompt && !hasLocationAuth && !locationError"
class="location-tip"
@click="retryGetLocation"
>
<view class="tip-content">
<view class="tip-icon">📍</view>
<view class="tip-text">点击获取您的位置信息来参与活动</view>
......@@ -42,7 +50,11 @@
</view>
<!-- 位置获取失败提示 -->
<view v-if="hasLocationAuth && locationError" class="location-tip location-error" @click="retryGetLocation">
<view
v-if="hasLocationAuth && locationError"
class="location-tip location-error"
@click="retryGetLocation"
>
<view class="tip-content">
<view class="tip-icon">⚠️</view>
<view class="tip-text">可能是网络问题,获取位置信息失败</view>
......@@ -54,7 +66,9 @@
type="primary"
size="large"
class="join-button"
:color="activityStatus.is_ended || !activityStatus.is_begin ? '#cccccc' : THEME_COLORS.PRIMARY"
:color="
activityStatus.is_ended || !activityStatus.is_begin ? '#cccccc' : THEME_COLORS.PRIMARY
"
:loading="isJoining || activityStatus.loading"
:disabled="activityStatus.is_ended || !activityStatus.is_begin"
@click="checkFamilyStatusAndJoinActivity"
......@@ -66,14 +80,8 @@
<!-- 底部导航 -->
<BottomNav />
<!-- 海报预览弹窗 -->
<nut-popup
v-model:visible="show_post"
position="center"
class="poster-preview-popup"
>
<nut-popup v-model:visible="show_post" position="center" class="poster-preview-popup">
<view class="wrapper">
<view class="preview-area" @click="onClickPost">
<image v-if="posterPath" :src="posterPath" mode="widthFix" />
......@@ -100,24 +108,16 @@
/>
<!-- 位置权限申请弹窗 -->
<nut-dialog
v-model:visible="showLocationDialog"
title="位置权限申请"
>
<nut-dialog v-model:visible="showLocationDialog" title="位置权限申请">
<template #default>
<view class=" text-gray-700 leading-loose text-sm text-left">
<view class="text-gray-700 leading-loose text-sm text-left">
{{ locationContent }}
</view>
</template>
<template #footer>
<nut-row :gutter="10">
<nut-col :span="12">
<nut-button
@click="onLocationCancel"
type="default"
size="normal"
block
>
<nut-button @click="onLocationCancel" type="default" size="normal" block>
暂不授权
</nut-button>
</nut-col>
......@@ -139,60 +139,70 @@
</template>
<script setup>
import { ref, onMounted, computed } from "vue"
import { ref, onMounted, computed } from 'vue'
import Taro, { useLoad } from '@tarojs/taro'
import "./index.less"
import './index.less'
import BottomNav from '../../components/BottomNav.vue'
import PosterBuilder from '../../components/PosterBuilder/index.vue'
import ShareButton from '../../components/ShareButton/index.vue'
// 接口信息
import { getMyFamiliesAPI } from '@/api/family'
import { getActivityStatusAPI } from '@/api/map'
import { detailAPI } from '@/api/map_activity'
import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect'
// 导入主题颜色
import { THEME_COLORS } from '@/utils/config';
import { THEME_COLORS } from '@/utils/config'
// Mock 数据
import { mockMapActivityDetailAPI } from '@/utils/mockData'
// 环境变量:是否使用 mock 数据
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
// 默认海报图
const defaultPoster = 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_8.jpg?imageMogr2/strip/quality/60';
const defaultPoster = ref(
'https://cdn.ipadbiz.cn/lls_prog/images/welcome_8.jpg?imageMogr2/strip/quality/60'
)
// 系统信息
const systemInfo = ref({});
const systemInfo = ref({})
// 活动ID(从 URL 参数获取)
const activityId = ref('')
/**
* 获取系统信息
*/
const getSystemInfo = () => {
try {
const info = Taro.getWindowInfo();
systemInfo.value = info;
const info = Taro.getWindowInfo()
systemInfo.value = info
} catch (error) {
console.error('获取系统信息失败:', error);
console.error('获取系统信息失败:', error)
}
};
}
/**
* 检测是否为 iPad 类型设备
*/
const isTabletDevice = computed(() => {
if (!systemInfo.value.screenWidth) {
return false;
return false
}
const { screenWidth, screenHeight } = systemInfo.value;
const screenRatio = screenWidth / screenHeight;
const { screenWidth, screenHeight } = systemInfo.value
const screenRatio = screenWidth / screenHeight
// iPad 类型设备通常屏幕比例在 0.7-0.8 之间(4:3 约为 0.75)
// 普通手机设备比例通常在 0.4-0.6 之间
return screenRatio > 0.65;
});
return screenRatio > 0.65
})
/**
* 计算图片显示模式
*/
const imageDisplayMode = computed(() => {
// iPad 类型设备使用 widthFix 模式,普通设备使用 aspectFill
return isTabletDevice.value ? 'widthFix' : 'aspectFill';
});
return isTabletDevice.value ? 'widthFix' : 'aspectFill'
})
/**
* 活动海报页面组件
......@@ -204,14 +214,14 @@ const hasLocationAuth = ref(false) // 是否已授权定位
const locationError = ref(false) // 位置获取是否失败
const isJoining = ref(false) // 是否正在加入活动
const userLocation = ref({ lng: null, lat: null }) // 用户位置信息
const hasJoinedFamily = ref(false);
const hasJoinedFamily = ref(false)
const showLocationPrompt = ref(false) // 是否显示定位权限提示
// 活动状态相关
const activityStatus = ref({
is_begin: false, // 活动是否已开始
is_ended: false, // 活动是否已结束
loading: false // 是否正在加载活动状态
loading: false, // 是否正在加载活动状态
})
// Dialog 相关状态
......@@ -219,7 +229,8 @@ const showLocationDialog = ref(false) // 是否显示位置权限申请弹窗
const pendingLocationCallback = ref(null) // 待执行的位置获取回调
// 位置权限申请说明内容
const locationContent = '为了提供更好的活动体验,我们需要获取您的位置信息:验证您是否在活动区域内, 我们承诺严格保护您的位置隐私,仅用于活动相关功能。'
const locationContent =
'为了提供更好的活动体验,我们需要获取您的位置信息:验证您是否在活动区域内, 我们承诺严格保护您的位置隐私,仅用于活动相关功能。'
// 海报生成相关状态
const show_post = ref(false) // 显示海报预览
......@@ -230,13 +241,15 @@ const nickname = ref('老来赛用户') // 用户昵称
// const avatar = ref('https://cdn.ipadbiz.cn/icon/tou@2x.png') // 用户头像
// 保存选项
const actions_save = ref([{
name: '保存至相册'
}])
const actions_save = ref([
{
name: '保存至相册',
},
])
// 海报配置
let base = {}
let qrcode_url = 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png' // Mock二维码
const base = {}
const qrcode_url = 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png' // Mock二维码
// Mock活动数据
const activityData = ref({
......@@ -244,19 +257,20 @@ const activityData = ref({
subtitle: '探索城市魅力,感受时尚脉搏',
dateRange: '2024年1月15日 - 2024年1月31日',
posterUrl: 'https://img.yzcdn.cn/vant/cat.jpeg', // 临时使用示例图片
description: '漫步南京路,感受上海的繁华与历史交融。从外滩到人民广场,体验这座城市独特的魅力和时尚气息。',
description:
'漫步南京路,感受上海的繁华与历史交融。从外滩到人民广场,体验这座城市独特的魅力和时尚气息。',
rules: [
'年满60岁的老年人可参与活动',
'需要在指定时间内完成所有打卡点',
'每个打卡点需上传照片验证',
'完成全部打卡可获得电子勋章和积分奖励'
'完成全部打卡可获得电子勋章和积分奖励',
],
rewards: [
'完成打卡获得500积分',
'获得专属电子勋章',
'有机会获得商户优惠券',
'参与月度积分排行榜'
]
'参与月度积分排行榜',
],
})
// 分享配置
......@@ -295,7 +309,7 @@ const getUserLocation = async (skipAuthCheck = false) => {
// 如果没有授权,先显示数据用途说明
if (hasLocationAuth !== true) {
return new Promise((resolve) => {
return new Promise(resolve => {
pendingLocationCallback.value = resolve
showLocationDialog.value = true
})
......@@ -306,12 +320,12 @@ const getUserLocation = async (skipAuthCheck = false) => {
type: 'gcj02',
altitude: false, // 不需要海拔信息,提高获取速度
isHighAccuracy: true, // 开启高精度定位
highAccuracyExpireTime: 4000 // 高精度定位超时时间
highAccuracyExpireTime: 4000, // 高精度定位超时时间
})
userLocation.value = {
lng: location.longitude,
lat: location.latitude
lat: location.latitude,
}
console.log('获取到用户位置:', userLocation.value)
......@@ -330,11 +344,11 @@ const getUserLocation = async (skipAuthCheck = false) => {
title: '需要位置权限',
content: '参与活动需要获取您的位置信息,请在设置中开启位置权限',
confirmText: '去设置',
success: (res) => {
success: res => {
if (res.confirm) {
Taro.openSetting()
}
}
},
})
} else if (error.errMsg && error.errMsg.includes('timeout')) {
// 定位超时
......@@ -342,7 +356,7 @@ const getUserLocation = async (skipAuthCheck = false) => {
Taro.showToast({
title: '定位超时,请检查网络或GPS',
icon: 'none',
duration: 3000
duration: 3000,
})
} else if (error.errMsg && error.errMsg.includes('fail')) {
// 定位失败,可能是GPS关闭或网络问题
......@@ -351,14 +365,14 @@ const getUserLocation = async (skipAuthCheck = false) => {
title: '定位失败',
content: '请确保已开启GPS定位服务,并检查网络连接是否正常',
showCancel: false,
confirmText: '我知道了'
confirmText: '我知道了',
})
} else {
// 其他未知错误
locationError.value = true
Taro.showToast({
title: '获取位置失败,请重试',
icon: 'none'
icon: 'none',
})
}
......@@ -412,7 +426,7 @@ const checkFamilyStatusAndJoinActivity = async () => {
if (activityStatus.value.is_ended) {
Taro.showToast({
title: '活动已结束',
icon: 'none'
icon: 'none',
})
return
}
......@@ -421,7 +435,7 @@ const checkFamilyStatusAndJoinActivity = async () => {
if (!activityStatus.value.is_begin) {
Taro.showToast({
title: '活动尚未开始,请耐心等待',
icon: 'none'
icon: 'none',
})
return
}
......@@ -433,14 +447,14 @@ const checkFamilyStatusAndJoinActivity = async () => {
content: '没有加入家庭是无法参加活动的',
cancelText: '关闭',
confirmText: '前往加入',
success: (res) => {
success: res => {
if (res.confirm) {
Taro.redirectTo({
url: '/pages/Welcome/index',
});
})
}
},
});
})
return
}
......@@ -459,8 +473,7 @@ const checkFamilyStatusAndJoinActivity = async () => {
// 正常参加活动流程
await handleJoinActivity()
};
}
/**
* 重新获取位置信息
......@@ -473,7 +486,7 @@ const retryGetLocation = async () => {
locationError.value = false
Taro.showToast({
title: '位置获取成功',
icon: 'success'
icon: 'success',
})
}
} catch (error) {
......@@ -489,7 +502,7 @@ const onLocationCancel = () => {
if (pendingLocationCallback.value) {
Taro.showToast({
title: '需要位置权限才能参与活动',
icon: 'none'
icon: 'none',
})
pendingLocationCallback.value(false)
pendingLocationCallback.value = null
......@@ -507,12 +520,12 @@ const onLocationConfirm = async () => {
type: 'gcj02',
altitude: false,
isHighAccuracy: true,
highAccuracyExpireTime: 4000
highAccuracyExpireTime: 4000,
})
userLocation.value = {
lng: location.longitude,
lat: location.latitude
lat: location.latitude,
}
console.log('获取到用户位置:', userLocation.value)
......@@ -567,14 +580,13 @@ const handleJoinActivity = async () => {
// 跳转到Activities页面,并传递位置参数
await Taro.navigateTo({
url: `/pages/Activities/index?current_lng=${userLocation.value.lng}&current_lat=${userLocation.value.lat}`
url: `/pages/Activities/index?current_lng=${userLocation.value.lng}&current_lat=${userLocation.value.lat}`,
})
} catch (error) {
console.error('参加活动失败:', error)
Taro.showToast({
title: '参加活动失败',
icon: 'none'
icon: 'none',
})
} finally {
isJoining.value = false
......@@ -599,21 +611,21 @@ const onShareActivity = () => {
* @returns {Object} 分享配置对象
*/
const onShareAppMessage = () => {
return {
title: '主题路线打卡活动等你参与',
path: addShareFlag('/pages/ActivitiesCover/index'),
success: (res) => {
// 分享成功
},
fail: (err) => {
// 分享失败
}
}
return {
title: '主题路线打卡活动等你参与',
path: addShareFlag('/pages/ActivitiesCover/index'),
success: res => {
// 分享成功
},
fail: err => {
// 分享失败
},
}
}
// 导出分享方法供Taro使用
defineExpose({
onShareAppMessage,
onShareAppMessage,
})
/**
......@@ -644,7 +656,7 @@ const onCancelSave = () => {
/**
* 选择保存方式
*/
const onSelectSave = (item) => {
const onSelectSave = item => {
if (item.name === '保存至相册') {
show_save.value = false
show_post.value = false
......@@ -872,7 +884,7 @@ const onSelectSave = (item) => {
/**
* 海报绘制成功回调
*/
const drawSuccess = (result) => {
const drawSuccess = result => {
console.log('绘制成功', result)
const { tempFilePath, errMsg } = result
if (errMsg === 'canvasToTempFilePath:ok') {
......@@ -883,7 +895,7 @@ const drawSuccess = (result) => {
Taro.showToast({
title: '生成失败,请稍后重试',
icon: 'none',
duration: 2500
duration: 2500,
})
}
}
......@@ -891,13 +903,13 @@ const drawSuccess = (result) => {
/**
* 海报绘制失败回调
*/
const drawFail = (result) => {
const drawFail = result => {
console.log('绘制失败', result)
Taro.hideLoading()
Taro.showToast({
title: '生成失败,请稍后重试',
icon: 'none',
duration: 2500
duration: 2500,
})
}
......@@ -911,20 +923,101 @@ const savePoster = () => {
Taro.showToast({
title: '已保存到相册',
icon: 'success',
duration: 2000
duration: 2000,
})
},
fail() {
Taro.showToast({
title: '保存失败',
icon: 'none',
duration: 2000
duration: 2000,
})
}
},
})
}
/**
* 将 API 数据转换为页面需要的 activityData 格式
* @param {Object} apiData - API 返回的活动详情数据
* @returns {Object} 页面活动数据对象
*/
const transformApiDataToActivityData = apiData => {
if (!apiData) {
return null
}
// 生成日期范围字符串
const dateRange = `${apiData.begin_date} - ${apiData.end_date}`
// 根据积分规则生成规则描述
const rules = [
`打卡任意1关,视为参与,奖励${apiData.first_checkin_points}积分`,
`打卡任意${apiData.required_checkin_count}关,视为完成,奖励${apiData.complete_points}积分`,
'不需要区分打卡点的先后次序',
]
// 生成奖励描述
const rewards = [
`首次打卡获得${apiData.first_checkin_points}积分`,
`完成${apiData.required_checkin_count}个打卡点获得${apiData.complete_points}积分`,
apiData.discount_title || '打卡点专属优惠',
]
return {
title: apiData.tittle || '活动标题',
subtitle: '探索城市魅力,感受时尚脉搏',
dateRange: dateRange,
posterUrl: apiData.cover || defaultPoster.value,
description: `欢迎参加${apiData.tittle}活动!`,
rules: rules,
rewards: rewards,
}
}
/**
* 获取活动详情
*/
const fetchActivityDetail = async () => {
try {
// 如果没有活动ID,不获取详情
if (!activityId.value) {
console.warn('[ActivitiesCover] 未提供活动ID,跳过详情获取')
return
}
console.log('[ActivitiesCover] 开始获取活动详情, ID:', activityId.value)
// 根据环境选择真实 API 或 mock API
const response = USE_MOCK_DATA
? await mockMapActivityDetailAPI({ id: activityId.value })
: await detailAPI({ id: activityId.value })
if (response.code === 1 && response.data) {
console.log('[ActivitiesCover] 活动详情获取成功:', response.data)
// 转换 API 数据为页面格式
const transformedData = transformApiDataToActivityData(response.data)
if (transformedData) {
activityData.value = transformedData
// 更新默认海报图
if (response.data.cover) {
defaultPoster.value = response.data.cover
}
// 更新活动状态
activityStatus.value.is_begin = Boolean(response.data.is_begin)
activityStatus.value.is_ended = Boolean(response.data.is_ended)
}
} else {
console.warn('[ActivitiesCover] 获取活动详情失败:', response.msg)
}
} catch (error) {
console.error('[ActivitiesCover] 获取活动详情异常:', error)
}
}
/**
* 初始化页面数据
*/
const initPageData = async () => {
......@@ -937,56 +1030,38 @@ const initPageData = async () => {
}
}
// 获取活动状态
await fetchActivityStatus()
// 获取活动详情(包含活动状态)
await fetchActivityDetail()
// 检查定位授权状态(不获取位置,只检查权限)
await checkLocationAuth()
}
/**
* 获取活动状态
*/
const fetchActivityStatus = async () => {
try {
activityStatus.value.loading = true
const { code, data } = await getActivityStatusAPI()
if (code === 1 && data) {
activityStatus.value.is_begin = Boolean(data.is_begin)
activityStatus.value.is_ended = Boolean(data.is_ended)
console.log('活动状态:', {
is_begin: activityStatus.value.is_begin ? '已开始' : '未开始',
is_ended: activityStatus.value.is_ended ? '已结束' : '进行中'
})
} else {
console.warn('获取活动状态失败:', data)
// 默认认为活动未开始且未结束,避免影响用户体验
activityStatus.value.is_begin = false
activityStatus.value.is_ended = false
}
} catch (error) {
console.error('获取活动状态异常:', error)
// 默认认为活动未开始且未结束,避免影响用户体验
activityStatus.value.is_begin = false
activityStatus.value.is_ended = false
} finally {
activityStatus.value.loading = false
// 处理页面加载时的授权检查
useLoad(options => {
console.log('[ActivitiesCover] 页面加载, 参数:', options)
// 获取活动 ID(如果有)
if (options.id) {
activityId.value = options.id
} else if (options.activity_id) {
activityId.value = options.activity_id
} else {
// 如果没有活动ID,使用默认ID
activityId.value = '1'
console.warn('[ActivitiesCover] 未提供活动ID,使用默认ID: 1')
}
}
// 处理页面加载时的授权检查
useLoad((options) => {
// 处理分享页面的授权逻辑
handleSharePageAuth(options, () => {
initPageData();
});
});
initPageData()
})
})
// 页面挂载时检查定位授权状态
onMounted(async () => {
// 获取系统信息
getSystemInfo();
getSystemInfo()
initPageData()
})
......@@ -994,6 +1069,6 @@ onMounted(async () => {
<script>
export default {
name: "ActivitiesCover",
};
name: 'ActivitiesCover',
}
</script>
......