quanyoumi/frontend/src/views/admin/ActivityManage.vue

3597 lines
99 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>
<div class="activity-manage">
<van-nav-bar
:title="activity.title"
left-arrow
@click-left="$router.back()"
>
<template #right>
<van-icon name="edit" @click="editActivity" />
</template>
</van-nav-bar>
<van-tabs v-model:active="activeTab" color="#FF6700" title-active-color="#FF6700">
<van-tab title="基本信息" name="info">
<div class="tab-content">
<!-- 统计摘要 -->
<div class="info-stats">
<div class="stat-box">
<van-icon name="bill-o" color="#FF6700" />
<div class="stat-text">
<div class="stat-num">¥{{ totalExpense }}</div>
<div class="stat-desc">总支出</div>
</div>
</div>
<div class="stat-box">
<van-icon name="friends-o" color="#f5576c" />
<div class="stat-text">
<div class="stat-num">{{ registrations.length }}</div>
<div class="stat-desc">报名人数</div>
</div>
</div>
<div class="stat-box">
<van-icon name="photo-o" color="#00f2fe" />
<div class="stat-text">
<div class="stat-num">{{ approvedPhotos.length + pendingPhotos.length }}</div>
<div class="stat-desc">媒体数量</div>
</div>
</div>
<div class="stat-box">
<van-icon name="clock-o" color="#ffa940" />
<div class="stat-text">
<div class="stat-num">{{ registrations.filter(r => r.status === 'pending').length + pendingPhotos.length }}</div>
<div class="stat-desc">待审核</div>
</div>
</div>
</div>
<!-- 基本信息 -->
<van-cell-group>
<van-cell title="活动标题" :value="activity.title" />
<van-cell title="人均费用" :value="activity.price ? `¥${activity.price}` : '免费'" />
<van-cell title="活动预算" :value="activity.budget ? `¥${activity.budget}` : '未设置'" />
<van-cell title="活动地点" :value="activity.location" />
<van-cell title="开始时间" :value="formatDate(activity.startTime)" />
<van-cell title="报名截止" :value="formatDate(activity.registrationDeadline)" />
<van-cell title="状态" :value="activity.status === 'upcoming' ? '进行中' : '已结束'" />
<van-cell title="报名开关" center>
<template #right-icon>
<van-switch
v-model="activity.registrationEnabled"
size="24px"
@change="toggleRegistration"
active-color="#FF6700"
/>
</template>
</van-cell>
<van-cell title="弹幕开关" center>
<template #right-icon>
<van-switch
v-model="activity.danmakuEnabled"
size="24px"
@change="toggleDanmaku"
active-color="#d4af37"
/>
</template>
</van-cell>
<van-cell title="活动描述" :label="activity.description" />
</van-cell-group>
</div>
</van-tab>
<van-tab title="经费管理" name="expenses">
<div class="tab-content">
<!-- 经费统计 -->
<div class="info-stats">
<div class="stat-box">
<van-icon name="balance-pay" color="#FF6700" />
<div class="stat-text">
<div class="stat-num">¥{{ totalExpense }}</div>
<div class="stat-desc">总支出</div>
</div>
</div>
<div class="stat-box">
<van-icon name="balance-o" color="#07c160" />
<div class="stat-text">
<div class="stat-num">¥{{ activity.budget || '0' }}</div>
<div class="stat-desc">预算</div>
</div>
</div>
<div class="stat-box">
<van-icon name="chart-trending-o" :color="activity.budget && totalExpense > activity.budget ? '#ee0a24' : '#ffa940'" />
<div class="stat-text">
<div class="stat-num" :style="{ color: activity.budget && totalExpense > activity.budget ? '#ee0a24' : '#333' }">
{{ activity.budget ? ((totalExpense / activity.budget) * 100).toFixed(1) : '0' }}%
</div>
<div class="stat-desc">使用率</div>
</div>
</div>
<div class="stat-box">
<van-icon name="records" color="#f5576c" />
<div class="stat-text">
<div class="stat-num">{{ expenses.length }}</div>
<div class="stat-desc">经费项</div>
</div>
</div>
</div>
<div class="expense-list">
<div v-for="expense in expenses" :key="expense.id" class="expense-item">
<div class="expense-icon" :style="{ background: getCategoryStyle(expense.category).gradient }">
<van-icon :name="getCategoryStyle(expense.category).icon" />
</div>
<div class="expense-content">
<div class="expense-name">{{ expense.item }}</div>
<div class="expense-meta">
<span class="expense-cat">{{ expense.category }}</span>
<span v-if="expense.note" class="expense-note">{{ expense.note }}</span>
</div>
</div>
<div class="expense-right">
<div class="expense-amount">¥{{ expense.amount }}</div>
<div class="expense-actions">
<van-icon name="edit" @click="editExpense(expense)" />
<van-icon name="delete-o" @click="deleteExpenseItem(expense.id)" />
</div>
</div>
</div>
</div>
<!-- 悬浮添加按钮 -->
<div class="float-add-btn" @click="addNewExpense">
<van-icon name="plus" />
</div>
</div>
</van-tab>
<van-tab title="赞助商" name="sponsors">
<div class="tab-content">
<div class="sponsor-list">
<div v-for="sponsor in sponsors" :key="sponsor.id" class="sponsor-card">
<div class="sponsor-logo-wrapper">
<div class="sponsor-logo" :style="sponsor.logo ? { backgroundImage: `url(${getImageUrl(sponsor.logo)})` } : {}">
<van-icon v-if="!sponsor.logo" name="shop-o" />
</div>
</div>
<div class="sponsor-details">
<div class="sponsor-header">
<span class="sponsor-name">{{ sponsor.name }}</span>
<span class="sponsor-badge" :class="sponsor.type === 'title' ? 'badge-title' : 'badge-sponsor'">
{{ sponsor.type === 'title' ? '冠名' : '赞助' }}
</span>
</div>
<div class="sponsor-meta">
<span class="sponsor-sort">排序: {{ sponsor.sortOrder || 0 }}</span>
<div v-if="sponsor.link" class="sponsor-link">
<van-icon name="link-o" />
<span>{{ sponsor.link }}</span>
</div>
</div>
</div>
<div class="sponsor-ops">
<van-icon name="edit" @click="editSponsor(sponsor)" />
<van-icon name="delete-o" @click="deleteSponsorItem(sponsor.id)" />
</div>
</div>
</div>
<van-empty v-if="!sponsors.length" description="暂无赞助商" />
<!-- 悬浮添加按钮 -->
<div class="float-add-btn" @click="addNewSponsor">
<van-icon name="plus" />
</div>
</div>
</van-tab>
<van-tab title="奖品管理" name="prizes">
<div class="tab-content">
<div class="prize-list">
<div v-for="prize in prizes" :key="prize.id" class="prize-manage-card">
<div class="prize-image-wrapper">
<div class="prize-image" :style="prize.image ? { backgroundImage: `url(${getImageUrl(prize.image)})` } : {}">
<van-icon v-if="!prize.image" name="gift-o" size="30" />
</div>
</div>
<div class="prize-details">
<div class="prize-header">
<span class="prize-level-badge" :class="`level-${prize.level}`">{{ prize.level }}</span>
<span class="prize-name">{{ prize.name }}</span>
</div>
<div v-if="prize.description" class="prize-desc-text">{{ prize.description }}</div>
<div class="prize-meta">
<span class="prize-quantity-text">数量: {{ prize.quantity }}</span>
<span class="prize-sort">排序: {{ prize.sortOrder || 0 }}</span>
</div>
</div>
<div class="prize-ops">
<van-icon name="edit" @click="editPrize(prize)" />
<van-icon name="delete-o" @click="deletePrizeItem(prize.id)" />
</div>
</div>
</div>
<van-empty v-if="!prizes.length" description="暂无奖品" />
<!-- 悬浮添加按钮 -->
<div class="float-add-btn" @click="addNewPrize">
<van-icon name="plus" />
</div>
</div>
</van-tab>
<van-tab title="报名情况" name="registrations">
<div class="tab-content">
<!-- 统计栏 -->
<div class="info-stats reg-stats">
<div class="stat-box" :class="{ active: activeFilter === 'pending' }" @click="setFilter('pending')">
<van-icon name="clock-o" color="#ffa940" />
<div class="stat-text">
<div class="stat-num">{{ registrations.filter(r => r.status === 'pending').length }}</div>
<div class="stat-desc">待审核</div>
</div>
</div>
<div class="stat-box" :class="{ active: activeFilter === 'approved' }" @click="setFilter('approved')">
<van-icon name="passed" color="#07c160" />
<div class="stat-text">
<div class="stat-num">{{ registrations.filter(r => r.status === 'approved').length }}</div>
<div class="stat-desc">已通过</div>
</div>
</div>
<div class="stat-box">
<van-icon name="friends" color="#1989fa" />
<div class="stat-text">
<div class="stat-num">{{ approvedTotalPeople }}</div>
<div class="stat-desc">参与人数</div>
</div>
</div>
<div class="stat-box" :class="{ active: activeFilter === 'rejected' }" @click="setFilter('rejected')">
<van-icon name="close" color="#ee0a24" />
<div class="stat-text">
<div class="stat-num">{{ registrations.filter(r => r.status === 'rejected').length }}</div>
<div class="stat-desc">已拒绝</div>
</div>
</div>
<div class="stat-box" :class="{ active: activeFilter === 'all' }" @click="setFilter('all')">
<van-icon name="friends-o" color="#FF6700" />
<div class="stat-text">
<div class="stat-num">{{ registrations.length }}</div>
<div class="stat-desc">总报名</div>
</div>
</div>
<div class="stat-box" :class="{ active: activeFilter === 'checkedIn' }" @click="setFilter('checkedIn')">
<van-icon name="success" color="#07c160" />
<div class="stat-text">
<div class="stat-num">{{ registrations.filter(r => r.checkInStatus).length }}</div>
<div class="stat-desc">已签到</div>
</div>
</div>
<div class="stat-box" :class="{ active: activeFilter === 'notCheckedIn' }" @click="setFilter('notCheckedIn')">
<van-icon name="clock" color="#999" />
<div class="stat-text">
<div class="stat-num">{{ registrations.filter(r => !r.checkInStatus).length }}</div>
<div class="stat-desc">未签到</div>
</div>
</div>
<div class="stat-box" :class="{ active: activeFilter === 'grouped' }" @click="setFilter('grouped')">
<van-icon name="chat-o" color="#1989fa" />
<div class="stat-text">
<div class="stat-num">{{ registrations.filter(r => r.groupStatus).length }}</div>
<div class="stat-desc">已进群</div>
</div>
</div>
<div class="stat-box" :class="{ active: activeFilter === 'notGrouped' }" @click="setFilter('notGrouped')">
<van-icon name="comment-o" color="#999" />
<div class="stat-text">
<div class="stat-num">{{ registrations.filter(r => !r.groupStatus).length }}</div>
<div class="stat-desc">未进群</div>
</div>
</div>
</div>
<!-- 搜索框 -->
<div class="search-box">
<van-search
v-model="regSearchKeyword"
placeholder="搜索手机号或车牌号"
@search="onRegSearch"
@clear="onRegSearchClear"
/>
</div>
<div class="reg-list">
<div v-for="reg in filteredRegistrations" :key="reg.id" class="reg-item" :class="{ 'checked-in': reg.checkInStatus, 'status-approved': reg.status === 'approved', 'status-rejected': reg.status === 'rejected' }" @click="showRegDetail(reg)">
<div class="reg-left">
<div class="reg-avatar">{{ reg.name ? reg.name.charAt(0) : '?' }}</div>
<div class="reg-main">
<div class="reg-top">
<span class="reg-name">{{ reg.name || '未知' }}</span>
</div>
<div class="reg-info">
<a :href="`tel:${reg.phone}`" class="info-link" @click.stop>
<van-icon name="phone-o" />{{ reg.phone }}
</a>
<span v-if="reg.carNumber" class="info-text car-number" @click.stop="copyCarNumber(reg.carNumber)">
<van-icon name="certificate" />{{ reg.carNumber }}
</span>
<span class="info-text people-count">
<van-icon name="friends-o" />{{ reg.peopleCount || 1 }}人
</span>
</div>
</div>
</div>
<div class="reg-right">
<div class="status-indicators">
<div
class="status-dot"
:class="{
'status-approved': reg.status === 'approved',
'status-rejected': reg.status === 'rejected',
'status-pending': reg.status === 'pending'
}"
:title="reg.status === 'approved' ? '已审核' : reg.status === 'rejected' ? '已拒绝' : '待审核'"
>
<van-icon v-if="reg.status === 'approved'" name="passed" />
<van-icon v-else-if="reg.status === 'rejected'" name="close" />
<van-icon v-else name="clock-o" />
</div>
<div
class="status-dot"
:class="{ 'status-active': reg.checkInStatus }"
:title="reg.checkInStatus ? '已签到' : '未签到'"
>
<van-icon name="location" />
</div>
<div
class="status-dot"
:class="{ 'status-active': reg.groupStatus }"
:title="reg.groupStatus ? '已进群' : '未进群'"
>
<van-icon name="chat-o" />
</div>
</div>
<van-icon name="arrow" color="#ccc" />
</div>
</div>
</div>
<van-empty v-if="filteredRegistrations.length === 0" :description="regSearchKeyword ? '未找到匹配的报名' : '暂无报名'" />
<!-- 悬浮添加按钮 -->
<div class="float-add-btn" @click="addNewRegistration">
<van-icon name="plus" />
</div>
</div>
</van-tab>
<van-tab title="媒体管理" name="media">
<div class="tab-content">
<!-- 统计摘要 -->
<div class="info-stats">
<div class="stat-box">
<van-icon name="photo-o" color="#FF6700" />
<div class="stat-text">
<div class="stat-num">{{ approvedPhotos.filter(p => p.type === 'image').length + pendingPhotos.filter(p => p.type === 'image').length }}</div>
<div class="stat-desc">图片</div>
</div>
</div>
<div class="stat-box">
<van-icon name="video-o" color="#f5576c" />
<div class="stat-text">
<div class="stat-num">{{ approvedPhotos.filter(p => p.type === 'video').length + pendingPhotos.filter(p => p.type === 'video').length }}</div>
<div class="stat-desc">视频</div>
</div>
</div>
<div class="stat-box">
<van-icon name="clock-o" color="#ffa940" />
<div class="stat-text">
<div class="stat-num">{{ pendingPhotos.length }}</div>
<div class="stat-desc">待审核</div>
</div>
</div>
<div class="stat-box">
<van-icon name="passed" color="#07c160" />
<div class="stat-text">
<div class="stat-num">{{ approvedPhotos.length }}</div>
<div class="stat-desc">已通过</div>
</div>
</div>
</div>
<!-- 待审核 -->
<div v-if="pendingPhotos.length" class="media-section">
<div class="section-title">
<span>待审核</span>
<van-badge :content="pendingPhotos.length" />
</div>
<div class="media-list">
<div v-for="photo in pendingPhotos" :key="photo.id" class="media-card">
<div class="card-thumb" @click="photo.type === 'image' ? previewImage(photo.url) : playVideo(photo.url)">
<van-image v-if="photo.type === 'image'" :src="photo.url" fit="cover" />
<div v-else class="video-thumb">
<video :src="photo.url"></video>
<van-icon name="play-circle-o" class="play-btn" />
</div>
</div>
<div class="card-info">
<div class="info-row">
<van-tag :type="photo.type === 'image' ? 'primary' : 'success'" plain size="mini">
{{ photo.type === 'image' ? '图片' : '视频' }}
</van-tag>
<span class="uploader">{{ photo.uploadedBy || '用户' }}</span>
</div>
<div class="action-row">
<van-button size="mini" type="success" plain @click="approvePhoto(photo.id, 'approved')">通过</van-button>
<van-button size="mini" type="danger" plain @click="approvePhoto(photo.id, 'rejected')">拒绝</van-button>
<van-button size="mini" type="warning" plain @click="deletePhoto(photo.id)">删除</van-button>
</div>
</div>
</div>
</div>
</div>
<!-- 已通过 -->
<div v-if="approvedPhotos.length" class="media-section">
<div class="section-title">
<span>已通过</span>
<van-badge :content="approvedPhotos.length" color="#07c160" />
</div>
<div class="media-grid">
<div v-for="photo in approvedPhotos" :key="photo.id" class="grid-item" @click="photo.type === 'image' ? previewImage(photo.url) : playVideo(photo.url)">
<van-image v-if="photo.type === 'image'" :src="photo.url" fit="cover" />
<div v-else class="video-thumb">
<video :src="photo.url"></video>
<van-icon name="play-circle-o" class="play-btn" />
</div>
<van-icon name="cross" class="del-btn" @click.stop="deletePhoto(photo.id)" />
</div>
</div>
</div>
<van-empty v-if="!pendingPhotos.length && !approvedPhotos.length" description="暂无媒体" />
<!-- 悬浮上传按钮 -->
<van-uploader
v-model="photoFiles"
multiple
:max-count="10"
:after-read="uploadPhotos"
accept="image/*,video/*"
>
<div class="float-add-btn">
<van-icon name="photograph" />
</div>
</van-uploader>
</div>
</van-tab>
<van-tab title="场地管理" name="venue">
<div class="tab-content">
<div class="venue-list">
<div v-for="image in venueImages" :key="image.id" class="venue-item">
<div class="venue-thumb" @click="previewImage(getImageUrl(image.image))">
<van-image :src="getImageUrl(image.image)" fit="cover" />
</div>
<div class="venue-actions">
<van-field
v-model.number="image.sortOrder"
type="number"
label="排序"
placeholder="数字越小越靠前"
@blur="updateVenueOrder(image.id, image.sortOrder)"
/>
<van-button
size="mini"
type="danger"
icon="delete-o"
@click="deleteVenueImage(image.id)"
>删除</van-button>
</div>
</div>
</div>
<van-empty v-if="!venueImages.length" description="暂无场地图片" />
<!-- 悬浮上传按钮 -->
<van-uploader
v-model="venueFiles"
:max-count="1"
:after-read="uploadVenueImage"
accept="image/*"
>
<div class="float-add-btn">
<van-icon name="photograph" />
</div>
</van-uploader>
</div>
</van-tab>
</van-tabs>
<!-- 编辑活动弹窗 -->
<van-popup v-model:show="showActivityForm" position="bottom" round>
<div class="form-popup">
<div class="popup-header">
<h3>编辑活动</h3>
<van-icon name="cross" @click="showActivityForm = false" />
</div>
<van-form @submit="submitActivity" class="popup-form">
<van-cell-group>
<van-field v-model="activityForm.title" label="活动标题" placeholder="请输入活动标题" required />
<van-field v-model="activityForm.description" label="活动描述" type="textarea" placeholder="请输入活动描述" rows="3" autosize />
<van-field
v-model="activityForm.price"
label="人均费用"
type="number"
placeholder="请输入人均费用"
>
<template #left-icon>
<van-icon name="gold-coin-o" style="margin-right: 8px;" />
</template>
<template #button>
<span style="color: #999;">元</span>
</template>
</van-field>
<van-field
v-model="activityForm.budget"
label="活动预算"
type="number"
placeholder="请输入预算金额"
>
<template #left-icon>
<van-icon name="balance-o" style="margin-right: 8px;" />
</template>
</van-field>
<van-field
v-model="activityForm.location"
label="活动地点"
placeholder="点击选择地点"
readonly
is-link
required
:rules="[{ required: true, message: '请选择活动地点' }]"
@click="showMapPicker = true"
>
<template #left-icon>
<van-icon name="location-o" style="margin-right: 8px;" />
</template>
</van-field>
<van-field
v-model="startTimeDisplay"
label="开始时间"
placeholder="请选择开始时间"
readonly
is-link
required
:rules="[{ required: true, message: '请选择开始时间' }]"
@click="showStartTimePicker = true"
>
<template #left-icon>
<van-icon name="clock-o" style="margin-right: 8px;" />
</template>
</van-field>
<van-field
v-model="deadlineDisplay"
label="报名截止"
placeholder="请选择报名截止时间"
readonly
is-link
required
:rules="[{ required: true, message: '请选择报名截止时间' }]"
@click="showDeadlinePicker = true"
>
<template #left-icon>
<van-icon name="calendar-o" style="margin-right: 8px;" />
</template>
</van-field>
</van-cell-group>
<div class="form-actions">
<van-button round block type="primary" native-type="submit" size="large">保存修改</van-button>
</div>
</van-form>
</div>
</van-popup>
<!-- 开始时间选择器 -->
<van-popup v-model:show="showStartTimePicker" position="bottom" round>
<div class="datetime-picker-popup">
<div class="picker-header">
<van-button size="small" @click="showStartTimePicker = false">取消</van-button>
<h3>选择开始时间</h3>
<van-button size="small" type="primary" @click="confirmStartTime">确定</van-button>
</div>
<div class="picker-body">
<input
type="datetime-local"
v-model="startTimeInput"
class="datetime-input"
/>
</div>
</div>
</van-popup>
<!-- 报名截止时间选择器 -->
<van-popup v-model:show="showDeadlinePicker" position="bottom" round>
<div class="datetime-picker-popup">
<div class="picker-header">
<van-button size="small" @click="showDeadlinePicker = false">取消</van-button>
<h3>选择报名截止时间</h3>
<van-button size="small" type="primary" @click="confirmDeadline">确定</van-button>
</div>
<div class="picker-body">
<input
type="datetime-local"
v-model="deadlineInput"
class="datetime-input"
/>
</div>
</div>
</van-popup>
<!-- 地图选点弹窗 -->
<van-popup v-model:show="showMapPicker" position="bottom" round :style="{ height: '80%' }">
<div class="map-picker">
<div class="map-picker-header">
<van-icon name="arrow-left" @click="showMapPicker = false" />
<h3>选择地点</h3>
<van-button type="primary" size="small" @click="confirmLocation">确定</van-button>
</div>
<div class="map-search">
<van-search
v-model="searchKeyword"
placeholder="搜索地点"
@search="searchLocation"
@clear="clearSearch"
show-action
action-text="搜索"
@click-right-icon="searchLocation"
/>
</div>
<div v-if="searchResults.length" class="search-results">
<div
v-for="(result, index) in searchResults"
:key="index"
class="search-result-item"
@click="selectSearchResult(result)"
>
<div class="result-name">{{ result.name }}</div>
<div class="result-address">{{ result.address }}</div>
</div>
</div>
<div id="map-picker-container-manage" class="map-container"></div>
<div class="selected-location">
<van-icon name="location-o" />
<span>{{ selectedLocation.name || '请在地图上选择位置' }}</span>
</div>
</div>
</van-popup>
<!-- 经费表单弹窗 -->
<van-popup v-model:show="showExpenseForm" position="bottom" round>
<div class="form-popup">
<div class="popup-header">
<h3>{{ expenseForm.id ? '编辑经费' : '添加经费' }}</h3>
<van-icon name="cross" @click="closeExpenseForm" />
</div>
<van-form @submit="submitExpense" class="popup-form">
<van-cell-group>
<van-field v-model="expenseForm.item" label="项目" placeholder="例如:场地费" required />
<van-field v-model="expenseForm.amount" label="金额" type="number" placeholder="请输入金额" required />
<van-field
v-model="expenseForm.category"
label="分类"
placeholder="选择或输入分类"
is-link
readonly
@click="showCategoryPicker = true"
/>
<van-field v-model="expenseForm.note" label="备注" type="textarea" placeholder="备注信息" rows="2" autosize />
<van-field name="media" label="照片/视频">
<template #input>
<div class="expense-media-upload">
<van-uploader
v-model="expenseMediaFiles"
multiple
:max-count="5"
accept="image/*,video/*"
:after-read="onExpenseMediaRead"
:disabled="expenseForm.media.length + expenseMediaFiles.length >= 5"
>
<van-button icon="photograph" type="primary" size="small">
上传 ({{ expenseForm.media.length + expenseMediaFiles.length }}/5)
</van-button>
</van-uploader>
<!-- 已有媒体 -->
<div v-if="expenseForm.media.length" class="existing-media">
<div class="media-section-title">已有媒体</div>
<div class="media-list">
<div v-for="(item, index) in expenseForm.media" :key="'existing-' + index" class="media-thumb">
<van-image v-if="item.type === 'image'" :src="item.url" fit="cover" />
<video v-else :src="item.url" class="video-thumb"></video>
<van-icon name="cross" class="remove-icon" @click="removeExpenseMedia(index)" />
</div>
</div>
</div>
<!-- 新上传媒体 -->
<div v-if="expenseMediaFiles.length" class="existing-media">
<div class="media-section-title">新上传</div>
<div class="media-list">
<div v-for="(item, index) in expenseMediaFiles" :key="'new-' + index" class="media-thumb">
<van-image v-if="item.content" :src="item.content" fit="cover" />
<van-icon name="cross" class="remove-icon" @click="removeNewExpenseMedia(index)" />
</div>
</div>
</div>
</div>
</template>
</van-field>
</van-cell-group>
<div class="form-actions">
<van-button round block type="primary" native-type="submit" size="large">保存</van-button>
</div>
</van-form>
</div>
</van-popup>
<!-- 分类选择器 -->
<van-popup v-model:show="showCategoryPicker" position="bottom" round>
<div class="category-picker">
<div class="picker-header">
<h3>选择分类</h3>
<van-icon name="cross" @click="showCategoryPicker = false" />
</div>
<div class="category-list">
<div
v-for="cat in expenseCategories"
:key="cat.name"
class="category-item"
@click="selectCategory(cat.name)"
>
<div class="category-icon" :style="{ background: cat.gradient }">
<van-icon :name="cat.icon" />
</div>
<span>{{ cat.name }}</span>
</div>
<div class="category-item" @click="showCustomCategory = true">
<div class="category-icon" style="background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);">
<van-icon name="add-o" />
</div>
<span>自定义</span>
</div>
</div>
</div>
</van-popup>
<!-- 自定义分类输入 -->
<van-dialog
v-model:show="showCustomCategory"
title="自定义分类"
show-cancel-button
@confirm="confirmCustomCategory"
>
<div style="padding: 16px;">
<van-field v-model="customCategoryName" placeholder="请输入分类名称" />
</div>
</van-dialog>
<!-- 赞助商表单弹窗 -->
<van-popup v-model:show="showSponsorForm" position="bottom" round>
<div class="form-popup">
<div class="popup-header">
<h3>{{ sponsorForm.id ? '编辑赞助商' : '添加赞助商' }}</h3>
<van-icon name="cross" @click="showSponsorForm = false" />
</div>
<van-form @submit="submitSponsor" class="popup-form">
<van-cell-group>
<van-field v-model="sponsorForm.name" label="名称" placeholder="请输入赞助商名称" required />
<van-field name="type" label="类型">
<template #input>
<van-radio-group v-model="sponsorForm.type" direction="horizontal">
<van-radio name="sponsor">赞助</van-radio>
<van-radio name="title">冠名</van-radio>
</van-radio-group>
</template>
</van-field>
<van-field
v-model.number="sponsorForm.sortOrder"
type="number"
label="排序"
placeholder="数字越小越靠前"
/>
<van-field v-model="sponsorForm.link" label="链接" placeholder="官网链接(选填)" />
<van-field name="logo" label="Logo">
<template #input>
<van-uploader
v-model="sponsorLogo"
:max-count="1"
:after-read="onLogoRead"
accept="image/*"
>
<van-button icon="plus" type="primary" size="small">选择图片</van-button>
</van-uploader>
</template>
</van-field>
</van-cell-group>
<div class="form-actions">
<van-button round block type="primary" native-type="submit" size="large">保存</van-button>
</div>
</van-form>
</div>
</van-popup>
<!-- 奖品表单弹窗 -->
<van-popup v-model:show="showPrizeForm" position="bottom" round>
<div class="form-popup">
<div class="popup-header">
<h3>{{ prizeForm.id ? '编辑奖品' : '添加奖品' }}</h3>
<van-icon name="cross" @click="showPrizeForm = false" />
</div>
<van-form @submit="submitPrize" class="popup-form">
<van-cell-group>
<van-field v-model="prizeForm.name" label="名称" placeholder="请输入奖品名称" required />
<van-field
v-model="prizeForm.level"
label="等级"
placeholder="选择等级"
is-link
readonly
@click="showLevelPicker = true"
required
/>
<van-field v-model="prizeForm.description" label="描述" type="textarea" placeholder="奖品描述(选填)" rows="2" autosize />
<van-field
v-model="prizeForm.source"
label="来源"
placeholder="选择来源"
is-link
readonly
@click="showSourcePicker = true"
required
/>
<van-field
v-if="prizeForm.source !== 'purchase'"
v-model="prizeForm.sponsor"
label="赞助者"
placeholder="请输入赞助者名称"
required
/>
<van-field
v-model.number="prizeForm.quantity"
type="number"
label="数量"
placeholder="请输入数量"
required
/>
<van-field
v-model.number="prizeForm.sortOrder"
type="number"
label="排序"
placeholder="数字越小越靠前"
/>
<van-field name="image" label="图片">
<template #input>
<van-uploader
v-model="prizeImage"
:max-count="1"
:after-read="onPrizeImageRead"
accept="image/*"
>
<van-button icon="plus" type="primary" size="small">选择图片</van-button>
</van-uploader>
</template>
</van-field>
</van-cell-group>
<div class="form-actions">
<van-button round block type="primary" native-type="submit" size="large">保存</van-button>
</div>
</van-form>
</div>
</van-popup>
<!-- 等级选择器 -->
<van-popup v-model:show="showLevelPicker" position="bottom" round>
<div class="level-picker">
<div class="picker-header">
<h3>选择等级</h3>
<van-icon name="cross" @click="showLevelPicker = false" />
</div>
<div class="level-list">
<div
v-for="level in prizeLevels"
:key="level"
class="level-item"
@click="selectLevel(level)"
>
<span class="prize-level-badge" :class="`level-${level}`">{{ level }}</span>
</div>
</div>
</div>
</van-popup>
<!-- 来源选择器 -->
<van-popup v-model:show="showSourcePicker" position="bottom" round>
<div class="level-picker">
<div class="picker-header">
<h3>选择来源</h3>
<van-icon name="cross" @click="showSourcePicker = false" />
</div>
<div class="level-list">
<div class="level-item" @click="selectSource('member')">
<van-icon name="friends-o" color="#FF6700" />
<span>群友赞助</span>
</div>
<div class="level-item" @click="selectSource('sponsor')">
<van-icon name="shop-o" color="#FF6700" />
<span>赞助商提供</span>
</div>
<div class="level-item" @click="selectSource('purchase')">
<van-icon name="shopping-cart-o" color="#999" />
<span>采购</span>
</div>
</div>
</div>
</van-popup>
<!-- 视频播放弹窗 -->
<van-popup v-model:show="showVideoPlayer" position="center" round :style="{ width: '90%' }">
<div class="video-player-popup">
<video :src="currentVideoUrl" controls autoplay class="popup-video"></video>
</div>
</van-popup>
<!-- 修改人数弹窗 -->
<van-popup v-model:show="showPeopleCountDialog" position="center" round :style="{ width: '80%' }">
<div class="people-count-dialog">
<div class="dialog-header">
<h3>修改人数</h3>
<van-icon name="cross" @click="showPeopleCountDialog = false" />
</div>
<div class="dialog-content">
<div class="count-info">
<span class="info-label">{{ currentEditReg.name }}</span>
<span class="info-desc">当前人数:{{ currentEditReg.peopleCount || 1 }}人</span>
</div>
<van-stepper v-model="newPeopleCount" min="1" max="20" />
</div>
<div class="dialog-actions">
<van-button block type="primary" @click="confirmPeopleCount">确定</van-button>
</div>
</div>
</van-popup>
<!-- 修改车牌号弹窗 -->
<van-popup v-model:show="showCarNumberDialog" position="center" round :style="{ width: '80%' }">
<div class="people-count-dialog">
<div class="dialog-header">
<h3>修改车牌号</h3>
<van-icon name="cross" @click="showCarNumberDialog = false" />
</div>
<div class="dialog-content">
<div class="count-info">
<span class="info-label">{{ currentEditReg.name }}</span>
<span class="info-desc">当前车牌号:{{ currentEditReg.carNumber || '未填写' }}</span>
</div>
<van-field v-model="newCarNumber" placeholder="请输入车牌号闽A12345" />
</div>
<div class="dialog-actions">
<van-button block type="primary" @click="confirmCarNumber">确定</van-button>
</div>
</div>
</van-popup>
<!-- 手动录入报名弹窗 -->
<van-popup v-model:show="showRegForm" position="bottom" round>
<div class="form-popup">
<div class="popup-header">
<h3>手动录入报名</h3>
<van-icon name="cross" @click="closeRegForm" />
</div>
<van-form @submit="submitRegistration" class="popup-form">
<van-cell-group>
<van-field
v-model="regForm.name"
label="姓名"
placeholder="请输入姓名"
required
:rules="[{ required: true, message: '请输入姓名' }]"
/>
<van-field
v-model="regForm.phone"
label="手机号"
type="tel"
placeholder="请输入手机号"
required
:rules="[{ required: true, message: '请输入手机号' }]"
/>
<van-field
v-model="regForm.carModel"
label="车型"
placeholder="请输入车型(选填)"
/>
<van-field
v-model="regForm.carNumber"
label="车牌号"
placeholder="请输入车牌号(选填)"
/>
<van-field
v-model.number="regForm.peopleCount"
type="number"
label="人数"
placeholder="请输入人数"
/>
<van-field
v-model="regForm.note"
label="备注"
type="textarea"
placeholder="备注信息(选填)"
rows="2"
autosize
/>
</van-cell-group>
<div class="form-actions">
<van-button round block type="primary" native-type="submit" size="large">提交报名</van-button>
</div>
</van-form>
</div>
</van-popup>
<!-- 报名详情弹窗 -->
<van-popup v-model:show="showRegDetailDialog" position="bottom" round>
<div class="reg-detail-popup">
<div class="popup-header">
<h3>报名详情</h3>
<van-icon name="cross" @click="showRegDetailDialog = false" />
</div>
<div class="detail-content">
<div class="detail-section">
<div class="detail-row">
<span class="detail-label">姓名</span>
<span class="detail-value">{{ currentReg.name || '未知' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">电话</span>
<a :href="`tel:${currentReg.phone}`" class="detail-value detail-link">
<van-icon name="phone-o" />{{ currentReg.phone }}
</a>
</div>
<div class="detail-row" v-if="currentReg.carNumber">
<span class="detail-label">车牌号</span>
<span class="detail-value detail-link" @click="editCarNumber(currentReg)">
<van-icon name="certificate" />{{ currentReg.carNumber }}
<van-icon name="edit" size="14" />
</span>
</div>
<div class="detail-row" v-if="currentReg.carModel">
<span class="detail-label">车型</span>
<span class="detail-value">{{ currentReg.carModel }}</span>
</div>
<div class="detail-row">
<span class="detail-label">人数</span>
<span class="detail-value detail-link" @click="editPeopleCount(currentReg)">
<van-icon name="friends-o" />{{ currentReg.peopleCount || 1 }}人
<van-icon name="edit" size="14" />
</span>
</div>
<div class="detail-row" v-if="currentReg.checkInTime">
<span class="detail-label">签到时间</span>
<span class="detail-value">{{ formatDate(currentReg.checkInTime) }}</span>
</div>
<div class="detail-row status-row">
<span class="detail-label">状态管理</span>
<div class="status-controls">
<div class="status-item">
<span class="status-name">审核(是否交钱)</span>
<van-tag :type="currentReg.status === 'approved' ? 'success' : currentReg.status === 'rejected' ? 'danger' : 'warning'">
{{ currentReg.status === 'approved' ? '已通过' : currentReg.status === 'rejected' ? '已拒绝' : '待审核' }}
</van-tag>
</div>
<div class="status-item">
<span class="status-name">签到(是否到现场)</span>
<van-switch
v-model="currentReg.checkInStatus"
size="20px"
@change="toggleCheckInFromDetail"
active-color="#07c160"
/>
</div>
<div class="status-item">
<span class="status-name">进群(是否加微信群)</span>
<van-switch
v-model="currentReg.groupStatus"
size="20px"
@change="toggleGroupStatusFromDetail"
active-color="#1989fa"
/>
</div>
</div>
</div>
</div>
</div>
<div class="detail-actions">
<div v-if="currentReg.status === 'pending'" class="action-buttons">
<van-button size="large" type="success" @click="approveFromDetail('approved')">通过审核</van-button>
<van-button size="large" type="danger" @click="approveFromDetail('rejected')">拒绝审核</van-button>
</div>
<van-button
block
size="large"
type="danger"
plain
@click="deleteRegistration"
:style="{ marginTop: currentReg.status === 'pending' ? '12px' : '0' }"
>
删除报名
</van-button>
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { showToast, closeToast, showConfirmDialog, ImagePreview } from 'vant';
import api from '../../api';
import { usePrizeManage } from './usePrizeManage';
const route = useRoute();
const router = useRouter();
const activeTab = ref('info');
// 获取完整图片URL的辅助函数
const getImageUrl = (path) => {
if (!path) return '';
if (path.startsWith('http://') || path.startsWith('https://')) {
return path;
}
return import.meta.env.DEV ? `http://localhost:3000${path}` : `https://mi.xyvan.cn${path}`;
};
const activity = ref({});
const expenses = ref([]);
const sponsors = ref([]);
const registrations = ref([]);
const sponsorLogo = ref([]);
const photoFiles = ref([]);
const pendingPhotos = ref([]);
const expenseMediaFiles = ref([]);
const approvedPhotos = ref([]);
const venueImages = ref([]);
const venueFiles = ref([]);
const showVideoPlayer = ref(false);
const currentVideoUrl = ref('');
const regSearchKeyword = ref('');
const showPeopleCountDialog = ref(false);
const currentEditReg = ref({});
const newPeopleCount = ref(1);
const showCarNumberDialog = ref(false);
const newCarNumber = ref('');
const showRegDetailDialog = ref(false);
const currentReg = ref({});
const activeFilter = ref('all'); // 过滤器状态
const showActivityForm = ref(false);
const showExpenseForm = ref(false);
const showSponsorForm = ref(false);
const showRegForm = ref(false);
const regForm = ref({
name: '',
phone: '',
carModel: '',
carNumber: '',
peopleCount: 1,
note: ''
});
// 奖品管理
const activityId = computed(() => route.params.id);
const {
prizes,
showPrizeForm,
showLevelPicker,
showSourcePicker,
prizeImage,
prizeForm,
prizeLevels,
loadPrizes,
editPrize,
onPrizeImageRead,
selectLevel,
selectSource,
submitPrize,
deletePrizeItem,
resetPrizeForm
} = usePrizeManage(activityId);
const showMapPicker = ref(false);
const showStartTimePicker = ref(false);
const showDeadlinePicker = ref(false);
const showCategoryPicker = ref(false);
const showCustomCategory = ref(false);
const customCategoryName = ref('');
const searchKeyword = ref('');
const searchResults = ref([]);
const selectedLocation = ref({});
// 经费分类预设
const expenseCategories = ref([
{ name: '场地', icon: 'shop-o', gradient: 'linear-gradient(135deg, #FF6700 0%, #FF8533 100%)' },
{ name: '餐饮', icon: 'food-o', gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
{ name: '交通', icon: 'logistics', gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
{ name: '住宿', icon: 'hotel-o', gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
{ name: '物料', icon: 'gift-o', gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
{ name: '人员', icon: 'friends-o', gradient: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' },
{ name: '其他', icon: 'more-o', gradient: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' }
]);
let pickerMap = null;
let pickerMarker = null;
let geocoder = null;
const activityForm = ref({
title: '',
description: '',
location: '',
locationLat: null,
locationLng: null,
startTime: '',
registrationDeadline: ''
});
// 日期时间选择器的值
const startTimeInput = ref('');
const deadlineInput = ref('');
// 显示格式化的日期时间
const startTimeDisplay = computed(() => {
if (!activityForm.value.startTime) return '';
return formatDateTime(activityForm.value.startTime);
});
const deadlineDisplay = computed(() => {
if (!activityForm.value.registrationDeadline) return '';
return formatDateTime(activityForm.value.registrationDeadline);
});
const expenseForm = ref({
id: null,
item: '',
amount: '',
category: '',
note: '',
media: []
});
const sponsorForm = ref({
id: null,
name: '',
type: 'sponsor',
link: '',
sortOrder: 0
});
const totalExpense = computed(() => {
return expenses.value.reduce((sum, e) => sum + parseFloat(e.amount), 0).toFixed(2);
});
// 审核通过的总人数(车主+带的人)
const approvedTotalPeople = computed(() => {
return registrations.value
.filter(r => r.status === 'approved')
.reduce((sum, r) => sum + (r.peopleCount || 1), 0);
});
// 过滤报名列表
const filteredRegistrations = computed(() => {
let result = registrations.value;
// 应用过滤器
if (activeFilter.value === 'pending') {
result = result.filter(r => r.status === 'pending');
} else if (activeFilter.value === 'approved') {
result = result.filter(r => r.status === 'approved');
} else if (activeFilter.value === 'rejected') {
result = result.filter(r => r.status === 'rejected');
} else if (activeFilter.value === 'checkedIn') {
result = result.filter(r => r.checkInStatus);
} else if (activeFilter.value === 'notCheckedIn') {
result = result.filter(r => !r.checkInStatus);
} else if (activeFilter.value === 'grouped') {
result = result.filter(r => r.groupStatus);
} else if (activeFilter.value === 'notGrouped') {
result = result.filter(r => !r.groupStatus);
}
// 应用搜索关键词
if (!regSearchKeyword.value || !regSearchKeyword.value.trim()) {
return result;
}
const keyword = regSearchKeyword.value.toLowerCase().trim();
return result.filter(reg => {
const phone = reg.phone ? reg.phone.toLowerCase() : '';
const carNumber = reg.carNumber ? reg.carNumber.toLowerCase() : '';
const name = reg.name ? reg.name.toLowerCase() : '';
return phone.includes(keyword) ||
carNumber.includes(keyword) ||
name.includes(keyword);
});
});
// 设置过滤器
const setFilter = (filter) => {
if (activeFilter.value === filter) {
activeFilter.value = 'all'; // 再次点击取消过滤
} else {
activeFilter.value = filter;
}
};
const onRegSearch = () => {
// 搜索逻辑已在 computed 中处理
};
const onRegSearchClear = () => {
regSearchKeyword.value = '';
};
const formatDate = (date) => {
return new Date(date).toLocaleString('zh-CN');
};
// 格式化日期时间显示
const formatDateTime = (dateTime) => {
if (!dateTime) return '';
const date = new Date(dateTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
// 安全解析datetime-local格式确保以本地时区创建Date对象
const parseDateTimeLocal = (dateTimeStr) => {
console.log('[时区转换] 输入的datetime-local字符串:', dateTimeStr);
const [datePart, timePart] = dateTimeStr.split('T');
const [year, month, day] = datePart.split('-').map(Number);
const [hours, minutes] = timePart.split(':').map(Number);
const localDate = new Date(year, month - 1, day, hours, minutes);
console.log('[时区转换] 创建的本地Date对象:', localDate.toString());
console.log('[时区转换] 转换为UTC (ISO):', localDate.toISOString());
console.log('[时区转换] 时区偏移(分钟):', localDate.getTimezoneOffset());
return localDate;
};
// 开始时间确认
const confirmStartTime = () => {
if (!startTimeInput.value) {
showToast('请选择开始时间');
return;
}
console.log('[开始时间] 用户选择的时间:', startTimeInput.value);
const selectedDate = parseDateTimeLocal(startTimeInput.value);
activityForm.value.startTime = selectedDate.toISOString();
console.log('[开始时间] 保存到表单的UTC时间:', activityForm.value.startTime);
showStartTimePicker.value = false;
};
// 报名截止时间确认
const confirmDeadline = () => {
if (!deadlineInput.value) {
showToast('请选择报名截止时间');
return;
}
console.log('[截止时间] 用户选择的时间:', deadlineInput.value);
const selectedDate = parseDateTimeLocal(deadlineInput.value);
if (activityForm.value.startTime && selectedDate > new Date(activityForm.value.startTime)) {
showToast('报名截止时间不能晚于活动开始时间');
return;
}
activityForm.value.registrationDeadline = selectedDate.toISOString();
console.log('[截止时间] 保存到表单的UTC时间:', activityForm.value.registrationDeadline);
showDeadlinePicker.value = false;
};
// 获取分类样式
const getCategoryStyle = (categoryName) => {
const category = expenseCategories.value.find(c => c.name === categoryName);
if (category) {
return { icon: category.icon, gradient: category.gradient };
}
// 默认样式
return {
icon: 'bill-o',
gradient: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)'
};
};
// 选择分类
const selectCategory = (categoryName) => {
expenseForm.value.category = categoryName;
showCategoryPicker.value = false;
};
// 确认自定义分类
const confirmCustomCategory = () => {
if (customCategoryName.value.trim()) {
expenseForm.value.category = customCategoryName.value.trim();
showCategoryPicker.value = false;
customCategoryName.value = '';
} else {
showToast('请输入分类名称');
}
};
const loadData = async () => {
try {
const { data } = await api.getActivity(route.params.id);
activity.value = {
...data.activity,
danmakuEnabled: Boolean(data.activity.danmakuEnabled),
registrationEnabled: Boolean(data.activity.registrationEnabled)
};
expenses.value = data.expenses || [];
sponsors.value = data.sponsors || [];
// 加载媒体
const allPhotos = data.photos || [];
pendingPhotos.value = allPhotos.filter(p => p.status === 'pending');
approvedPhotos.value = allPhotos.filter(p => p.status === 'approved');
const { data: regs } = await api.getAllRegistrations({ activityId: route.params.id });
registrations.value = regs;
// 加载奖品
await loadPrizes();
// 加载场地图片
await loadVenueImages();
} catch (error) {
showToast('加载失败');
}
};
// 添加新奖品
const addNewPrize = () => {
resetPrizeForm();
showPrizeForm.value = true;
};
const editActivity = () => {
activityForm.value = { ...activity.value };
console.log('[编辑活动] 原始活动数据:', activity.value);
// 设置日期时间选择器的值
if (activity.value.startTime) {
console.log('[编辑活动] 数据库的开始时间(UTC):', activity.value.startTime);
const startDate = new Date(activity.value.startTime);
console.log('[编辑活动] 转换为本地Date对象:', startDate.toString());
startTimeInput.value = formatDateTimeLocal(startDate);
console.log('[编辑活动] 显示在选择器的时间:', startTimeInput.value);
}
if (activity.value.registrationDeadline) {
console.log('[编辑活动] 数据库的截止时间(UTC):', activity.value.registrationDeadline);
const deadlineDate = new Date(activity.value.registrationDeadline);
console.log('[编辑活动] 转换为本地Date对象:', deadlineDate.toString());
deadlineInput.value = formatDateTimeLocal(deadlineDate);
console.log('[编辑活动] 显示在选择器的时间:', deadlineInput.value);
}
showActivityForm.value = true;
};
// 格式化为 datetime-local 输入格式
const formatDateTimeLocal = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
};
const submitActivity = async () => {
try {
const formData = { ...activityForm.value };
// 清理空字符串转换为null
if (formData.budget === '' || formData.budget === undefined) {
formData.budget = null;
}
if (formData.price === '' || formData.price === undefined) {
formData.price = null;
}
await api.updateActivity(activity.value.id, formData);
showToast('保存成功');
showActivityForm.value = false;
loadData();
} catch (error) {
showToast('保存失败');
}
};
const toggleRegistration = async (value) => {
try {
await api.updateActivity(activity.value.id, { registrationEnabled: value });
showToast(value ? '报名已开启' : '报名已关闭');
} catch (error) {
showToast('操作失败');
activity.value.registrationEnabled = !value;
}
};
const toggleDanmaku = async (value) => {
try {
await api.updateActivity(activity.value.id, { danmakuEnabled: value });
showToast(value ? '弹幕已开启' : '弹幕已关闭');
} catch (error) {
showToast('操作失败');
activity.value.danmakuEnabled = !value;
}
};
const submitExpense = async () => {
const loadingToast = showToast({
type: 'loading',
message: '保存中...',
forbidClick: true,
duration: 0
});
try {
const formData = new FormData();
formData.append('activityId', activity.value.id);
formData.append('item', expenseForm.value.item);
formData.append('amount', expenseForm.value.amount);
formData.append('category', expenseForm.value.category);
formData.append('note', expenseForm.value.note);
// 只在编辑时添加现有媒体
if (expenseForm.value.id && expenseForm.value.media && expenseForm.value.media.length > 0) {
formData.append('existingMedia', JSON.stringify(expenseForm.value.media));
}
// 添加新上传的媒体文件
expenseMediaFiles.value.forEach(item => {
if (item.file) {
formData.append('media', item.file);
}
});
if (expenseForm.value.id) {
await api.updateExpense(expenseForm.value.id, formData);
showToast('更新成功');
} else {
await api.addExpense(formData);
showToast('添加成功');
}
closeExpenseForm();
loadData();
} catch (error) {
showToast(expenseForm.value.id ? '更新失败' : '添加失败');
} finally {
loadingToast.clear();
}
};
const editExpense = (expense) => {
expenseForm.value = {
...expense,
media: expense.media || []
};
expenseMediaFiles.value = [];
showExpenseForm.value = true;
};
// 添加新经费
const addNewExpense = () => {
expenseForm.value = { id: null, item: '', amount: '', category: '', note: '', media: [] };
expenseMediaFiles.value = [];
showExpenseForm.value = true;
};
const onExpenseMediaRead = (file) => {
// 验证文件大小和类型
const fileItem = Array.isArray(file) ? file[0] : file;
const maxSize = 50 * 1024 * 1024; // 50MB
if (fileItem.file.size > maxSize) {
showToast('文件大小不能超过50MB');
// 移除超大文件
const index = expenseMediaFiles.value.findIndex(f => f.file === fileItem.file);
if (index > -1) {
expenseMediaFiles.value.splice(index, 1);
}
return;
}
};
const removeExpenseMedia = (index) => {
expenseForm.value.media.splice(index, 1);
};
// 添加新报名
const addNewRegistration = () => {
regForm.value = {
name: '',
phone: '',
carModel: '',
carNumber: '',
peopleCount: 1,
note: ''
};
showRegForm.value = true;
};
// 关闭报名表单
const closeRegForm = () => {
showRegForm.value = false;
regForm.value = {
name: '',
phone: '',
carModel: '',
carNumber: '',
peopleCount: 1,
note: ''
};
};
// 提交报名
const submitRegistration = async () => {
try {
await api.submitRegistration({
activityId: activity.value.id,
...regForm.value
});
showToast('报名成功');
closeRegForm();
loadData();
} catch (error) {
showToast('报名失败');
}
};;
const removeNewExpenseMedia = (index) => {
expenseMediaFiles.value.splice(index, 1);
};
const closeExpenseForm = () => {
showExpenseForm.value = false;
expenseForm.value = { id: null, item: '', amount: '', category: '', note: '', media: [] };
expenseMediaFiles.value = [];
};
const deleteExpenseItem = async (id) => {
try {
await showConfirmDialog({
title: '确认删除',
message: '确定要删除这条经费记录吗?',
});
await api.deleteExpense(id);
showToast('删除成功');
loadData();
} catch (error) {
// 用户取消或删除失败
if (error !== 'cancel') {
showToast('删除失败');
}
}
};
const submitSponsor = async () => {
// 验证必填字段
if (!sponsorForm.value.name || !sponsorForm.value.name.trim()) {
showToast('请输入赞助商名称');
return;
}
console.log('开始提交赞助商...');
console.log('表单数据:', sponsorForm.value);
console.log('Logo 文件:', sponsorLogo.value);
let loadingToast = null;
try {
const formData = new FormData();
if (!sponsorForm.value.id) {
formData.append('activityId', activity.value.id);
}
formData.append('name', sponsorForm.value.name);
formData.append('type', sponsorForm.value.type);
formData.append('link', sponsorForm.value.link || '');
formData.append('sortOrder', sponsorForm.value.sortOrder || 0);
// 检查是否有 Logo 需要上传
const hasLogo = sponsorLogo.value.length > 0 && sponsorLogo.value[0].file;
if (hasLogo) {
const logoSize = (sponsorLogo.value[0].file.size / 1024 / 1024).toFixed(2);
console.log('准备上传 Logo大小:', logoSize, 'MB');
loadingToast = showToast({
type: 'loading',
message: `正在上传 Logo (${logoSize}MB)...`,
forbidClick: true,
duration: 0
});
formData.append('logo', sponsorLogo.value[0].file);
} else if (sponsorLogo.value.length === 0 && sponsorForm.value.logo) {
// 用户删除了logo
formData.append('removeLogo', 'true');
console.log('用户删除了 Logo');
loadingToast = showToast({
type: 'loading',
message: '正在保存...',
forbidClick: true,
duration: 0
});
} else {
console.log('没有 Logo仅保存数据');
loadingToast = showToast({
type: 'loading',
message: '正在保存...',
forbidClick: true,
duration: 0
});
}
console.log('开始调用 API...');
const startTime = Date.now();
if (sponsorForm.value.id) {
await api.updateSponsor(sponsorForm.value.id, formData);
} else {
await api.addSponsor(formData);
}
const endTime = Date.now();
console.log('API 调用完成,耗时:', (endTime - startTime) / 1000, '秒');
// 显示成功提示
showToast({
type: 'success',
message: sponsorForm.value.id ? '更新成功' : '添加成功',
duration: 2000
});
// 关闭弹窗并重置表单
showSponsorForm.value = false;
sponsorForm.value = { id: null, name: '', type: 'sponsor', link: '', sortOrder: 0 };
sponsorLogo.value = [];
console.log('开始重新加载数据...');
// 重新加载数据(不等待,避免阻塞)
loadData();
console.log('数据加载完成');
} catch (error) {
console.error('赞助商提交失败:', error);
console.error('错误详情:', error.response);
const errorMsg = error.response?.data?.error || error.message || '操作失败';
showToast({
type: 'fail',
message: errorMsg,
duration: 3000
});
} finally {
if (loadingToast) {
loadingToast.clear();
}
}
};
const editSponsor = (sponsor) => {
sponsorForm.value = { ...sponsor };
// 回显Logo - 需要使用完整URL
if (sponsor.logo) {
const logoUrl = sponsor.logo.startsWith('http') ? sponsor.logo :
(import.meta.env.DEV ? `http://localhost:3000${sponsor.logo}` : `https://mi.xyvan.cn${sponsor.logo}`);
sponsorLogo.value = [{ url: logoUrl }];
} else {
sponsorLogo.value = [];
}
showSponsorForm.value = true;
};
// 添加新赞助商
const addNewSponsor = () => {
sponsorForm.value = { id: null, name: '', type: 'sponsor', link: '', sortOrder: 0 };
sponsorLogo.value = [];
showSponsorForm.value = true;
};
const onLogoRead = (file) => {
// 文件选择后的回调,显示文件已选择
if (file.file) {
const sizeMB = (file.file.size / 1024 / 1024).toFixed(2);
showToast(`已选择: ${file.file.name} (${sizeMB}MB)`);
}
};
const deleteSponsorItem = async (id) => {
try {
await showConfirmDialog({
title: '确认删除',
message: '确定要删除这个赞助商吗?',
});
await api.deleteSponsor(id);
showToast('删除成功');
loadData();
} catch (error) {
// 用户取消或删除失败
if (error !== 'cancel') {
showToast('删除失败');
}
}
};
const approve = async (id, status) => {
try {
await api.approveRegistration(id, status);
showToast('操作成功');
loadData();
} catch (error) {
showToast('操作失败');
}
};
// 签到/取消签到
const toggleCheckIn = async (id) => {
try {
await api.checkInRegistration(id);
showToast('操作成功');
loadData();
} catch (error) {
showToast('操作失败');
}
};
// 复制车牌号
const copyCarNumber = async (carNumber) => {
try {
await navigator.clipboard.writeText(carNumber);
showToast('车牌号已复制');
} catch (error) {
showToast('复制失败');
}
};
// 修改人数
const editPeopleCount = (reg) => {
currentEditReg.value = reg;
newPeopleCount.value = reg.peopleCount || 1;
showPeopleCountDialog.value = true;
};
const confirmPeopleCount = async () => {
try {
await api.updateRegistration(currentEditReg.value.id, { peopleCount: newPeopleCount.value });
showToast('修改成功');
showPeopleCountDialog.value = false;
loadData();
} catch (error) {
showToast('修改失败');
}
};
// 修改车牌号
const editCarNumber = (reg) => {
currentEditReg.value = reg;
newCarNumber.value = reg.carNumber || '';
showCarNumberDialog.value = true;
};
const confirmCarNumber = async () => {
try {
await api.updateRegistration(currentEditReg.value.id, { carNumber: newCarNumber.value });
showToast('修改成功');
showCarNumberDialog.value = false;
loadData();
} catch (error) {
showToast('修改失败');
}
};
// 显示报名详情
const showRegDetail = (reg) => {
currentReg.value = reg;
showRegDetailDialog.value = true;
};
// 从详情弹窗签到
const toggleCheckInFromDetail = async () => {
const originalStatus = currentReg.value.checkInStatus;
try {
await api.checkInRegistration(currentReg.value.id);
showToast('操作成功');
await loadData();
// 重新打开详情弹窗,显示更新后的数据
const updatedReg = registrations.value.find(r => r.id === currentReg.value.id);
if (updatedReg) {
currentReg.value = updatedReg;
}
} catch (error) {
showToast('操作失败');
currentReg.value.checkInStatus = originalStatus;
}
};
// 从详情弹窗审核
const approveFromDetail = async (status) => {
try {
await api.approveRegistration(currentReg.value.id, status);
showToast('操作成功');
showRegDetailDialog.value = false;
loadData();
} catch (error) {
showToast('操作失败');
}
};
// 从详情弹窗切换进群状态
const toggleGroupStatusFromDetail = async () => {
try {
await api.groupRegistration(currentReg.value.id);
showToast('操作成功');
await loadData();
// 重新打开详情弹窗,显示更新后的数据
const updatedReg = registrations.value.find(r => r.id === currentReg.value.id);
if (updatedReg) {
currentReg.value = updatedReg;
}
} catch (error) {
showToast('操作失败');
}
};
// 删除报名
const deleteRegistration = async () => {
try {
await showConfirmDialog({
title: '确认删除',
message: '删除后用户可以重新报名,确定要删除这条报名信息吗?'
});
await api.deleteRegistration(currentReg.value.id);
showToast('删除成功');
showRegDetailDialog.value = false;
loadData();
} catch (error) {
if (error !== 'cancel') {
showToast('删除失败');
}
}
};
// 媒体管理相关方法
const uploadPhotos = async (files) => {
const fileArray = Array.isArray(files) ? files : [files];
const totalSize = fileArray.reduce((sum, f) => sum + f.file.size, 0);
const totalSizeMB = (totalSize / 1024 / 1024).toFixed(2);
const fileCount = fileArray.length;
const loadingToast = showToast({
type: 'loading',
message: `正在上传 ${fileCount} 个文件 (${totalSizeMB}MB)...`,
forbidClick: true,
duration: 0
});
try {
const formData = new FormData();
formData.append('activityId', activity.value.id);
fileArray.forEach(file => formData.append('photos', file.file));
await api.uploadPhotos(formData);
// 显示成功提示
showToast({
type: 'success',
message: `成功上传 ${fileCount} 个文件`,
duration: 2000
});
// 清空文件列表
photoFiles.value = [];
// 重新加载数据(不等待,避免阻塞)
loadData();
} catch (error) {
console.error('上传失败:', error);
showToast({
type: 'fail',
message: error.response?.data?.error || '上传失败',
duration: 3000
});
} finally {
loadingToast.clear();
}
};
const approvePhoto = async (id, status) => {
try {
await api.approvePhoto(id, status);
showToast(status === 'approved' ? '已通过' : '已拒绝');
loadData();
} catch (error) {
console.error('操作失败:', error);
showToast('操作失败');
}
};
const deletePhoto = async (id) => {
try {
await showConfirmDialog({
title: '确认删除',
message: '删除后无法恢复,确定要删除吗?',
});
await api.deletePhoto(id);
showToast('删除成功');
loadData();
} catch (error) {
// 用户取消或删除失败
if (error !== 'cancel') {
console.error('删除失败:', error);
showToast('删除失败');
}
}
};
const previewImage = (url) => {
ImagePreview([url]);
};
const playVideo = (url) => {
currentVideoUrl.value = url;
showVideoPlayer.value = true;
};
// 场地管理相关方法
const loadVenueImages = async () => {
try {
const { data } = await api.getVenueImages(route.params.id);
venueImages.value = data;
} catch (error) {
console.error('加载场地图片失败:', error);
}
};
const uploadVenueImage = async (file) => {
const loadingToast = showToast({
type: 'loading',
message: '正在上传...',
forbidClick: true,
duration: 0
});
try {
const formData = new FormData();
formData.append('image', file.file);
formData.append('activityId', route.params.id);
formData.append('sortOrder', venueImages.value.length);
await api.uploadVenueImage(formData);
showToast('上传成功');
venueFiles.value = [];
await loadVenueImages();
} catch (error) {
console.error('上传失败:', error);
showToast('上传失败');
} finally {
loadingToast.clear();
}
};
const deleteVenueImage = async (id) => {
try {
await showConfirmDialog({
title: '确认删除',
message: '删除后无法恢复,确定要删除吗?',
});
await api.deleteVenueImage(id);
showToast('删除成功');
await loadVenueImages();
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error);
showToast('删除失败');
}
}
};
const updateVenueOrder = async (id, sortOrder) => {
try {
await api.updateVenueImageOrder(id, sortOrder);
showToast('排序已更新');
await loadVenueImages();
} catch (error) {
console.error('更新失败:', error);
showToast('更新失败');
}
};
// 地图选点相关与Dashboard相同
const initMapPicker = async () => {
try {
console.log('开始初始化地图选择器...');
// 等待地图加载
const AMap = await window.loadAMap();
console.log('地图API加载完成');
if (pickerMap) {
console.log('销毁旧地图实例');
pickerMap.destroy();
}
const center = activityForm.value.locationLat && activityForm.value.locationLng
? [activityForm.value.locationLng, activityForm.value.locationLat]
: [118.6762, 24.8742];
console.log('地图中心点:', center);
pickerMap = new AMap.Map('map-picker-container-manage', {
zoom: 15,
center: center
});
console.log('地图实例创建完成');
geocoder = new AMap.Geocoder();
console.log('地理编码器创建完成');
pickerMarker = new AMap.Marker({
position: center,
map: pickerMap,
draggable: true
});
console.log('地图标记创建完成');
if (activityForm.value.location) {
selectedLocation.value = {
name: activityForm.value.location,
lat: activityForm.value.locationLat,
lng: activityForm.value.locationLng
};
}
pickerMap.on('click', (e) => {
const { lng, lat } = e.lnglat;
pickerMarker.setPosition([lng, lat]);
selectedLocation.value = {
name: '获取地址中...',
lat: lat,
lng: lng
};
if (geocoder) {
geocoder.getAddress([lng, lat], (status, result) => {
if (status === 'complete' && result.info === 'OK') {
const address = result.regeocode.formattedAddress;
selectedLocation.value.name = address;
} else {
console.warn('逆地理编码失败:', status, result);
selectedLocation.value.name = `泉州市区 (${lat.toFixed(4)}, ${lng.toFixed(4)})`;
}
});
} else {
selectedLocation.value.name = `泉州市区 (${lat.toFixed(4)}, ${lng.toFixed(4)})`;
}
});
pickerMarker.on('dragend', (e) => {
const { lng, lat } = e.lnglat;
selectedLocation.value = {
name: '获取地址中...',
lat: lat,
lng: lng
};
if (geocoder) {
geocoder.getAddress([lng, lat], (status, result) => {
if (status === 'complete' && result.info === 'OK') {
const address = result.regeocode.formattedAddress;
selectedLocation.value.name = address;
} else {
console.warn('逆地理编码失败:', status, result);
selectedLocation.value.name = `泉州市区 (${lat.toFixed(4)}, ${lng.toFixed(4)})`;
}
});
} else {
selectedLocation.value.name = `泉州市区 (${lat.toFixed(4)}, ${lng.toFixed(4)})`;
}
});
console.log('地图初始化完成');
} catch (error) {
console.error('地图初始化失败:', error);
showToast('地图加载失败: ' + error.message);
}
};
const searchLocation = async () => {
if (!searchKeyword.value.trim()) {
showToast('请输入搜索关键词');
return;
}
try {
console.log('开始搜索地点:', searchKeyword.value);
const AMap = await window.loadAMap();
if (!AMap.PlaceSearch) {
console.error('PlaceSearch插件未加载');
showToast('地图搜索功能未就绪');
return;
}
const placeSearch = new AMap.PlaceSearch({
pageSize: 10,
pageIndex: 1,
city: '泉州',
citylimit: false,
extensions: 'base'
});
placeSearch.search(searchKeyword.value, (status, result) => {
console.log('搜索状态:', status);
console.log('搜索结果:', result);
if (status === 'complete' && result.info === 'OK') {
if (result.poiList && result.poiList.pois && result.poiList.pois.length > 0) {
searchResults.value = result.poiList.pois.map(poi => ({
name: poi.name,
address: poi.address || poi.district || '',
lat: poi.location.lat,
lng: poi.location.lng
}));
console.log('搜索成功,找到', searchResults.value.length, '个结果');
} else {
searchResults.value = [];
showToast('未找到相关地点');
}
} else if (status === 'no_data') {
searchResults.value = [];
showToast('未找到相关地点');
} else {
console.error('搜索失败:', status, result);
searchResults.value = [];
showToast('搜索失败: ' + (result.info || status));
}
});
} catch (error) {
console.error('搜索异常:', error);
showToast('搜索失败: ' + error.message);
}
};
const selectSearchResult = (result) => {
selectedLocation.value = result;
pickerMarker.setPosition([result.lng, result.lat]);
pickerMap.setCenter([result.lng, result.lat]);
searchResults.value = [];
searchKeyword.value = '';
};
const clearSearch = () => {
searchResults.value = [];
};
const confirmLocation = () => {
if (selectedLocation.value.name) {
activityForm.value.location = selectedLocation.value.name;
activityForm.value.locationLat = selectedLocation.value.lat;
activityForm.value.locationLng = selectedLocation.value.lng;
showMapPicker.value = false;
} else {
showToast('请选择地点');
}
};
watch(showMapPicker, (val) => {
if (val) {
setTimeout(() => {
initMapPicker();
}, 300);
}
});
onMounted(loadData);
</script>
<style scoped>
@import './prize-manage-styles.css';
.activity-manage {
min-height: 100vh;
background: #f8f9fa;
}
/* 活动描述换行支持 */
.activity-manage :deep(.van-cell__label) {
white-space: pre-line;
}
/* Tab 样式优化 */
.activity-manage :deep(.van-tabs__nav) {
background: #fff;
}
.activity-manage :deep(.van-tabs__line) {
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
height: 3px;
border-radius: 3px;
}
.activity-manage :deep(.van-tab) {
font-weight: 500;
color: #666;
}
.activity-manage :deep(.van-tab--active) {
font-weight: 700;
color: #FF6700;
}
.tab-content {
padding: 16px;
min-height: calc(100vh - 140px);
position: relative;
}
/* 悬浮添加按钮 */
.float-add-btn {
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
box-shadow: 0 4px 12px rgba(255, 103, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 24px;
cursor: pointer;
transition: all 0.3s ease;
}
.float-add-btn:active {
transform: scale(0.9);
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
}
/* 媒体管理页面的悬浮按钮需要固定定位 */
.tab-content .van-uploader {
position: fixed;
right: 20px;
bottom: 80px;
z-index: 100;
}
/* 其他页面的悬浮按钮 */
.tab-content > .float-add-btn {
position: fixed;
right: 20px;
bottom: 80px;
z-index: 100;
}
/* 统一的信息统计卡片 */
.info-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 16px;
}
/* 报名情况tab的统计栏使用3列布局 */
.tab-content .info-stats.reg-stats {
grid-template-columns: repeat(3, 1fr);
}
.stat-box {
background: #fff;
border-radius: 12px;
padding: 14px;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
border: 1px solid #f5f5f5;
cursor: pointer;
transition: all 0.2s ease;
}
.stat-box:active {
transform: scale(0.98);
}
.stat-box.active {
border-color: #FF6700;
background: #fff7f0;
box-shadow: 0 2px 8px rgba(255, 103, 0, 0.15);
}
.stat-box .van-icon {
font-size: 26px;
flex-shrink: 0;
}
.stat-text {
flex: 1;
min-width: 0;
}
.stat-num {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 2px;
line-height: 1.2;
}
.stat-desc {
font-size: 12px;
color: #999;
}
.expense-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.expense-item {
background: #fff;
border-radius: 12px;
padding: 12px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.expense-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
flex-shrink: 0;
}
.expense-content {
flex: 1;
min-width: 0;
}
.expense-name {
font-size: 15px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.expense-meta {
display: flex;
gap: 8px;
font-size: 12px;
color: #999;
}
.expense-cat {
color: #FF6700;
}
.expense-note {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.expense-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
}
.expense-amount {
font-size: 16px;
font-weight: 700;
color: #333;
}
.expense-actions {
display: flex;
gap: 10px;
color: #999;
font-size: 16px;
}
.expense-actions .van-icon:active {
color: #FF6700;
}
/* 赞助商 */
.sponsor-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 80px;
}
.sponsor-card {
background: #fff;
border-radius: 12px;
padding: 12px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
border: 1px solid #f0f0f0;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.sponsor-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.sponsor-card:active::before {
opacity: 1;
}
.sponsor-logo-wrapper {
flex-shrink: 0;
}
.sponsor-logo {
width: 50px;
height: 50px;
border-radius: 8px;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
background-color: #f8f9fa;
border: 2px solid #e8e8e8;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: all 0.3s ease;
}
.sponsor-logo .van-icon {
font-size: 24px;
color: #bbb;
}
.sponsor-card:active .sponsor-logo {
transform: scale(0.95);
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}
.sponsor-details {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.sponsor-header {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.sponsor-name {
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
line-height: 1.4;
}
.sponsor-badge {
padding: 3px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
color: #fff;
white-space: nowrap;
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
.badge-title {
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
}
.badge-sponsor {
background: linear-gradient(135deg, #07c160 0%, #00d68f 100%);
}
.sponsor-meta {
display: flex;
align-items: center;
gap: 12px;
margin-top: 6px;
}
.sponsor-sort {
font-size: 12px;
color: #999;
padding: 2px 8px;
background: #f5f5f5;
border-radius: 4px;
white-space: nowrap;
}
.sponsor-link {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #FF6700;
overflow: hidden;
flex: 1;
}
.sponsor-link .van-icon {
font-size: 13px;
flex-shrink: 0;
}
.sponsor-link span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sponsor-ops {
display: flex;
flex-direction: column;
gap: 12px;
flex-shrink: 0;
}
.sponsor-ops .van-icon {
font-size: 20px;
color: #999;
cursor: pointer;
transition: all 0.2s ease;
padding: 8px;
border-radius: 8px;
background: #f8f9fa;
}
.sponsor-ops .van-icon:first-child:active {
color: #FF6700;
background: #fff7f0;
transform: scale(0.9);
}
.sponsor-ops .van-icon:last-child:active {
color: #ee0a24;
background: #fff0f1;
transform: scale(0.9);
}
/* 报名列表 */
.search-box {
margin-bottom: 12px;
}
.reg-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.reg-item {
background: #fff;
border-radius: 16px;
padding: 14px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
border: 2px solid #f5f5f5;
cursor: pointer;
transition: all 0.2s ease;
}
.reg-item:active {
background: #fafafa;
}
.reg-item.status-approved {
border-color: #52c41a;
}
.reg-item.status-rejected {
border-color: #ff4d4f;
}
.reg-item.checked-in {
border-color: #FF6700;
background: #fff7f0;
}
.reg-left {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.reg-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
font-weight: 600;
flex-shrink: 0;
}
.reg-main {
flex: 1;
min-width: 0;
}
.reg-top {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.reg-name {
font-size: 15px;
font-weight: 600;
color: #333;
}
.reg-tags {
display: flex;
gap: 6px;
}
.reg-info {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
font-size: 13px;
}
.reg-right {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.reg-right .van-icon {
font-size: 16px;
}
.status-indicators {
display: flex;
gap: 6px;
align-items: center;
}
.status-dot {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
transition: all 0.3s ease;
}
.status-dot .van-icon {
font-size: 14px;
color: #999;
}
.status-dot.status-approved {
background: #e8f5e9;
}
.status-dot.status-approved .van-icon {
color: #07c160;
}
.status-dot.status-rejected {
background: #ffebee;
}
.status-dot.status-rejected .van-icon {
color: #ee0a24;
}
.status-dot.status-pending {
background: #fff3e0;
}
.status-dot.status-pending .van-icon {
color: #ffa940;
}
.status-dot.status-active {
background: #e3f2fd;
}
.status-dot.status-active .van-icon {
color: #1989fa;
}
.info-link,
.info-text {
display: inline-flex;
align-items: center;
gap: 4px;
color: #666;
}
.info-link {
color: #FF6700;
text-decoration: none;
}
.info-link:active {
opacity: 0.7;
}
.info-text .van-icon,
.info-link .van-icon {
font-size: 13px;
color: #999;
}
.car-number {
color: #FF6700;
cursor: pointer;
}
.car-number:active {
opacity: 0.7;
}
.people-count {
color: #FF6700;
cursor: pointer;
}
.people-count:active {
opacity: 0.7;
}
/* 移除旧的 reg-right 样式,已在上面重新定义 */
/* 弹窗 */
.form-popup {
max-height: 80vh;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 16px 16px 0 0;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
}
.popup-header h3 {
font-size: 18px;
margin: 0;
color: #1a1a1a;
font-weight: 700;
}
.popup-header .van-icon {
font-size: 20px;
color: #999;
}
.popup-form {
padding: 0;
overflow-y: auto;
flex: 1;
}
.popup-form .van-cell-group {
margin: 0;
background: #fff;
}
.popup-form .van-cell {
padding: 14px 20px;
}
.popup-form .van-field__label {
width: 80px;
color: #333;
font-weight: 600;
font-size: 14px;
}
.popup-form .van-field__control {
color: #1a1a1a;
font-size: 14px;
}
.popup-form .van-field__control::placeholder {
color: #bbb;
}
.form-actions {
margin: 0;
padding: 16px 20px 20px;
flex-shrink: 0;
background: #fff;
border-top: 1px solid #f5f5f5;
}
.form-actions .van-button {
height: 48px;
font-size: 16px;
font-weight: 600;
}
/* 地图选点 */
.map-picker {
height: 100%;
display: flex;
flex-direction: column;
}
.map-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
.map-picker-header h3 {
flex: 1;
text-align: center;
font-size: 16px;
margin: 0;
color: #333;
}
.map-picker-header .van-icon {
font-size: 20px;
color: #333;
}
.map-search {
padding: 12px 16px;
background: #fff;
}
.search-results {
max-height: 200px;
overflow-y: auto;
background: #fff;
border-bottom: 1px solid #f0f0f0;
}
.search-result-item {
padding: 12px 16px;
border-bottom: 1px solid #f5f5f5;
cursor: pointer;
}
.search-result-item:active {
background: #f5f5f5;
}
.result-name {
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
.result-address {
font-size: 12px;
color: #999;
}
.map-container {
flex: 1;
width: 100%;
}
.selected-location {
padding: 12px 16px;
background: #fff;
border-top: 1px solid #f0f0f0;
display: flex;
align-items: center;
gap: 8px;
}
.selected-location .van-icon {
color: #667eea;
font-size: 18px;
}
.selected-location span {
flex: 1;
font-size: 14px;
color: #333;
}
/* 媒体管理 */
.media-section {
margin-top: 20px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 15px;
font-weight: 700;
color: #333;
}
.media-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.media-card {
background: #fff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
border: 1px solid #f0f0f0;
}
.card-thumb {
width: 100%;
height: 180px;
position: relative;
background: #f5f5f5;
cursor: pointer;
}
.card-thumb .van-image {
width: 100%;
height: 100%;
}
.video-thumb {
width: 100%;
height: 100%;
position: relative;
}
.video-thumb video {
width: 100%;
height: 100%;
object-fit: cover;
}
.play-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 36px;
color: rgba(255,255,255,0.9);
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
pointer-events: none;
}
.card-info {
padding: 10px 12px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.uploader {
font-size: 12px;
color: #999;
}
.action-row {
display: flex;
gap: 6px;
}
.action-row .van-button {
flex: 1;
}
.media-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.grid-item {
position: relative;
width: 100%;
padding-top: 100%;
background: #f5f5f5;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
}
.grid-item .van-image,
.grid-item .video-thumb {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.del-btn {
position: absolute;
top: 4px;
right: 4px;
width: 22px;
height: 22px;
background: rgba(0,0,0,0.6);
border-radius: 50%;
color: #fff;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.del-btn:active {
background: rgba(0,0,0,0.8);
}
.video-player-popup {
padding: 20px;
background: #000;
border-radius: 16px;
}
.popup-video {
width: 100%;
max-height: 60vh;
border-radius: 12px;
}
/* 场地管理 */
.venue-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.venue-item {
background: #fff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
border: 1px solid #f0f0f0;
}
.venue-thumb {
width: 100%;
height: 200px;
background: #f5f5f5;
cursor: pointer;
}
.venue-thumb .van-image {
width: 100%;
height: 100%;
}
.venue-actions {
padding: 12px;
display: flex;
gap: 8px;
align-items: center;
}
.venue-actions .van-field {
flex: 1;
}
.venue-actions .van-button {
flex-shrink: 0;
}
/* 分类选择器 */
.category-picker {
padding: 20px;
max-height: 60vh;
overflow-y: auto;
}
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.picker-header h3 {
font-size: 18px;
margin: 0;
color: #1a1a1a;
font-weight: 700;
}
.picker-header .van-icon {
font-size: 20px;
color: #999;
cursor: pointer;
}
.category-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.category-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 8px;
background: #f8f9fa;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.category-item:active {
transform: scale(0.95);
background: #f0f0f0;
}
.category-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 22px;
}
.category-item span {
font-size: 13px;
color: #333;
font-weight: 500;
}
.datetime-picker-popup {
background: #fff;
border-radius: 16px 16px 0 0;
overflow: hidden;
}
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
}
.picker-header h3 {
font-size: 16px;
margin: 0;
color: #333;
font-weight: 600;
}
.picker-body {
padding: 20px;
}
.datetime-input {
width: 100%;
padding: 12px;
font-size: 16px;
border: 1px solid #e8e8e8;
border-radius: 8px;
outline: none;
color: #333;
}
.datetime-input:focus {
border-color: #667eea;
}
.category-item:active {
transform: scale(0.95);
background: #e9ecef;
}
.category-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 24px;
}
/* 经费媒体上传 */
.expense-media-upload {
width: 100%;
}
.existing-media {
margin-top: 12px;
}
.media-section-title {
font-size: 13px;
color: #666;
margin-bottom: 8px;
font-weight: 500;
}
.media-list {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.media-thumb {
position: relative;
width: 80px;
height: 80px;
border-radius: 8px;
overflow: hidden;
background: #f5f5f5;
}
.media-thumb .van-image,
.media-thumb .video-thumb {
width: 100%;
height: 100%;
object-fit: cover;
}
.media-thumb .remove-icon {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
background: rgba(0,0,0,0.6);
border-radius: 50%;
color: #fff;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
/* 人数修改弹窗 */
.people-count-dialog {
padding: 20px;
background: #fff;
border-radius: 16px;
}
.people-count-dialog .dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.people-count-dialog .dialog-header h3 {
font-size: 18px;
font-weight: 700;
color: #333;
margin: 0;
}
.people-count-dialog .dialog-header .van-icon {
font-size: 20px;
color: #999;
cursor: pointer;
}
.people-count-dialog .dialog-content {
margin-bottom: 20px;
}
.people-count-dialog .count-info {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 20px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
}
.people-count-dialog .info-label {
font-size: 15px;
font-weight: 600;
color: #333;
}
.people-count-dialog .info-desc {
font-size: 13px;
color: #666;
}
.people-count-dialog .van-stepper {
margin: 0 auto;
}
.people-count-dialog .dialog-actions {
margin-top: 20px;
}
.people-count-dialog .dialog-actions .van-button {
height: 44px;
font-size: 16px;
font-weight: 600;
}
/* 已签到状态特殊样式 */
.reg-item.checked-in {
border-color: #FF6700;
background: #fff7f0;
}
/* 报名详情弹窗 */
.reg-detail-popup {
max-height: 70vh;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 16px 16px 0 0;
}
.detail-content {
flex: 1;
overflow-y: auto;
padding: 0 20px 20px;
}
.detail-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
}
.detail-row.status-row {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.status-controls {
display: flex;
flex-direction: column;
gap: 12px;
background: #f7f8fa;
padding: 12px;
border-radius: 8px;
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.status-name {
font-size: 14px;
color: #333;
font-weight: 500;
}
.detail-row:last-child {
border-bottom: none;
}
.detail-label {
font-size: 14px;
color: #666;
font-weight: 500;
}
.detail-value {
font-size: 14px;
color: #333;
font-weight: 600;
display: flex;
align-items: center;
gap: 4px;
}
.detail-link {
color: #FF6700;
cursor: pointer;
}
.detail-link:active {
opacity: 0.7;
}
.detail-actions {
padding: 16px 20px 20px;
border-top: 1px solid #f5f5f5;
}
.action-buttons {
display: flex;
gap: 12px;
}
.action-buttons .van-button {
flex: 1;
}
</style>