hookehuyr

feat(ActivitiesDetail): 创建活动详情页面

- 基于 ActivitiesCover 创建新页面
- 完全使用 map_activity.js 新接口(detailAPI)
- 支持从地图活动列表跳转
- 动态渲染积分规则

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -22,19 +22,23 @@
<!-- 底部按钮区域 -->
<view class="bottom-section">
<!-- 积分规则说明 -->
<view
v-if="activityData.rules && activityData.rules.length"
class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4 opacity-90"
>
<view 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"
>• 打卡任意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
>
• {{ rule }}
</text>
</view>
<!-- 未授权定位提示 - 仅在用户点击参加活动且未授权时显示 -->
<view
......@@ -147,27 +151,18 @@ import PosterBuilder from '../../components/PosterBuilder/index.vue'
import ShareButton from '../../components/ShareButton/index.vue'
// 接口信息
import { getMyFamiliesAPI } from '@/api/family'
import { detailAPI } from '@/api/map_activity'
import { getActivityStatusAPI } from '@/api/map'
import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect'
// 导入主题颜色
import { THEME_COLORS } from '@/utils/config'
// Mock 数据
import { mockMapActivityDetailAPI } from '@/utils/mockData'
// 环境变量:是否使用 mock 数据
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
// 默认海报图
const defaultPoster = ref(
const defaultPoster =
'https://cdn.ipadbiz.cn/lls_prog/images/welcome_8.jpg?imageMogr2/strip/quality/60'
)
// 系统信息
const systemInfo = ref({})
// 活动ID(从 URL 参数获取)
const activityId = ref('')
/**
* 获取系统信息
*/
......@@ -937,87 +932,6 @@ const savePoster = () => {
}
/**
* 将 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 () => {
......@@ -1030,28 +944,46 @@ const initPageData = async () => {
}
}
// 获取活动详情(包含活动状态)
await fetchActivityDetail()
// 获取活动状态
await fetchActivityStatus()
// 检查定位授权状态(不获取位置,只检查权限)
await checkLocationAuth()
}
// 处理页面加载时的授权检查
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')
/**
* 获取活动状态
*/
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 => {
// 处理分享页面的授权逻辑
handleSharePageAuth(options, () => {
initPageData()
......
/*
* @Date: 2026-02-09
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-02-09
* @FilePath: /lls_program/src/pages/ActivitiesDetail/index.config.js
* @Description: 活动详情页面配置 - 支持多活动详情展示
*/
export default {
navigationBarTitleText: '活动详情',
enableShareAppMessage: true,
usingComponents: {},
}
.activities-cover-container {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
}
// 背景图片
.background-image {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: calc(100vh - 150rpx); // 减去底部区域的高度,确保背景图不被遮挡
// height: calc(100vh); // 减去底部区域的高度,确保背景图不被遮挡
object-fit: cover;
object-position: top center;
z-index: 1;
}
// 为容器添加背景色,避免下方空白
// .activities-cover-container::before {
// content: '';
// position: absolute;
// top: 0;
// left: 0;
// width: 100%;
// height: 100%;
// background: linear-gradient(180deg, #f0f8ff 0%, #e6f3ff 50%, #ddeeff 100%);
// z-index: 0;
// }
// 分享按钮包装器
.share-button-wrapper {
position: absolute;
top: 40rpx;
right: 40rpx;
z-index: 10;
}
// 底部区域
.bottom-section {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 40rpx;
padding-bottom: 180rpx; // 为底部导航留出空间
// background: linear-gradient(
// transparent 0%,
// rgba(0, 0, 0, 0.1) 20%,
// rgba(0, 0, 0, 0.3) 50%,
// rgba(0, 0, 0, 0.6) 80%,
// rgba(0, 0, 0, 0.8) 100%
// );
// backdrop-filter: blur(30rpx);
// -webkit-backdrop-filter: blur(30rpx);
z-index: 5;
// 增加渐变高度,让过渡更自然
min-height: 300rpx;
}
.location-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24rpx;
background-color: rgba(255, 247, 230, 0.95);
border: 1rpx solid rgba(255, 213, 145, 0.9);
border-radius: 16rpx;
margin-bottom: 32rpx;
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
cursor: pointer;
transition: all 0.3s ease;
box-shadow:
0 4rpx 16rpx rgba(255, 213, 145, 0.3),
0 2rpx 8rpx rgba(0, 0, 0, 0.1);
&:active {
background-color: rgba(255, 247, 230, 0.8);
transform: scale(0.98);
box-shadow:
0 2rpx 8rpx rgba(255, 213, 145, 0.2),
0 1rpx 4rpx rgba(0, 0, 0, 0.1);
}
.tip-content {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8rpx;
}
}
.tip-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.tip-text {
font-size: 26rpx;
color: #d46b08;
font-weight: 500;
}
.tip-retry {
font-size: 22rpx;
color: #1890ff;
text-decoration: underline;
}
.location-error {
background-color: rgba(255, 241, 240, 0.95);
border: 1rpx solid rgba(255, 163, 158, 0.9);
box-shadow:
0 4rpx 16rpx rgba(255, 163, 158, 0.3),
0 2rpx 8rpx rgba(0, 0, 0, 0.1);
&:active {
background-color: rgba(255, 241, 240, 0.8);
box-shadow:
0 2rpx 8rpx rgba(255, 163, 158, 0.2),
0 1rpx 4rpx rgba(0, 0, 0, 0.1);
}
.tip-text {
color: #cf1322;
}
}
.join-button {
width: 100%;
height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
box-shadow:
0 8rpx 32rpx rgba(84, 171, 174, 0.5),
0 4rpx 16rpx rgba(0, 0, 0, 0.2);
backdrop-filter: blur(15rpx);
-webkit-backdrop-filter: blur(15rpx);
border: 1rpx solid rgba(255, 255, 255, 0.2);
&:active {
transform: translateY(2rpx);
box-shadow:
0 4rpx 16rpx rgba(84, 171, 174, 0.4),
0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
&.nut-button--primary {
background: linear-gradient(135deg, rgba(84, 171, 174, 0.95) 0%, rgba(74, 151, 154, 0.95) 100%);
border: 1rpx solid rgba(255, 255, 255, 0.3);
box-shadow:
0 8rpx 32rpx rgba(84, 171, 174, 0.4),
0 4rpx 16rpx rgba(0, 0, 0, 0.2);
color: white;
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.2);
&:active {
transform: translateY(2rpx);
box-shadow:
0 4rpx 16rpx rgba(84, 171, 174, 0.3),
0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
}
// 禁用状态样式 - 覆盖NutUI默认的半透明效果
&.nut-button--disabled {
opacity: 1 !important; // 覆盖默认的透明度
background: #cccccc !important; // 实心灰色背景
color: #fff !important; // 深灰色文字
border: 1rpx solid #ccc !important;
box-shadow: 0 4rpx 16rpx rgba(204, 204, 204, 0.3) !important;
&:active {
transform: none; // 禁用时不响应点击效果
box-shadow: 0 4rpx 16rpx rgba(204, 204, 204, 0.3) !important;
}
}
}
// 弹窗样式
.share-popup {
.nut-popup__content {
border-radius: 24rpx 24rpx 0 0;
padding: 40rpx;
}
}
.share-title {
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 40rpx;
color: #333;
}
.share-options {
display: flex;
justify-content: space-around;
margin-bottom: 40rpx;
}
.share-option {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
border-radius: 12rpx;
&:active {
background-color: #f5f5f5;
}
}
.share-icon {
width: 80rpx;
height: 80rpx;
margin-bottom: 16rpx;
border-radius: 12rpx;
background-color: #1890ff;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 36rpx;
}
.share-text {
font-size: 24rpx;
color: #666;
}
.cancel-button {
width: 100%;
height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
background-color: #f5f5f5;
color: #666;
border: none;
}
// 海报预览弹窗
.poster-preview-popup {
.nut-popup__content {
width: 90%;
max-width: 600rpx;
border-radius: 24rpx;
padding: 40rpx;
background-color: white;
}
}
.poster-preview {
width: 100%;
border-radius: 12rpx;
margin-bottom: 40rpx;
}
.preview-actions {
display: flex;
gap: 20rpx;
}
.preview-button {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
&.primary {
background-color: #1890ff;
color: white;
border: none;
}
&.secondary {
background-color: #f5f5f5;
color: #666;
border: none;
}
}
<!--
* @Date: 2026-02-09
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-02-09
* @FilePath: /lls_program/src/pages/ActivitiesDetail/index.vue
* @Description: 活动详情页面 - 完全使用 map_activity.js 新接口,支持多活动详情展示
-->
<template>
<view class="activities-cover-container">
<!-- 背景图片 -->
<image :src="defaultPoster" class="background-image" :mode="imageDisplayMode" />
<!-- 分享按钮组件 -->
<ShareButton
:activity-data="activityData"
:share-config="shareConfig"
@share-activity="onShareActivity"
@share-poster="onSharePoster"
class="share-button-wrapper"
/>
<!-- 底部按钮区域 -->
<view class="bottom-section">
<!-- 积分规则说明 -->
<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>
<!-- 未授权定位提示 - 仅在用户点击参加活动且未授权时显示 -->
<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>
</view>
<!-- <view class="tip-retry">点击重新获取</view> -->
</view>
<!-- 位置获取失败提示 -->
<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>
</view>
<!-- <view class="tip-retry">点击重新获取</view> -->
</view>
<nut-button
type="primary"
size="large"
class="join-button"
:color="
activityStatus.is_ended || !activityStatus.is_begin ? '#cccccc' : THEME_COLORS.PRIMARY
"
:loading="isJoining || activityStatus.loading"
:disabled="activityStatus.is_ended || !activityStatus.is_begin"
@click="checkFamilyStatusAndJoinActivity"
>
{{ getButtonText() }}
</nut-button>
</view>
<!-- 底部导航 -->
<BottomNav />
<!-- 海报预览弹窗 -->
<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" />
</view>
</view>
</nut-popup>
<!-- 海报生成组件 -->
<PosterBuilder
v-if="startDraw"
custom-style="position: fixed; left: 200%;"
:config="base"
@success="drawSuccess"
@fail="drawFail"
/>
<!-- 保存选项弹窗 -->
<nut-action-sheet
v-model:visible="show_save"
:menu-items="actions_save"
@choose="onSelectSave"
@cancel="onCancelSave"
cancel-txt="取消"
/>
<!-- 位置权限申请弹窗 -->
<nut-dialog v-model:visible="showLocationDialog" title="位置权限申请">
<template #default>
<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>
</nut-col>
<nut-col :span="12">
<nut-button
@click="onLocationConfirm"
type="primary"
size="normal"
:color="THEME_COLORS.PRIMARY"
block
>
同意授权
</nut-button>
</nut-col>
</nut-row>
</template>
</nut-dialog>
</view>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import Taro, { useLoad } from '@tarojs/taro'
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 { detailAPI } from '@/api/map_activity'
import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect'
// 导入主题颜色
import { THEME_COLORS } from '@/utils/config'
// Mock 数据
import { mockMapActivityDetailAPI } from '@/utils/mockData'
// 环境变量:是否使用 mock 数据
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
// 默认海报图
const defaultPoster = ref(
'https://cdn.ipadbiz.cn/lls_prog/images/welcome_8.jpg?imageMogr2/strip/quality/60'
)
// 系统信息
const systemInfo = ref({})
// 活动ID(从 URL 参数获取)
const activityId = ref('')
/**
* 获取系统信息
*/
const getSystemInfo = () => {
try {
const info = Taro.getWindowInfo()
systemInfo.value = info
} catch (error) {
console.error('获取系统信息失败:', error)
}
}
/**
* 检测是否为 iPad 类型设备
*/
const isTabletDevice = computed(() => {
if (!systemInfo.value.screenWidth) {
return false
}
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
})
/**
* 计算图片显示模式
*/
const imageDisplayMode = computed(() => {
// iPad 类型设备使用 widthFix 模式,普通设备使用 aspectFill
return isTabletDevice.value ? 'widthFix' : 'aspectFill'
})
/**
* 活动海报页面组件
* 功能:展示活动信息、处理定位授权、跳转到活动页面
*/
// 页面状态
const hasLocationAuth = ref(false) // 是否已授权定位
const locationError = ref(false) // 位置获取是否失败
const isJoining = ref(false) // 是否正在加入活动
const userLocation = ref({ lng: null, lat: null }) // 用户位置信息
const hasJoinedFamily = ref(false)
const showLocationPrompt = ref(false) // 是否显示定位权限提示
// 活动状态相关
const activityStatus = ref({
is_begin: false, // 活动是否已开始
is_ended: false, // 活动是否已结束
loading: false, // 是否正在加载活动状态
})
// Dialog 相关状态
const showLocationDialog = ref(false) // 是否显示位置权限申请弹窗
const pendingLocationCallback = ref(null) // 待执行的位置获取回调
// 位置权限申请说明内容
const locationContent =
'为了提供更好的活动体验,我们需要获取您的位置信息:验证您是否在活动区域内, 我们承诺严格保护您的位置隐私,仅用于活动相关功能。'
// 海报生成相关状态
const show_post = ref(false) // 显示海报预览
const show_save = ref(false) // 显示保存弹窗
const startDraw = ref(false) // 开始绘制海报
const posterPath = ref('') // 海报路径
const nickname = ref('老来赛用户') // 用户昵称
// const avatar = ref('https://cdn.ipadbiz.cn/icon/tou@2x.png') // 用户头像
// 保存选项
const actions_save = ref([
{
name: '保存至相册',
},
])
// 海报配置
const base = {}
const qrcode_url = 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png' // Mock二维码
// Mock活动数据
const activityData = ref({
title: '南京路商圈时尚Citywalk',
subtitle: '探索城市魅力,感受时尚脉搏',
dateRange: '2024年1月15日 - 2024年1月31日',
posterUrl: 'https://img.yzcdn.cn/vant/cat.jpeg', // 临时使用示例图片
description:
'漫步南京路,感受上海的繁华与历史交融。从外滩到人民广场,体验这座城市独特的魅力和时尚气息。',
rules: [
'年满60岁的老年人可参与活动',
'需要在指定时间内完成所有打卡点',
'每个打卡点需上传照片验证',
'完成全部打卡可获得电子勋章和积分奖励',
],
rewards: [
'完成打卡获得500积分',
'获得专属电子勋章',
'有机会获得商户优惠券',
'参与月度积分排行榜',
],
})
// 分享配置
const shareConfig = ref({
title: '主题路线打卡活动等你参与',
path: '/pages/ActivitiesCover/index',
imageUrl: '',
})
/**
* 检查定位授权状态
*/
const checkLocationAuth = async () => {
try {
const authSetting = await Taro.getSetting()
hasLocationAuth.value = authSetting.authSetting['scope.userLocation'] === true
console.log('定位授权状态:', hasLocationAuth.value)
} catch (error) {
console.error('检查定位授权失败:', error)
hasLocationAuth.value = false
}
}
/**
* 获取用户位置信息
* @param {boolean} skipAuthCheck - 是否跳过授权检查(当调用方已经检查过授权状态时)
*/
const getUserLocation = async (skipAuthCheck = false) => {
try {
locationError.value = false // 重置错误状态
// 如果没有跳过授权检查,则检查权限状态
if (!skipAuthCheck) {
const authSetting = await Taro.getSetting()
const hasLocationAuth = authSetting.authSetting['scope.userLocation']
// 如果没有授权,先显示数据用途说明
if (hasLocationAuth !== true) {
return new Promise(resolve => {
pendingLocationCallback.value = resolve
showLocationDialog.value = true
})
}
}
const location = await Taro.getLocation({
type: 'gcj02',
altitude: false, // 不需要海拔信息,提高获取速度
isHighAccuracy: true, // 开启高精度定位
highAccuracyExpireTime: 4000, // 高精度定位超时时间
})
userLocation.value = {
lng: location.longitude,
lat: location.latitude,
}
console.log('获取到用户位置:', userLocation.value)
// 获取位置成功后隐藏提示
showLocationPrompt.value = false
hasLocationAuth.value = true
return true
} catch (error) {
console.error('获取位置失败:', error)
if (error.errMsg && error.errMsg.includes('auth deny')) {
// 用户拒绝授权,引导用户手动开启
hasLocationAuth.value = false
locationError.value = false
await Taro.showModal({
title: '需要位置权限',
content: '参与活动需要获取您的位置信息,请在设置中开启位置权限',
confirmText: '去设置',
success: res => {
if (res.confirm) {
Taro.openSetting()
}
},
})
} else if (error.errMsg && error.errMsg.includes('timeout')) {
// 定位超时
locationError.value = true
Taro.showToast({
title: '定位超时,请检查网络或GPS',
icon: 'none',
duration: 3000,
})
} else if (error.errMsg && error.errMsg.includes('fail')) {
// 定位失败,可能是GPS关闭或网络问题
locationError.value = true
await Taro.showModal({
title: '定位失败',
content: '请确保已开启GPS定位服务,并检查网络连接是否正常',
showCancel: false,
confirmText: '我知道了',
})
} else {
// 其他未知错误
locationError.value = true
Taro.showToast({
title: '获取位置失败,请重试',
icon: 'none',
})
}
return false
}
}
/**
* 获取按钮显示文本
*/
const getButtonText = () => {
// 如果活动已结束,显示"活动已结束"
if (activityStatus.value.is_ended) {
return '活动已结束'
}
// 如果活动未开始,显示"活动未开始"
if (!activityStatus.value.is_begin) {
return '活动未开始'
}
// 如果没有加入家庭,显示"加入家庭"
if (!hasJoinedFamily.value) {
return '立即参加'
}
// 如果位置获取失败,显示"重新定位"
if (locationError.value) {
return '重新定位'
}
// 如果需要显示位置提示,说明需要授权
if (showLocationPrompt.value) {
return '授权定位'
}
// 如果已有定位授权且有位置信息,显示"进入活动"
if (hasLocationAuth.value && userLocation.value.lng && userLocation.value.lat) {
return '立即进入'
}
// 默认显示"立即参加"
return '立即参加'
}
/**
* 检查用户是否加入家庭并处理参加活动按钮点击
*/
const checkFamilyStatusAndJoinActivity = async () => {
// 如果活动已结束,显示提示
if (activityStatus.value.is_ended) {
Taro.showToast({
title: '活动已结束',
icon: 'none',
})
return
}
// 如果活动未开始,显示提示
if (!activityStatus.value.is_begin) {
Taro.showToast({
title: '活动尚未开始,请耐心等待',
icon: 'none',
})
return
}
// 如果没有加入家庭,引导用户加入家庭
if (!hasJoinedFamily.value) {
Taro.showModal({
title: '提示',
content: '没有加入家庭是无法参加活动的',
cancelText: '关闭',
confirmText: '前往加入',
success: res => {
if (res.confirm) {
Taro.redirectTo({
url: '/pages/Welcome/index',
})
}
},
})
return
}
// 如果位置获取失败,重新获取位置
if (locationError.value) {
await retryGetLocation()
return
}
// 如果需要显示位置提示,说明需要授权
if (showLocationPrompt.value) {
showLocationPrompt.value = false // 隐藏提示,直接尝试获取位置
await handleJoinActivity()
return
}
// 正常参加活动流程
await handleJoinActivity()
}
/**
* 重新获取位置信息
*/
const retryGetLocation = async () => {
try {
const success = await getUserLocation(false) // 不跳过授权检查,重新处理授权逻辑
if (success) {
hasLocationAuth.value = true
locationError.value = false
Taro.showToast({
title: '位置获取成功',
icon: 'success',
})
}
} catch (error) {
console.error('重新获取位置失败:', error)
}
}
/**
* 位置权限申请弹窗 - 取消操作
*/
const onLocationCancel = () => {
showLocationDialog.value = false
if (pendingLocationCallback.value) {
Taro.showToast({
title: '需要位置权限才能参与活动',
icon: 'none',
})
pendingLocationCallback.value(false)
pendingLocationCallback.value = null
}
}
/**
* 位置权限申请弹窗 - 同意授权
*/
const onLocationConfirm = async () => {
showLocationDialog.value = false
try {
const location = await Taro.getLocation({
type: 'gcj02',
altitude: false,
isHighAccuracy: true,
highAccuracyExpireTime: 4000,
})
userLocation.value = {
lng: location.longitude,
lat: location.latitude,
}
console.log('获取到用户位置:', userLocation.value)
showLocationPrompt.value = false
hasLocationAuth.value = true
if (pendingLocationCallback.value) {
pendingLocationCallback.value(true)
pendingLocationCallback.value = null
}
} catch (error) {
console.error('获取位置失败:', error)
if (pendingLocationCallback.value) {
pendingLocationCallback.value(false)
pendingLocationCallback.value = null
}
}
}
/**
* 处理参加活动按钮点击
*/
const handleJoinActivity = async () => {
isJoining.value = true
try {
// 检查定位授权状态
const authSetting = await Taro.getSetting()
const hasLocationPermission = authSetting.authSetting['scope.userLocation']
if (hasLocationPermission === false) {
// 用户之前拒绝过授权,显示提示让用户手动开启
showLocationPrompt.value = true
isJoining.value = false
return
} else if (hasLocationPermission === undefined) {
// 未请求过授权,直接尝试获取位置(会触发授权弹窗)
const success = await getUserLocation(false) // 不跳过授权检查,让getUserLocation处理授权逻辑
if (!success) {
showLocationPrompt.value = true
isJoining.value = false
return
}
} else {
// 已有授权,直接获取位置
const success = await getUserLocation(true) // 跳过授权检查,直接获取位置
if (!success) {
isJoining.value = false
return
}
}
// 跳转到Activities页面,并传递位置参数
await Taro.navigateTo({
url: `/pages/Activities/index?current_lng=${userLocation.value.lng}&current_lat=${userLocation.value.lat}`,
})
} catch (error) {
console.error('参加活动失败:', error)
Taro.showToast({
title: '参加活动失败',
icon: 'none',
})
} finally {
isJoining.value = false
}
}
/**
* 处理分享活动事件
*/
const onShareActivity = () => {
console.log('分享活动海报')
// 分享给朋友
// Taro.showToast({
// title: '请点击右上角分享给朋友',
// icon: 'none',
// duration: 2000
// });
}
/**
* 定义分享给朋友的内容
* @returns {Object} 分享配置对象
*/
const onShareAppMessage = () => {
return {
title: '主题路线打卡活动等你参与',
path: addShareFlag('/pages/ActivitiesCover/index'),
success: res => {
// 分享成功
},
fail: err => {
// 分享失败
},
}
}
// 导出分享方法供Taro使用
defineExpose({
onShareAppMessage,
})
/**
* 处理分享海报事件
*/
const onSharePoster = () => {
console.log('分享海报')
Taro.navigateTo({
url: '/pages/PosterCheckin/index',
})
}
/**
* 点击海报预览
*/
const onClickPost = () => {
show_save.value = true
}
/**
* 取消保存
*/
const onCancelSave = () => {
show_save.value = false
show_post.value = false
}
/**
* 选择保存方式
*/
const onSelectSave = item => {
if (item.name === '保存至相册') {
show_save.value = false
show_post.value = false
savePoster()
}
}
/**
* 开始生成海报
*/
// const startGeneratePoster = async () => {
// // 配置海报参数
// base = {
// width: 1024,
// height: 1334,
// backgroundColor: '',
// debug: false,
// blocks: [
// { // 上部分canvas画布高度
// x: 40,
// y: 20,
// width: 950,
// height: 950,
// paddingLeft: 0,
// paddingRight: 0,
// borderWidth: 1,
// borderColor: '#fff',
// backgroundColor: '#fff',
// borderRadiusGroup: [16, 16, 0, 0],
// },
// { // 活动时间背景图
// x: 40,
// y: 730,
// height: 75,
// paddingLeft: 80,
// paddingRight: 0,
// borderWidth: 0,
// text: {
// x: 0,
// y: 0,
// text: activityData.value.dateRange,
// fontSize: 40,
// color: '#222',
// opacity: 1,
// baseLine: 'top',
// lineHeight: 48,
// lineNum: 2,
// textAlign: 'left',
// zIndex: 0,
// },
// backgroundColor: '#FFF9F3',
// borderRadiusGroup: [0, 25, 25, 0],
// },
// { // 活动地点背景图
// x: 40,
// y: 830,
// height: 75,
// paddingLeft: 80,
// paddingRight: 0,
// borderWidth: 0,
// text: {
// x: 0,
// y: 0,
// text: '上海市黄浦区南京东路',
// fontSize: 40,
// color: '#222',
// opacity: 1,
// baseLine: 'top',
// lineHeight: 48,
// lineNum: 2,
// textAlign: 'left',
// zIndex: 0,
// },
// backgroundColor: '#FFF9F3',
// borderRadiusGroup: [0, 25, 25, 0],
// },
// { // 下部分canvas画布高度
// x: 40,
// y: 1060,
// width: 950,
// height: 250,
// paddingLeft: 0,
// paddingRight: 0,
// borderWidth: 1,
// borderColor: '#fff',
// backgroundColor: '#fff',
// borderRadiusGroup: [0, 0, 16, 16],
// }
// ],
// texts: [
// {
// x: 80,
// y: 630,
// text: activityData.value.title,
// fontSize: 50,
// color: '#000',
// opacity: 1,
// baseLine: 'middle',
// lineHeight: 60,
// lineNum: 2,
// textAlign: 'left',
// width: 800,
// zIndex: 999,
// fontFamily: 'Monospace',
// },
// {
// x: 135,
// y: 770,
// text: activityData.value.dateRange,
// fontSize: 40,
// color: '#222',
// opacity: 1,
// baseLine: 'middle',
// lineHeight: 48,
// lineNum: 2,
// textAlign: 'left',
// zIndex: 999,
// },
// {
// x: 135,
// y: 870,
// text: '上海市黄浦区南京东路',
// fontSize: 40,
// color: '#222',
// opacity: 1,
// baseLine: 'middle',
// lineHeight: 48,
// lineNum: 2,
// textAlign: 'left',
// zIndex: 999,
// },
// {
// x: 300,
// y: 1150,
// text: nickname.value,
// fontSize: 50,
// color: '#333',
// opacity: 1,
// baseLine: 'middle',
// textAlign: 'left',
// lineHeight: 50,
// lineNum: 1,
// zIndex: 999,
// },
// {
// x: 300,
// y: 1220,
// text: '邀请你一起来活动!',
// fontSize: 42,
// color: '#8F9399',
// opacity: 1,
// baseLine: 'middle',
// textAlign: 'left',
// lineHeight: 42,
// lineNum: 1,
// zIndex: 999,
// }
// ],
// images: [
// {
// url: qrcode_url,
// width: 949,
// height: 108,
// x: 40,
// y: 960,
// zIndex: 10,
// },
// {
// url: qrcode_url,
// width: 950,
// height: 500,
// x: 40,
// y: 20,
// borderRadiusGroup: [18, 18, 0, 0],
// zIndex: 10,
// },
// {
// url: qrcode_url,
// width: 40,
// height: 40,
// x: 80,
// y: 750,
// borderRadius: 100,
// borderWidth: 0,
// zIndex: 10,
// },
// {
// url: qrcode_url,
// width: 35,
// height: 40,
// x: 80,
// y: 850,
// borderRadius: 100,
// borderWidth: 0,
// zIndex: 10,
// },
// {
// url: qrcode_url,
// width: 170,
// height: 170,
// x: 80,
// y: 1090,
// borderRadius: 100,
// borderWidth: 0,
// zIndex: 10,
// },
// {
// url: qrcode_url,
// width: 170,
// height: 170,
// x: 750,
// y: 1090,
// borderRadius: 100,
// borderWidth: 0,
// zIndex: 10,
// },
// ],
// lines: []
// }
// startDraw.value = true
// if (!posterPath.value) Taro.showLoading({ title: '生成海报中...' })
// }
/**
* 海报绘制成功回调
*/
const drawSuccess = result => {
console.log('绘制成功', result)
const { tempFilePath, errMsg } = result
if (errMsg === 'canvasToTempFilePath:ok') {
posterPath.value = tempFilePath
Taro.hideLoading()
} else {
Taro.hideLoading()
Taro.showToast({
title: '生成失败,请稍后重试',
icon: 'none',
duration: 2500,
})
}
}
/**
* 海报绘制失败回调
*/
const drawFail = result => {
console.log('绘制失败', result)
Taro.hideLoading()
Taro.showToast({
title: '生成失败,请稍后重试',
icon: 'none',
duration: 2500,
})
}
/**
* 保存海报到相册
*/
const savePoster = () => {
Taro.saveImageToPhotosAlbum({
filePath: posterPath.value,
success() {
Taro.showToast({
title: '已保存到相册',
icon: 'success',
duration: 2000,
})
},
fail() {
Taro.showToast({
title: '保存失败',
icon: 'none',
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 () => {
// 获取用户是否加入家庭
const { code, data } = await getMyFamiliesAPI()
if (code) {
// 如果加入家庭
if (data?.families?.length) {
hasJoinedFamily.value = true
}
}
// 获取活动详情(包含活动状态)
await fetchActivityDetail()
// 检查定位授权状态(不获取位置,只检查权限)
await checkLocationAuth()
}
// 处理页面加载时的授权检查
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')
}
// 处理分享页面的授权逻辑
handleSharePageAuth(options, () => {
initPageData()
})
})
// 页面挂载时检查定位授权状态
onMounted(async () => {
// 获取系统信息
getSystemInfo()
initPageData()
})
</script>
<script>
export default {
name: 'ActivitiesCover',
}
</script>