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/ ...@@ -40,3 +40,4 @@ coverage/
40 .claude/ 40 .claude/
41 CLAUDE.md 41 CLAUDE.md
42 **/CLAUDE.md 42 **/CLAUDE.md
43 +.tmp/
......
1 +# ActivitiesCover 页面测试指南
2 +
3 +**测试日期**: 2026-02-09
4 +**测试目标**: 验证 map_activity detail API 是否能覆盖 ActivitiesCover 页面的数据需求
5 +
6 +---
7 +
8 +## 📊 数据覆盖分析
9 +
10 +### ✅ API 完全覆盖的字段
11 +
12 +| 页面需求 | API 字段 | 数据类型 | 状态 |
13 +|---------|---------|---------|------|
14 +| 活动标题 | `tittle` | string | ✅ 完全匹配 |
15 +| 封面图 | `cover` | string | ✅ 完全匹配 |
16 +| 开始时间 | `begin_date` | string | ✅ 完全匹配 |
17 +| 结束时间 | `end_date` | string | ✅ 完全匹配 |
18 +| 活动是否开始 | `is_begin` | boolean | ✅ 完全匹配 |
19 +| 活动是否结束 | `is_ended` | boolean | ✅ 完全匹配 |
20 +| 首次打卡积分 | `first_checkin_points` | integer | ✅ 完全匹配 |
21 +| 完成打卡积分 | `complete_points` | integer | ✅ 完全匹配 |
22 +| 需要打卡次数 | `required_checkin_count` | integer | ✅ 完全匹配 |
23 +
24 +### ⚠️ 需要转换的字段
25 +
26 +| 页面需求 | 数据来源 | 转换逻辑 | 状态 |
27 +|---------|---------|---------|------|
28 +| 副标题 (`subtitle`) | 硬编码 | 固定文案 | ✅ 已处理 |
29 +| 日期范围 (`dateRange`) | `begin_date` + `end_date` | 字符串拼接 | ✅ 已处理 |
30 +| 活动描述 (`description`) | API 数据 | 模板生成 | ✅ 已处理 |
31 +| 活动规则 (`rules`) | 积分相关字段 | 数组生成 | ✅ 已处理 |
32 +| 奖励列表 (`rewards`) | 积分相关字段 | 数组生成 | ✅ 已处理 |
33 +
34 +---
35 +
36 +## 🔧 代码修改说明
37 +
38 +### 1. 新增导入
39 +
40 +```javascript
41 +import { detailAPI } from '@/api/map_activity'
42 +import { mockMapActivityDetailAPI } from '@/utils/mockData'
43 +```
44 +
45 +### 2. 添加环境变量
46 +
47 +```javascript
48 +// 开发环境使用 mock 数据,生产环境使用真实 API
49 +const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
50 +```
51 +
52 +### 3. 数据转换函数
53 +
54 +```javascript
55 +/**
56 + * 将 API 数据转换为页面需要的 activityData 格式
57 + * @param {Object} apiData - API 返回的活动详情数据
58 + * @returns {Object} 页面活动数据对象
59 + */
60 +const transformApiDataToActivityData = (apiData) => {
61 + if (!apiData) return null
62 +
63 + // 生成日期范围字符串
64 + const dateRange = `${apiData.begin_date} - ${apiData.end_date}`
65 +
66 + // 根据积分规则生成规则描述
67 + const rules = [
68 + `打卡任意1关,视为参与,奖励${apiData.first_checkin_points}积分`,
69 + `打卡任意${apiData.required_checkin_count}关,视为完成,奖励${apiData.complete_points}积分`,
70 + '不需要区分打卡点的先后次序'
71 + ]
72 +
73 + // 生成奖励描述
74 + const rewards = [
75 + `首次打卡获得${apiData.first_checkin_points}积分`,
76 + `完成${apiData.required_checkin_count}个打卡点获得${apiData.complete_points}积分`,
77 + apiData.discount_title || '打卡点专属优惠'
78 + ]
79 +
80 + return {
81 + title: apiData.tittle || '活动标题',
82 + subtitle: '探索城市魅力,感受时尚脉搏',
83 + dateRange: dateRange,
84 + posterUrl: apiData.cover || defaultPoster.value,
85 + description: `欢迎参加${apiData.tittle}活动!`,
86 + rules: rules,
87 + rewards: rewards
88 + }
89 +}
90 +```
91 +
92 +### 4. 获取活动详情
93 +
94 +```javascript
95 +/**
96 + * 获取活动详情
97 + */
98 +const fetchActivityDetail = async () => {
99 + try {
100 + if (!activityId.value) {
101 + console.warn('[ActivitiesCover] 未提供活动ID,跳过详情获取')
102 + return
103 + }
104 +
105 + console.log('[ActivitiesCover] 开始获取活动详情, ID:', activityId.value)
106 +
107 + // 根据环境选择真实 API 或 mock API
108 + const response = USE_MOCK_DATA
109 + ? await mockMapActivityDetailAPI({ id: activityId.value })
110 + : await detailAPI({ id: activityId.value })
111 +
112 + if (response.code === 1 && response.data) {
113 + console.log('[ActivitiesCover] 活动详情获取成功:', response.data)
114 +
115 + // 转换 API 数据为页面格式
116 + const transformedData = transformApiDataToActivityData(response.data)
117 + if (transformedData) {
118 + activityData.value = transformedData
119 +
120 + // 更新默认海报图
121 + if (response.data.cover) {
122 + defaultPoster.value = response.data.cover
123 + }
124 +
125 + // 更新活动状态
126 + activityStatus.value.is_begin = Boolean(response.data.is_begin)
127 + activityStatus.value.is_ended = Boolean(response.data.is_ended)
128 + }
129 + }
130 + } catch (error) {
131 + console.error('[ActivitiesCover] 获取活动详情异常:', error)
132 + }
133 +}
134 +```
135 +
136 +### 5. 移除旧逻辑
137 +
138 +- ❌ 移除了 `fetchActivityStatus` 函数(不再需要单独获取活动状态)
139 +- ❌ 移除了 `getActivityStatusAPI` 导入
140 +- ✅ 活动状态现在通过 `fetchActivityDetail` 统一获取
141 +
142 +### 6. 动态积分规则显示
143 +
144 +```vue
145 +<!-- 积分规则说明 - 使用 v-for 动态渲染 -->
146 +<view v-if="activityData.rules && activityData.rules.length" class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4 opacity-90">
147 + <text class="text-blue-500 text-base font-medium block mb-2">积分规则说明:</text>
148 + <text
149 + v-for="(rule, index) in activityData.rules"
150 + :key="index"
151 + class="text-blue-500 text-sm leading-relaxed block mb-1"
152 + style="padding-left: 20rpx; text-indent: -20rpx;"
153 + >
154 + • {{ rule }}
155 + </text>
156 +</view>
157 +```
158 +
159 +---
160 +
161 +## 🧪 测试步骤
162 +
163 +### 方式 1: Mock 数据测试(开发环境)
164 +
165 +```bash
166 +# 1. 启动开发服务器
167 +pnpm run dev:weapp
168 +
169 +# 2. 打开微信开发者工具,导入 dist 目录
170 +
171 +# 3. 访问页面(带活动 ID 参数)
172 +/pages/ActivitiesCover/index?id=1
173 +# 或
174 +/pages/ActivitiesCover/index?activity_id=1
175 +
176 +# 4. 检查控制台日志,应该看到:
177 +# [ActivitiesCover] 页面加载, 参数: {id: "1"}
178 +# [ActivitiesCover] 开始获取活动详情, ID: 1
179 +# [Mock] detailAPI - 活动详情,ID:1
180 +# [ActivitiesCover] 活动详情获取成功: {...}
181 +```
182 +
183 +**预期结果**:
184 +- ✅ 页面显示活动标题、封面图
185 +- ✅ 日期范围正确显示
186 +- ✅ 积分规则动态显示(不是硬编码)
187 +- ✅ "立即参加" 按钮状态正确(根据活动状态)
188 +
189 +### 方式 2: 真实 API 测试(生产环境)
190 +
191 +```bash
192 +# 1. 修改环境变量(确保不是开发环境)
193 +# NODE_ENV=production
194 +
195 +# 2. 重新构建
196 +pnpm run build:weapp
197 +
198 +# 3. 在微信开发者工具中测试
199 +```
200 +
201 +**预期结果**:
202 +- ✅ 真实 API 数据正确显示
203 +- ✅ 所有字段正确映射
204 +
205 +---
206 +
207 +## 📝 Mock 数据示例
208 +
209 +### 输入参数
210 +
211 +```javascript
212 +{
213 + id: "1"
214 +}
215 +```
216 +
217 +### Mock 返回数据
218 +
219 +```javascript
220 +{
221 + code: 1,
222 + msg: "success",
223 + data: {
224 + url: "https://example.com/map",
225 + id: "1",
226 + cover: "https://picsum.photos/400/300?random=1",
227 + tittle: "公园晨跑打卡",
228 + begin_date: "2025.01.15",
229 + end_date: "2025.02.28",
230 + is_ended: false,
231 + is_begin: true,
232 + first_checkin_points: 10,
233 + required_checkin_count: 5,
234 + complete_points: 50,
235 + discount_title: "打卡点优惠信息"
236 + }
237 +}
238 +```
239 +
240 +### 转换后的页面数据
241 +
242 +```javascript
243 +{
244 + title: "公园晨跑打卡",
245 + subtitle: "探索城市魅力,感受时尚脉搏",
246 + dateRange: "2025.01.15 - 2025.02.28",
247 + posterUrl: "https://picsum.photos/400/300?random=1",
248 + description: "欢迎参加公园晨跑打卡活动!",
249 + rules: [
250 + "打卡任意1关,视为参与,奖励10积分",
251 + "打卡任意5关,视为完成,奖励50积分",
252 + "不需要区分打卡点的先后次序"
253 + ],
254 + rewards: [
255 + "首次打卡获得10积分",
256 + "完成5个打卡点获得50积分",
257 + "打卡点优惠信息"
258 + ]
259 +}
260 +```
261 +
262 +---
263 +
264 +## ✅ 验收标准
265 +
266 +### 功能验收
267 +
268 +- [ ] 页面能正确加载活动详情
269 +- [ ] 活动标题正确显示
270 +- [ ] 封面图正确显示
271 +- [ ] 日期范围格式正确
272 +- [ ] 积分规则动态生成并显示
273 +- [ ] 活动状态(已开始/已结束)正确反映在按钮状态上
274 +
275 +### 技术验收
276 +
277 +- [ ] 开发环境使用 mock 数据
278 +- [ ] 生产环境使用真实 API
279 +- [ ] 数据转换逻辑正确
280 +- [ ] 错误处理完善
281 +- [ ] 控制台日志清晰
282 +
283 +---
284 +
285 +## 🐛 已知问题
286 +
287 +### 1. API 字段拼写问题
288 +
289 +**问题描述**: API 返回的字段是 `tittle`(拼写错误),不是 `title`
290 +
291 +**解决方案**: 代码中使用 `apiData.tittle`,与 API 保持一致
292 +
293 +### 2. 活动 ID 参数
294 +
295 +**问题描述**: 可能使用 `id``activity_id` 作为参数名
296 +
297 +**解决方案**:
298 +```javascript
299 +// 支持两种参数名
300 +if (options.id) {
301 + activityId.value = options.id
302 +} else if (options.activity_id) {
303 + activityId.value = options.activity_id
304 +} else {
305 + // 默认使用 ID: 1
306 + activityId.value = '1'
307 +}
308 +```
309 +
310 +---
311 +
312 +## 📚 相关文件
313 +
314 +- **页面代码**: `src/pages/ActivitiesCover/index.vue`
315 +- **API 定义**: `src/api/map_activity.js`
316 +- **Mock 数据**: `src/utils/mockData.js`
317 +- **API 文档**: `docs/api-specs/map_activity/detail.md`
318 +
319 +---
320 +
321 +## 🎯 下一步
322 +
323 +1. ✅ 完成功能开发
324 +2. ⏳ 进行真机测试
325 +3. ⏳ 测试不同活动 ID
326 +4. ⏳ 测试边界情况(无网络、API 错误等)
327 +5. ⏳ 优化用户体验
328 +
329 +---
330 +
331 +**测试人员**: Claude Code
332 +**测试日期**: 2026-02-09
333 +**测试状态**: ✅ 开发完成,等待测试
...@@ -8,11 +8,7 @@ ...@@ -8,11 +8,7 @@
8 <template> 8 <template>
9 <view class="activities-cover-container"> 9 <view class="activities-cover-container">
10 <!-- 背景图片 --> 10 <!-- 背景图片 -->
11 - <image 11 + <image :src="defaultPoster" class="background-image" :mode="imageDisplayMode" />
12 - :src="defaultPoster"
13 - class="background-image"
14 - :mode="imageDisplayMode"
15 - />
16 12
17 <!-- 分享按钮组件 --> 13 <!-- 分享按钮组件 -->
18 <ShareButton 14 <ShareButton
...@@ -26,14 +22,26 @@ ...@@ -26,14 +22,26 @@
26 <!-- 底部按钮区域 --> 22 <!-- 底部按钮区域 -->
27 <view class="bottom-section"> 23 <view class="bottom-section">
28 <!-- 积分规则说明 --> 24 <!-- 积分规则说明 -->
29 - <view class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4 opacity-90"> 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 + >
30 <text class="text-blue-500 text-base font-medium block mb-2">积分规则说明:</text> 29 <text class="text-blue-500 text-base font-medium block mb-2">积分规则说明:</text>
31 - <text class="text-blue-500 text-sm leading-relaxed block mb-1" style="padding-left: 20rpx; text-indent: -20rpx;">• 打卡任意1关,视为参与,奖励1000积分</text> 30 + <text
32 - <text class="text-blue-500 text-sm leading-relaxed block mb-1" style="padding-left: 20rpx; text-indent: -20rpx;">• 打卡任意7关,视为完成,奖励5000积分</text> 31 + v-for="(rule, index) in activityData.rules"
33 - <text class="text-blue-500 text-sm leading-relaxed block mb-1" style="padding-left: 20rpx; text-indent: -20rpx;">• 不需要区分打卡点的先后次序</text> 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>
34 </view> 38 </view>
35 <!-- 未授权定位提示 - 仅在用户点击参加活动且未授权时显示 --> 39 <!-- 未授权定位提示 - 仅在用户点击参加活动且未授权时显示 -->
36 - <view v-if="showLocationPrompt && !hasLocationAuth && !locationError" class="location-tip" @click="retryGetLocation"> 40 + <view
41 + v-if="showLocationPrompt && !hasLocationAuth && !locationError"
42 + class="location-tip"
43 + @click="retryGetLocation"
44 + >
37 <view class="tip-content"> 45 <view class="tip-content">
38 <view class="tip-icon">📍</view> 46 <view class="tip-icon">📍</view>
39 <view class="tip-text">点击获取您的位置信息来参与活动</view> 47 <view class="tip-text">点击获取您的位置信息来参与活动</view>
...@@ -42,7 +50,11 @@ ...@@ -42,7 +50,11 @@
42 </view> 50 </view>
43 51
44 <!-- 位置获取失败提示 --> 52 <!-- 位置获取失败提示 -->
45 - <view v-if="hasLocationAuth && locationError" class="location-tip location-error" @click="retryGetLocation"> 53 + <view
54 + v-if="hasLocationAuth && locationError"
55 + class="location-tip location-error"
56 + @click="retryGetLocation"
57 + >
46 <view class="tip-content"> 58 <view class="tip-content">
47 <view class="tip-icon">⚠️</view> 59 <view class="tip-icon">⚠️</view>
48 <view class="tip-text">可能是网络问题,获取位置信息失败</view> 60 <view class="tip-text">可能是网络问题,获取位置信息失败</view>
...@@ -54,7 +66,9 @@ ...@@ -54,7 +66,9 @@
54 type="primary" 66 type="primary"
55 size="large" 67 size="large"
56 class="join-button" 68 class="join-button"
57 - :color="activityStatus.is_ended || !activityStatus.is_begin ? '#cccccc' : THEME_COLORS.PRIMARY" 69 + :color="
70 + activityStatus.is_ended || !activityStatus.is_begin ? '#cccccc' : THEME_COLORS.PRIMARY
71 + "
58 :loading="isJoining || activityStatus.loading" 72 :loading="isJoining || activityStatus.loading"
59 :disabled="activityStatus.is_ended || !activityStatus.is_begin" 73 :disabled="activityStatus.is_ended || !activityStatus.is_begin"
60 @click="checkFamilyStatusAndJoinActivity" 74 @click="checkFamilyStatusAndJoinActivity"
...@@ -66,14 +80,8 @@ ...@@ -66,14 +80,8 @@
66 <!-- 底部导航 --> 80 <!-- 底部导航 -->
67 <BottomNav /> 81 <BottomNav />
68 82
69 -
70 -
71 <!-- 海报预览弹窗 --> 83 <!-- 海报预览弹窗 -->
72 - <nut-popup 84 + <nut-popup v-model:visible="show_post" position="center" class="poster-preview-popup">
73 - v-model:visible="show_post"
74 - position="center"
75 - class="poster-preview-popup"
76 - >
77 <view class="wrapper"> 85 <view class="wrapper">
78 <view class="preview-area" @click="onClickPost"> 86 <view class="preview-area" @click="onClickPost">
79 <image v-if="posterPath" :src="posterPath" mode="widthFix" /> 87 <image v-if="posterPath" :src="posterPath" mode="widthFix" />
...@@ -100,24 +108,16 @@ ...@@ -100,24 +108,16 @@
100 /> 108 />
101 109
102 <!-- 位置权限申请弹窗 --> 110 <!-- 位置权限申请弹窗 -->
103 - <nut-dialog 111 + <nut-dialog v-model:visible="showLocationDialog" title="位置权限申请">
104 - v-model:visible="showLocationDialog"
105 - title="位置权限申请"
106 - >
107 <template #default> 112 <template #default>
108 - <view class=" text-gray-700 leading-loose text-sm text-left"> 113 + <view class="text-gray-700 leading-loose text-sm text-left">
109 {{ locationContent }} 114 {{ locationContent }}
110 </view> 115 </view>
111 </template> 116 </template>
112 <template #footer> 117 <template #footer>
113 <nut-row :gutter="10"> 118 <nut-row :gutter="10">
114 <nut-col :span="12"> 119 <nut-col :span="12">
115 - <nut-button 120 + <nut-button @click="onLocationCancel" type="default" size="normal" block>
116 - @click="onLocationCancel"
117 - type="default"
118 - size="normal"
119 - block
120 - >
121 暂不授权 121 暂不授权
122 </nut-button> 122 </nut-button>
123 </nut-col> 123 </nut-col>
...@@ -139,60 +139,70 @@ ...@@ -139,60 +139,70 @@
139 </template> 139 </template>
140 140
141 <script setup> 141 <script setup>
142 -import { ref, onMounted, computed } from "vue" 142 +import { ref, onMounted, computed } from 'vue'
143 import Taro, { useLoad } from '@tarojs/taro' 143 import Taro, { useLoad } from '@tarojs/taro'
144 -import "./index.less" 144 +import './index.less'
145 import BottomNav from '../../components/BottomNav.vue' 145 import BottomNav from '../../components/BottomNav.vue'
146 import PosterBuilder from '../../components/PosterBuilder/index.vue' 146 import PosterBuilder from '../../components/PosterBuilder/index.vue'
147 import ShareButton from '../../components/ShareButton/index.vue' 147 import ShareButton from '../../components/ShareButton/index.vue'
148 // 接口信息 148 // 接口信息
149 import { getMyFamiliesAPI } from '@/api/family' 149 import { getMyFamiliesAPI } from '@/api/family'
150 -import { getActivityStatusAPI } from '@/api/map' 150 +import { detailAPI } from '@/api/map_activity'
151 import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect' 151 import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect'
152 // 导入主题颜色 152 // 导入主题颜色
153 -import { THEME_COLORS } from '@/utils/config'; 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'
154 159
155 // 默认海报图 160 // 默认海报图
156 -const defaultPoster = 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_8.jpg?imageMogr2/strip/quality/60'; 161 +const defaultPoster = ref(
162 + 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_8.jpg?imageMogr2/strip/quality/60'
163 +)
157 164
158 // 系统信息 165 // 系统信息
159 -const systemInfo = ref({}); 166 +const systemInfo = ref({})
167 +
168 +// 活动ID(从 URL 参数获取)
169 +const activityId = ref('')
160 170
161 /** 171 /**
162 * 获取系统信息 172 * 获取系统信息
163 */ 173 */
164 const getSystemInfo = () => { 174 const getSystemInfo = () => {
165 try { 175 try {
166 - const info = Taro.getWindowInfo(); 176 + const info = Taro.getWindowInfo()
167 - systemInfo.value = info; 177 + systemInfo.value = info
168 } catch (error) { 178 } catch (error) {
169 - console.error('获取系统信息失败:', error); 179 + console.error('获取系统信息失败:', error)
170 } 180 }
171 -}; 181 +}
172 182
173 /** 183 /**
174 * 检测是否为 iPad 类型设备 184 * 检测是否为 iPad 类型设备
175 */ 185 */
176 const isTabletDevice = computed(() => { 186 const isTabletDevice = computed(() => {
177 if (!systemInfo.value.screenWidth) { 187 if (!systemInfo.value.screenWidth) {
178 - return false; 188 + return false
179 } 189 }
180 190
181 - const { screenWidth, screenHeight } = systemInfo.value; 191 + const { screenWidth, screenHeight } = systemInfo.value
182 - const screenRatio = screenWidth / screenHeight; 192 + const screenRatio = screenWidth / screenHeight
183 193
184 // iPad 类型设备通常屏幕比例在 0.7-0.8 之间(4:3 约为 0.75) 194 // iPad 类型设备通常屏幕比例在 0.7-0.8 之间(4:3 约为 0.75)
185 // 普通手机设备比例通常在 0.4-0.6 之间 195 // 普通手机设备比例通常在 0.4-0.6 之间
186 - return screenRatio > 0.65; 196 + return screenRatio > 0.65
187 -}); 197 +})
188 198
189 /** 199 /**
190 * 计算图片显示模式 200 * 计算图片显示模式
191 */ 201 */
192 const imageDisplayMode = computed(() => { 202 const imageDisplayMode = computed(() => {
193 // iPad 类型设备使用 widthFix 模式,普通设备使用 aspectFill 203 // iPad 类型设备使用 widthFix 模式,普通设备使用 aspectFill
194 - return isTabletDevice.value ? 'widthFix' : 'aspectFill'; 204 + return isTabletDevice.value ? 'widthFix' : 'aspectFill'
195 -}); 205 +})
196 206
197 /** 207 /**
198 * 活动海报页面组件 208 * 活动海报页面组件
...@@ -204,14 +214,14 @@ const hasLocationAuth = ref(false) // 是否已授权定位 ...@@ -204,14 +214,14 @@ const hasLocationAuth = ref(false) // 是否已授权定位
204 const locationError = ref(false) // 位置获取是否失败 214 const locationError = ref(false) // 位置获取是否失败
205 const isJoining = ref(false) // 是否正在加入活动 215 const isJoining = ref(false) // 是否正在加入活动
206 const userLocation = ref({ lng: null, lat: null }) // 用户位置信息 216 const userLocation = ref({ lng: null, lat: null }) // 用户位置信息
207 -const hasJoinedFamily = ref(false); 217 +const hasJoinedFamily = ref(false)
208 const showLocationPrompt = ref(false) // 是否显示定位权限提示 218 const showLocationPrompt = ref(false) // 是否显示定位权限提示
209 219
210 // 活动状态相关 220 // 活动状态相关
211 const activityStatus = ref({ 221 const activityStatus = ref({
212 is_begin: false, // 活动是否已开始 222 is_begin: false, // 活动是否已开始
213 is_ended: false, // 活动是否已结束 223 is_ended: false, // 活动是否已结束
214 - loading: false // 是否正在加载活动状态 224 + loading: false, // 是否正在加载活动状态
215 }) 225 })
216 226
217 // Dialog 相关状态 227 // Dialog 相关状态
...@@ -219,7 +229,8 @@ const showLocationDialog = ref(false) // 是否显示位置权限申请弹窗 ...@@ -219,7 +229,8 @@ const showLocationDialog = ref(false) // 是否显示位置权限申请弹窗
219 const pendingLocationCallback = ref(null) // 待执行的位置获取回调 229 const pendingLocationCallback = ref(null) // 待执行的位置获取回调
220 230
221 // 位置权限申请说明内容 231 // 位置权限申请说明内容
222 -const locationContent = '为了提供更好的活动体验,我们需要获取您的位置信息:验证您是否在活动区域内, 我们承诺严格保护您的位置隐私,仅用于活动相关功能。' 232 +const locationContent =
233 + '为了提供更好的活动体验,我们需要获取您的位置信息:验证您是否在活动区域内, 我们承诺严格保护您的位置隐私,仅用于活动相关功能。'
223 234
224 // 海报生成相关状态 235 // 海报生成相关状态
225 const show_post = ref(false) // 显示海报预览 236 const show_post = ref(false) // 显示海报预览
...@@ -230,13 +241,15 @@ const nickname = ref('老来赛用户') // 用户昵称 ...@@ -230,13 +241,15 @@ const nickname = ref('老来赛用户') // 用户昵称
230 // const avatar = ref('https://cdn.ipadbiz.cn/icon/tou@2x.png') // 用户头像 241 // const avatar = ref('https://cdn.ipadbiz.cn/icon/tou@2x.png') // 用户头像
231 242
232 // 保存选项 243 // 保存选项
233 -const actions_save = ref([{ 244 +const actions_save = ref([
234 - name: '保存至相册' 245 + {
235 -}]) 246 + name: '保存至相册',
247 + },
248 +])
236 249
237 // 海报配置 250 // 海报配置
238 -let base = {} 251 +const base = {}
239 -let qrcode_url = 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png' // Mock二维码 252 +const qrcode_url = 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png' // Mock二维码
240 253
241 // Mock活动数据 254 // Mock活动数据
242 const activityData = ref({ 255 const activityData = ref({
...@@ -244,19 +257,20 @@ const activityData = ref({ ...@@ -244,19 +257,20 @@ const activityData = ref({
244 subtitle: '探索城市魅力,感受时尚脉搏', 257 subtitle: '探索城市魅力,感受时尚脉搏',
245 dateRange: '2024年1月15日 - 2024年1月31日', 258 dateRange: '2024年1月15日 - 2024年1月31日',
246 posterUrl: 'https://img.yzcdn.cn/vant/cat.jpeg', // 临时使用示例图片 259 posterUrl: 'https://img.yzcdn.cn/vant/cat.jpeg', // 临时使用示例图片
247 - description: '漫步南京路,感受上海的繁华与历史交融。从外滩到人民广场,体验这座城市独特的魅力和时尚气息。', 260 + description:
261 + '漫步南京路,感受上海的繁华与历史交融。从外滩到人民广场,体验这座城市独特的魅力和时尚气息。',
248 rules: [ 262 rules: [
249 '年满60岁的老年人可参与活动', 263 '年满60岁的老年人可参与活动',
250 '需要在指定时间内完成所有打卡点', 264 '需要在指定时间内完成所有打卡点',
251 '每个打卡点需上传照片验证', 265 '每个打卡点需上传照片验证',
252 - '完成全部打卡可获得电子勋章和积分奖励' 266 + '完成全部打卡可获得电子勋章和积分奖励',
253 ], 267 ],
254 rewards: [ 268 rewards: [
255 '完成打卡获得500积分', 269 '完成打卡获得500积分',
256 '获得专属电子勋章', 270 '获得专属电子勋章',
257 '有机会获得商户优惠券', 271 '有机会获得商户优惠券',
258 - '参与月度积分排行榜' 272 + '参与月度积分排行榜',
259 - ] 273 + ],
260 }) 274 })
261 275
262 // 分享配置 276 // 分享配置
...@@ -295,7 +309,7 @@ const getUserLocation = async (skipAuthCheck = false) => { ...@@ -295,7 +309,7 @@ const getUserLocation = async (skipAuthCheck = false) => {
295 309
296 // 如果没有授权,先显示数据用途说明 310 // 如果没有授权,先显示数据用途说明
297 if (hasLocationAuth !== true) { 311 if (hasLocationAuth !== true) {
298 - return new Promise((resolve) => { 312 + return new Promise(resolve => {
299 pendingLocationCallback.value = resolve 313 pendingLocationCallback.value = resolve
300 showLocationDialog.value = true 314 showLocationDialog.value = true
301 }) 315 })
...@@ -306,12 +320,12 @@ const getUserLocation = async (skipAuthCheck = false) => { ...@@ -306,12 +320,12 @@ const getUserLocation = async (skipAuthCheck = false) => {
306 type: 'gcj02', 320 type: 'gcj02',
307 altitude: false, // 不需要海拔信息,提高获取速度 321 altitude: false, // 不需要海拔信息,提高获取速度
308 isHighAccuracy: true, // 开启高精度定位 322 isHighAccuracy: true, // 开启高精度定位
309 - highAccuracyExpireTime: 4000 // 高精度定位超时时间 323 + highAccuracyExpireTime: 4000, // 高精度定位超时时间
310 }) 324 })
311 325
312 userLocation.value = { 326 userLocation.value = {
313 lng: location.longitude, 327 lng: location.longitude,
314 - lat: location.latitude 328 + lat: location.latitude,
315 } 329 }
316 330
317 console.log('获取到用户位置:', userLocation.value) 331 console.log('获取到用户位置:', userLocation.value)
...@@ -330,11 +344,11 @@ const getUserLocation = async (skipAuthCheck = false) => { ...@@ -330,11 +344,11 @@ const getUserLocation = async (skipAuthCheck = false) => {
330 title: '需要位置权限', 344 title: '需要位置权限',
331 content: '参与活动需要获取您的位置信息,请在设置中开启位置权限', 345 content: '参与活动需要获取您的位置信息,请在设置中开启位置权限',
332 confirmText: '去设置', 346 confirmText: '去设置',
333 - success: (res) => { 347 + success: res => {
334 if (res.confirm) { 348 if (res.confirm) {
335 Taro.openSetting() 349 Taro.openSetting()
336 } 350 }
337 - } 351 + },
338 }) 352 })
339 } else if (error.errMsg && error.errMsg.includes('timeout')) { 353 } else if (error.errMsg && error.errMsg.includes('timeout')) {
340 // 定位超时 354 // 定位超时
...@@ -342,7 +356,7 @@ const getUserLocation = async (skipAuthCheck = false) => { ...@@ -342,7 +356,7 @@ const getUserLocation = async (skipAuthCheck = false) => {
342 Taro.showToast({ 356 Taro.showToast({
343 title: '定位超时,请检查网络或GPS', 357 title: '定位超时,请检查网络或GPS',
344 icon: 'none', 358 icon: 'none',
345 - duration: 3000 359 + duration: 3000,
346 }) 360 })
347 } else if (error.errMsg && error.errMsg.includes('fail')) { 361 } else if (error.errMsg && error.errMsg.includes('fail')) {
348 // 定位失败,可能是GPS关闭或网络问题 362 // 定位失败,可能是GPS关闭或网络问题
...@@ -351,14 +365,14 @@ const getUserLocation = async (skipAuthCheck = false) => { ...@@ -351,14 +365,14 @@ const getUserLocation = async (skipAuthCheck = false) => {
351 title: '定位失败', 365 title: '定位失败',
352 content: '请确保已开启GPS定位服务,并检查网络连接是否正常', 366 content: '请确保已开启GPS定位服务,并检查网络连接是否正常',
353 showCancel: false, 367 showCancel: false,
354 - confirmText: '我知道了' 368 + confirmText: '我知道了',
355 }) 369 })
356 } else { 370 } else {
357 // 其他未知错误 371 // 其他未知错误
358 locationError.value = true 372 locationError.value = true
359 Taro.showToast({ 373 Taro.showToast({
360 title: '获取位置失败,请重试', 374 title: '获取位置失败,请重试',
361 - icon: 'none' 375 + icon: 'none',
362 }) 376 })
363 } 377 }
364 378
...@@ -412,7 +426,7 @@ const checkFamilyStatusAndJoinActivity = async () => { ...@@ -412,7 +426,7 @@ const checkFamilyStatusAndJoinActivity = async () => {
412 if (activityStatus.value.is_ended) { 426 if (activityStatus.value.is_ended) {
413 Taro.showToast({ 427 Taro.showToast({
414 title: '活动已结束', 428 title: '活动已结束',
415 - icon: 'none' 429 + icon: 'none',
416 }) 430 })
417 return 431 return
418 } 432 }
...@@ -421,7 +435,7 @@ const checkFamilyStatusAndJoinActivity = async () => { ...@@ -421,7 +435,7 @@ const checkFamilyStatusAndJoinActivity = async () => {
421 if (!activityStatus.value.is_begin) { 435 if (!activityStatus.value.is_begin) {
422 Taro.showToast({ 436 Taro.showToast({
423 title: '活动尚未开始,请耐心等待', 437 title: '活动尚未开始,请耐心等待',
424 - icon: 'none' 438 + icon: 'none',
425 }) 439 })
426 return 440 return
427 } 441 }
...@@ -433,14 +447,14 @@ const checkFamilyStatusAndJoinActivity = async () => { ...@@ -433,14 +447,14 @@ const checkFamilyStatusAndJoinActivity = async () => {
433 content: '没有加入家庭是无法参加活动的', 447 content: '没有加入家庭是无法参加活动的',
434 cancelText: '关闭', 448 cancelText: '关闭',
435 confirmText: '前往加入', 449 confirmText: '前往加入',
436 - success: (res) => { 450 + success: res => {
437 if (res.confirm) { 451 if (res.confirm) {
438 Taro.redirectTo({ 452 Taro.redirectTo({
439 url: '/pages/Welcome/index', 453 url: '/pages/Welcome/index',
440 - }); 454 + })
441 } 455 }
442 }, 456 },
443 - }); 457 + })
444 return 458 return
445 } 459 }
446 460
...@@ -459,8 +473,7 @@ const checkFamilyStatusAndJoinActivity = async () => { ...@@ -459,8 +473,7 @@ const checkFamilyStatusAndJoinActivity = async () => {
459 473
460 // 正常参加活动流程 474 // 正常参加活动流程
461 await handleJoinActivity() 475 await handleJoinActivity()
462 -}; 476 +}
463 -
464 477
465 /** 478 /**
466 * 重新获取位置信息 479 * 重新获取位置信息
...@@ -473,7 +486,7 @@ const retryGetLocation = async () => { ...@@ -473,7 +486,7 @@ const retryGetLocation = async () => {
473 locationError.value = false 486 locationError.value = false
474 Taro.showToast({ 487 Taro.showToast({
475 title: '位置获取成功', 488 title: '位置获取成功',
476 - icon: 'success' 489 + icon: 'success',
477 }) 490 })
478 } 491 }
479 } catch (error) { 492 } catch (error) {
...@@ -489,7 +502,7 @@ const onLocationCancel = () => { ...@@ -489,7 +502,7 @@ const onLocationCancel = () => {
489 if (pendingLocationCallback.value) { 502 if (pendingLocationCallback.value) {
490 Taro.showToast({ 503 Taro.showToast({
491 title: '需要位置权限才能参与活动', 504 title: '需要位置权限才能参与活动',
492 - icon: 'none' 505 + icon: 'none',
493 }) 506 })
494 pendingLocationCallback.value(false) 507 pendingLocationCallback.value(false)
495 pendingLocationCallback.value = null 508 pendingLocationCallback.value = null
...@@ -507,12 +520,12 @@ const onLocationConfirm = async () => { ...@@ -507,12 +520,12 @@ const onLocationConfirm = async () => {
507 type: 'gcj02', 520 type: 'gcj02',
508 altitude: false, 521 altitude: false,
509 isHighAccuracy: true, 522 isHighAccuracy: true,
510 - highAccuracyExpireTime: 4000 523 + highAccuracyExpireTime: 4000,
511 }) 524 })
512 525
513 userLocation.value = { 526 userLocation.value = {
514 lng: location.longitude, 527 lng: location.longitude,
515 - lat: location.latitude 528 + lat: location.latitude,
516 } 529 }
517 530
518 console.log('获取到用户位置:', userLocation.value) 531 console.log('获取到用户位置:', userLocation.value)
...@@ -567,14 +580,13 @@ const handleJoinActivity = async () => { ...@@ -567,14 +580,13 @@ const handleJoinActivity = async () => {
567 580
568 // 跳转到Activities页面,并传递位置参数 581 // 跳转到Activities页面,并传递位置参数
569 await Taro.navigateTo({ 582 await Taro.navigateTo({
570 - url: `/pages/Activities/index?current_lng=${userLocation.value.lng}&current_lat=${userLocation.value.lat}` 583 + url: `/pages/Activities/index?current_lng=${userLocation.value.lng}&current_lat=${userLocation.value.lat}`,
571 }) 584 })
572 -
573 } catch (error) { 585 } catch (error) {
574 console.error('参加活动失败:', error) 586 console.error('参加活动失败:', error)
575 Taro.showToast({ 587 Taro.showToast({
576 title: '参加活动失败', 588 title: '参加活动失败',
577 - icon: 'none' 589 + icon: 'none',
578 }) 590 })
579 } finally { 591 } finally {
580 isJoining.value = false 592 isJoining.value = false
...@@ -602,12 +614,12 @@ const onShareAppMessage = () => { ...@@ -602,12 +614,12 @@ const onShareAppMessage = () => {
602 return { 614 return {
603 title: '主题路线打卡活动等你参与', 615 title: '主题路线打卡活动等你参与',
604 path: addShareFlag('/pages/ActivitiesCover/index'), 616 path: addShareFlag('/pages/ActivitiesCover/index'),
605 - success: (res) => { 617 + success: res => {
606 // 分享成功 618 // 分享成功
607 }, 619 },
608 - fail: (err) => { 620 + fail: err => {
609 // 分享失败 621 // 分享失败
610 - } 622 + },
611 } 623 }
612 } 624 }
613 625
...@@ -644,7 +656,7 @@ const onCancelSave = () => { ...@@ -644,7 +656,7 @@ const onCancelSave = () => {
644 /** 656 /**
645 * 选择保存方式 657 * 选择保存方式
646 */ 658 */
647 -const onSelectSave = (item) => { 659 +const onSelectSave = item => {
648 if (item.name === '保存至相册') { 660 if (item.name === '保存至相册') {
649 show_save.value = false 661 show_save.value = false
650 show_post.value = false 662 show_post.value = false
...@@ -872,7 +884,7 @@ const onSelectSave = (item) => { ...@@ -872,7 +884,7 @@ const onSelectSave = (item) => {
872 /** 884 /**
873 * 海报绘制成功回调 885 * 海报绘制成功回调
874 */ 886 */
875 -const drawSuccess = (result) => { 887 +const drawSuccess = result => {
876 console.log('绘制成功', result) 888 console.log('绘制成功', result)
877 const { tempFilePath, errMsg } = result 889 const { tempFilePath, errMsg } = result
878 if (errMsg === 'canvasToTempFilePath:ok') { 890 if (errMsg === 'canvasToTempFilePath:ok') {
...@@ -883,7 +895,7 @@ const drawSuccess = (result) => { ...@@ -883,7 +895,7 @@ const drawSuccess = (result) => {
883 Taro.showToast({ 895 Taro.showToast({
884 title: '生成失败,请稍后重试', 896 title: '生成失败,请稍后重试',
885 icon: 'none', 897 icon: 'none',
886 - duration: 2500 898 + duration: 2500,
887 }) 899 })
888 } 900 }
889 } 901 }
...@@ -891,13 +903,13 @@ const drawSuccess = (result) => { ...@@ -891,13 +903,13 @@ const drawSuccess = (result) => {
891 /** 903 /**
892 * 海报绘制失败回调 904 * 海报绘制失败回调
893 */ 905 */
894 -const drawFail = (result) => { 906 +const drawFail = result => {
895 console.log('绘制失败', result) 907 console.log('绘制失败', result)
896 Taro.hideLoading() 908 Taro.hideLoading()
897 Taro.showToast({ 909 Taro.showToast({
898 title: '生成失败,请稍后重试', 910 title: '生成失败,请稍后重试',
899 icon: 'none', 911 icon: 'none',
900 - duration: 2500 912 + duration: 2500,
901 }) 913 })
902 } 914 }
903 915
...@@ -911,20 +923,101 @@ const savePoster = () => { ...@@ -911,20 +923,101 @@ const savePoster = () => {
911 Taro.showToast({ 923 Taro.showToast({
912 title: '已保存到相册', 924 title: '已保存到相册',
913 icon: 'success', 925 icon: 'success',
914 - duration: 2000 926 + duration: 2000,
915 }) 927 })
916 }, 928 },
917 fail() { 929 fail() {
918 Taro.showToast({ 930 Taro.showToast({
919 title: '保存失败', 931 title: '保存失败',
920 icon: 'none', 932 icon: 'none',
921 - duration: 2000 933 + duration: 2000,
922 }) 934 })
923 - } 935 + },
924 }) 936 })
925 } 937 }
926 938
927 /** 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 +/**
928 * 初始化页面数据 1021 * 初始化页面数据
929 */ 1022 */
930 const initPageData = async () => { 1023 const initPageData = async () => {
...@@ -937,56 +1030,38 @@ const initPageData = async () => { ...@@ -937,56 +1030,38 @@ const initPageData = async () => {
937 } 1030 }
938 } 1031 }
939 1032
940 - // 获取活动状态 1033 + // 获取活动详情(包含活动状态)
941 - await fetchActivityStatus() 1034 + await fetchActivityDetail()
942 1035
943 // 检查定位授权状态(不获取位置,只检查权限) 1036 // 检查定位授权状态(不获取位置,只检查权限)
944 await checkLocationAuth() 1037 await checkLocationAuth()
945 } 1038 }
946 1039
947 -/** 1040 +// 处理页面加载时的授权检查
948 - * 获取活动状态 1041 +useLoad(options => {
949 - */ 1042 + console.log('[ActivitiesCover] 页面加载, 参数:', options)
950 -const fetchActivityStatus = async () => { 1043 +
951 - try { 1044 + // 获取活动 ID(如果有)
952 - activityStatus.value.loading = true 1045 + if (options.id) {
953 - const { code, data } = await getActivityStatusAPI() 1046 + activityId.value = options.id
954 - 1047 + } else if (options.activity_id) {
955 - if (code === 1 && data) { 1048 + activityId.value = options.activity_id
956 - activityStatus.value.is_begin = Boolean(data.is_begin)
957 - activityStatus.value.is_ended = Boolean(data.is_ended)
958 - console.log('活动状态:', {
959 - is_begin: activityStatus.value.is_begin ? '已开始' : '未开始',
960 - is_ended: activityStatus.value.is_ended ? '已结束' : '进行中'
961 - })
962 } else { 1049 } else {
963 - console.warn('获取活动状态失败:', data) 1050 + // 如果没有活动ID,使用默认ID
964 - // 默认认为活动未开始且未结束,避免影响用户体验 1051 + activityId.value = '1'
965 - activityStatus.value.is_begin = false 1052 + console.warn('[ActivitiesCover] 未提供活动ID,使用默认ID: 1')
966 - activityStatus.value.is_ended = false
967 } 1053 }
968 - } catch (error) {
969 - console.error('获取活动状态异常:', error)
970 - // 默认认为活动未开始且未结束,避免影响用户体验
971 - activityStatus.value.is_begin = false
972 - activityStatus.value.is_ended = false
973 - } finally {
974 - activityStatus.value.loading = false
975 - }
976 -}
977 1054
978 -// 处理页面加载时的授权检查
979 -useLoad((options) => {
980 // 处理分享页面的授权逻辑 1055 // 处理分享页面的授权逻辑
981 handleSharePageAuth(options, () => { 1056 handleSharePageAuth(options, () => {
982 - initPageData(); 1057 + initPageData()
983 - }); 1058 + })
984 -}); 1059 +})
985 1060
986 // 页面挂载时检查定位授权状态 1061 // 页面挂载时检查定位授权状态
987 onMounted(async () => { 1062 onMounted(async () => {
988 // 获取系统信息 1063 // 获取系统信息
989 - getSystemInfo(); 1064 + getSystemInfo()
990 1065
991 initPageData() 1066 initPageData()
992 }) 1067 })
...@@ -994,6 +1069,6 @@ onMounted(async () => { ...@@ -994,6 +1069,6 @@ onMounted(async () => {
994 1069
995 <script> 1070 <script>
996 export default { 1071 export default {
997 - name: "ActivitiesCover", 1072 + name: 'ActivitiesCover',
998 -}; 1073 +}
999 </script> 1074 </script>
......