You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

925 lines
25 KiB

2 weeks ago
const app = getApp<IAppOption>()
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 {
2 weeks ago
id: number
name: string
sort: number
activityCount: number
}
interface AgendaItem {
agendaTime: string
2 weeks ago
title: string
description: string
sort: number
2 weeks ago
}
2 weeks ago
Page({
2 weeks ago
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 }>,
2 weeks ago
title: '',
type: 1,
typeOptions: [
{ id: 1, name: '讲座' },
{ id: 2, name: '比赛' },
{ id: 3, name: '社团' },
{ id: 4, name: '志愿' },
{ id: 5, name: '文体' },
{ id: 6, name: '其他' },
],
typeOther: '',
summary: '',
2 weeks ago
startTime: '',
endTime: '',
detail: '',
detailImages: [] as string[],
2 weeks ago
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 weeks ago
// 步骤2 报名签到设置
2 weeks ago
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,
1 week ago
// 编辑模式:传入的活动ID
editId: 0,
isEdit: false,
2 weeks ago
},
1 week ago
async onLoad(options: any) {
await app.waitLogin({ type: 1 })
1 week ago
// 判断是否为编辑模式
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)
}
1 week ago
// 编辑模式:从接口回显数据,忽略本地草稿
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)
}
},
1 week ago
// 获取活动详情并回显到表单
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<number, string> = { 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<string, any> = {}) {
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<string, any> = {}
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<string, any> = {}) {
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<string, any>) {
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<string, any>) {
this.setData(patch)
1 week ago
// 编辑模式下不保存本地草稿缓存
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)
}
2 weeks ago
},
// ========== 步骤切换 ==========
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
2 weeks ago
},
onNextStep() {
// 先验证当前步骤
if (!this.validateCurrentStep()) {
return
}
2 weeks ago
const next = this.data.currentStep + 1
if (next <= 4) this.setAndSave({ currentStep: next })
2 weeks ago
},
onPrevStep() {
const prev = this.data.currentStep - 1
if (prev >= 1) this.setAndSave({ currentStep: prev })
2 weeks ago
},
// ========== 图片上传 ==========
// 上传成功后,直接添加到列表(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 })
2 weeks ago
},
// ========== 输入绑定 ==========
onInputChange(e: WechatMiniprogram.Input) {
const { field } = e.currentTarget.dataset
this.setAndSave({ [field]: e.detail.value })
2 weeks ago
},
onTextareaChange(e: WechatMiniprogram.TextareaInput) {
const { field } = e.currentTarget.dataset
this.setAndSave({ [field]: e.detail.value })
2 weeks ago
},
// ========== 时间选择 ==========
// 格式化日期时间
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) {
2 weeks ago
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,
})
2 weeks ago
},
// ========== 活动类型选择 ==========
onSelectType(e: WechatMiniprogram.TouchEvent) {
2 weeks ago
const { value } = e.currentTarget.dataset
this.setAndSave({ type: value })
},
// ========== 活动等级选择 ==========
onSelectLevel(e: WechatMiniprogram.TouchEvent) {
const { id } = e.currentTarget.dataset
this.setAndSave({ levelId: id })
2 weeks ago
},
// ========== 活动分类选择(多选) ==========
2 weeks ago
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 })
2 weeks ago
},
// ========== 议程管理 ==========
onAddAgenda() {
const agendas = this.data.agendas
agendas.push({
agendaTime: '',
2 weeks ago
title: '',
description: '',
sort: agendas.length,
2 weeks ago
})
this.setAndSave({ agendas })
2 weeks ago
},
onRemoveAgenda(e: WechatMiniprogram.TouchEvent) {
const { index } = e.currentTarget.dataset
const agendas = this.data.agendas.filter((_, i) => i !== index)
this.setAndSave({ agendas })
2 weeks ago
},
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 })
2 weeks ago
},
// 打开议程时间选择器
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)
2 weeks ago
const agendas = this.data.agendas
agendas[agendaDatePickerIndex].agendaTime = formattedTime
this.setAndSave({
showAgendaDatePicker: false,
agendas,
})
2 weeks ago
},
// ========== 报名签到设置 ==========
onToggleRegister(e: WechatMiniprogram.TouchEvent) {
const { value } = e.currentTarget.dataset
this.setAndSave({ needRegister: value === 'yes' })
2 weeks ago
},
onToggleRegisterLimit(e: WechatMiniprogram.TouchEvent) {
const { value } = e.currentTarget.dataset
this.setAndSave({ registerLimit: value })
2 weeks ago
},
onSelectCheckinWay(e: WechatMiniprogram.TouchEvent) {
const { value } = e.currentTarget.dataset
this.setAndSave({ checkinWay: value })
2 weeks ago
},
// ========== 底部操作 ==========
onSaveDraft() {
this.submitActivity(1) // activityStatus = 1 (草稿)
2 weeks ago
},
onSubmit() {
wx.showModal({
title: '确认提交',
content: '提交后将进入审核流程,是否继续?',
success: (res) => {
if (res.confirm) {
this.submitActivity(2) // activityStatus = 2 (待审核)
2 weeks ago
}
},
})
},
// 提交活动申请
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
1 week ago
// 草稿模式不做严格校验,仅提交已填内容
// 正式提交(activityStatus=2)才校验必填字段
if (activityStatus === 2) {
// 校验必填字段
if (!coverImageList.length) {
wx.showToast({ title: '请上传活动头图', icon: 'error' })
return
}
1 week ago
if (!title.trim()) {
wx.showToast({ title: '请输入活动标题', icon: 'error' })
return
}
1 week ago
if (!startTime) {
wx.showToast({ title: '请选择活动开始时间', icon: 'error' })
return
}
1 week ago
if (!endTime) {
wx.showToast({ title: '请选择活动结束时间', icon: 'error' })
return
}
1 week ago
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
}
}
1 week ago
// 校验签到设置
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<string, number> = {
dynamic: 1,
fixed: 2,
none: 3,
}
const params: Record<string, any> = {
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,
}
1 week ago
// 编辑模式传入 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()
1 week ago
// 跳转到结果页面(编辑模式使用 editId)
const activityId = this.data.isEdit ? this.data.editId : res.activityId
wx.redirectTo({
1 week ago
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 })
}
},
2 weeks ago
onGoHome() {
this.clearDraft()
2 weeks ago
wx.switchTab({ url: '/pages/index/index' })
},
})
2 weeks ago
export {}