| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509 |
- <template>
- <div class="activity-form-container">
- <h2 class="page-title">{{ isEdit ? '编辑活动' : '创建活动' }}</h2>
-
- <el-card shadow="never">
- <el-form
- ref="activityFormRef"
- :model="activityForm"
- :rules="activityRules"
- label-width="120px"
- >
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="活动名称" prop="name">
- <el-input v-model="activityForm.name" placeholder="请输入活动名称" />
- </el-form-item>
- </el-col>
-
- <el-col :span="12">
- <el-form-item label="活动类别" prop="category">
- <el-input v-model="activityForm.category" placeholder="如:讲座、展览、派对等" />
- </el-form-item>
- </el-col>
- </el-row>
-
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="活动类型" prop="type">
- <el-radio-group v-model="activityForm.type">
- <el-radio :label="1">线上</el-radio>
- <el-radio :label="2">线下</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
-
- <el-col :span="12">
- <el-form-item label="活动地点" prop="location">
- <el-input v-model="activityForm.location" placeholder="请输入活动地点" />
- </el-form-item>
- </el-col>
- </el-row>
-
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="最大人数" prop="maxParticipants">
- <el-input-number
- v-model="activityForm.maxParticipants"
- :min="1"
- :max="10000"
- placeholder="请输入最大参与人数"
- style="width: 100%"
- />
- </el-form-item>
- </el-col>
-
- <el-col :span="12">
- <el-form-item label="活动费用" prop="price">
- <el-input-number
- v-model="activityForm.price"
- :min="0"
- :precision="2"
- placeholder="请输入活动费用"
- style="width: 100%"
- >
- <template #prefix>¥</template>
- </el-input-number>
- <div class="form-tip">填0表示免费</div>
- </el-form-item>
- </el-col>
- </el-row>
-
- <el-form-item label="活动封面" prop="coverImage">
- <el-upload
- class="cover-uploader"
- action="#"
- :show-file-list="false"
- :before-upload="handleBeforeUpload"
- :http-request="handleUpload"
- >
- <img v-if="activityForm.coverImage" :src="activityForm.coverImage" class="cover-preview" />
- <el-icon v-else class="cover-uploader-icon"><Plus /></el-icon>
- </el-upload>
- <div class="upload-tip">建议尺寸:750x500px,支持jpg、png格式</div>
- </el-form-item>
-
- <el-form-item label="活动时间" prop="timeRange">
- <el-date-picker
- v-model="timeRange"
- type="datetimerange"
- range-separator="至"
- start-placeholder="开始时间"
- end-placeholder="结束时间"
- value-format="YYYY-MM-DD HH:mm:ss"
- :clearable="true"
- :editable="false"
- style="width: 100%"
- @change="handleTimeRangeChange"
- />
- <div class="form-tip">请选择完整的活动时间范围(必须包含开始时间和结束时间)</div>
- </el-form-item>
-
- <el-form-item label="报名截止时间" prop="registrationEndTime">
- <el-date-picker
- v-model="activityForm.registrationEndTime"
- type="datetime"
- placeholder="选择报名截止时间"
- value-format="YYYY-MM-DD HH:mm:ss"
- style="width: 100%"
- />
- </el-form-item>
-
- <el-form-item label="活动描述" prop="description">
- <el-input
- v-model="activityForm.description"
- type="textarea"
- :rows="6"
- placeholder="请输入活动详细描述"
- />
- </el-form-item>
-
- <el-form-item label="注意事项" prop="notes">
- <el-input
- v-model="activityForm.notes"
- type="textarea"
- :rows="4"
- placeholder="请输入活动注意事项,每行一条"
- />
- </el-form-item>
-
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="活动状态" prop="status">
- <el-radio-group v-model="activityForm.status">
- <el-radio :label="1">未开始</el-radio>
- <el-radio :label="2">进行中</el-radio>
- <el-radio :label="3">已结束</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
-
- <el-col :span="12">
- <el-form-item label="热门活动" prop="isHot">
- <el-switch v-model="activityForm.isHot" />
- <span class="form-tip" style="margin-left: 10px">开启后将在首页展示</span>
- </el-form-item>
- </el-col>
- </el-row>
-
- <el-form-item>
- <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
- {{ isEdit ? '保存修改' : '创建活动' }}
- </el-button>
- <el-button @click="$router.back()">取消</el-button>
- </el-form-item>
- </el-form>
- </el-card>
- </div>
- </template>
- <script setup>
- import { ref, reactive, onMounted, nextTick } from 'vue'
- import { useRoute, useRouter } from 'vue-router'
- import { ElMessage } from 'element-plus'
- import { Plus } from '@element-plus/icons-vue'
- import request from '@/utils/request'
- import { API_ENDPOINTS, API_BASE_URL } from '@/config/api'
- const route = useRoute()
- const router = useRouter()
- const isEdit = ref(false)
- const submitLoading = ref(false)
- const activityFormRef = ref(null)
- const timeRange = ref([])
- const activityForm = reactive({
- id: null,
- name: '',
- type: 2,
- category: '',
- maxParticipants: 50,
- location: '',
- startTime: null,
- endTime: null,
- coverImage: '',
- price: 0,
- status: 1,
- registrationEndTime: null,
- description: '',
- notes: '',
- isHot: false
- })
- // 自定义时间范围验证
- const validateTimeRange = (rule, value, callback) => {
- console.log('验证时间范围:', {
- value,
- startTime: activityForm.startTime,
- endTime: activityForm.endTime,
- isEdit: isEdit.value
- })
-
- // 优先检查 activityForm 中的时间数据(不管是编辑还是创建模式)
- if (activityForm.startTime && activityForm.endTime) {
- console.log('✅ 时间验证通过(使用 activityForm 中的数据)')
- callback()
- return
- }
-
- // 如果 activityForm 中没有,检查 timeRange
- if (!value || !Array.isArray(value) || value.length !== 2) {
- console.log('❌ 时间验证失败:timeRange 不是有效数组')
- callback(new Error('请选择完整的活动时间范围'))
- return
- }
-
- // 检查是否两个时间都已选择
- if (!value[0] || !value[1]) {
- console.log('❌ 时间验证失败:开始或结束时间为空')
- callback(new Error('请选择完整的活动时间范围'))
- return
- }
-
- console.log('✅ 时间验证通过(使用 timeRange)')
- callback()
- }
- const activityRules = {
- name: [{ required: true, message: '请输入活动名称', trigger: 'blur' }],
- type: [{ required: true, message: '请选择活动类型', trigger: 'change' }],
- category: [{ required: true, message: '请输入活动类别', trigger: 'blur' }],
- maxParticipants: [{ required: true, message: '请输入最大参与人数', trigger: 'blur' }],
- location: [{ required: true, message: '请输入活动地点', trigger: 'blur' }],
- coverImage: [{ required: true, message: '请上传活动封面', trigger: 'change' }],
- timeRange: [{ validator: validateTimeRange, trigger: 'change' }],
- registrationEndTime: [{ required: true, message: '请选择报名截止时间', trigger: 'change' }],
- description: [{ required: true, message: '请输入活动描述', trigger: 'blur' }]
- }
- // 时间范围变化处理
- const handleTimeRangeChange = (value) => {
- console.log('📅 时间范围变化:', value)
- console.log('📅 value类型:', typeof value, '是否数组:', Array.isArray(value))
-
- if (value && Array.isArray(value)) {
- console.log('📅 数组长度:', value.length)
- console.log('📅 开始时间:', value[0])
- console.log('📅 结束时间:', value[1])
-
- if (value.length === 2 && value[0] && value[1]) {
- // 时间范围选择完整
- activityForm.startTime = value[0]
- activityForm.endTime = value[1]
- console.log('✅ 时间数据已同步到 activityForm')
- console.log(' - startTime:', activityForm.startTime)
- console.log(' - endTime:', activityForm.endTime)
-
- // 清除该字段的验证错误
- if (activityFormRef.value) {
- activityFormRef.value.clearValidate('timeRange')
- console.log('✅ 已清除 timeRange 验证错误')
- }
- } else if (value.length < 2) {
- // 时间范围不完整
- console.warn('⚠️ 时间范围不完整,请选择开始和结束时间')
- } else if (!value[0] || !value[1]) {
- console.warn('⚠️ 开始时间或结束时间为空')
- }
- } else if (!value) {
- // 清空时间
- activityForm.startTime = null
- activityForm.endTime = null
- console.log('🗑️ 时间已清空')
- }
- }
- // 上传前检查
- const handleBeforeUpload = (file) => {
- const isImage = file.type.startsWith('image/')
- const isLt5M = file.size / 1024 / 1024 < 5
-
- if (!isImage) {
- ElMessage.error('只能上传图片文件')
- return false
- }
- if (!isLt5M) {
- ElMessage.error('图片大小不能超过 5MB')
- return false
- }
- return true
- }
- // 上传图片
- const handleUpload = async (options) => {
- const formData = new FormData()
- formData.append('file', options.file)
-
- try {
- const response = await request.post(API_ENDPOINTS.UPLOAD_IMAGE, formData, {
- headers: { 'Content-Type': 'multipart/form-data' }
- })
-
- if (response.code === 200) {
- activityForm.coverImage = response.data.url || response.data
- ElMessage.success('上传成功')
- }
- } catch (error) {
- console.error('上传失败:', error)
- ElMessage.error('上传失败')
- }
- }
- // 规范化图片地址,兼容相对路径和缺少协议的情况
- const resolveImageUrl = (url) => {
- if (!url || typeof url !== 'string') return ''
- const normalized = url.trim()
- if (/^https?:\/\//i.test(normalized)) return normalized
- if (normalized.startsWith('//')) return `${window.location.protocol}${normalized}`
- const base = API_BASE_URL || window.location.origin
- if (normalized.startsWith('/')) return `${base}${normalized}`
- return `${base}/${normalized}`
- }
- // 加载活动详情
- const loadActivityDetail = async (id) => {
- try {
- const response = await request.get(`${API_ENDPOINTS.ACTIVITY_DETAIL}/${id}`)
-
- if (response.code === 200) {
- const data = response.data
- // 映射字段,兼容不同的命名方式
- Object.assign(activityForm, {
- id: data.id,
- name: data.name || '',
- type: data.type || 2,
- category: data.category || '',
- maxParticipants: data.maxParticipants || data.max_participants || 50,
- location: data.location || '',
- startTime: data.startTime || data.start_time || null,
- endTime: data.endTime || data.end_time || null,
- coverImage: resolveImageUrl(data.coverImage || data.cover_image || data.cover || ''),
- price: data.price || 0,
- status: data.status || 1,
- registrationEndTime: data.registrationEndTime || data.registration_end_time || null,
- description: data.description || '',
- notes: data.notes || '',
- isHot: data.isHot === true || data.isHot === 1 || data.isHot === '1' || data.is_hot === true || data.is_hot === 1
- })
-
- if (activityForm.startTime && activityForm.endTime) {
- timeRange.value = [activityForm.startTime, activityForm.endTime]
- }
-
- // 等待 DOM 更新后清除表单验证错误
- await nextTick()
- if (activityFormRef.value) {
- activityFormRef.value.clearValidate()
- }
- }
- } catch (error) {
- console.error('加载活动详情失败:', error)
- ElMessage.error('加载活动详情失败')
- }
- }
- // 提交表单
- const handleSubmit = async () => {
- if (!activityFormRef.value) return
-
- console.log('=== 开始提交表单 ===')
- console.log('📝 表单模式:', isEdit.value ? '编辑' : '创建')
- console.log('📝 timeRange:', timeRange.value)
- console.log('📝 activityForm.startTime:', activityForm.startTime)
- console.log('📝 activityForm.endTime:', activityForm.endTime)
-
- try {
- // 先处理时间数据(确保同步)
- if (timeRange.value && Array.isArray(timeRange.value) && timeRange.value.length === 2) {
- activityForm.startTime = timeRange.value[0]
- activityForm.endTime = timeRange.value[1]
- console.log('✅ 从 timeRange 同步时间到 activityForm')
- }
-
- console.log('📝 完整表单数据:', JSON.parse(JSON.stringify(activityForm)))
-
- // 验证表单(会调用自定义验证器)
- console.log('🔍 开始验证表单...')
- await activityFormRef.value.validate()
- console.log('✅ 表单验证通过')
-
- // 创建模式下额外检查时间数据
- if (!isEdit.value) {
- if (!activityForm.startTime || !activityForm.endTime) {
- console.error('❌ 创建模式:时间数据不完整')
- ElMessage.warning('请选择完整的活动时间范围(开始时间和结束时间)')
- return
- }
- console.log('✅ 创建模式:时间数据检查通过')
- }
-
- // 编辑模式下检查时间数据
- if (isEdit.value) {
- if (!activityForm.startTime || !activityForm.endTime) {
- console.error('❌ 编辑模式:时间数据不完整')
- ElMessage.warning('活动时间数据不完整,请重新选择')
- return
- }
- console.log('✅ 编辑模式:时间数据检查通过')
- }
-
- submitLoading.value = true
-
- const url = isEdit.value
- ? `${API_ENDPOINTS.ACTIVITY_UPDATE}/${activityForm.id}`
- : API_ENDPOINTS.ACTIVITY_CREATE
- const method = isEdit.value ? 'put' : 'post'
-
- console.log('🚀 发送请求:', method.toUpperCase(), url)
- const response = await request[method](url, activityForm)
-
- if (response.code === 200) {
- console.log('✅ 提交成功')
- ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
- router.push('/activity/list')
- }
- } catch (error) {
- console.error('❌ 提交失败:', error)
-
- // 检查是否是验证错误
- if (error && typeof error === 'object') {
- console.log('📋 验证错误详情:', error)
-
- // 统计验证失败的字段
- const errorFields = Object.keys(error)
- console.log('❌ 验证失败的字段:', errorFields)
-
- if (errorFields.length > 0) {
- ElMessage.error(`请检查以下字段:${errorFields.join('、')}`)
- } else {
- ElMessage.error('请检查表单填写是否完整')
- }
- }
- } finally {
- submitLoading.value = false
- }
- }
- onMounted(() => {
- if (route.params.id) {
- isEdit.value = true
- loadActivityDetail(route.params.id)
- }
- })
- </script>
- <style scoped>
- .activity-form-container {
- padding: 0;
- }
- .page-title {
- font-size: 24px;
- font-weight: bold;
- color: #333;
- margin: 0 0 20px 0;
- }
- .cover-uploader {
- display: inline-block;
- }
- .cover-uploader :deep(.el-upload) {
- border: 1px dashed #d9d9d9;
- border-radius: 6px;
- cursor: pointer;
- position: relative;
- overflow: hidden;
- transition: all 0.3s;
- width: 300px;
- height: 200px;
- }
- .cover-uploader :deep(.el-upload:hover) {
- border-color: #409EFF;
- }
- .cover-uploader-icon {
- font-size: 28px;
- color: #8c939d;
- width: 300px;
- height: 200px;
- text-align: center;
- line-height: 200px;
- }
- .cover-preview {
- width: 300px;
- height: 200px;
- object-fit: cover;
- }
- .upload-tip,
- .form-tip {
- font-size: 12px;
- color: #999;
- margin-top: 8px;
- }
- </style>
|