Files
property-resident-side/property-uniapp-project/pageSubPack/activity-list/activityList.vue

703 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="container">
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<!-- 顶部标题栏 固定 -->
<view class="top-bar">
<uni-nav-bar left-icon="left" title="活动列表" backgroundColor="none" :border="false" :statusBar="false"
@clickLeft="narBack" />
</view>
<!-- 滚动区域从banner开始滚动 -->
<scroll-view class="scroll-container" scroll-y="true" @scrolltolower="loadMoreData"
:lower-threshold="100">
<!-- 顶部图片 -->
<view class="top-img">
<image class="banner-img" src="http://47.104.199.163:9090/images/home/banner.png" mode="aspectFill"></image>
</view>
<!-- 选项卡 -->
<view class="tabs-container">
<view class="tab-item" :class="{ 'active': activeTab === 'list' }" @tap="switchTab('list')">
活动列表
<view v-if="activeTab === 'list'" class="tab-indicator"></view>
</view>
<view class="tab-item" :class="{ 'active': activeTab === 'my' }" @tap="switchTab('my')">
我的活动
<view v-if="activeTab === 'my'" class="tab-indicator"></view>
</view>
</view>
<!-- 我的活动状态筛选 -->
<view v-if="activeTab === 'my'" class="filter-container">
<scroll-view class="filter-scroll" scroll-x>
<view class="filter-buttons">
<view v-for="filter in filters" :key="filter.value" class="filter-btn"
:class="{ 'active': myFilter === filter.value }" @tap="switchFilter(filter.value)">
{{ filter.label }}
</view>
</view>
</scroll-view>
</view>
<!-- 活动列表 -->
<view class="activity-list">
<!-- 空状态 -->
<view v-if="displayedActivities.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">
{{ activeTab === 'list' ? '暂无活动' : '暂无参与的活动' }}
</text>
</view>
<!-- 活动卡片 -->
<view v-for="activity in displayedActivities" :key="activity.id" class="activity-card"
@tap="viewActivityDetail(activity)">
<view class="status-badge">
{{activity.statusText}}
</view>
<view v-if="activity.applied && activeTab === 'list'" class="signed-badge">
<text>已报名</text>
</view>
<view class="activity-info">
<view class="u-demo-block__content">
<up-row justify="space-between" customStyle="margin-bottom: 10px">
<up-col span="4">
<view class="info-image-left">
<image :src="activity.coverImage" mode="aspectFill" style="width: 100%;height: 100%;border-radius: 16rpx;"></image>
</view>
</up-col>
<up-col span="8">
<view class="demo-layout bg-purple">
<view class="activity-title">
{{ activity.title }}
</view>
<view class="activity-detail">
<text class="detail-text">时间{{ formatDate(activity.startTime) }}
{{ formatDate(activity.endTime) }}</text>
</view>
<view class="activity-detail">
<text class="detail-text-address">地点{{ activity.address }}</text>
</view>
<view class="activity-detail">
<text class="detail-text-num">
<text v-if="activity.quota !== 0">活动名额{{ activity.quota }},</text>
已报名{{ activity.appliedCount }}
</text>
</view>
</view>
</up-col>
</up-row>
</view>
<view class="action-buttons">
<button v-if="activeTab === 'list' && activity.applied === false && activity.statusText !== '已结束'" class="action-btn signup-btn"
@tap.stop="signupActivity(activity)">
立即报名
</button>
<button v-if="activeTab === 'list' && activity.applied === true && activity.statusText !== '已结束'" class="action-btn signup-btn"
@tap.stop="cancelActivity(activity)">
取消报名
</button>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="hasMoreData && displayedActivities.length > 0" class="load-more-container">
<view v-if="!isLoading" class="load-more-btn" @tap="loadMoreData">
<text>加载更多</text>
<uni-icons type="arrow-down" size="16" color="#007AFF"></uni-icons>
</view>
<view v-else class="loading-container">
<uni-icons type="spinner-cycle" size="20" color="#007AFF" class="loading-icon"></uni-icons>
<text>正在加载...</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, watch } from 'vue'
import { onLoad, onUnload, onShow } from '@dcloudio/uni-app'
import moment from 'moment';
import { getActivityList,getMyActivityList} from '../api/api.js'
// ===================== 响应式数据 =====================
const activeTab = ref('list') // 'list' 或 'my'
const myFilter = ref('') // 我的活动筛选条件:''全部, '0'未开始, '1'进行中, '2'已结束
const isLoading = ref(false)
const hasMoreData = ref(true)
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const displayedActivities = ref([]) // 当前显示的活动列表
const statusBarHeight = ref(uni.getSystemInfoSync().statusBarHeight || 0)
// 筛选条件status ''全部, '0'未开始, '1'进行中, '2'已结束
const filters = ref([
{ label: '全部', value: '' },
{ label: '未开始', value: '0' },
{ label: '进行中', value: '1' },
{ label: '已结束', value: '2' }
])
// ===================== 核心方法 =====================
/**
* 重置并刷新第一页数据
*/
const resetAndLoad = () => {
currentPage.value = 1
displayedActivities.value = []
hasMoreData.value = true
getActivityListData(1)
}
/**
* 返回上一级页面
*/
const narBack = () => {
const sourcePage = uni.getStorageSync('sourcePage')
if (sourcePage === "index") {
uni.switchTab({ url: "/pages/index/index" })
} else if (sourcePage === "serviceIndex") {
uni.switchTab({ url: "/pages/serviceIndex/serviceIndex" })
} else {
uni.navigateBack()
}
}
/**
* 切换选项卡(活动列表/我的活动)
* @param {string} tab - 选项卡类型
*/
const switchTab = (tab) => {
activeTab.value = tab
if (tab === 'list') {
resetAndLoad()
} else {
resetAndLoadMy()
}
}
/**
* 重置并刷新我的活动第一页数据
*/
const resetAndLoadMy = () => {
currentPage.value = 1
displayedActivities.value = []
hasMoreData.value = true
getMyActivityListData(1)
}
/**
* 切换我的活动筛选条件
* @param {string} filter - 筛选值
*/
const switchFilter = (filter) => {
myFilter.value = filter
resetAndLoadMy()
}
/**
* 获取活动列表数据
* @param {number} pageNum - 页码
*/
const getActivityListData = async (pageNum = 1) => {
// 防止重复加载
if (isLoading.value) return
isLoading.value = true
try {
// 构造请求参数(活动列表)
const params = {
pageNum: pageNum,
pageSize: pageSize.value,
villageId:uni.getStorageSync('currentVillageId')
}
const res = await getActivityList(params)
if (res.code === 200) {
const list = res.rows || []
total.value = res.total || 0
// 第一页覆盖数据,下一页追加数据
if (pageNum === 1) {
displayedActivities.value = list
} else {
displayedActivities.value = [...displayedActivities.value, ...list]
}
// 判断是否还有更多数据
hasMoreData.value = displayedActivities.value.length < total.value
currentPage.value = pageNum
} else {
uni.showToast({ title: res.msg || "加载失败", icon: 'error' })
if (pageNum === 1) displayedActivities.value = []
}
} catch (err) {
uni.showToast({ title: err.msg || "网络异常", icon: 'none' })
if (pageNum === 1) displayedActivities.value = []
} finally {
isLoading.value = false
}
}
/**
* 获取我的活动列表数据
* @param {number} pageNum - 页码
*/
const getMyActivityListData = async (pageNum = 1) => {
if (isLoading.value) return
isLoading.value = true
try {
const params = {
pageNum: pageNum,
pageSize: pageSize.value,
villageId:uni.getStorageSync('currentVillageId')
}
// status 为空时不传,返回全部
if (myFilter.value !== '') {
params.status = myFilter.value
}
const res = await getMyActivityList(params)
if (res.code === 200) {
const list = res.data || []
total.value = res.total || 0
if (pageNum === 1) {
displayedActivities.value = list
} else {
displayedActivities.value = [...displayedActivities.value, ...list]
}
hasMoreData.value = displayedActivities.value.length < total.value
currentPage.value = pageNum
} else {
uni.showToast({ title: res.msg || '加载失败', icon: 'error' })
if (pageNum === 1) displayedActivities.value = []
}
} catch (err) {
uni.showToast({ title: err.msg || '网络异常', icon: 'none' })
if (pageNum === 1) displayedActivities.value = []
} finally {
isLoading.value = false
}
}
/**
* 加载更多数据
*/
const loadMoreData = () => {
if (isLoading.value || !hasMoreData.value) return
if (activeTab.value === 'list') {
getActivityListData(currentPage.value + 1)
} else {
getMyActivityListData(currentPage.value + 1)
}
}
/**
* 格式化日期
* @param {string} date - 原始日期字符串
* @returns {string} 格式化后的日期
*/
const formatDate = (date) => {
if (!date) return ''
return moment(date).format('YYYY-MM-DD HH:mm')
}
/**
* 报名活动(跳转详情页)
* @param {object} activity - 活动对象
*/
const signupActivity = (activity) => {
uni.navigateTo({
url: `/pageSubPack/activity-list/activityListDetails?id=${activity.id}&title=${activity.title}&applied=${activity.applied}`
})
}
/**
* 取消报名(跳转详情页)
* @param {object} activity - 活动对象
*/
const cancelActivity = (activity) => {
uni.navigateTo({
url: `/pageSubPack/activity-list/activityListDetails?id=${activity.id}&title=${activity.title}&applied=${activity.applied}`
})
}
/**
* 查看活动详情(跳转详情页)
* @param {object} activity - 活动对象
*/
const viewActivityDetail = (activity) => {
uni.navigateTo({
url: `/pageSubPack/activity-list/activityListDetails?id=${activity.id}&title=${activity.title}&applied=${activity.applied}`
})
}
// ===================== 生命周期 & 监听 =====================
/**
* 页面显示时刷新数据
*/
onShow(() => {
if (activeTab.value === 'list') {
getActivityListData(1)
} else {
getMyActivityListData(1)
}
})
/**
* 页面加载时监听刷新事件(来自详情页)
*/
onLoad(() => {
// 监听详情页报名/取消报名后的刷新通知
uni.$on('refreshActivityList', (data) => {
getActivityListData(1)
})
})
/**
* 页面卸载时移除事件监听(避免内存泄漏)
*/
onUnload(() => {
uni.$off('refreshActivityList')
})
/**
* 监听我的活动筛选条件变化
*/
watch(() => myFilter.value, () => {
if (activeTab.value === 'my') {
resetAndLoadMy()
}
})
</script>
<style scoped>
/* 页面容器 */
.container {
width: 100%;
height: 100vh;
background-color: #fff;
display: flex;
flex-direction: column;
padding: 0;
}
/* 状态栏 - 自适应设备状态栏高度 */
.status-bar {
/* height: var(--status-bar-height); */
/* height: 46px; */
width: 100%;
flex-shrink: 0;
}
/* 顶部标题 固定 */
.top-bar {
width: 100%;
flex-shrink: 0;
background: #fff;
}
/* 滚动区域 */
.scroll-container {
flex: 1;
width: 100%;
overflow-y: auto;
padding: 0 30rpx;
box-sizing: border-box;
}
/* 顶部图片 */
.top-img {
width: 100%;
height: 280rpx;
margin: 20rpx 0;
}
.banner-img {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
/* 顶部选项卡 */
.tabs-container {
display: flex;
top: 0;
z-index: 10;
margin-bottom: 20rpx;
}
.tab-item {
display: flex;
text-align: center;
padding: 20rpx 32rpx;
font-size: 32rpx;
color: #666;
position: relative;
}
.tab-item.active {
color: #2F77FD;
font-weight: 500;
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 120rpx;
height: 8rpx;
background: linear-gradient(to bottom, #2F77FD, #CEDFFF);
/* background-color: #2F77FD; */
border-radius: 4rpx;
}
/* 筛选容器 */
.filter-container {
background-color: #fff;
padding: 20rpx 0;
border-bottom: 1rpx solid #eee;
}
.filter-scroll {
width: 100%;
white-space: nowrap;
}
.filter-buttons {
display: inline-flex;
padding: 0 10rpx;
}
.filter-btn {
display: inline-block;
padding: 12rpx 30rpx;
margin-right: 20rpx;
font-size: 28rpx;
color: #666;
background-color: #f8f8f8;
border-radius: 25rpx;
border: 1rpx solid #eee;
white-space: nowrap;
flex-shrink: 0;
}
.filter-btn.active {
color: #fff;
background-color: #2F77FD;
}
/* 活动列表 */
.activity-list {
width: 100%;
padding-bottom: 40rpx;
}
/* 活动卡片 */
.activity-card {
background-color: #fff;
border-radius: 16rpx;
padding: 12rpx;
margin-bottom: 30rpx;
/* box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); */
position: relative;
border: 1rpx solid #e1e1e1;
}
/* 活动状态标签 */
.status-badge {
position: absolute;
top: 33rpx;
left: 11rpx;
padding: 6rpx 20rpx;
font-size: 22rpx;
border-radius: 15rpx 0rpx 15rpx 0rpx;
color: #fff;
z-index: 1;
background: repeating-linear-gradient(to right, #FD7373, #FB3F3F);
}
/* 已报名标识 */
.signed-badge {
position: absolute;
top: 20rpx;
right: 8rpx;
display: flex;
align-items: center;
padding: 6rpx 14rpx;
color: #007AFF;
font-size: 22rpx;
border-radius: 20rpx;
z-index: 1;
background-color: rgba(0, 122, 255, 0.08);
}
.signed-badge text {
margin-left: 8rpx;
}
/* 活动信息 */
.activity-info {
padding-top: 24rpx;
}
/* 活动信息 */
.info-image-left {
width: 170rpx;
height: 220rpx;
}
.activity-title {
font-size: 30rpx;
font-weight: 500;
color: #222;
margin-bottom: 20rpx;
line-height: 1.4;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.activity-detail {
display: flex;
align-items: flex-start;
margin-bottom: 16rpx;
color: #999;
font-size: 24rpx;
line-height: 1.4;
}
.activity-detail .uni-icons {
margin-right: 15rpx;
margin-top: 4rpx;
flex-shrink: 0;
}
.detail-text-num {
font-size: 28rpx;
color: #666666;
}
.detail-text {
flex: 1;
font-size: 20rpx;
}
.detail-text-address {
flex: 1;
font-size: 22rpx;
}
/* 操作按钮 */
.action-buttons {
margin-top: 20rpx;
display: flex;
justify-content: flex-end;
}
.action-btn {
padding: 0 26rpx;
height: 56rpx;
line-height: 56rpx;
font-size: 24rpx;
border-radius: 28rpx;
margin: 0;
min-width: 140rpx;
}
.signup-btn {
background-color: #007AFF;
color: #fff;
}
.signup-btn:active {
background-color: #0056cc;
}
/* 加载更多 */
.load-more-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
color: #999;
}
.load-more-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 40rpx;
background-color: #f8f8f8;
border-radius: 50rpx;
color: #007AFF;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.load-more-btn text {
margin-right: 10rpx;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 40rpx;
background-color: #f8f8f8;
border-radius: 50rpx;
color: #007AFF;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.loading-icon {
margin-right: 10rpx;
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #ccc;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
</style>