hookehuyr

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

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -22,19 +22,23 @@
<!-- 底部按钮区域 -->
<view class="bottom-section">
<!-- 积分规则说明 -->
<view
v-if="activityData.rules && activityData.rules.length"
class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4 opacity-90"
>
<view class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4 opacity-90">
<text class="text-blue-500 text-base font-medium block mb-2">积分规则说明:</text>
<text
v-for="(rule, index) in activityData.rules"
:key="index"
class="text-blue-500 text-sm leading-relaxed block mb-1"
style="padding-left: 20rpx; text-indent: -20rpx"
>• 打卡任意1关,视为参与,奖励1000积分</text
>
<text
class="text-blue-500 text-sm leading-relaxed block mb-1"
style="padding-left: 20rpx; text-indent: -20rpx"
>• 打卡任意7关,视为完成,奖励5000积分</text
>
<text
class="text-blue-500 text-sm leading-relaxed block mb-1"
style="padding-left: 20rpx; text-indent: -20rpx"
>• 不需要区分打卡点的先后次序</text
>
• {{ rule }}
</text>
</view>
<!-- 未授权定位提示 - 仅在用户点击参加活动且未授权时显示 -->
<view
......@@ -147,27 +151,18 @@ import PosterBuilder from '../../components/PosterBuilder/index.vue'
import ShareButton from '../../components/ShareButton/index.vue'
// 接口信息
import { getMyFamiliesAPI } from '@/api/family'
import { detailAPI } from '@/api/map_activity'
import { getActivityStatusAPI } from '@/api/map'
import { handleSharePageAuth, addShareFlag } from '@/utils/authRedirect'
// 导入主题颜色
import { THEME_COLORS } from '@/utils/config'
// Mock 数据
import { mockMapActivityDetailAPI } from '@/utils/mockData'
// 环境变量:是否使用 mock 数据
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
// 默认海报图
const defaultPoster = ref(
const defaultPoster =
'https://cdn.ipadbiz.cn/lls_prog/images/welcome_8.jpg?imageMogr2/strip/quality/60'
)
// 系统信息
const systemInfo = ref({})
// 活动ID(从 URL 参数获取)
const activityId = ref('')
/**
* 获取系统信息
*/
......@@ -937,87 +932,6 @@ const savePoster = () => {
}
/**
* 将 API 数据转换为页面需要的 activityData 格式
* @param {Object} apiData - API 返回的活动详情数据
* @returns {Object} 页面活动数据对象
*/
const transformApiDataToActivityData = apiData => {
if (!apiData) {
return null
}
// 生成日期范围字符串
const dateRange = `${apiData.begin_date} - ${apiData.end_date}`
// 根据积分规则生成规则描述
const rules = [
`打卡任意1关,视为参与,奖励${apiData.first_checkin_points}积分`,
`打卡任意${apiData.required_checkin_count}关,视为完成,奖励${apiData.complete_points}积分`,
'不需要区分打卡点的先后次序',
]
// 生成奖励描述
const rewards = [
`首次打卡获得${apiData.first_checkin_points}积分`,
`完成${apiData.required_checkin_count}个打卡点获得${apiData.complete_points}积分`,
apiData.discount_title || '打卡点专属优惠',
]
return {
title: apiData.tittle || '活动标题',
subtitle: '探索城市魅力,感受时尚脉搏',
dateRange: dateRange,
posterUrl: apiData.cover || defaultPoster.value,
description: `欢迎参加${apiData.tittle}活动!`,
rules: rules,
rewards: rewards,
}
}
/**
* 获取活动详情
*/
const fetchActivityDetail = async () => {
try {
// 如果没有活动ID,不获取详情
if (!activityId.value) {
console.warn('[ActivitiesCover] 未提供活动ID,跳过详情获取')
return
}
console.log('[ActivitiesCover] 开始获取活动详情, ID:', activityId.value)
// 根据环境选择真实 API 或 mock API
const response = USE_MOCK_DATA
? await mockMapActivityDetailAPI({ id: activityId.value })
: await detailAPI({ id: activityId.value })
if (response.code === 1 && response.data) {
console.log('[ActivitiesCover] 活动详情获取成功:', response.data)
// 转换 API 数据为页面格式
const transformedData = transformApiDataToActivityData(response.data)
if (transformedData) {
activityData.value = transformedData
// 更新默认海报图
if (response.data.cover) {
defaultPoster.value = response.data.cover
}
// 更新活动状态
activityStatus.value.is_begin = Boolean(response.data.is_begin)
activityStatus.value.is_ended = Boolean(response.data.is_ended)
}
} else {
console.warn('[ActivitiesCover] 获取活动详情失败:', response.msg)
}
} catch (error) {
console.error('[ActivitiesCover] 获取活动详情异常:', error)
}
}
/**
* 初始化页面数据
*/
const initPageData = async () => {
......@@ -1030,28 +944,46 @@ const initPageData = async () => {
}
}
// 获取活动详情(包含活动状态)
await fetchActivityDetail()
// 获取活动状态
await fetchActivityStatus()
// 检查定位授权状态(不获取位置,只检查权限)
await checkLocationAuth()
}
// 处理页面加载时的授权检查
useLoad(options => {
console.log('[ActivitiesCover] 页面加载, 参数:', options)
// 获取活动 ID(如果有)
if (options.id) {
activityId.value = options.id
} else if (options.activity_id) {
activityId.value = options.activity_id
/**
* 获取活动状态
*/
const fetchActivityStatus = async () => {
try {
activityStatus.value.loading = true
const { code, data } = await getActivityStatusAPI()
if (code === 1 && data) {
activityStatus.value.is_begin = Boolean(data.is_begin)
activityStatus.value.is_ended = Boolean(data.is_ended)
console.log('活动状态:', {
is_begin: activityStatus.value.is_begin ? '已开始' : '未开始',
is_ended: activityStatus.value.is_ended ? '已结束' : '进行中',
})
} else {
// 如果没有活动ID,使用默认ID
activityId.value = '1'
console.warn('[ActivitiesCover] 未提供活动ID,使用默认ID: 1')
console.warn('获取活动状态失败:', data)
// 默认认为活动未开始且未结束,避免影响用户体验
activityStatus.value.is_begin = false
activityStatus.value.is_ended = false
}
} catch (error) {
console.error('获取活动状态异常:', error)
// 默认认为活动未开始且未结束,避免影响用户体验
activityStatus.value.is_begin = false
activityStatus.value.is_ended = false
} finally {
activityStatus.value.loading = false
}
}
// 处理页面加载时的授权检查
useLoad(options => {
// 处理分享页面的授权逻辑
handleSharePageAuth(options, () => {
initPageData()
......
/*
* @Date: 2026-02-09
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-02-09
* @FilePath: /lls_program/src/pages/ActivitiesDetail/index.config.js
* @Description: 活动详情页面配置 - 支持多活动详情展示
*/
export default {
navigationBarTitleText: '活动详情',
enableShareAppMessage: true,
usingComponents: {},
}
.activities-cover-container {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
}
// 背景图片
.background-image {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: calc(100vh - 150rpx); // 减去底部区域的高度,确保背景图不被遮挡
// height: calc(100vh); // 减去底部区域的高度,确保背景图不被遮挡
object-fit: cover;
object-position: top center;
z-index: 1;
}
// 为容器添加背景色,避免下方空白
// .activities-cover-container::before {
// content: '';
// position: absolute;
// top: 0;
// left: 0;
// width: 100%;
// height: 100%;
// background: linear-gradient(180deg, #f0f8ff 0%, #e6f3ff 50%, #ddeeff 100%);
// z-index: 0;
// }
// 分享按钮包装器
.share-button-wrapper {
position: absolute;
top: 40rpx;
right: 40rpx;
z-index: 10;
}
// 底部区域
.bottom-section {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 40rpx;
padding-bottom: 180rpx; // 为底部导航留出空间
// background: linear-gradient(
// transparent 0%,
// rgba(0, 0, 0, 0.1) 20%,
// rgba(0, 0, 0, 0.3) 50%,
// rgba(0, 0, 0, 0.6) 80%,
// rgba(0, 0, 0, 0.8) 100%
// );
// backdrop-filter: blur(30rpx);
// -webkit-backdrop-filter: blur(30rpx);
z-index: 5;
// 增加渐变高度,让过渡更自然
min-height: 300rpx;
}
.location-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24rpx;
background-color: rgba(255, 247, 230, 0.95);
border: 1rpx solid rgba(255, 213, 145, 0.9);
border-radius: 16rpx;
margin-bottom: 32rpx;
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
cursor: pointer;
transition: all 0.3s ease;
box-shadow:
0 4rpx 16rpx rgba(255, 213, 145, 0.3),
0 2rpx 8rpx rgba(0, 0, 0, 0.1);
&:active {
background-color: rgba(255, 247, 230, 0.8);
transform: scale(0.98);
box-shadow:
0 2rpx 8rpx rgba(255, 213, 145, 0.2),
0 1rpx 4rpx rgba(0, 0, 0, 0.1);
}
.tip-content {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8rpx;
}
}
.tip-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.tip-text {
font-size: 26rpx;
color: #d46b08;
font-weight: 500;
}
.tip-retry {
font-size: 22rpx;
color: #1890ff;
text-decoration: underline;
}
.location-error {
background-color: rgba(255, 241, 240, 0.95);
border: 1rpx solid rgba(255, 163, 158, 0.9);
box-shadow:
0 4rpx 16rpx rgba(255, 163, 158, 0.3),
0 2rpx 8rpx rgba(0, 0, 0, 0.1);
&:active {
background-color: rgba(255, 241, 240, 0.8);
box-shadow:
0 2rpx 8rpx rgba(255, 163, 158, 0.2),
0 1rpx 4rpx rgba(0, 0, 0, 0.1);
}
.tip-text {
color: #cf1322;
}
}
.join-button {
width: 100%;
height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
box-shadow:
0 8rpx 32rpx rgba(84, 171, 174, 0.5),
0 4rpx 16rpx rgba(0, 0, 0, 0.2);
backdrop-filter: blur(15rpx);
-webkit-backdrop-filter: blur(15rpx);
border: 1rpx solid rgba(255, 255, 255, 0.2);
&:active {
transform: translateY(2rpx);
box-shadow:
0 4rpx 16rpx rgba(84, 171, 174, 0.4),
0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
&.nut-button--primary {
background: linear-gradient(135deg, rgba(84, 171, 174, 0.95) 0%, rgba(74, 151, 154, 0.95) 100%);
border: 1rpx solid rgba(255, 255, 255, 0.3);
box-shadow:
0 8rpx 32rpx rgba(84, 171, 174, 0.4),
0 4rpx 16rpx rgba(0, 0, 0, 0.2);
color: white;
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.2);
&:active {
transform: translateY(2rpx);
box-shadow:
0 4rpx 16rpx rgba(84, 171, 174, 0.3),
0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
}
// 禁用状态样式 - 覆盖NutUI默认的半透明效果
&.nut-button--disabled {
opacity: 1 !important; // 覆盖默认的透明度
background: #cccccc !important; // 实心灰色背景
color: #fff !important; // 深灰色文字
border: 1rpx solid #ccc !important;
box-shadow: 0 4rpx 16rpx rgba(204, 204, 204, 0.3) !important;
&:active {
transform: none; // 禁用时不响应点击效果
box-shadow: 0 4rpx 16rpx rgba(204, 204, 204, 0.3) !important;
}
}
}
// 弹窗样式
.share-popup {
.nut-popup__content {
border-radius: 24rpx 24rpx 0 0;
padding: 40rpx;
}
}
.share-title {
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 40rpx;
color: #333;
}
.share-options {
display: flex;
justify-content: space-around;
margin-bottom: 40rpx;
}
.share-option {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
border-radius: 12rpx;
&:active {
background-color: #f5f5f5;
}
}
.share-icon {
width: 80rpx;
height: 80rpx;
margin-bottom: 16rpx;
border-radius: 12rpx;
background-color: #1890ff;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 36rpx;
}
.share-text {
font-size: 24rpx;
color: #666;
}
.cancel-button {
width: 100%;
height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
background-color: #f5f5f5;
color: #666;
border: none;
}
// 海报预览弹窗
.poster-preview-popup {
.nut-popup__content {
width: 90%;
max-width: 600rpx;
border-radius: 24rpx;
padding: 40rpx;
background-color: white;
}
}
.poster-preview {
width: 100%;
border-radius: 12rpx;
margin-bottom: 40rpx;
}
.preview-actions {
display: flex;
gap: 20rpx;
}
.preview-button {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
&.primary {
background-color: #1890ff;
color: white;
border: none;
}
&.secondary {
background-color: #f5f5f5;
color: #666;
border: none;
}
}
This diff is collapsed. Click to expand it.