hookehuyr

feat(分享功能): 重构分享按钮为组件并添加打卡列表页面

- 将分享按钮提取为独立组件,支持活动分享和海报分享
- 添加打卡列表页面,展示已打卡地点信息
- 更新组件类型声明和路由配置
...@@ -27,6 +27,7 @@ declare module 'vue' { ...@@ -27,6 +27,7 @@ declare module 'vue' {
27 PrimaryButton: typeof import('./src/components/PrimaryButton.vue')['default'] 27 PrimaryButton: typeof import('./src/components/PrimaryButton.vue')['default']
28 RouterLink: typeof import('vue-router')['RouterLink'] 28 RouterLink: typeof import('vue-router')['RouterLink']
29 RouterView: typeof import('vue-router')['RouterView'] 29 RouterView: typeof import('vue-router')['RouterView']
30 + ShareButton: typeof import('./src/components/ShareButton/index.vue')['default']
30 TabBar: typeof import('./src/components/TabBar.vue')['default'] 31 TabBar: typeof import('./src/components/TabBar.vue')['default']
31 TotalPointsDisplay: typeof import('./src/components/TotalPointsDisplay.vue')['default'] 32 TotalPointsDisplay: typeof import('./src/components/TotalPointsDisplay.vue')['default']
32 WeRunAuth: typeof import('./src/components/WeRunAuth.vue')['default'] 33 WeRunAuth: typeof import('./src/components/WeRunAuth.vue')['default']
......
...@@ -34,6 +34,7 @@ export default { ...@@ -34,6 +34,7 @@ export default {
34 'pages/UploadMedia/index', 34 'pages/UploadMedia/index',
35 'pages/FamilyRank/index', 35 'pages/FamilyRank/index',
36 'pages/PosterCheckin/index', 36 'pages/PosterCheckin/index',
37 + 'pages/CheckinList/index',
37 ], 38 ],
38 window: { 39 window: {
39 backgroundTextStyle: 'light', 40 backgroundTextStyle: 'light',
......
1 +<!--
2 + * @Description: 分享按钮组件 - 点击后弹出分享选项
3 +-->
4 +<template>
5 + <view class="share-button-container">
6 + <!-- 分享按钮 -->
7 + <view @tap="toggleShareOptions" class="share-button">
8 + <text>分享</text>
9 + </view>
10 +
11 + <!-- 分享选项气泡弹窗 -->
12 + <view v-if="showOptions" class="share-popover">
13 + <view class="popover-arrow"></view>
14 + <view class="popover-content">
15 + <view @tap="handleShareActivity" class="popover-item">
16 + <text>活动</text>
17 + </view>
18 + <view class="popover-divider"></view>
19 + <view @tap="handleSharePoster" class="popover-item">
20 + <text>海报</text>
21 + </view>
22 + </view>
23 + </view>
24 +
25 + <!-- 遮罩层 -->
26 + <view v-if="showOptions" class="popover-mask" @tap="hideShareOptions"></view>
27 + </view>
28 +</template>
29 +
30 +<script setup>
31 +import { ref } from 'vue'
32 +import Taro from '@tarojs/taro'
33 +
34 +// 组件属性
35 +const props = defineProps({
36 + // 活动数据
37 + activityData: {
38 + type: Object,
39 + default: () => ({})
40 + }
41 +})
42 +
43 +// 组件事件
44 +const emit = defineEmits(['shareActivity', 'sharePoster'])
45 +
46 +// 响应式数据
47 +const showOptions = ref(false)
48 +
49 +/**
50 + * 切换分享选项显示状态
51 + */
52 +const toggleShareOptions = () => {
53 + showOptions.value = !showOptions.value
54 +}
55 +
56 +/**
57 + * 隐藏分享选项
58 + */
59 +const hideShareOptions = () => {
60 + showOptions.value = false
61 +}
62 +
63 +/**
64 + * 处理活动分享
65 + */
66 +const handleShareActivity = () => {
67 + hideShareOptions()
68 + emit('shareActivity', props.activityData)
69 +}
70 +
71 +/**
72 + * 处理海报分享
73 + */
74 +const handleSharePoster = () => {
75 + hideShareOptions()
76 + emit('sharePoster', props.activityData)
77 +}
78 +</script>
79 +
80 +<style lang="less">
81 +.share-button-container {
82 + position: fixed;
83 + top: 40rpx;
84 + right: 40rpx;
85 + z-index: 1000;
86 +}
87 +
88 +.share-button {
89 + color: white;
90 + width: 80rpx;
91 + height: 80rpx;
92 + border-radius: 50%;
93 + background: rgba(0, 0, 0, 0.6);
94 + display: flex;
95 + align-items: center;
96 + justify-content: center;
97 + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
98 + transition: all 0.3s ease;
99 + backdrop-filter: blur(10rpx);
100 +
101 + &:active {
102 + transform: scale(0.95);
103 + background: rgba(0, 0, 0, 0.8);
104 + }
105 +}
106 +
107 +.share-icon {
108 + font-size: 32rpx;
109 + color: white;
110 +}
111 +
112 +// 气泡弹窗样式
113 +.share-popover {
114 + position: fixed;
115 + top: 140rpx;
116 + right: 40rpx;
117 + z-index: 9999;
118 + animation: popoverFadeIn 0.2s ease;
119 +}
120 +
121 +.popover-arrow {
122 + position: absolute;
123 + top: -12rpx;
124 + right: 30rpx;
125 + width: 0;
126 + height: 0;
127 + border-left: 12rpx solid transparent;
128 + border-right: 12rpx solid transparent;
129 + border-bottom: 12rpx solid white;
130 +}
131 +
132 +.popover-content {
133 + background: white;
134 + border-radius: 16rpx;
135 + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
136 + overflow: hidden;
137 + min-width: 160rpx;
138 +}
139 +
140 +.popover-item {
141 + padding: 24rpx 32rpx;
142 + font-size: 28rpx;
143 + color: #333;
144 + text-align: center;
145 + transition: background-color 0.2s ease;
146 +
147 + &:active {
148 + background-color: #f5f5f5;
149 + }
150 +}
151 +
152 +.popover-divider {
153 + height: 1rpx;
154 + background-color: #f0f0f0;
155 + margin: 0 16rpx;
156 +}
157 +
158 +.popover-mask {
159 + position: fixed;
160 + top: 0;
161 + left: 0;
162 + right: 0;
163 + bottom: 0;
164 + z-index: 9998;
165 +}
166 +
167 +// 动画
168 +@keyframes popoverFadeIn {
169 + from {
170 + opacity: 0;
171 + transform: translateY(-10rpx);
172 + }
173 + to {
174 + opacity: 1;
175 + transform: translateY(0);
176 + }
177 +}
178 +</style>
...@@ -29,26 +29,12 @@ ...@@ -29,26 +29,12 @@
29 z-index: 0; 29 z-index: 0;
30 } 30 }
31 31
32 -// 分享按钮 32 +// 分享按钮包装器
33 -.share-button { 33 +.share-button-wrapper {
34 position: absolute; 34 position: absolute;
35 top: 40rpx; 35 top: 40rpx;
36 right: 40rpx; 36 right: 40rpx;
37 - width: 80rpx;
38 - height: 80rpx;
39 - background-color: rgba(0, 0, 0, 0.6);
40 - border-radius: 50%;
41 - display: flex;
42 - align-items: center;
43 - justify-content: center;
44 - color: white;
45 - font-size: 28rpx;
46 z-index: 10; 37 z-index: 10;
47 - backdrop-filter: blur(10rpx);
48 -
49 - &:active {
50 - background-color: rgba(0, 0, 0, 0.8);
51 - }
52 } 38 }
53 39
54 // 底部区域 40 // 底部区域
......
1 <!-- 1 <!--
2 * @Date: 2022-09-19 14:11:06 2 * @Date: 2022-09-19 14:11:06
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-09-03 13:31:52 4 + * @LastEditTime: 2025-09-03 14:47:33
5 * @FilePath: /lls_program/src/pages/ActivitiesCover/index.vue 5 * @FilePath: /lls_program/src/pages/ActivitiesCover/index.vue
6 * @Description: 活动海报页面 - 展示活动信息并处理定位授权 6 * @Description: 活动海报页面 - 展示活动信息并处理定位授权
7 --> 7 -->
...@@ -14,10 +14,13 @@ ...@@ -14,10 +14,13 @@
14 mode="scaleToFill" 14 mode="scaleToFill"
15 /> 15 />
16 16
17 - <!-- 分享按钮 --> 17 + <!-- 分享按钮组件 -->
18 - <view @tap="shareActivity" class="share-button"> 18 + <ShareButton
19 - <text>分享</text> 19 + :activity-data="activityData"
20 - </view> 20 + @share-activity="onShareActivity"
21 + @share-poster="onSharePoster"
22 + class="share-button-wrapper"
23 + />
21 24
22 <!-- 底部按钮区域 --> 25 <!-- 底部按钮区域 -->
23 <view class="bottom-section"> 26 <view class="bottom-section">
...@@ -41,14 +44,7 @@ ...@@ -41,14 +44,7 @@
41 <!-- 底部导航 --> 44 <!-- 底部导航 -->
42 <BottomNav /> 45 <BottomNav />
43 46
44 - <!-- 分享选项弹窗 --> 47 +
45 - <nut-action-sheet
46 - v-model:visible="show_share"
47 - :menu-items="share_options"
48 - @choose="onSelectShare"
49 - @cancel="onCancelShare"
50 - cancel-txt="取消"
51 - />
52 48
53 <!-- 海报预览弹窗 --> 49 <!-- 海报预览弹窗 -->
54 <nut-popup 50 <nut-popup
...@@ -90,6 +86,7 @@ import Taro from '@tarojs/taro' ...@@ -90,6 +86,7 @@ import Taro from '@tarojs/taro'
90 import "./index.less" 86 import "./index.less"
91 import BottomNav from '../../components/BottomNav.vue' 87 import BottomNav from '../../components/BottomNav.vue'
92 import PosterBuilder from '../../components/PosterBuilder/index.vue' 88 import PosterBuilder from '../../components/PosterBuilder/index.vue'
89 +import ShareButton from '../../components/ShareButton/index.vue'
93 // 接口信息 90 // 接口信息
94 import { getMyFamiliesAPI } from '@/api/family' 91 import { getMyFamiliesAPI } from '@/api/family'
95 92
...@@ -107,7 +104,6 @@ const userLocation = ref({ lng: null, lat: null }) // 用户位置信息 ...@@ -107,7 +104,6 @@ const userLocation = ref({ lng: null, lat: null }) // 用户位置信息
107 const hasJoinedFamily = ref(false); 104 const hasJoinedFamily = ref(false);
108 105
109 // 海报生成相关状态 106 // 海报生成相关状态
110 -const show_share = ref(false) // 显示分享弹窗
111 const show_post = ref(false) // 显示海报预览 107 const show_post = ref(false) // 显示海报预览
112 const show_save = ref(false) // 显示保存弹窗 108 const show_save = ref(false) // 显示保存弹窗
113 const startDraw = ref(false) // 开始绘制海报 109 const startDraw = ref(false) // 开始绘制海报
...@@ -115,11 +111,6 @@ const posterPath = ref('') // 海报路径 ...@@ -115,11 +111,6 @@ const posterPath = ref('') // 海报路径
115 const nickname = ref('老来赛用户') // 用户昵称 111 const nickname = ref('老来赛用户') // 用户昵称
116 const avatar = ref('https://cdn.ipadbiz.cn/icon/tou@2x.png') // 用户头像 112 const avatar = ref('https://cdn.ipadbiz.cn/icon/tou@2x.png') // 用户头像
117 113
118 -// 分享选项
119 -const share_options = [
120 - { name: '海报' },
121 -]
122 -
123 // 保存选项 114 // 保存选项
124 const actions_save = ref([{ 115 const actions_save = ref([{
125 name: '保存至相册' 116 name: '保存至相册'
...@@ -272,28 +263,22 @@ const handleJoinActivity = async () => { ...@@ -272,28 +263,22 @@ const handleJoinActivity = async () => {
272 } 263 }
273 264
274 /** 265 /**
275 - * 分享活动 266 + * 处理分享活动事件
276 - */
277 -const shareActivity = () => {
278 - show_share.value = true
279 -}
280 -
281 -/**
282 - * 取消分享
283 */ 267 */
284 -const onCancelShare = () => { 268 +const onShareActivity = () => {
285 - show_share.value = false 269 + console.log('分享活动海报')
270 + show_post.value = true
271 + startGeneratePoster()
286 } 272 }
287 273
288 /** 274 /**
289 - * 选择分享方式 275 + * 处理分享海报事件
290 */ 276 */
291 -const onSelectShare = (item) => { 277 +const onSharePoster = () => {
292 - show_share.value = false 278 + console.log('分享海报')
293 - if (item.name === '海报') { 279 + Taro.navigateTo({
294 - show_post.value = true 280 + url: '/pages/CheckinList/index',
295 - startGeneratePoster() 281 + })
296 - }
297 } 282 }
298 283
299 /** 284 /**
......
1 +/*
2 + * @Date: 2025-09-03 14:53:06
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-09-03 14:53:18
5 + * @FilePath: /lls_program/src/pages/CheckinList/index.config.js
6 + * @Description: 文件描述
7 + */
8 +export default {
9 + navigationBarTitleText: '打卡列表',
10 + usingComponents: {
11 + },
12 +}
1 +<!--
2 + * @Date: 2025-01-15 10:00:00
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-09-03 14:54:12
5 + * @FilePath: /lls_program/src/pages/CheckinList/index.vue
6 + * @Description: 打卡列表页面 - 显示已打卡的地点列表
7 +-->
8 +<template>
9 + <view class="checkin-list-container">
10 + <!-- 活动信息 -->
11 + <!-- <view class="activity-info">
12 + <view class="activity-title">{{ activityData.title }}</view>
13 + <view class="activity-subtitle">{{ activityData.subtitle }}</view>
14 + <view class="progress-info">
15 + <text>已打卡 {{ checkedInCount }}/{{ totalLocations }} 个地点</text>
16 + </view>
17 + </view> -->
18 +
19 + <!-- 打卡地点列表 -->
20 + <view class="checkin-list">
21 + <view
22 + v-for="(location, index) in locationList"
23 + :key="index"
24 + class="checkin-item"
25 + >
26 + <view class="location-info">
27 + <view class="location-name">{{ location.name }}</view>
28 + <view class="location-address">{{ location.address }}</view>
29 + <view class="checkin-time" v-if="location.checkedIn">
30 + 打卡时间:{{ location.checkinTime }}
31 + </view>
32 + </view>
33 +
34 + <view class="action-button">
35 + <nut-button
36 + v-if="location.checkedIn && location.hasPhoto"
37 + type="primary"
38 + size="normal"
39 + color="#4d96ea"
40 + @click="viewPhoto(location)"
41 + >
42 + 查看照片
43 + </nut-button>
44 + <nut-button
45 + v-else-if="location.checkedIn && !location.hasPhoto"
46 + type="warning"
47 + size="normal"
48 + @click="uploadPhoto(location)"
49 + >
50 + 上传照片
51 + </nut-button>
52 + <nut-button
53 + v-else
54 + type="default"
55 + size="normal"
56 + disabled
57 + >
58 + 未打卡
59 + </nut-button>
60 + </view>
61 + </view>
62 + </view>
63 +
64 + <!-- 底部导航 -->
65 + <BottomNav />
66 + </view>
67 +</template>
68 +
69 +<script setup>
70 +import { ref, computed, onMounted } from 'vue'
71 +import Taro from '@tarojs/taro'
72 +import BottomNav from '../../components/BottomNav.vue'
73 +
74 +/**
75 + * 打卡列表页面组件
76 + * 功能:显示活动的打卡地点列表,支持查看照片和上传照片
77 + */
78 +
79 +// Mock活动数据
80 +const activityData = ref({
81 + title: '南京路商圈时尚Citywalk',
82 + subtitle: '探索城市魅力,感受时尚脉搏',
83 + dateRange: '2024年1月15日 - 2024年1月31日'
84 +})
85 +
86 +// Mock打卡地点数据
87 +const locationList = ref([
88 + {
89 + id: 1,
90 + name: '外滩观景台',
91 + address: '上海市黄浦区中山东一路',
92 + checkedIn: true,
93 + hasPhoto: true,
94 + checkinTime: '2024-01-16 09:30',
95 + photoUrl: 'https://img.yzcdn.cn/vant/cat.jpeg'
96 + },
97 + {
98 + id: 2,
99 + name: '南京路步行街',
100 + address: '上海市黄浦区南京东路',
101 + checkedIn: true,
102 + hasPhoto: false,
103 + checkinTime: '2024-01-16 10:45',
104 + photoUrl: ''
105 + },
106 + {
107 + id: 3,
108 + name: '人民广场',
109 + address: '上海市黄浦区人民大道',
110 + checkedIn: true,
111 + hasPhoto: true,
112 + checkinTime: '2024-01-16 14:20',
113 + photoUrl: 'https://img.yzcdn.cn/vant/cat.jpeg'
114 + },
115 + {
116 + id: 4,
117 + name: '豫园商城',
118 + address: '上海市黄浦区方浜中路',
119 + checkedIn: false,
120 + hasPhoto: false,
121 + checkinTime: '',
122 + photoUrl: ''
123 + },
124 + {
125 + id: 5,
126 + name: '城隍庙',
127 + address: '上海市黄浦区方浜中路',
128 + checkedIn: false,
129 + hasPhoto: false,
130 + checkinTime: '',
131 + photoUrl: ''
132 + }
133 +])
134 +
135 +// 计算已打卡数量
136 +const checkedInCount = computed(() => {
137 + return locationList.value.filter(location => location.checkedIn).length
138 +})
139 +
140 +// 总地点数量
141 +const totalLocations = computed(() => {
142 + return locationList.value.length
143 +})
144 +
145 +/**
146 + * 查看照片 - 跳转到海报打卡页面
147 + */
148 +const viewPhoto = (location) => {
149 + Taro.navigateTo({
150 + url: `/pages/PosterCheckin/index?locationId=${location.id}&mode=view`
151 + })
152 +}
153 +
154 +/**
155 + * 上传照片 - 跳转到海报打卡页面
156 + */
157 +const uploadPhoto = (location) => {
158 + Taro.navigateTo({
159 + url: `/pages/PosterCheckin/index?locationId=${location.id}&mode=upload`
160 + })
161 +}
162 +
163 +/**
164 + * 页面加载时初始化数据
165 + */
166 +onMounted(() => {
167 + console.log('打卡列表页面加载完成')
168 + // 这里可以调用API获取真实的打卡数据
169 + // loadCheckinData()
170 +})
171 +
172 +/**
173 + * 加载打卡数据(预留接口)
174 + */
175 +const loadCheckinData = async () => {
176 + try {
177 + // 调用API获取打卡数据
178 + // const response = await getCheckinListAPI()
179 + // locationList.value = response.data
180 + console.log('加载打卡数据')
181 + } catch (error) {
182 + console.error('加载打卡数据失败:', error)
183 + Taro.showToast({
184 + title: '加载数据失败',
185 + icon: 'error'
186 + })
187 + }
188 +}
189 +</script>
190 +
191 +<style lang="less">
192 +.checkin-list-container {
193 + min-height: 100vh;
194 + background-color: #f5f5f5;
195 + padding-bottom: 120rpx;
196 +}
197 +
198 +.header {
199 + display: flex;
200 + align-items: center;
201 + justify-content: space-between;
202 + padding: 20rpx 32rpx;
203 + background-color: #fff;
204 + border-bottom: 1rpx solid #eee;
205 +
206 + .back-button {
207 + width: 60rpx;
208 + height: 60rpx;
209 + display: flex;
210 + align-items: center;
211 + justify-content: center;
212 +
213 + .back-icon {
214 + font-size: 36rpx;
215 + color: #333;
216 + }
217 + }
218 +
219 + .title {
220 + font-size: 36rpx;
221 + font-weight: 600;
222 + color: #333;
223 + }
224 +
225 + .placeholder {
226 + width: 60rpx;
227 + }
228 +}
229 +
230 +.activity-info {
231 + background-color: #fff;
232 + padding: 32rpx;
233 + margin-bottom: 20rpx;
234 +
235 + .activity-title {
236 + font-size: 36rpx;
237 + font-weight: 600;
238 + color: #333;
239 + margin-bottom: 12rpx;
240 + }
241 +
242 + .activity-subtitle {
243 + font-size: 28rpx;
244 + color: #666;
245 + margin-bottom: 20rpx;
246 + }
247 +
248 + .progress-info {
249 + font-size: 26rpx;
250 + color: #3B82F6;
251 + background-color: #EBF4FF;
252 + padding: 12rpx 20rpx;
253 + border-radius: 20rpx;
254 + display: inline-block;
255 + }
256 +}
257 +
258 +.checkin-list {
259 + .checkin-item {
260 + background-color: #fff;
261 + margin-bottom: 20rpx;
262 + padding: 32rpx;
263 + display: flex;
264 + align-items: center;
265 + justify-content: space-between;
266 +
267 + .location-info {
268 + flex: 1;
269 +
270 + .location-name {
271 + font-size: 32rpx;
272 + font-weight: 600;
273 + color: #333;
274 + margin-bottom: 8rpx;
275 + }
276 +
277 + .location-address {
278 + font-size: 26rpx;
279 + color: #666;
280 + margin-bottom: 8rpx;
281 + }
282 +
283 + .checkin-time {
284 + font-size: 24rpx;
285 + color: #999;
286 + }
287 + }
288 +
289 + .action-button {
290 + margin-left: 20rpx;
291 + }
292 + }
293 +}
294 +</style>