3597 lines
99 KiB
Vue
3597 lines
99 KiB
Vue
<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>
|