2025-07-19 20:00:08 +08:00

669 lines
16 KiB
Vue

<template>
<view class="activity-container" :style="containerStyle">
<common-header title="活动管理" theme="activity" @back="goBack">
</common-header>
<!-- 统计卡片 -->
<stat-card
title="活动统计"
icon="calendar"
:stats="statsData"
:loading="statsLoading"
@stat-click="handleStatClick" />
<!-- 搜索和筛选 -->
<view class="search-filter-section">
<uni-search-bar
v-model="searchText"
placeholder="搜索活动名称"
:focus="false"
@confirm="searchActivity"
@clear="clearSearch" />
<uni-segmented-control
:current="currentTab"
:values="tabList"
@clickItem="switchTab"
activeColor="#ff5722" />
</view>
<!-- 活动列表 -->
<scroll-view
class="scroll-view"
scroll-y
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore">
<!-- 空状态 -->
<view v-if="!loading && activityList.length === 0" class="empty-state">
<uni-icons type="calendar" size="80" color="#ddd" />
<text class="empty-text">暂无活动数据</text>
<uni-button type="primary" size="small" @click="addActivity">创建第一个活动</uni-button>
</view>
<!-- 活动列表 -->
<view v-else class="activity-list">
<view
v-for="(item, index) in activityList"
:key="item.id || index"
class="activity-item"
@click="viewActivity(item)">
<view class="item-header">
<view class="title-section">
<text class="activity-title">{{ item.title || '暂无标题' }}</text>
<view class="status-badge" :class="{
'draft': item.status === 0,
'published': item.status === 1,
'active': item.status === 2,
'finished': item.status === 3
}">
{{ getStatusText(item.status) }}
</view>
</view>
<text class="participant-count">{{ item.participantCount || 0 }}人参与</text>
</view>
<text class="activity-desc">{{ item.description || '暂无描述' }}</text>
<view class="item-info">
<view class="info-item">
<uni-icons type="calendar" size="14" color="#666" />
<text class="info-text">{{ formatTime(item.startTime) }} - {{ formatTime(item.endTime) }}</text>
</view>
<view class="info-item">
<uni-icons type="person" size="14" color="#666" />
<text class="info-text">创建于 {{ formatTime(item.createTime) }}</text>
</view>
</view>
<view class="item-actions" @click.stop>
<uni-button
class="action-btn edit-btn"
size="mini"
@click="editActivity(item)">
编辑
</uni-button>
<uni-button
v-if="item.status === 0"
class="action-btn publish-btn"
size="mini"
type="primary"
@click="publishActivity(item)">
发布
</uni-button>
<uni-button
class="action-btn delete-btn"
size="mini"
type="warn"
@click="deleteActivity(item)">
删除
</uni-button>
</view>
</view>
</view>
<!-- 加载更多 -->
<uni-load-more :status="loadStatus" />
</scroll-view>
<!-- 浮动操作按钮 -->
<view class="fab" @click="addActivity">
<uni-icons type="plus" size="24" color="#fff" />
</view>
</view>
</template>
<script>
import { getActivityList, deleteActivity, publishActivity } from '@/api/brewery/activity'
import CommonHeader from '@/components/common-header/common-header.vue'
import StatCard from '@/components/stat-card/stat-card.vue'
export default {
components: {
CommonHeader,
StatCard
},
data() {
return {
searchText: '',
activityList: [],
loadStatus: 'more',
pageNum: 1,
pageSize: 10,
loading: false,
refreshing: false,
statsLoading: true,
currentTab: 0,
tabList: ['全部', '草稿', '已发布', '已结束'],
headerHeight: 96, // 默认头部高度
statsData: [
{
value: 0,
label: '总活动数',
icon: 'calendar',
color: '#ff5722',
clickable: true
},
{
value: 0,
label: '进行中',
icon: 'checkmarkempty',
color: '#4caf50',
clickable: true
},
{
value: 0,
label: '待发布',
icon: 'clock',
color: '#ff9800',
clickable: true
}
]
}
},
computed: {
containerStyle() {
return {
paddingTop: this.headerHeight + 'px'
}
}
},
onLoad() {
this.initData()
this.listenHeaderHeight()
this.calculateHeaderHeight()
},
onUnload() {
uni.$off('header-height-updated', this.onHeaderHeightUpdated)
},
onShow() {
// 页面显示时刷新数据
this.refreshStats()
},
methods: {
async initData() {
this.loading = true
try {
await Promise.all([
this.getActivityList(),
this.getStatsData()
])
} finally {
this.loading = false
this.statsLoading = false
}
},
async getStatsData() {
try {
// 模拟获取统计数据
await new Promise(resolve => setTimeout(resolve, 1000))
this.statsData = [
{
value: Math.floor(Math.random() * 100),
label: '总活动数',
icon: 'calendar',
color: '#ff5722',
clickable: true,
trend: { type: 'up', text: '↑ 12%' }
},
{
value: Math.floor(Math.random() * 50),
label: '进行中',
icon: 'checkmarkempty',
color: '#4caf50',
clickable: true,
trend: { type: 'stable', text: '→ 0%' }
},
{
value: Math.floor(Math.random() * 20),
label: '待发布',
icon: 'clock',
color: '#ff9800',
clickable: true,
trend: { type: 'down', text: '↓ 5%' }
}
]
} catch (error) {
console.log('获取统计数据失败', error)
}
},
goBack() {
uni.navigateBack()
},
async getActivityList() {
try {
if (this.pageNum === 1) {
this.loading = true
}
this.loadStatus = 'loading'
const params = {
pageNum: this.pageNum,
pageSize: this.pageSize,
title: this.searchText,
status: this.currentTab === 0 ? '' : this.currentTab - 1
}
const res = await getActivityList(params)
if (this.pageNum === 1) {
this.activityList = res.rows || []
} else {
this.activityList = [...this.activityList, ...(res.rows || [])]
}
this.loadStatus = (res.rows?.length || 0) < this.pageSize ? 'noMore' : 'more'
} catch (error) {
this.loadStatus = 'more'
this.$modal.showToast('获取活动列表失败')
} finally {
this.loading = false
this.refreshing = false
}
},
// 下拉刷新
onRefresh() {
this.refreshing = true
this.pageNum = 1
this.getActivityList()
},
// 上拉加载
onLoadMore() {
if (this.loadStatus === 'more') {
this.pageNum++
this.getActivityList()
}
},
// 刷新统计数据
refreshStats() {
this.statsLoading = true
setTimeout(() => {
this.getStatsData()
}, 100)
},
// 搜索
searchActivity() {
this.pageNum = 1
this.getActivityList()
},
// 清空搜索
clearSearch() {
this.searchText = ''
this.searchActivity()
},
// 切换标签
switchTab(e) {
this.currentTab = e.currentIndex
this.pageNum = 1
this.getActivityList()
},
// 统计卡片点击
handleStatClick({ item, index }) {
console.log('统计卡片点击', item, index)
// 可以根据点击的统计项进行筛选
},
addActivity() {
uni.navigateTo({
url: '/subpages/activity/add'
})
},
viewActivity(item) {
uni.navigateTo({
url: `/subpages/activity/detail?id=${item.id}`
})
},
editActivity(item) {
uni.navigateTo({
url: `/subpages/activity/edit?id=${item.id}`
})
},
async deleteActivity(item) {
try {
const res = await this.$modal.showConfirm('确定删除此活动吗?')
if (res.confirm) {
await deleteActivity(item.id)
this.$modal.showToast('删除成功')
this.pageNum = 1
this.getActivityList()
}
} catch (error) {
this.$modal.showToast('删除失败')
}
},
async publishActivity(item) {
try {
const res = await this.$modal.showConfirm('确定发布此活动吗?')
if (res.confirm) {
await publishActivity(item.id)
this.$modal.showToast('发布成功')
this.pageNum = 1
this.getActivityList()
}
} catch (error) {
this.$modal.showToast('发布失败')
}
},
formatTime(time) {
if (!time) return ''
return time.substring(0, 16)
},
getStatusText(status) {
const statusMap = {
0: '草稿',
1: '已发布',
2: '进行中',
3: '已结束'
}
return statusMap[status] || '未知'
},
getStatusClass(status) {
const classMap = {
0: 'draft',
1: 'published',
2: 'active',
3: 'finished'
}
return classMap[status] || 'unknown'
},
// 监听头部高度变化
listenHeaderHeight() {
uni.$on('header-height-updated', this.onHeaderHeightUpdated)
},
// 头部高度更新处理
onHeaderHeightUpdated(data) {
this.headerHeight = data.headerHeight
this.$nextTick(() => {
// 动态更新页面顶部间距
const query = uni.createSelectorQuery().in(this)
query.select('.activity-container').boundingClientRect()
query.exec((res) => {
if (res[0]) {
// 可以在这里执行额外的布局调整
console.log('页面头部高度已更新:', this.headerHeight)
}
})
})
},
// 直接计算头部高度
calculateHeaderHeight() {
try {
const systemInfo = uni.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 0
let navBarHeight = 44
// #ifdef MP-WEIXIN
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
if (menuButtonInfo) {
const topGap = menuButtonInfo.top - statusBarHeight
const bottomGap = topGap
navBarHeight = topGap + menuButtonInfo.height + bottomGap
}
} catch (e) {
console.warn('获取胶囊按钮信息失败:', e)
}
// #endif
// 确保最小高度
if (navBarHeight < 44) {
navBarHeight = 44
}
this.headerHeight = statusBarHeight + navBarHeight
console.log('计算的头部高度:', this.headerHeight)
} catch (e) {
console.warn('计算头部高度失败:', e)
this.headerHeight = 96 // 使用默认值
}
}
}
}
</script>
<style lang="scss" scoped>
.activity-container {
min-height: 100vh;
background: linear-gradient(180deg, #f8f9fa 0%, #f5f7fa 100%);
padding-bottom: 140rpx;
box-sizing: border-box;
// 头部间距将通过动态样式设置
// 如果动态样式不生效,使用默认值
padding-top: 96px;
}
.search-filter-section {
background: #fff;
padding: 28rpx 24rpx;
margin: 20rpx 20rpx 16rpx;
border-radius: 20rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06);
border: 1rpx solid #f0f0f0;
}
.scroll-view {
height: calc(100vh - 400rpx);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 40rpx;
text-align: center;
.empty-text {
font-size: 28rpx;
color: #999;
margin: 30rpx 0 40rpx;
}
}
.activity-list {
padding: 0 20rpx;
}
.activity-item {
background: #fff;
border-radius: 20rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.08);
border: 1rpx solid #f5f5f5;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.12);
}
.item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
.title-section {
flex: 1;
display: flex;
align-items: center;
gap: 16rpx;
.activity-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
line-height: 1.3;
}
.status-badge {
padding: 8rpx 16rpx;
border-radius: 24rpx;
font-size: 22rpx;
color: #fff;
font-weight: 500;
white-space: nowrap;
&.draft {
background: linear-gradient(45deg, #ff9800, #ffb74d);
box-shadow: 0 2rpx 8rpx rgba(255, 152, 0, 0.3);
}
&.published {
background: linear-gradient(45deg, #4caf50, #66bb6a);
box-shadow: 0 2rpx 8rpx rgba(76, 175, 80, 0.3);
}
&.active {
background: linear-gradient(45deg, #2196f3, #42a5f5);
box-shadow: 0 2rpx 8rpx rgba(33, 150, 243, 0.3);
}
&.finished {
background: linear-gradient(45deg, #666, #888);
box-shadow: 0 2rpx 8rpx rgba(102, 102, 102, 0.3);
}
}
}
.participant-count {
font-size: 26rpx;
color: #ff5722;
font-weight: 600;
background: rgba(255, 87, 34, 0.1);
padding: 8rpx 12rpx;
border-radius: 16rpx;
white-space: nowrap;
}
}
.activity-desc {
font-size: 28rpx;
color: #666;
line-height: 1.4;
margin-bottom: 20rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.item-info {
margin-bottom: 20rpx;
.info-item {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.info-text {
font-size: 24rpx;
color: #999;
margin-left: 8rpx;
}
}
}
.item-actions {
display: flex;
gap: 16rpx;
.action-btn {
flex: 1;
height: 64rpx;
line-height: 64rpx;
font-size: 26rpx;
border-radius: 32rpx;
font-weight: 500;
transition: all 0.3s ease;
&.edit-btn {
background: #f8f9fa;
color: #666;
border: 1rpx solid #e9ecef;
&:active {
background: #e9ecef;
transform: scale(0.98);
}
}
&.publish-btn {
background: linear-gradient(45deg, #4caf50, #66bb6a);
border: none;
box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.3);
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(76, 175, 80, 0.4);
}
}
&.delete-btn {
background: linear-gradient(45deg, #f44336, #e57373);
border: none;
box-shadow: 0 4rpx 12rpx rgba(244, 67, 54, 0.3);
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(244, 67, 54, 0.4);
}
}
}
}
}
.fab {
position: fixed;
right: 40rpx;
bottom: 120rpx;
width: 112rpx;
height: 112rpx;
border-radius: 50%;
background: linear-gradient(135deg, #ff5722, #ff7043);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 32rpx rgba(255, 87, 34, 0.4);
z-index: 999;
transition: all 0.3s ease;
&:active {
transform: scale(0.9);
}
&:hover {
box-shadow: 0 12rpx 40rpx rgba(255, 87, 34, 0.5);
transform: translateY(-4rpx);
}
}
</style>