hookehuyr

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

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -22,19 +22,23 @@ ...@@ -22,19 +22,23 @@
22 <!-- 底部按钮区域 --> 22 <!-- 底部按钮区域 -->
23 <view class="bottom-section"> 23 <view class="bottom-section">
24 <!-- 积分规则说明 --> 24 <!-- 积分规则说明 -->
25 - <view 25 + <view class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4 opacity-90">
26 - v-if="activityData.rules && activityData.rules.length"
27 - class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4 opacity-90"
28 - >
29 <text class="text-blue-500 text-base font-medium block mb-2">积分规则说明:</text> 26 <text class="text-blue-500 text-base font-medium block mb-2">积分规则说明:</text>
30 <text 27 <text
31 - v-for="(rule, index) in activityData.rules"
32 - :key="index"
33 class="text-blue-500 text-sm leading-relaxed block mb-1" 28 class="text-blue-500 text-sm leading-relaxed block mb-1"
34 style="padding-left: 20rpx; text-indent: -20rpx" 29 style="padding-left: 20rpx; text-indent: -20rpx"
30 + >• 打卡任意1关,视为参与,奖励1000积分</text
31 + >
32 + <text
33 + class="text-blue-500 text-sm leading-relaxed block mb-1"
34 + style="padding-left: 20rpx; text-indent: -20rpx"
35 + >• 打卡任意7关,视为完成,奖励5000积分</text
36 + >
37 + <text
38 + class="text-blue-500 text-sm leading-relaxed block mb-1"
39 + style="padding-left: 20rpx; text-indent: -20rpx"
40 + >• 不需要区分打卡点的先后次序</text
35 > 41 >
36 - • {{ rule }}
37 - </text>
38 </view> 42 </view>
39 <!-- 未授权定位提示 - 仅在用户点击参加活动且未授权时显示 --> 43 <!-- 未授权定位提示 - 仅在用户点击参加活动且未授权时显示 -->
40 <view 44 <view
...@@ -147,27 +151,18 @@ import PosterBuilder from '../../components/PosterBuilder/index.vue' ...@@ -147,27 +151,18 @@ import PosterBuilder from '../../components/PosterBuilder/index.vue'
147 import ShareButton from '../../components/ShareButton/index.vue' 151 import ShareButton from '../../components/ShareButton/index.vue'
148 // 接口信息 152 // 接口信息
149 import { getMyFamiliesAPI } from '@/api/family' 153 import { getMyFamiliesAPI } from '@/api/family'
150 -import { detailAPI } from '@/api/map_activity' 154 +import { getActivityStatusAPI } from '@/api/map'
151 import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect' 155 import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect'
152 // 导入主题颜色 156 // 导入主题颜色
153 import { THEME_COLORS } from '@/utils/config' 157 import { THEME_COLORS } from '@/utils/config'
154 -// Mock 数据
155 -import { mockMapActivityDetailAPI } from '@/utils/mockData'
156 -
157 -// 环境变量:是否使用 mock 数据
158 -const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
159 158
160 // 默认海报图 159 // 默认海报图
161 -const defaultPoster = ref( 160 +const defaultPoster =
162 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_8.jpg?imageMogr2/strip/quality/60' 161 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_8.jpg?imageMogr2/strip/quality/60'
163 -)
164 162
165 // 系统信息 163 // 系统信息
166 const systemInfo = ref({}) 164 const systemInfo = ref({})
167 165
168 -// 活动ID(从 URL 参数获取)
169 -const activityId = ref('')
170 -
171 /** 166 /**
172 * 获取系统信息 167 * 获取系统信息
173 */ 168 */
...@@ -937,87 +932,6 @@ const savePoster = () => { ...@@ -937,87 +932,6 @@ const savePoster = () => {
937 } 932 }
938 933
939 /** 934 /**
940 - * 将 API 数据转换为页面需要的 activityData 格式
941 - * @param {Object} apiData - API 返回的活动详情数据
942 - * @returns {Object} 页面活动数据对象
943 - */
944 -const transformApiDataToActivityData = apiData => {
945 - if (!apiData) {
946 - return null
947 - }
948 -
949 - // 生成日期范围字符串
950 - const dateRange = `${apiData.begin_date} - ${apiData.end_date}`
951 -
952 - // 根据积分规则生成规则描述
953 - const rules = [
954 - `打卡任意1关,视为参与,奖励${apiData.first_checkin_points}积分`,
955 - `打卡任意${apiData.required_checkin_count}关,视为完成,奖励${apiData.complete_points}积分`,
956 - '不需要区分打卡点的先后次序',
957 - ]
958 -
959 - // 生成奖励描述
960 - const rewards = [
961 - `首次打卡获得${apiData.first_checkin_points}积分`,
962 - `完成${apiData.required_checkin_count}个打卡点获得${apiData.complete_points}积分`,
963 - apiData.discount_title || '打卡点专属优惠',
964 - ]
965 -
966 - return {
967 - title: apiData.tittle || '活动标题',
968 - subtitle: '探索城市魅力,感受时尚脉搏',
969 - dateRange: dateRange,
970 - posterUrl: apiData.cover || defaultPoster.value,
971 - description: `欢迎参加${apiData.tittle}活动!`,
972 - rules: rules,
973 - rewards: rewards,
974 - }
975 -}
976 -
977 -/**
978 - * 获取活动详情
979 - */
980 -const fetchActivityDetail = async () => {
981 - try {
982 - // 如果没有活动ID,不获取详情
983 - if (!activityId.value) {
984 - console.warn('[ActivitiesCover] 未提供活动ID,跳过详情获取')
985 - return
986 - }
987 -
988 - console.log('[ActivitiesCover] 开始获取活动详情, ID:', activityId.value)
989 -
990 - // 根据环境选择真实 API 或 mock API
991 - const response = USE_MOCK_DATA
992 - ? await mockMapActivityDetailAPI({ id: activityId.value })
993 - : await detailAPI({ id: activityId.value })
994 -
995 - if (response.code === 1 && response.data) {
996 - console.log('[ActivitiesCover] 活动详情获取成功:', response.data)
997 -
998 - // 转换 API 数据为页面格式
999 - const transformedData = transformApiDataToActivityData(response.data)
1000 - if (transformedData) {
1001 - activityData.value = transformedData
1002 -
1003 - // 更新默认海报图
1004 - if (response.data.cover) {
1005 - defaultPoster.value = response.data.cover
1006 - }
1007 -
1008 - // 更新活动状态
1009 - activityStatus.value.is_begin = Boolean(response.data.is_begin)
1010 - activityStatus.value.is_ended = Boolean(response.data.is_ended)
1011 - }
1012 - } else {
1013 - console.warn('[ActivitiesCover] 获取活动详情失败:', response.msg)
1014 - }
1015 - } catch (error) {
1016 - console.error('[ActivitiesCover] 获取活动详情异常:', error)
1017 - }
1018 -}
1019 -
1020 -/**
1021 * 初始化页面数据 935 * 初始化页面数据
1022 */ 936 */
1023 const initPageData = async () => { 937 const initPageData = async () => {
...@@ -1030,28 +944,46 @@ const initPageData = async () => { ...@@ -1030,28 +944,46 @@ const initPageData = async () => {
1030 } 944 }
1031 } 945 }
1032 946
1033 - // 获取活动详情(包含活动状态) 947 + // 获取活动状态
1034 - await fetchActivityDetail() 948 + await fetchActivityStatus()
1035 949
1036 // 检查定位授权状态(不获取位置,只检查权限) 950 // 检查定位授权状态(不获取位置,只检查权限)
1037 await checkLocationAuth() 951 await checkLocationAuth()
1038 } 952 }
1039 953
1040 -// 处理页面加载时的授权检查 954 +/**
1041 -useLoad(options => { 955 + * 获取活动状态
1042 - console.log('[ActivitiesCover] 页面加载, 参数:', options) 956 + */
1043 - 957 +const fetchActivityStatus = async () => {
1044 - // 获取活动 ID(如果有) 958 + try {
1045 - if (options.id) { 959 + activityStatus.value.loading = true
1046 - activityId.value = options.id 960 + const { code, data } = await getActivityStatusAPI()
1047 - } else if (options.activity_id) { 961 +
1048 - activityId.value = options.activity_id 962 + if (code === 1 && data) {
1049 - } else { 963 + activityStatus.value.is_begin = Boolean(data.is_begin)
1050 - // 如果没有活动ID,使用默认ID 964 + activityStatus.value.is_ended = Boolean(data.is_ended)
1051 - activityId.value = '1' 965 + console.log('活动状态:', {
1052 - console.warn('[ActivitiesCover] 未提供活动ID,使用默认ID: 1') 966 + is_begin: activityStatus.value.is_begin ? '已开始' : '未开始',
967 + is_ended: activityStatus.value.is_ended ? '已结束' : '进行中',
968 + })
969 + } else {
970 + console.warn('获取活动状态失败:', data)
971 + // 默认认为活动未开始且未结束,避免影响用户体验
972 + activityStatus.value.is_begin = false
973 + activityStatus.value.is_ended = false
974 + }
975 + } catch (error) {
976 + console.error('获取活动状态异常:', error)
977 + // 默认认为活动未开始且未结束,避免影响用户体验
978 + activityStatus.value.is_begin = false
979 + activityStatus.value.is_ended = false
980 + } finally {
981 + activityStatus.value.loading = false
1053 } 982 }
983 +}
1054 984
985 +// 处理页面加载时的授权检查
986 +useLoad(options => {
1055 // 处理分享页面的授权逻辑 987 // 处理分享页面的授权逻辑
1056 handleSharePageAuth(options, () => { 988 handleSharePageAuth(options, () => {
1057 initPageData() 989 initPageData()
......
1 +/*
2 + * @Date: 2026-02-09
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2026-02-09
5 + * @FilePath: /lls_program/src/pages/ActivitiesDetail/index.config.js
6 + * @Description: 活动详情页面配置 - 支持多活动详情展示
7 + */
8 +export default {
9 + navigationBarTitleText: '活动详情',
10 + enableShareAppMessage: true,
11 + usingComponents: {},
12 +}
1 +.activities-cover-container {
2 + position: relative;
3 + width: 100%;
4 + height: 100vh;
5 + overflow: hidden;
6 +}
7 +
8 +// 背景图片
9 +.background-image {
10 + position: absolute;
11 + top: 0;
12 + left: 0;
13 + width: 100vw;
14 + height: calc(100vh - 150rpx); // 减去底部区域的高度,确保背景图不被遮挡
15 + // height: calc(100vh); // 减去底部区域的高度,确保背景图不被遮挡
16 + object-fit: cover;
17 + object-position: top center;
18 + z-index: 1;
19 +}
20 +
21 +// 为容器添加背景色,避免下方空白
22 +// .activities-cover-container::before {
23 +// content: '';
24 +// position: absolute;
25 +// top: 0;
26 +// left: 0;
27 +// width: 100%;
28 +// height: 100%;
29 +// background: linear-gradient(180deg, #f0f8ff 0%, #e6f3ff 50%, #ddeeff 100%);
30 +// z-index: 0;
31 +// }
32 +
33 +// 分享按钮包装器
34 +.share-button-wrapper {
35 + position: absolute;
36 + top: 40rpx;
37 + right: 40rpx;
38 + z-index: 10;
39 +}
40 +
41 +// 底部区域
42 +.bottom-section {
43 + position: absolute;
44 + bottom: 0;
45 + left: 0;
46 + right: 0;
47 + padding: 40rpx;
48 + padding-bottom: 180rpx; // 为底部导航留出空间
49 + // background: linear-gradient(
50 + // transparent 0%,
51 + // rgba(0, 0, 0, 0.1) 20%,
52 + // rgba(0, 0, 0, 0.3) 50%,
53 + // rgba(0, 0, 0, 0.6) 80%,
54 + // rgba(0, 0, 0, 0.8) 100%
55 + // );
56 + // backdrop-filter: blur(30rpx);
57 + // -webkit-backdrop-filter: blur(30rpx);
58 + z-index: 5;
59 +
60 + // 增加渐变高度,让过渡更自然
61 + min-height: 300rpx;
62 +}
63 +
64 +.location-tip {
65 + display: flex;
66 + flex-direction: column;
67 + align-items: center;
68 + justify-content: center;
69 + padding: 24rpx;
70 + background-color: rgba(255, 247, 230, 0.95);
71 + border: 1rpx solid rgba(255, 213, 145, 0.9);
72 + border-radius: 16rpx;
73 + margin-bottom: 32rpx;
74 + backdrop-filter: blur(20rpx);
75 + -webkit-backdrop-filter: blur(20rpx);
76 + cursor: pointer;
77 + transition: all 0.3s ease;
78 + box-shadow:
79 + 0 4rpx 16rpx rgba(255, 213, 145, 0.3),
80 + 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
81 +
82 + &:active {
83 + background-color: rgba(255, 247, 230, 0.8);
84 + transform: scale(0.98);
85 + box-shadow:
86 + 0 2rpx 8rpx rgba(255, 213, 145, 0.2),
87 + 0 1rpx 4rpx rgba(0, 0, 0, 0.1);
88 + }
89 +
90 + .tip-content {
91 + display: flex;
92 + align-items: center;
93 + justify-content: center;
94 + margin-bottom: 8rpx;
95 + }
96 +}
97 +
98 +.tip-icon {
99 + font-size: 32rpx;
100 + margin-right: 12rpx;
101 +}
102 +
103 +.tip-text {
104 + font-size: 26rpx;
105 + color: #d46b08;
106 + font-weight: 500;
107 +}
108 +
109 +.tip-retry {
110 + font-size: 22rpx;
111 + color: #1890ff;
112 + text-decoration: underline;
113 +}
114 +
115 +.location-error {
116 + background-color: rgba(255, 241, 240, 0.95);
117 + border: 1rpx solid rgba(255, 163, 158, 0.9);
118 + box-shadow:
119 + 0 4rpx 16rpx rgba(255, 163, 158, 0.3),
120 + 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
121 +
122 + &:active {
123 + background-color: rgba(255, 241, 240, 0.8);
124 + box-shadow:
125 + 0 2rpx 8rpx rgba(255, 163, 158, 0.2),
126 + 0 1rpx 4rpx rgba(0, 0, 0, 0.1);
127 + }
128 +
129 + .tip-text {
130 + color: #cf1322;
131 + }
132 +}
133 +
134 +.join-button {
135 + width: 100%;
136 + height: 88rpx;
137 + border-radius: 44rpx;
138 + font-size: 32rpx;
139 + font-weight: 600;
140 + box-shadow:
141 + 0 8rpx 32rpx rgba(84, 171, 174, 0.5),
142 + 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
143 + backdrop-filter: blur(15rpx);
144 + -webkit-backdrop-filter: blur(15rpx);
145 + border: 1rpx solid rgba(255, 255, 255, 0.2);
146 +
147 + &:active {
148 + transform: translateY(2rpx);
149 + box-shadow:
150 + 0 4rpx 16rpx rgba(84, 171, 174, 0.4),
151 + 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
152 + }
153 +
154 + &.nut-button--primary {
155 + background: linear-gradient(135deg, rgba(84, 171, 174, 0.95) 0%, rgba(74, 151, 154, 0.95) 100%);
156 + border: 1rpx solid rgba(255, 255, 255, 0.3);
157 + box-shadow:
158 + 0 8rpx 32rpx rgba(84, 171, 174, 0.4),
159 + 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
160 + color: white;
161 + text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.2);
162 +
163 + &:active {
164 + transform: translateY(2rpx);
165 + box-shadow:
166 + 0 4rpx 16rpx rgba(84, 171, 174, 0.3),
167 + 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
168 + }
169 + }
170 +
171 + // 禁用状态样式 - 覆盖NutUI默认的半透明效果
172 + &.nut-button--disabled {
173 + opacity: 1 !important; // 覆盖默认的透明度
174 + background: #cccccc !important; // 实心灰色背景
175 + color: #fff !important; // 深灰色文字
176 + border: 1rpx solid #ccc !important;
177 + box-shadow: 0 4rpx 16rpx rgba(204, 204, 204, 0.3) !important;
178 +
179 + &:active {
180 + transform: none; // 禁用时不响应点击效果
181 + box-shadow: 0 4rpx 16rpx rgba(204, 204, 204, 0.3) !important;
182 + }
183 + }
184 +}
185 +
186 +// 弹窗样式
187 +.share-popup {
188 + .nut-popup__content {
189 + border-radius: 24rpx 24rpx 0 0;
190 + padding: 40rpx;
191 + }
192 +}
193 +
194 +.share-title {
195 + font-size: 32rpx;
196 + font-weight: bold;
197 + text-align: center;
198 + margin-bottom: 40rpx;
199 + color: #333;
200 +}
201 +
202 +.share-options {
203 + display: flex;
204 + justify-content: space-around;
205 + margin-bottom: 40rpx;
206 +}
207 +
208 +.share-option {
209 + display: flex;
210 + flex-direction: column;
211 + align-items: center;
212 + padding: 20rpx;
213 + border-radius: 12rpx;
214 +
215 + &:active {
216 + background-color: #f5f5f5;
217 + }
218 +}
219 +
220 +.share-icon {
221 + width: 80rpx;
222 + height: 80rpx;
223 + margin-bottom: 16rpx;
224 + border-radius: 12rpx;
225 + background-color: #1890ff;
226 + display: flex;
227 + align-items: center;
228 + justify-content: center;
229 + color: white;
230 + font-size: 36rpx;
231 +}
232 +
233 +.share-text {
234 + font-size: 24rpx;
235 + color: #666;
236 +}
237 +
238 +.cancel-button {
239 + width: 100%;
240 + height: 88rpx;
241 + border-radius: 44rpx;
242 + font-size: 32rpx;
243 + background-color: #f5f5f5;
244 + color: #666;
245 + border: none;
246 +}
247 +
248 +// 海报预览弹窗
249 +.poster-preview-popup {
250 + .nut-popup__content {
251 + width: 90%;
252 + max-width: 600rpx;
253 + border-radius: 24rpx;
254 + padding: 40rpx;
255 + background-color: white;
256 + }
257 +}
258 +
259 +.poster-preview {
260 + width: 100%;
261 + border-radius: 12rpx;
262 + margin-bottom: 40rpx;
263 +}
264 +
265 +.preview-actions {
266 + display: flex;
267 + gap: 20rpx;
268 +}
269 +
270 +.preview-button {
271 + flex: 1;
272 + height: 80rpx;
273 + border-radius: 40rpx;
274 + font-size: 28rpx;
275 +
276 + &.primary {
277 + background-color: #1890ff;
278 + color: white;
279 + border: none;
280 + }
281 +
282 + &.secondary {
283 + background-color: #f5f5f5;
284 + color: #666;
285 + border: none;
286 + }
287 +}
1 +<!--
2 + * @Date: 2026-02-09
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2026-02-09
5 + * @FilePath: /lls_program/src/pages/ActivitiesDetail/index.vue
6 + * @Description: 活动详情页面 - 完全使用 map_activity.js 新接口,支持多活动详情展示
7 +-->
8 +<template>
9 + <view class="activities-cover-container">
10 + <!-- 背景图片 -->
11 + <image :src="defaultPoster" class="background-image" :mode="imageDisplayMode" />
12 +
13 + <!-- 分享按钮组件 -->
14 + <ShareButton
15 + :activity-data="activityData"
16 + :share-config="shareConfig"
17 + @share-activity="onShareActivity"
18 + @share-poster="onSharePoster"
19 + class="share-button-wrapper"
20 + />
21 +
22 + <!-- 底部按钮区域 -->
23 + <view class="bottom-section">
24 + <!-- 积分规则说明 -->
25 + <view
26 + v-if="activityData.rules && activityData.rules.length"
27 + class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4 opacity-90"
28 + >
29 + <text class="text-blue-500 text-base font-medium block mb-2">积分规则说明:</text>
30 + <text
31 + v-for="(rule, index) in activityData.rules"
32 + :key="index"
33 + class="text-blue-500 text-sm leading-relaxed block mb-1"
34 + style="padding-left: 20rpx; text-indent: -20rpx"
35 + >
36 + • {{ rule }}
37 + </text>
38 + </view>
39 + <!-- 未授权定位提示 - 仅在用户点击参加活动且未授权时显示 -->
40 + <view
41 + v-if="showLocationPrompt && !hasLocationAuth && !locationError"
42 + class="location-tip"
43 + @click="retryGetLocation"
44 + >
45 + <view class="tip-content">
46 + <view class="tip-icon">📍</view>
47 + <view class="tip-text">点击获取您的位置信息来参与活动</view>
48 + </view>
49 + <!-- <view class="tip-retry">点击重新获取</view> -->
50 + </view>
51 +
52 + <!-- 位置获取失败提示 -->
53 + <view
54 + v-if="hasLocationAuth && locationError"
55 + class="location-tip location-error"
56 + @click="retryGetLocation"
57 + >
58 + <view class="tip-content">
59 + <view class="tip-icon">⚠️</view>
60 + <view class="tip-text">可能是网络问题,获取位置信息失败</view>
61 + </view>
62 + <!-- <view class="tip-retry">点击重新获取</view> -->
63 + </view>
64 +
65 + <nut-button
66 + type="primary"
67 + size="large"
68 + class="join-button"
69 + :color="
70 + activityStatus.is_ended || !activityStatus.is_begin ? '#cccccc' : THEME_COLORS.PRIMARY
71 + "
72 + :loading="isJoining || activityStatus.loading"
73 + :disabled="activityStatus.is_ended || !activityStatus.is_begin"
74 + @click="checkFamilyStatusAndJoinActivity"
75 + >
76 + {{ getButtonText() }}
77 + </nut-button>
78 + </view>
79 +
80 + <!-- 底部导航 -->
81 + <BottomNav />
82 +
83 + <!-- 海报预览弹窗 -->
84 + <nut-popup v-model:visible="show_post" position="center" class="poster-preview-popup">
85 + <view class="wrapper">
86 + <view class="preview-area" @click="onClickPost">
87 + <image v-if="posterPath" :src="posterPath" mode="widthFix" />
88 + </view>
89 + </view>
90 + </nut-popup>
91 +
92 + <!-- 海报生成组件 -->
93 + <PosterBuilder
94 + v-if="startDraw"
95 + custom-style="position: fixed; left: 200%;"
96 + :config="base"
97 + @success="drawSuccess"
98 + @fail="drawFail"
99 + />
100 +
101 + <!-- 保存选项弹窗 -->
102 + <nut-action-sheet
103 + v-model:visible="show_save"
104 + :menu-items="actions_save"
105 + @choose="onSelectSave"
106 + @cancel="onCancelSave"
107 + cancel-txt="取消"
108 + />
109 +
110 + <!-- 位置权限申请弹窗 -->
111 + <nut-dialog v-model:visible="showLocationDialog" title="位置权限申请">
112 + <template #default>
113 + <view class="text-gray-700 leading-loose text-sm text-left">
114 + {{ locationContent }}
115 + </view>
116 + </template>
117 + <template #footer>
118 + <nut-row :gutter="10">
119 + <nut-col :span="12">
120 + <nut-button @click="onLocationCancel" type="default" size="normal" block>
121 + 暂不授权
122 + </nut-button>
123 + </nut-col>
124 + <nut-col :span="12">
125 + <nut-button
126 + @click="onLocationConfirm"
127 + type="primary"
128 + size="normal"
129 + :color="THEME_COLORS.PRIMARY"
130 + block
131 + >
132 + 同意授权
133 + </nut-button>
134 + </nut-col>
135 + </nut-row>
136 + </template>
137 + </nut-dialog>
138 + </view>
139 +</template>
140 +
141 +<script setup>
142 +import { ref, onMounted, computed } from 'vue'
143 +import Taro, { useLoad } from '@tarojs/taro'
144 +import './index.less'
145 +import BottomNav from '../../components/BottomNav.vue'
146 +import PosterBuilder from '../../components/PosterBuilder/index.vue'
147 +import ShareButton from '../../components/ShareButton/index.vue'
148 +// 接口信息
149 +import { getMyFamiliesAPI } from '@/api/family'
150 +import { detailAPI } from '@/api/map_activity'
151 +import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect'
152 +// 导入主题颜色
153 +import { THEME_COLORS } from '@/utils/config'
154 +// Mock 数据
155 +import { mockMapActivityDetailAPI } from '@/utils/mockData'
156 +
157 +// 环境变量:是否使用 mock 数据
158 +const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
159 +
160 +// 默认海报图
161 +const defaultPoster = ref(
162 + 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_8.jpg?imageMogr2/strip/quality/60'
163 +)
164 +
165 +// 系统信息
166 +const systemInfo = ref({})
167 +
168 +// 活动ID(从 URL 参数获取)
169 +const activityId = ref('')
170 +
171 +/**
172 + * 获取系统信息
173 + */
174 +const getSystemInfo = () => {
175 + try {
176 + const info = Taro.getWindowInfo()
177 + systemInfo.value = info
178 + } catch (error) {
179 + console.error('获取系统信息失败:', error)
180 + }
181 +}
182 +
183 +/**
184 + * 检测是否为 iPad 类型设备
185 + */
186 +const isTabletDevice = computed(() => {
187 + if (!systemInfo.value.screenWidth) {
188 + return false
189 + }
190 +
191 + const { screenWidth, screenHeight } = systemInfo.value
192 + const screenRatio = screenWidth / screenHeight
193 +
194 + // iPad 类型设备通常屏幕比例在 0.7-0.8 之间(4:3 约为 0.75)
195 + // 普通手机设备比例通常在 0.4-0.6 之间
196 + return screenRatio > 0.65
197 +})
198 +
199 +/**
200 + * 计算图片显示模式
201 + */
202 +const imageDisplayMode = computed(() => {
203 + // iPad 类型设备使用 widthFix 模式,普通设备使用 aspectFill
204 + return isTabletDevice.value ? 'widthFix' : 'aspectFill'
205 +})
206 +
207 +/**
208 + * 活动海报页面组件
209 + * 功能:展示活动信息、处理定位授权、跳转到活动页面
210 + */
211 +
212 +// 页面状态
213 +const hasLocationAuth = ref(false) // 是否已授权定位
214 +const locationError = ref(false) // 位置获取是否失败
215 +const isJoining = ref(false) // 是否正在加入活动
216 +const userLocation = ref({ lng: null, lat: null }) // 用户位置信息
217 +const hasJoinedFamily = ref(false)
218 +const showLocationPrompt = ref(false) // 是否显示定位权限提示
219 +
220 +// 活动状态相关
221 +const activityStatus = ref({
222 + is_begin: false, // 活动是否已开始
223 + is_ended: false, // 活动是否已结束
224 + loading: false, // 是否正在加载活动状态
225 +})
226 +
227 +// Dialog 相关状态
228 +const showLocationDialog = ref(false) // 是否显示位置权限申请弹窗
229 +const pendingLocationCallback = ref(null) // 待执行的位置获取回调
230 +
231 +// 位置权限申请说明内容
232 +const locationContent =
233 + '为了提供更好的活动体验,我们需要获取您的位置信息:验证您是否在活动区域内, 我们承诺严格保护您的位置隐私,仅用于活动相关功能。'
234 +
235 +// 海报生成相关状态
236 +const show_post = ref(false) // 显示海报预览
237 +const show_save = ref(false) // 显示保存弹窗
238 +const startDraw = ref(false) // 开始绘制海报
239 +const posterPath = ref('') // 海报路径
240 +const nickname = ref('老来赛用户') // 用户昵称
241 +// const avatar = ref('https://cdn.ipadbiz.cn/icon/tou@2x.png') // 用户头像
242 +
243 +// 保存选项
244 +const actions_save = ref([
245 + {
246 + name: '保存至相册',
247 + },
248 +])
249 +
250 +// 海报配置
251 +const base = {}
252 +const qrcode_url = 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png' // Mock二维码
253 +
254 +// Mock活动数据
255 +const activityData = ref({
256 + title: '南京路商圈时尚Citywalk',
257 + subtitle: '探索城市魅力,感受时尚脉搏',
258 + dateRange: '2024年1月15日 - 2024年1月31日',
259 + posterUrl: 'https://img.yzcdn.cn/vant/cat.jpeg', // 临时使用示例图片
260 + description:
261 + '漫步南京路,感受上海的繁华与历史交融。从外滩到人民广场,体验这座城市独特的魅力和时尚气息。',
262 + rules: [
263 + '年满60岁的老年人可参与活动',
264 + '需要在指定时间内完成所有打卡点',
265 + '每个打卡点需上传照片验证',
266 + '完成全部打卡可获得电子勋章和积分奖励',
267 + ],
268 + rewards: [
269 + '完成打卡获得500积分',
270 + '获得专属电子勋章',
271 + '有机会获得商户优惠券',
272 + '参与月度积分排行榜',
273 + ],
274 +})
275 +
276 +// 分享配置
277 +const shareConfig = ref({
278 + title: '主题路线打卡活动等你参与',
279 + path: '/pages/ActivitiesCover/index',
280 + imageUrl: '',
281 +})
282 +
283 +/**
284 + * 检查定位授权状态
285 + */
286 +const checkLocationAuth = async () => {
287 + try {
288 + const authSetting = await Taro.getSetting()
289 + hasLocationAuth.value = authSetting.authSetting['scope.userLocation'] === true
290 + console.log('定位授权状态:', hasLocationAuth.value)
291 + } catch (error) {
292 + console.error('检查定位授权失败:', error)
293 + hasLocationAuth.value = false
294 + }
295 +}
296 +
297 +/**
298 + * 获取用户位置信息
299 + * @param {boolean} skipAuthCheck - 是否跳过授权检查(当调用方已经检查过授权状态时)
300 + */
301 +const getUserLocation = async (skipAuthCheck = false) => {
302 + try {
303 + locationError.value = false // 重置错误状态
304 +
305 + // 如果没有跳过授权检查,则检查权限状态
306 + if (!skipAuthCheck) {
307 + const authSetting = await Taro.getSetting()
308 + const hasLocationAuth = authSetting.authSetting['scope.userLocation']
309 +
310 + // 如果没有授权,先显示数据用途说明
311 + if (hasLocationAuth !== true) {
312 + return new Promise(resolve => {
313 + pendingLocationCallback.value = resolve
314 + showLocationDialog.value = true
315 + })
316 + }
317 + }
318 +
319 + const location = await Taro.getLocation({
320 + type: 'gcj02',
321 + altitude: false, // 不需要海拔信息,提高获取速度
322 + isHighAccuracy: true, // 开启高精度定位
323 + highAccuracyExpireTime: 4000, // 高精度定位超时时间
324 + })
325 +
326 + userLocation.value = {
327 + lng: location.longitude,
328 + lat: location.latitude,
329 + }
330 +
331 + console.log('获取到用户位置:', userLocation.value)
332 + // 获取位置成功后隐藏提示
333 + showLocationPrompt.value = false
334 + hasLocationAuth.value = true
335 + return true
336 + } catch (error) {
337 + console.error('获取位置失败:', error)
338 +
339 + if (error.errMsg && error.errMsg.includes('auth deny')) {
340 + // 用户拒绝授权,引导用户手动开启
341 + hasLocationAuth.value = false
342 + locationError.value = false
343 + await Taro.showModal({
344 + title: '需要位置权限',
345 + content: '参与活动需要获取您的位置信息,请在设置中开启位置权限',
346 + confirmText: '去设置',
347 + success: res => {
348 + if (res.confirm) {
349 + Taro.openSetting()
350 + }
351 + },
352 + })
353 + } else if (error.errMsg && error.errMsg.includes('timeout')) {
354 + // 定位超时
355 + locationError.value = true
356 + Taro.showToast({
357 + title: '定位超时,请检查网络或GPS',
358 + icon: 'none',
359 + duration: 3000,
360 + })
361 + } else if (error.errMsg && error.errMsg.includes('fail')) {
362 + // 定位失败,可能是GPS关闭或网络问题
363 + locationError.value = true
364 + await Taro.showModal({
365 + title: '定位失败',
366 + content: '请确保已开启GPS定位服务,并检查网络连接是否正常',
367 + showCancel: false,
368 + confirmText: '我知道了',
369 + })
370 + } else {
371 + // 其他未知错误
372 + locationError.value = true
373 + Taro.showToast({
374 + title: '获取位置失败,请重试',
375 + icon: 'none',
376 + })
377 + }
378 +
379 + return false
380 + }
381 +}
382 +
383 +/**
384 + * 获取按钮显示文本
385 + */
386 +const getButtonText = () => {
387 + // 如果活动已结束,显示"活动已结束"
388 + if (activityStatus.value.is_ended) {
389 + return '活动已结束'
390 + }
391 +
392 + // 如果活动未开始,显示"活动未开始"
393 + if (!activityStatus.value.is_begin) {
394 + return '活动未开始'
395 + }
396 +
397 + // 如果没有加入家庭,显示"加入家庭"
398 + if (!hasJoinedFamily.value) {
399 + return '立即参加'
400 + }
401 +
402 + // 如果位置获取失败,显示"重新定位"
403 + if (locationError.value) {
404 + return '重新定位'
405 + }
406 +
407 + // 如果需要显示位置提示,说明需要授权
408 + if (showLocationPrompt.value) {
409 + return '授权定位'
410 + }
411 +
412 + // 如果已有定位授权且有位置信息,显示"进入活动"
413 + if (hasLocationAuth.value && userLocation.value.lng && userLocation.value.lat) {
414 + return '立即进入'
415 + }
416 +
417 + // 默认显示"立即参加"
418 + return '立即参加'
419 +}
420 +
421 +/**
422 + * 检查用户是否加入家庭并处理参加活动按钮点击
423 + */
424 +const checkFamilyStatusAndJoinActivity = async () => {
425 + // 如果活动已结束,显示提示
426 + if (activityStatus.value.is_ended) {
427 + Taro.showToast({
428 + title: '活动已结束',
429 + icon: 'none',
430 + })
431 + return
432 + }
433 +
434 + // 如果活动未开始,显示提示
435 + if (!activityStatus.value.is_begin) {
436 + Taro.showToast({
437 + title: '活动尚未开始,请耐心等待',
438 + icon: 'none',
439 + })
440 + return
441 + }
442 +
443 + // 如果没有加入家庭,引导用户加入家庭
444 + if (!hasJoinedFamily.value) {
445 + Taro.showModal({
446 + title: '提示',
447 + content: '没有加入家庭是无法参加活动的',
448 + cancelText: '关闭',
449 + confirmText: '前往加入',
450 + success: res => {
451 + if (res.confirm) {
452 + Taro.redirectTo({
453 + url: '/pages/Welcome/index',
454 + })
455 + }
456 + },
457 + })
458 + return
459 + }
460 +
461 + // 如果位置获取失败,重新获取位置
462 + if (locationError.value) {
463 + await retryGetLocation()
464 + return
465 + }
466 +
467 + // 如果需要显示位置提示,说明需要授权
468 + if (showLocationPrompt.value) {
469 + showLocationPrompt.value = false // 隐藏提示,直接尝试获取位置
470 + await handleJoinActivity()
471 + return
472 + }
473 +
474 + // 正常参加活动流程
475 + await handleJoinActivity()
476 +}
477 +
478 +/**
479 + * 重新获取位置信息
480 + */
481 +const retryGetLocation = async () => {
482 + try {
483 + const success = await getUserLocation(false) // 不跳过授权检查,重新处理授权逻辑
484 + if (success) {
485 + hasLocationAuth.value = true
486 + locationError.value = false
487 + Taro.showToast({
488 + title: '位置获取成功',
489 + icon: 'success',
490 + })
491 + }
492 + } catch (error) {
493 + console.error('重新获取位置失败:', error)
494 + }
495 +}
496 +
497 +/**
498 + * 位置权限申请弹窗 - 取消操作
499 + */
500 +const onLocationCancel = () => {
501 + showLocationDialog.value = false
502 + if (pendingLocationCallback.value) {
503 + Taro.showToast({
504 + title: '需要位置权限才能参与活动',
505 + icon: 'none',
506 + })
507 + pendingLocationCallback.value(false)
508 + pendingLocationCallback.value = null
509 + }
510 +}
511 +
512 +/**
513 + * 位置权限申请弹窗 - 同意授权
514 + */
515 +const onLocationConfirm = async () => {
516 + showLocationDialog.value = false
517 +
518 + try {
519 + const location = await Taro.getLocation({
520 + type: 'gcj02',
521 + altitude: false,
522 + isHighAccuracy: true,
523 + highAccuracyExpireTime: 4000,
524 + })
525 +
526 + userLocation.value = {
527 + lng: location.longitude,
528 + lat: location.latitude,
529 + }
530 +
531 + console.log('获取到用户位置:', userLocation.value)
532 + showLocationPrompt.value = false
533 + hasLocationAuth.value = true
534 +
535 + if (pendingLocationCallback.value) {
536 + pendingLocationCallback.value(true)
537 + pendingLocationCallback.value = null
538 + }
539 + } catch (error) {
540 + console.error('获取位置失败:', error)
541 + if (pendingLocationCallback.value) {
542 + pendingLocationCallback.value(false)
543 + pendingLocationCallback.value = null
544 + }
545 + }
546 +}
547 +
548 +/**
549 + * 处理参加活动按钮点击
550 + */
551 +const handleJoinActivity = async () => {
552 + isJoining.value = true
553 +
554 + try {
555 + // 检查定位授权状态
556 + const authSetting = await Taro.getSetting()
557 + const hasLocationPermission = authSetting.authSetting['scope.userLocation']
558 +
559 + if (hasLocationPermission === false) {
560 + // 用户之前拒绝过授权,显示提示让用户手动开启
561 + showLocationPrompt.value = true
562 + isJoining.value = false
563 + return
564 + } else if (hasLocationPermission === undefined) {
565 + // 未请求过授权,直接尝试获取位置(会触发授权弹窗)
566 + const success = await getUserLocation(false) // 不跳过授权检查,让getUserLocation处理授权逻辑
567 + if (!success) {
568 + showLocationPrompt.value = true
569 + isJoining.value = false
570 + return
571 + }
572 + } else {
573 + // 已有授权,直接获取位置
574 + const success = await getUserLocation(true) // 跳过授权检查,直接获取位置
575 + if (!success) {
576 + isJoining.value = false
577 + return
578 + }
579 + }
580 +
581 + // 跳转到Activities页面,并传递位置参数
582 + await Taro.navigateTo({
583 + url: `/pages/Activities/index?current_lng=${userLocation.value.lng}&current_lat=${userLocation.value.lat}`,
584 + })
585 + } catch (error) {
586 + console.error('参加活动失败:', error)
587 + Taro.showToast({
588 + title: '参加活动失败',
589 + icon: 'none',
590 + })
591 + } finally {
592 + isJoining.value = false
593 + }
594 +}
595 +
596 +/**
597 + * 处理分享活动事件
598 + */
599 +const onShareActivity = () => {
600 + console.log('分享活动海报')
601 + // 分享给朋友
602 + // Taro.showToast({
603 + // title: '请点击右上角分享给朋友',
604 + // icon: 'none',
605 + // duration: 2000
606 + // });
607 +}
608 +
609 +/**
610 + * 定义分享给朋友的内容
611 + * @returns {Object} 分享配置对象
612 + */
613 +const onShareAppMessage = () => {
614 + return {
615 + title: '主题路线打卡活动等你参与',
616 + path: addShareFlag('/pages/ActivitiesCover/index'),
617 + success: res => {
618 + // 分享成功
619 + },
620 + fail: err => {
621 + // 分享失败
622 + },
623 + }
624 +}
625 +
626 +// 导出分享方法供Taro使用
627 +defineExpose({
628 + onShareAppMessage,
629 +})
630 +
631 +/**
632 + * 处理分享海报事件
633 + */
634 +const onSharePoster = () => {
635 + console.log('分享海报')
636 + Taro.navigateTo({
637 + url: '/pages/PosterCheckin/index',
638 + })
639 +}
640 +
641 +/**
642 + * 点击海报预览
643 + */
644 +const onClickPost = () => {
645 + show_save.value = true
646 +}
647 +
648 +/**
649 + * 取消保存
650 + */
651 +const onCancelSave = () => {
652 + show_save.value = false
653 + show_post.value = false
654 +}
655 +
656 +/**
657 + * 选择保存方式
658 + */
659 +const onSelectSave = item => {
660 + if (item.name === '保存至相册') {
661 + show_save.value = false
662 + show_post.value = false
663 + savePoster()
664 + }
665 +}
666 +
667 +/**
668 + * 开始生成海报
669 + */
670 +// const startGeneratePoster = async () => {
671 +// // 配置海报参数
672 +// base = {
673 +// width: 1024,
674 +// height: 1334,
675 +// backgroundColor: '',
676 +// debug: false,
677 +// blocks: [
678 +// { // 上部分canvas画布高度
679 +// x: 40,
680 +// y: 20,
681 +// width: 950,
682 +// height: 950,
683 +// paddingLeft: 0,
684 +// paddingRight: 0,
685 +// borderWidth: 1,
686 +// borderColor: '#fff',
687 +// backgroundColor: '#fff',
688 +// borderRadiusGroup: [16, 16, 0, 0],
689 +// },
690 +// { // 活动时间背景图
691 +// x: 40,
692 +// y: 730,
693 +// height: 75,
694 +// paddingLeft: 80,
695 +// paddingRight: 0,
696 +// borderWidth: 0,
697 +// text: {
698 +// x: 0,
699 +// y: 0,
700 +// text: activityData.value.dateRange,
701 +// fontSize: 40,
702 +// color: '#222',
703 +// opacity: 1,
704 +// baseLine: 'top',
705 +// lineHeight: 48,
706 +// lineNum: 2,
707 +// textAlign: 'left',
708 +// zIndex: 0,
709 +// },
710 +// backgroundColor: '#FFF9F3',
711 +// borderRadiusGroup: [0, 25, 25, 0],
712 +// },
713 +// { // 活动地点背景图
714 +// x: 40,
715 +// y: 830,
716 +// height: 75,
717 +// paddingLeft: 80,
718 +// paddingRight: 0,
719 +// borderWidth: 0,
720 +// text: {
721 +// x: 0,
722 +// y: 0,
723 +// text: '上海市黄浦区南京东路',
724 +// fontSize: 40,
725 +// color: '#222',
726 +// opacity: 1,
727 +// baseLine: 'top',
728 +// lineHeight: 48,
729 +// lineNum: 2,
730 +// textAlign: 'left',
731 +// zIndex: 0,
732 +// },
733 +// backgroundColor: '#FFF9F3',
734 +// borderRadiusGroup: [0, 25, 25, 0],
735 +// },
736 +// { // 下部分canvas画布高度
737 +// x: 40,
738 +// y: 1060,
739 +// width: 950,
740 +// height: 250,
741 +// paddingLeft: 0,
742 +// paddingRight: 0,
743 +// borderWidth: 1,
744 +// borderColor: '#fff',
745 +// backgroundColor: '#fff',
746 +// borderRadiusGroup: [0, 0, 16, 16],
747 +// }
748 +// ],
749 +// texts: [
750 +// {
751 +// x: 80,
752 +// y: 630,
753 +// text: activityData.value.title,
754 +// fontSize: 50,
755 +// color: '#000',
756 +// opacity: 1,
757 +// baseLine: 'middle',
758 +// lineHeight: 60,
759 +// lineNum: 2,
760 +// textAlign: 'left',
761 +// width: 800,
762 +// zIndex: 999,
763 +// fontFamily: 'Monospace',
764 +// },
765 +// {
766 +// x: 135,
767 +// y: 770,
768 +// text: activityData.value.dateRange,
769 +// fontSize: 40,
770 +// color: '#222',
771 +// opacity: 1,
772 +// baseLine: 'middle',
773 +// lineHeight: 48,
774 +// lineNum: 2,
775 +// textAlign: 'left',
776 +// zIndex: 999,
777 +// },
778 +// {
779 +// x: 135,
780 +// y: 870,
781 +// text: '上海市黄浦区南京东路',
782 +// fontSize: 40,
783 +// color: '#222',
784 +// opacity: 1,
785 +// baseLine: 'middle',
786 +// lineHeight: 48,
787 +// lineNum: 2,
788 +// textAlign: 'left',
789 +// zIndex: 999,
790 +// },
791 +// {
792 +// x: 300,
793 +// y: 1150,
794 +// text: nickname.value,
795 +// fontSize: 50,
796 +// color: '#333',
797 +// opacity: 1,
798 +// baseLine: 'middle',
799 +// textAlign: 'left',
800 +// lineHeight: 50,
801 +// lineNum: 1,
802 +// zIndex: 999,
803 +// },
804 +// {
805 +// x: 300,
806 +// y: 1220,
807 +// text: '邀请你一起来活动!',
808 +// fontSize: 42,
809 +// color: '#8F9399',
810 +// opacity: 1,
811 +// baseLine: 'middle',
812 +// textAlign: 'left',
813 +// lineHeight: 42,
814 +// lineNum: 1,
815 +// zIndex: 999,
816 +// }
817 +// ],
818 +// images: [
819 +// {
820 +// url: qrcode_url,
821 +// width: 949,
822 +// height: 108,
823 +// x: 40,
824 +// y: 960,
825 +// zIndex: 10,
826 +// },
827 +// {
828 +// url: qrcode_url,
829 +// width: 950,
830 +// height: 500,
831 +// x: 40,
832 +// y: 20,
833 +// borderRadiusGroup: [18, 18, 0, 0],
834 +// zIndex: 10,
835 +// },
836 +// {
837 +// url: qrcode_url,
838 +// width: 40,
839 +// height: 40,
840 +// x: 80,
841 +// y: 750,
842 +// borderRadius: 100,
843 +// borderWidth: 0,
844 +// zIndex: 10,
845 +// },
846 +// {
847 +// url: qrcode_url,
848 +// width: 35,
849 +// height: 40,
850 +// x: 80,
851 +// y: 850,
852 +// borderRadius: 100,
853 +// borderWidth: 0,
854 +// zIndex: 10,
855 +// },
856 +// {
857 +// url: qrcode_url,
858 +// width: 170,
859 +// height: 170,
860 +// x: 80,
861 +// y: 1090,
862 +// borderRadius: 100,
863 +// borderWidth: 0,
864 +// zIndex: 10,
865 +// },
866 +// {
867 +// url: qrcode_url,
868 +// width: 170,
869 +// height: 170,
870 +// x: 750,
871 +// y: 1090,
872 +// borderRadius: 100,
873 +// borderWidth: 0,
874 +// zIndex: 10,
875 +// },
876 +// ],
877 +// lines: []
878 +// }
879 +
880 +// startDraw.value = true
881 +// if (!posterPath.value) Taro.showLoading({ title: '生成海报中...' })
882 +// }
883 +
884 +/**
885 + * 海报绘制成功回调
886 + */
887 +const drawSuccess = result => {
888 + console.log('绘制成功', result)
889 + const { tempFilePath, errMsg } = result
890 + if (errMsg === 'canvasToTempFilePath:ok') {
891 + posterPath.value = tempFilePath
892 + Taro.hideLoading()
893 + } else {
894 + Taro.hideLoading()
895 + Taro.showToast({
896 + title: '生成失败,请稍后重试',
897 + icon: 'none',
898 + duration: 2500,
899 + })
900 + }
901 +}
902 +
903 +/**
904 + * 海报绘制失败回调
905 + */
906 +const drawFail = result => {
907 + console.log('绘制失败', result)
908 + Taro.hideLoading()
909 + Taro.showToast({
910 + title: '生成失败,请稍后重试',
911 + icon: 'none',
912 + duration: 2500,
913 + })
914 +}
915 +
916 +/**
917 + * 保存海报到相册
918 + */
919 +const savePoster = () => {
920 + Taro.saveImageToPhotosAlbum({
921 + filePath: posterPath.value,
922 + success() {
923 + Taro.showToast({
924 + title: '已保存到相册',
925 + icon: 'success',
926 + duration: 2000,
927 + })
928 + },
929 + fail() {
930 + Taro.showToast({
931 + title: '保存失败',
932 + icon: 'none',
933 + duration: 2000,
934 + })
935 + },
936 + })
937 +}
938 +
939 +/**
940 + * 将 API 数据转换为页面需要的 activityData 格式
941 + * @param {Object} apiData - API 返回的活动详情数据
942 + * @returns {Object} 页面活动数据对象
943 + */
944 +const transformApiDataToActivityData = apiData => {
945 + if (!apiData) {
946 + return null
947 + }
948 +
949 + // 生成日期范围字符串
950 + const dateRange = `${apiData.begin_date} - ${apiData.end_date}`
951 +
952 + // 根据积分规则生成规则描述
953 + const rules = [
954 + `打卡任意1关,视为参与,奖励${apiData.first_checkin_points}积分`,
955 + `打卡任意${apiData.required_checkin_count}关,视为完成,奖励${apiData.complete_points}积分`,
956 + '不需要区分打卡点的先后次序',
957 + ]
958 +
959 + // 生成奖励描述
960 + const rewards = [
961 + `首次打卡获得${apiData.first_checkin_points}积分`,
962 + `完成${apiData.required_checkin_count}个打卡点获得${apiData.complete_points}积分`,
963 + apiData.discount_title || '打卡点专属优惠',
964 + ]
965 +
966 + return {
967 + title: apiData.tittle || '活动标题',
968 + subtitle: '探索城市魅力,感受时尚脉搏',
969 + dateRange: dateRange,
970 + posterUrl: apiData.cover || defaultPoster.value,
971 + description: `欢迎参加${apiData.tittle}活动!`,
972 + rules: rules,
973 + rewards: rewards,
974 + }
975 +}
976 +
977 +/**
978 + * 获取活动详情
979 + */
980 +const fetchActivityDetail = async () => {
981 + try {
982 + // 如果没有活动ID,不获取详情
983 + if (!activityId.value) {
984 + console.warn('[ActivitiesCover] 未提供活动ID,跳过详情获取')
985 + return
986 + }
987 +
988 + console.log('[ActivitiesCover] 开始获取活动详情, ID:', activityId.value)
989 +
990 + // 根据环境选择真实 API 或 mock API
991 + const response = USE_MOCK_DATA
992 + ? await mockMapActivityDetailAPI({ id: activityId.value })
993 + : await detailAPI({ id: activityId.value })
994 +
995 + if (response.code === 1 && response.data) {
996 + console.log('[ActivitiesCover] 活动详情获取成功:', response.data)
997 +
998 + // 转换 API 数据为页面格式
999 + const transformedData = transformApiDataToActivityData(response.data)
1000 + if (transformedData) {
1001 + activityData.value = transformedData
1002 +
1003 + // 更新默认海报图
1004 + if (response.data.cover) {
1005 + defaultPoster.value = response.data.cover
1006 + }
1007 +
1008 + // 更新活动状态
1009 + activityStatus.value.is_begin = Boolean(response.data.is_begin)
1010 + activityStatus.value.is_ended = Boolean(response.data.is_ended)
1011 + }
1012 + } else {
1013 + console.warn('[ActivitiesCover] 获取活动详情失败:', response.msg)
1014 + }
1015 + } catch (error) {
1016 + console.error('[ActivitiesCover] 获取活动详情异常:', error)
1017 + }
1018 +}
1019 +
1020 +/**
1021 + * 初始化页面数据
1022 + */
1023 +const initPageData = async () => {
1024 + // 获取用户是否加入家庭
1025 + const { code, data } = await getMyFamiliesAPI()
1026 + if (code) {
1027 + // 如果加入家庭
1028 + if (data?.families?.length) {
1029 + hasJoinedFamily.value = true
1030 + }
1031 + }
1032 +
1033 + // 获取活动详情(包含活动状态)
1034 + await fetchActivityDetail()
1035 +
1036 + // 检查定位授权状态(不获取位置,只检查权限)
1037 + await checkLocationAuth()
1038 +}
1039 +
1040 +// 处理页面加载时的授权检查
1041 +useLoad(options => {
1042 + console.log('[ActivitiesCover] 页面加载, 参数:', options)
1043 +
1044 + // 获取活动 ID(如果有)
1045 + if (options.id) {
1046 + activityId.value = options.id
1047 + } else if (options.activity_id) {
1048 + activityId.value = options.activity_id
1049 + } else {
1050 + // 如果没有活动ID,使用默认ID
1051 + activityId.value = '1'
1052 + console.warn('[ActivitiesCover] 未提供活动ID,使用默认ID: 1')
1053 + }
1054 +
1055 + // 处理分享页面的授权逻辑
1056 + handleSharePageAuth(options, () => {
1057 + initPageData()
1058 + })
1059 +})
1060 +
1061 +// 页面挂载时检查定位授权状态
1062 +onMounted(async () => {
1063 + // 获取系统信息
1064 + getSystemInfo()
1065 +
1066 + initPageData()
1067 +})
1068 +</script>
1069 +
1070 +<script>
1071 +export default {
1072 + name: 'ActivitiesCover',
1073 +}
1074 +</script>