const app = getApp() const DRAFT_KEY = 'actAdd:draft' interface ILevelItem { id: number name: string code: string sort: number } interface ICategoryItem { id: number name: string code: string icon: string sort: number isEnabled: number } interface ITagItem { id: number name: string sort: number activityCount: number } interface AgendaItem { agendaTime: string title: string description: string sort: number } Page({ data: { currentStep: 1, steps: [ { label: '基本信息', field: 'basic' }, { label: '报名签到设置', field: 'signup' }, { label: '活动议程', field: 'agenda' }, ], // 步骤1 基本信息 coverImageList: [] as Array<{ uid: string; url: string; type: string; name: string; size: number }>, title: '', type: 1, typeOptions: [ { id: 1, name: '讲座' }, { id: 2, name: '比赛' }, { id: 3, name: '社团' }, { id: 4, name: '志愿' }, { id: 5, name: '文体' }, { id: 6, name: '其他' }, ], typeOther: '', summary: '', startTime: '', endTime: '', detail: '', detailImages: [] as string[], location: '', organizer: '', contactName: '', contactPhone: '', // 活动等级 levelList: [] as ILevelItem[], levelId: 0, // 活动分类 categoryList: [] as ICategoryItem[], selectedCategoryIds: [] as number[], categoryTags: [] as Array<{ id: number; name: string; isSelected: boolean }>, // 用于显示选中状态 // 活动标签 tagList: [] as ITagItem[], selectedTagIds: [] as number[], tagTags: [] as Array<{ id: number; name: string; isSelected: boolean }>, // 用于显示选中状态 // 步骤2 报名签到设置 needRegister: true, registerStartTime: '', registerEndTime: '', registerLimit: 'unlimited', registerLimitCount: '', registerCondition: '', checkinWay: 'dynamic', checkinStartTime: '', checkinEndTime: '', // datetime-picker 相关 showDatePicker: false, datePickerField: '', // 当前选择的时间字段 datePickerValue: new Date().getTime(), // 当前选择的日期时间戳 datePickerMinDate: new Date(2020, 0, 1).getTime(), // 最小日期 datePickerMaxDate: new Date(2030, 11, 31).getTime(), // 最大日期 // 步骤3 活动议程 agendas: [{ agendaTime: '', title: '', description: '', sort: 0 }] as AgendaItem[], nextAgendaId: 2, // 议程时间选择器相关 showAgendaDatePicker: false, agendaDatePickerIndex: -1, // 当前编辑的议程索引 agendaDatePickerValue: new Date().getTime(), // 提交状态 submitting: false, // 编辑模式:传入的活动ID editId: 0, isEdit: false, }, async onLoad(options: any) { await app.waitLogin({ type: 1 }) // 判断是否为编辑模式 const editId = options?.id ? Number(options.id) : 0 const isEdit = editId > 0 this.setData({ editId, isEdit }) // 设置导航栏标题 wx.setNavigationBarTitle({ title: isEdit ? '编辑活动' : '创建活动' }) try { await Promise.all([this.fetchLevelList(), this.fetchCategoryList(), this.fetchTagList()]) } catch (err) { console.error('初始化数据失败:', err) } // 编辑模式:从接口回显数据,忽略本地草稿 if (isEdit) { await this.fetchActivityDetail(editId) return } // 新建模式:检查是否有本地草稿 try { const draft = wx.getStorageSync(DRAFT_KEY) as any if (draft && Object.keys(draft).length) { wx.showModal({ title: '检测到未完成的活动', content: '上次未完成的表单已保存,是否继续编辑?', confirmText: '继续编辑', cancelText: '重新开始', success: (res) => { if (res.confirm) { this.restoreDraft(draft) } else { this.clearDraft() } }, }) } } catch (err) { console.error('读取本地草稿失败:', err) } }, // 获取活动详情并回显到表单 async fetchActivityDetail(id: number) { wx.showLoading({ title: '加载中...' }) try { const res = await wx.ajax({ url: `/activity/detail?id=${id}`, method: 'GET', data: {}, }) if (!res) return // 签到方式映射 const checkinWayMap: Record = { 1: 'dynamic', 2: 'fixed', 3: 'none' } // 封面图片转为本地上传格式 const coverImageList = (res.mainImages || []).map((url: string, index: number) => ({ uid: `cover_${index}`, url, type: 'image', name: url.split('/').pop() || '', size: 0, status: 'success', progress: 100, })) // 议程数据 const agendas = (res.agendas || []).map((item: any, index: number) => ({ agendaTime: item.agendaTime || `${item.agendaDate || ''} ${item.agendaTime || ''}`, title: item.title || '', description: item.description || '', sort: index, })) // 如果没有议程,提供一个空行 if (agendas.length === 0) { agendas.push({ agendaTime: '', title: '', description: '', sort: 0 }) } // 分类和标签 const selectedCategoryIds = res.categoryIds || [] const selectedTagIds = (res.tags || []).map((t: any) => typeof t === 'object' ? t.id : t) // 报名设置 const needRegister = res.regType === 1 const registerLimit = res.quota > 0 ? 'limited' : 'unlimited' const registerLimitCount = res.quota > 0 ? String(res.quota) : '' this.setData({ coverImageList, title: res.name || '', type: res.type || 1, typeOther: res.typeOther || '', summary: res.summary || '', startTime: res.startAt || '', endTime: res.endAt || '', detail: res.description || '', detailImages: res.detailImages || [], location: res.location || '', organizer: res.organizer || '', contactName: res.contactName || '', contactPhone: res.contactPhone || '', levelId: res.levelId || 0, selectedCategoryIds, selectedTagIds, needRegister, registerStartTime: res.regStartAt || '', registerEndTime: res.regEndAt || '', registerLimit, registerLimitCount, registerCondition: res.regCondition || '', checkinWay: checkinWayMap[res.checkinType] || 'dynamic', checkinStartTime: res.checkinStartTime || '', checkinEndTime: res.checkinEndTime || '', agendas, }) // 重建标签选中状态 const categoryTags = (this.data.categoryList || []).map((item: ICategoryItem) => ({ id: item.id, name: item.name, isSelected: selectedCategoryIds.includes(item.id), })) const tagTags = (this.data.tagList || []).map((item: ITagItem) => ({ id: item.id, name: item.name, isSelected: selectedTagIds.includes(item.id), })) this.setData({ categoryTags, tagTags }) } catch (err) { console.error('获取活动详情失败:', err) wx.showToast({ title: '加载失败', icon: 'none' }) } finally { wx.hideLoading() } }, // ========== 草稿管理 ========== buildDraft(partial: Record = {}) { const keys = [ 'currentStep', 'coverImageList', 'title', 'type', 'typeOther', 'summary', 'startTime', 'endTime', 'detail', 'detailImages', 'location', 'organizer', 'contactName', 'contactPhone', 'levelId', 'selectedCategoryIds', 'selectedTagIds', 'needRegister', 'registerStartTime', 'registerEndTime', 'registerLimit', 'registerLimitCount', 'registerCondition', 'checkinWay', 'checkinStartTime', 'checkinEndTime', 'agendas', ] const draft: Record = {} keys.forEach((k) => { if (Object.prototype.hasOwnProperty.call(partial, k)) { draft[k] = partial[k] } else { draft[k] = (this.data as any)[k] } }) return draft }, saveDraft(partial: Record = {}) { try { const draft = this.buildDraft(partial) wx.setStorageSync(DRAFT_KEY, draft) } catch (err) { console.error('保存草稿失败:', err) } }, clearDraft() { try { wx.removeStorageSync(DRAFT_KEY) } catch { /* ignore */ } }, restoreDraft(draft: Record) { if (!draft) return const safeDraft = { ...draft } safeDraft.coverImageList = safeDraft.coverImageList || [] safeDraft.detailImages = safeDraft.detailImages || [] safeDraft.agendas = safeDraft.agendas || [] safeDraft.selectedCategoryIds = safeDraft.selectedCategoryIds || [] safeDraft.selectedTagIds = safeDraft.selectedTagIds || [] this.setData(safeDraft) // 重建标签选中状态显示 const categoryTags = (this.data.categoryList || []).map((item: ICategoryItem) => ({ id: item.id, name: item.name, isSelected: safeDraft.selectedCategoryIds.includes(item.id), })) const tagTags = (this.data.tagList || []).map((item: ITagItem) => ({ id: item.id, name: item.name, isSelected: safeDraft.selectedTagIds.includes(item.id), })) this.setData({ categoryTags, tagTags }) }, setAndSave(patch: Record) { this.setData(patch) // 编辑模式下不保存本地草稿缓存 if (!this.data.isEdit) { this.saveDraft(patch) } }, // 获取活动等级列表 async fetchLevelList() { try { const res = await wx.ajax({ url: '/activity-level/list', method: 'GET', data: {}, }) if (res && res.list) { this.setData({ levelList: res.list }) } } catch (err) { console.error('获取活动等级列表失败:', err) } }, // 获取活动分类列表 async fetchCategoryList() { try { const res = await wx.ajax({ url: '/activity-category/list', method: 'GET', data: {}, }) if (res && res.list) { const { selectedCategoryIds } = this.data const categoryTags = res.list.map((item: ICategoryItem) => ({ id: item.id, name: item.name, isSelected: selectedCategoryIds.includes(item.id), })) this.setData({ categoryList: res.list, categoryTags, }) } } catch (err) { console.error('获取活动分类列表失败:', err) } }, // 获取活动标签列表 async fetchTagList() { try { const res = await wx.ajax({ url: '/activity-tag/list', method: 'GET', data: {}, }) if (res && res.list) { const { selectedTagIds } = this.data const tagTags = res.list.map((item: ITagItem) => ({ id: item.id, name: item.name, isSelected: selectedTagIds.includes(item.id), })) this.setData({ tagList: res.list, tagTags, }) } } catch (err) { console.error('获取活动标签列表失败:', err) } }, // ========== 步骤切换 ========== goStep(step: number) { if (step < 1 || step > 4) return this.setAndSave({ currentStep: step }) }, // 验证当前步骤的必填项 validateCurrentStep(): boolean { const { currentStep, coverImageList, title, startTime, endTime, location, needRegister, registerStartTime, registerEndTime, checkinWay, checkinStartTime, checkinEndTime, } = this.data // 步骤1:基本信息 if (currentStep === 1) { if (!coverImageList.length) { wx.showToast({ title: '请上传活动头图', icon: 'none' }) return false } if (!title.trim()) { wx.showToast({ title: '请输入活动标题', icon: 'none' }) return false } if (!startTime) { wx.showToast({ title: '请选择活动开始时间', icon: 'none' }) return false } if (!endTime) { wx.showToast({ title: '请选择活动结束时间', icon: 'none' }) return false } if (!location.trim()) { wx.showToast({ title: '请输入活动地点', icon: 'none' }) return false } } // 步骤2:报名签到设置 if (currentStep === 2) { if (needRegister) { if (!registerStartTime) { wx.showToast({ title: '请选择报名开始时间', icon: 'none' }) return false } if (!registerEndTime) { wx.showToast({ title: '请选择报名截止时间', icon: 'none' }) return false } } // 签到时间校验(动态二维码或固定二维码时必填) if (checkinWay !== 'none') { if (!checkinStartTime) { wx.showToast({ title: '请选择签到开始时间', icon: 'none' }) return false } if (!checkinEndTime) { wx.showToast({ title: '请选择签到结束时间', icon: 'none' }) return false } } } // 步骤3:活动议程(可选,不做必填验证) if (currentStep === 3) { // 议程可选,不做验证 } return true }, onNextStep() { // 先验证当前步骤 if (!this.validateCurrentStep()) { return } const next = this.data.currentStep + 1 if (next <= 4) this.setAndSave({ currentStep: next }) }, onPrevStep() { const prev = this.data.currentStep - 1 if (prev >= 1) this.setAndSave({ currentStep: prev }) }, // ========== 图片上传 ========== // 上传成功后,直接添加到列表(maxCount=1,只保留一个) onCoverSuccess(e: WechatMiniprogram.CustomEvent) { const { file } = e.detail this.setAndSave({ coverImageList: [file] }) }, // 上传失败后,显示错误 onCoverError(_e: WechatMiniprogram.CustomEvent) { wx.showToast({ title: '上传失败,请重试', icon: 'none' }) }, // 删除封面图片 handleDelCover() { this.setAndSave({ coverImageList: [] }) }, onDetailImageSuccess(e: WechatMiniprogram.CustomEvent) { const { urls } = e.detail this.setAndSave({ detailImages: urls }) }, // ========== 输入绑定 ========== onInputChange(e: WechatMiniprogram.Input) { const { field } = e.currentTarget.dataset this.setAndSave({ [field]: e.detail.value }) }, onTextareaChange(e: WechatMiniprogram.TextareaInput) { const { field } = e.currentTarget.dataset this.setAndSave({ [field]: e.detail.value }) }, // ========== 时间选择 ========== // 格式化日期时间 formatDateTime(timestamp: number): string { const date = new Date(timestamp) 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}` }, // 打开时间选择器 onOpenDatePicker(e: WechatMiniprogram.TouchEvent) { const { field } = e.currentTarget.dataset const currentValue = (this.data as any)[field] || '' // 根据当前值设置 picker 的初始值 let pickerValue = new Date().getTime() if (currentValue) { const parsedDate = new Date(currentValue.replace(/-/g, '/')) if (!Number.isNaN(parsedDate.getTime())) { pickerValue = parsedDate.getTime() } } this.setData({ showDatePicker: true, datePickerField: field, datePickerValue: pickerValue, }) }, // 关闭时间选择器 onCloseDatePicker() { this.setData({ showDatePicker: false }) }, // 确认时间选择 onConfirmDatePicker(e: WechatMiniprogram.CustomEvent) { const { datePickerField } = this.data const timestamp = e.detail const formattedTime = this.formatDateTime(timestamp) this.setAndSave({ showDatePicker: false, [datePickerField]: formattedTime, }) }, // ========== 活动类型选择 ========== onSelectType(e: WechatMiniprogram.TouchEvent) { const { value } = e.currentTarget.dataset this.setAndSave({ type: value }) }, // ========== 活动等级选择 ========== onSelectLevel(e: WechatMiniprogram.TouchEvent) { const { id } = e.currentTarget.dataset this.setAndSave({ levelId: id }) }, // ========== 活动分类选择(多选) ========== onSelectCategory(e: WechatMiniprogram.TouchEvent) { const { id } = e.currentTarget.dataset let { selectedCategoryIds, categoryTags } = this.data const index = selectedCategoryIds.indexOf(id) if (index > -1) { selectedCategoryIds = selectedCategoryIds.filter((item) => item !== id) } else { selectedCategoryIds = [...selectedCategoryIds, id] } categoryTags = categoryTags.map((item) => ({ ...item, isSelected: selectedCategoryIds.includes(item.id), })) this.setAndSave({ selectedCategoryIds, categoryTags }) }, // ========== 活动标签选择(多选) ========== onSelectTag(e: WechatMiniprogram.TouchEvent) { const { id } = e.currentTarget.dataset let { selectedTagIds, tagTags } = this.data const index = selectedTagIds.indexOf(id) if (index > -1) { selectedTagIds = selectedTagIds.filter((item) => item !== id) } else { selectedTagIds = [...selectedTagIds, id] } tagTags = tagTags.map((item) => ({ ...item, isSelected: selectedTagIds.includes(item.id), })) this.setAndSave({ selectedTagIds, tagTags }) }, // ========== 议程管理 ========== onAddAgenda() { const agendas = this.data.agendas agendas.push({ agendaTime: '', title: '', description: '', sort: agendas.length, }) this.setAndSave({ agendas }) }, onRemoveAgenda(e: WechatMiniprogram.TouchEvent) { const { index } = e.currentTarget.dataset const agendas = this.data.agendas.filter((_, i) => i !== index) this.setAndSave({ agendas }) }, onAgendaInput(e: WechatMiniprogram.Input | WechatMiniprogram.TextareaInput) { const { index, field } = e.currentTarget.dataset const agendas = this.data.agendas agendas[index][field] = e.detail.value this.setAndSave({ agendas }) }, // 打开议程时间选择器 onOpenAgendaDatePicker(e: WechatMiniprogram.TouchEvent) { const { index } = e.currentTarget.dataset const agenda = this.data.agendas[index] const currentValue = agenda.agendaTime || '' let pickerValue = new Date().getTime() if (currentValue) { const parsedDate = new Date(currentValue.replace(/-/g, '/')) if (!Number.isNaN(parsedDate.getTime())) { pickerValue = parsedDate.getTime() } } this.setData({ showAgendaDatePicker: true, agendaDatePickerIndex: index, agendaDatePickerValue: pickerValue, }) }, // 关闭议程时间选择器 onCloseAgendaDatePicker() { this.setData({ showAgendaDatePicker: false }) }, // 确认议程时间选择 onConfirmAgendaDatePicker(e: WechatMiniprogram.CustomEvent) { const { agendaDatePickerIndex } = this.data const timestamp = e.detail const formattedTime = this.formatDateTime(timestamp) const agendas = this.data.agendas agendas[agendaDatePickerIndex].agendaTime = formattedTime this.setAndSave({ showAgendaDatePicker: false, agendas, }) }, // ========== 报名签到设置 ========== onToggleRegister(e: WechatMiniprogram.TouchEvent) { const { value } = e.currentTarget.dataset this.setAndSave({ needRegister: value === 'yes' }) }, onToggleRegisterLimit(e: WechatMiniprogram.TouchEvent) { const { value } = e.currentTarget.dataset this.setAndSave({ registerLimit: value }) }, onSelectCheckinWay(e: WechatMiniprogram.TouchEvent) { const { value } = e.currentTarget.dataset this.setAndSave({ checkinWay: value }) }, // ========== 底部操作 ========== onSaveDraft() { this.submitActivity(1) // activityStatus = 1 (草稿) }, onSubmit() { wx.showModal({ title: '确认提交', content: '提交后将进入审核流程,是否继续?', success: (res) => { if (res.confirm) { this.submitActivity(2) // activityStatus = 2 (待审核) } }, }) }, // 提交活动申请 async submitActivity(activityStatus: number) { const { coverImageList, title, type, typeOther, summary, detail, startTime, endTime, location, organizer, contactName, contactPhone, levelId, selectedCategoryIds, selectedTagIds, needRegister, registerStartTime, registerEndTime, registerLimit, registerLimitCount, registerCondition, checkinWay, checkinStartTime, checkinEndTime, agendas, detailImages, submitting, } = this.data if (submitting) return // 草稿模式不做严格校验,仅提交已填内容 // 正式提交(activityStatus=2)才校验必填字段 if (activityStatus === 2) { // 校验必填字段 if (!coverImageList.length) { wx.showToast({ title: '请上传活动头图', icon: 'error' }) return } if (!title.trim()) { wx.showToast({ title: '请输入活动标题', icon: 'error' }) return } if (!startTime) { wx.showToast({ title: '请选择活动开始时间', icon: 'error' }) return } if (!endTime) { wx.showToast({ title: '请选择活动结束时间', icon: 'error' }) return } if (!location.trim()) { wx.showToast({ title: '请输入活动地点', icon: 'error' }) return } // 校验报名设置 if (needRegister) { if (!registerStartTime) { wx.showToast({ title: '请选择报名开始时间', icon: 'error' }) return } if (!registerEndTime) { wx.showToast({ title: '请选择报名截止时间', icon: 'error' }) return } } // 校验签到设置 if (checkinWay !== 'none') { if (!checkinStartTime) { wx.showToast({ title: '请选择签到开始时间', icon: 'error' }) return } if (!checkinEndTime) { wx.showToast({ title: '请选择签到结束时间', icon: 'error' }) return } } // 议程可选,不做必填验证 } this.setData({ submitting: true }) wx.showLoading({ title: activityStatus === 1 ? '保存中...' : '提交中...' }) try { const checkinTypeMap: Record = { dynamic: 1, fixed: 2, none: 3, } const params: Record = { mainImages: coverImageList.map((item) => item.url), name: title, type, typeOther: type === 6 ? typeOther : '', summary, detailImages, description: detail, checkinType: checkinTypeMap[checkinWay], regType: needRegister ? 1 : 2, regCondition: registerCondition, quota: registerLimit === 'limited' ? Number(registerLimitCount) : 0, regStartAt: needRegister ? registerStartTime : '', regEndAt: needRegister ? registerEndTime : '', startAt: startTime, endAt: endTime, location, organizer, contactName, contactPhone, tagIds: selectedTagIds, categoryIds: selectedCategoryIds, levelId, checkinStartAt: checkinWay !== 'none' ? checkinStartTime : '', checkinEndAt: checkinWay !== 'none' ? checkinEndTime : '', // 过滤掉空的议程(没有标题的议程不提交) agendas: agendas .filter((item) => item.title.trim()) .map((item, index) => ({ agendaTime: item.agendaTime, title: item.title, description: item.description, sort: index, })), activityStatus, } // 编辑模式传入 id,统一使用 /activity/apply 接口 if (this.data.isEdit) { params.id = this.data.editId } const res = await wx.ajax({ url: '/activity/apply', method: 'POST', data: params, }) wx.hideLoading() if (res) { wx.showToast({ title: activityStatus === 1 ? '已保存草稿' : '提交成功', icon: 'success', }) // 清理本地草稿 this.clearDraft() // 跳转到结果页面(编辑模式使用 editId) const activityId = this.data.isEdit ? this.data.editId : res.activityId wx.redirectTo({ url: `/pages/actAddResult/index?id=${activityId}&status=${res.status || ''}`, }) } } catch (err: any) { wx.hideLoading() const message = err?.message || '提交失败' wx.showToast({ title: message, icon: 'error' }) } finally { this.setData({ submitting: false }) } }, onGoHome() { this.clearDraft() wx.switchTab({ url: '/pages/index/index' }) }, }) export {}