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 +}
This diff is collapsed. Click to expand it.