Browse Source

feat: 完成反馈功能开发与多项页面优化

本次提交新增了用户反馈页面,在个人中心添加了反馈入口;优化了活动报名结果页、活动详情页、通知页面等多个页面的样式与逻辑,修复了部分交互问题,同时完善了全局登录状态管理与自定义tabbar的未读红点逻辑,升级了部分页面的skyline渲染配置。
master
kola-web 1 week ago
parent
commit
6318973a2f
  1. 13
      project.private.config.json
  2. 15
      src/app.json
  3. 18
      src/app.ts
  4. 14
      src/custom-tab-bar/index.scss
  5. 21
      src/custom-tab-bar/index.ts
  6. 2
      src/custom-tab-bar/index.wxml
  7. BIN
      src/images/bg7.png
  8. BIN
      src/images/icon58.png
  9. BIN
      src/images/icon59.png
  10. BIN
      src/images/icon60.png
  11. BIN
      src/images/icon90.png
  12. BIN
      src/images/icon91.png
  13. 4
      src/pages/actAdd/index.json
  14. 133
      src/pages/actAdd/index.ts
  15. 100
      src/pages/actAdd/index.wxml
  16. 4
      src/pages/actAddResult/index.ts
  17. 2
      src/pages/actAddResult/index.wxml
  18. 1
      src/pages/actDetail/index.scss
  19. 62
      src/pages/actDetail/index.ts
  20. 22
      src/pages/actDetail/index.wxml
  21. 4
      src/pages/actPoster/index.json
  22. 20
      src/pages/actPoster/index.scss
  23. 268
      src/pages/actPoster/index.ts
  24. 29
      src/pages/actPoster/index.wxml
  25. 59
      src/pages/actResult/index.scss
  26. 32
      src/pages/actResult/index.ts
  27. 35
      src/pages/actResult/index.wxml
  28. 5
      src/pages/feedback/index.json
  29. 26
      src/pages/feedback/index.scss
  30. 8
      src/pages/feedback/index.ts
  31. 9
      src/pages/feedback/index.wxml
  32. 5
      src/pages/my/index.ts
  33. 16
      src/pages/my/index.wxml
  34. 4
      src/pages/myAct/index.json
  35. 45
      src/pages/myAct/index.scss
  36. 232
      src/pages/myAct/index.ts
  37. 103
      src/pages/myAct/index.wxml
  38. 5
      src/pages/myComment/index.wxml
  39. 5
      src/pages/notice/index.json
  40. 7
      src/pages/notice/index.scss
  41. 220
      src/pages/notice/index.ts
  42. 79
      src/pages/notice/index.wxml
  43. 119
      src/pages/noticeDetail/index.ts
  44. 29
      src/pages/noticeDetail/index.wxml
  45. 4
      typings/index.d.ts

13
project.private.config.json

@ -4,11 +4,18 @@
"miniprogram": { "miniprogram": {
"list": [ "list": [
{ {
"name": "我要反馈",
"pathName": "pages/feedback/index",
"query": "",
"scene": null,
"launchMode": "default"
},
{
"name": "活动报名成功页", "name": "活动报名成功页",
"pathName": "pages/actResult/index", "pathName": "pages/actResult/index",
"query": "id=59", "query": "id=59",
"scene": null, "launchMode": "default",
"launchMode": "default" "scene": null
}, },
{ {
"name": "活动分享页", "name": "活动分享页",
@ -165,7 +172,7 @@
"urlCheck": true, "urlCheck": true,
"coverView": true, "coverView": true,
"lazyloadPlaceholderEnable": false, "lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false, "skylineRenderEnable": true,
"preloadBackgroundData": false, "preloadBackgroundData": false,
"autoAudits": false, "autoAudits": false,
"useApiHook": true, "useApiHook": true,

15
src/app.json

@ -20,7 +20,8 @@
"pages/myAct/index", "pages/myAct/index",
"pages/myAgent/index", "pages/myAgent/index",
"pages/myComment/index", "pages/myComment/index",
"pages/schedule/index" "pages/schedule/index",
"pages/feedback/index"
], ],
"window": { "window": {
"backgroundTextStyle": "light", "backgroundTextStyle": "light",
@ -65,5 +66,15 @@
"navbar": "/components/navbar/index" "navbar": "/components/navbar/index"
}, },
"requiredPrivateInfos": [], "requiredPrivateInfos": [],
"permission": {} "permission": {},
"lazyCodeLoading": "requiredComponents",
"rendererOptions": {
"skyline": {
"defaultDisplayBlock": true,
"disableABTest": true,
"sdkVersionBegin": "3.0.1",
"sdkVersionEnd": "15.255.255"
}
},
"componentFramework": "glass-easel"
} }

18
src/app.ts

@ -26,6 +26,7 @@ App<IAppOption>({
accessToken: '', accessToken: '',
needBind: true, needBind: true,
loginPromise: Promise.resolve(),
}, },
onLaunch() { onLaunch() {
this.autoUpdate() this.autoUpdate()
@ -38,9 +39,10 @@ App<IAppOption>({
if (options.query?.scene) { if (options.query?.scene) {
this.globalData.scene = parseScene(options.query!.scene) as any this.globalData.scene = parseScene(options.query!.scene) as any
} }
this.startLogin() this.globalData.loginPromise = this.startLogin()
}, },
startLogin(callback?: () => void) { startLogin(callback?: () => void): Promise<void> {
return new Promise<void>((resolve, reject) => {
wx.login({ wx.login({
success: (res) => { success: (res) => {
// 调用静默登录接口 // 调用静默登录接口
@ -67,33 +69,31 @@ App<IAppOption>({
if (callback) { if (callback) {
callback() callback()
} }
resolve()
}) })
.catch((err: any) => { .catch((err: any) => {
// 静默失败,不提示用户 // 静默失败,不提示用户
console.error('静默登录请求失败:', err) console.error('静默登录请求失败:', err)
reject(err)
}) })
}, },
fail: (err) => { fail: (err) => {
// 静默失败,不提示用户 // 静默失败,不提示用户
console.error('wx.login 失败:', err) console.error('wx.login 失败:', err)
reject(err)
}, },
}) })
})
}, },
waitLogin({ type }: { type?: 0 | 1 } = { type: 0 }) { waitLogin({ type }: { type?: 0 | 1 } = { type: 0 }) {
return new Promise<void>((resolve) => { return this.globalData.loginPromise.then(() => {
const checkLogin = () => {
// type = 0:不需要登录即可访问 // type = 0:不需要登录即可访问
if (type === 0) { if (type === 0) {
resolve()
return return
} }
if (type === 1 && this.globalData.needBind) { if (type === 1 && this.globalData.needBind) {
this.redirectToLogin() this.redirectToLogin()
} }
resolve()
}
checkLogin()
}) })
}, },

14
src/custom-tab-bar/index.scss

@ -10,4 +10,18 @@
color: rgba(74, 184, 253, 1); color: rgba(74, 184, 253, 1);
} }
} }
.hot {
position: absolute;
top: 8rpx;
right: 50%;
margin-right: -36rpx;
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background-color: rgba(253, 91, 89, 1);
opacity: 0;
&.show {
opacity: 1;
}
}
} }

21
src/custom-tab-bar/index.ts

@ -9,6 +9,7 @@ Component({
isChild: 0, isChild: 0,
active: 0, active: 0,
showNoticeDot: false,
list: [ list: [
{ {
pagePath: '/pages/index/index', pagePath: '/pages/index/index',
@ -51,6 +52,9 @@ Component({
active, active,
anyWhere: app.globalData.anyWhere, anyWhere: app.globalData.anyWhere,
}) })
// 获取未读数
this.fetchUnreadCount()
}, },
}, },
methods: { methods: {
@ -63,5 +67,22 @@ Component({
url: pagePath, url: pagePath,
}) })
}, },
// 获取通知未读数
async fetchUnreadCount() {
try {
await app.waitLogin({ type: 1 })
const res = await wx.ajax({
url: '/notification/unread-count',
method: 'GET',
data: {},
})
if (res) {
this.setData({ showNoticeDot: res.totalUnread > 0 })
}
} catch (err) {
console.error('获取未读数失败:', err)
}
},
}, },
}) })

2
src/custom-tab-bar/index.wxml

@ -8,7 +8,7 @@
icon="{{imageUrl}}{{active==index ? item.iconActive : item.icon}}.png" icon="{{imageUrl}}{{active==index ? item.iconActive : item.icon}}.png"
> >
<view class="name {{index==active && 'active'}}">{{item.text}}</view> <view class="name {{index==active && 'active'}}">{{item.text}}</view>
<view class="hot"></view> <view class="hot {{showNoticeDot && index == 2 ? 'show' : ''}}"></view>
</van-tabbar-item> </van-tabbar-item>
</block> </block>
</van-tabbar> </van-tabbar>

BIN
src/images/bg7.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
src/images/icon58.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 73 KiB

BIN
src/images/icon59.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 132 KiB

BIN
src/images/icon60.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
src/images/icon90.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
src/images/icon91.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

4
src/pages/actAdd/index.json

@ -3,6 +3,8 @@
"navigationStyle": "default", "navigationStyle": "default",
"usingComponents": { "usingComponents": {
"upload-file": "/components/uploadFile/index", "upload-file": "/components/uploadFile/index",
"van-icon": "@vant/weapp/icon/index" "van-icon": "@vant/weapp/icon/index",
"van-popup": "@vant/weapp/popup/index",
"van-datetime-picker": "@vant/weapp/datetime-picker/index"
} }
} }

133
src/pages/actAdd/index.ts

@ -26,7 +26,6 @@ interface ITagItem {
} }
interface AgendaItem { interface AgendaItem {
agendaDate: string
agendaTime: string agendaTime: string
title: string title: string
description: string description: string
@ -91,10 +90,22 @@ Page({
checkinStartTime: '', checkinStartTime: '',
checkinEndTime: '', 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 活动议程 // 步骤3 活动议程
agendas: [{ agendaDate: '', agendaTime: '', title: '', description: '', sort: 0 }] as AgendaItem[], agendas: [{ agendaTime: '', title: '', description: '', sort: 0 }] as AgendaItem[],
nextAgendaId: 2, nextAgendaId: 2,
// 议程时间选择器相关
showAgendaDatePicker: false,
agendaDatePickerIndex: -1, // 当前编辑的议程索引
agendaDatePickerValue: new Date().getTime(),
// 提交状态 // 提交状态
submitting: false, submitting: false,
}, },
@ -308,7 +319,6 @@ Page({
checkinWay, checkinWay,
checkinStartTime, checkinStartTime,
checkinEndTime, checkinEndTime,
agendas,
} = this.data } = this.data
// 步骤1:基本信息 // 步骤1:基本信息
@ -361,12 +371,9 @@ Page({
} }
} }
// 步骤3:活动议程 // 步骤3:活动议程(可选,不做必填验证)
if (currentStep === 3) { if (currentStep === 3) {
if (!agendas.length || !agendas[0].title.trim()) { // 议程可选,不做验证
wx.showToast({ title: '请添加活动议程', icon: 'none' })
return false
}
} }
return true return true
@ -421,9 +428,53 @@ Page({
}, },
// ========== 时间选择 ========== // ========== 时间选择 ==========
onPickTime(e: WechatMiniprogram.PickerChange) { // 格式化日期时间
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 { field } = e.currentTarget.dataset
this.setAndSave({ [field]: e.detail.value }) 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,
})
}, },
// ========== 活动类型选择 ========== // ========== 活动类型选择 ==========
@ -482,7 +533,6 @@ Page({
onAddAgenda() { onAddAgenda() {
const agendas = this.data.agendas const agendas = this.data.agendas
agendas.push({ agendas.push({
agendaDate: '',
agendaTime: '', agendaTime: '',
title: '', title: '',
description: '', description: '',
@ -504,11 +554,45 @@ Page({
this.setAndSave({ agendas }) this.setAndSave({ agendas })
}, },
onAgendaTime(e: WechatMiniprogram.PickerChange) { // 打开议程时间选择器
const { index, field } = e.currentTarget.dataset 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 const agendas = this.data.agendas
agendas[index][field] = e.detail.value as string agendas[agendaDatePickerIndex].agendaTime = formattedTime
this.setAndSave({ agendas })
this.setAndSave({
showAgendaDatePicker: false,
agendas,
})
}, },
// ========== 报名签到设置 ========== // ========== 报名签到设置 ==========
@ -552,6 +636,7 @@ Page({
type, type,
typeOther, typeOther,
summary, summary,
detail,
startTime, startTime,
endTime, endTime,
location, location,
@ -623,11 +708,7 @@ Page({
} }
} }
// 校验议程 // 议程可选,不做必填验证
if (!agendas.length || !agendas[0].title.trim()) {
wx.showToast({ title: '请添加活动议程', icon: 'error' })
return
}
this.setData({ submitting: true }) this.setData({ submitting: true })
wx.showLoading({ title: activityStatus === 1 ? '保存中...' : '提交中...' }) wx.showLoading({ title: activityStatus === 1 ? '保存中...' : '提交中...' })
@ -646,6 +727,7 @@ Page({
typeOther: type === 6 ? typeOther : '', typeOther: type === 6 ? typeOther : '',
summary, summary,
detailImages, detailImages,
description: detail,
checkinType: checkinTypeMap[checkinWay], checkinType: checkinTypeMap[checkinWay],
regType: needRegister ? 1 : 2, regType: needRegister ? 1 : 2,
regCondition: registerCondition, regCondition: registerCondition,
@ -661,8 +743,15 @@ Page({
tagIds: selectedTagIds, tagIds: selectedTagIds,
categoryIds: selectedCategoryIds, categoryIds: selectedCategoryIds,
levelId, levelId,
agendas: agendas.map((item, index) => ({ checkinStartAt: checkinWay !== 'none' ? checkinStartTime : '',
...item, 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, sort: index,
})), })),
activityStatus, activityStatus,

100
src/pages/actAdd/index.wxml

@ -71,18 +71,14 @@
<text>活动时间</text> <text>活动时间</text>
<text class="required">*</text> <text class="required">*</text>
</view> </view>
<picker mode="date" value="{{startTime}}" end="{{endTime}}" data-field="startTime" bindchange="onPickTime"> <view class="form-picker" data-field="startTime" bind:tap="onOpenDatePicker">
<view class="form-picker"> <text class="picker-value {{startTime ? '' : 'is-placeholder'}}">{{startTime || '开始时间'}}</text>
<text class="picker-value {{startTime ? '' : 'is-placeholder'}}">{{startTime || '请选择开始时间'}}</text>
<image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image> <image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image>
</view> </view>
</picker> <view class="form-picker" data-field="endTime" bind:tap="onOpenDatePicker">
<picker mode="date" value="{{endTime}}" start="{{startTime}}" data-field="endTime" bindchange="onPickTime"> <text class="picker-value {{endTime ? '' : 'is-placeholder'}}">{{endTime || '结束时间'}}</text>
<view class="form-picker">
<text class="picker-value {{endTime ? '' : 'is-placeholder'}}">{{endTime || '请选择结束时间'}}</text>
<image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image> <image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image>
</view> </view>
</picker>
</view> </view>
<view class="form-field"> <view class="form-field">
@ -246,34 +242,26 @@
<text>报名时间</text> <text>报名时间</text>
<text class="required">*</text> <text class="required">*</text>
</view> </view>
<picker <view
mode="date" class="form-picker"
value="{{registerStartTime}}"
end="{{registerEndTime}}"
data-field="registerStartTime" data-field="registerStartTime"
bindchange="onPickTime" bind:tap="onOpenDatePicker"
> >
<view class="form-picker">
<view class="picker-value {{registerStartTime ? '' : 'is-placeholder'}}"> <view class="picker-value {{registerStartTime ? '' : 'is-placeholder'}}">
{{registerStartTime || '请选择开始时间'}} {{registerStartTime || '开始时间'}}
</view> </view>
<image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image> <image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image>
</view> </view>
</picker> <view
<picker class="form-picker"
mode="date"
value="{{registerEndTime}}"
start="{{registerStartTime}}"
data-field="registerEndTime" data-field="registerEndTime"
bindchange="onPickTime" bind:tap="onOpenDatePicker"
> >
<view class="form-picker">
<view class="picker-value {{registerEndTime ? '' : 'is-placeholder'}}"> <view class="picker-value {{registerEndTime ? '' : 'is-placeholder'}}">
{{registerEndTime || '请选择结束时间'}} {{registerEndTime || '结束时间'}}
</view> </view>
<image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image> <image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image>
</view> </view>
</picker>
</view> </view>
<!-- 报名人数限制 --> <!-- 报名人数限制 -->
@ -388,34 +376,26 @@
<text>签到时间</text> <text>签到时间</text>
<text class="required">*</text> <text class="required">*</text>
</view> </view>
<picker <view
mode="date" class="form-picker"
value="{{checkinStartTime}}"
end="{{checkinEndTime}}"
data-field="checkinStartTime" data-field="checkinStartTime"
bindchange="onPickTime" bind:tap="onOpenDatePicker"
> >
<view class="form-picker">
<view class="picker-value {{checkinStartTime ? '' : 'is-placeholder'}}"> <view class="picker-value {{checkinStartTime ? '' : 'is-placeholder'}}">
{{checkinStartTime || '请选择签到开始时间'}} {{checkinStartTime || '开始时间'}}
</view> </view>
<image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image> <image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image>
</view> </view>
</picker> <view
<picker class="form-picker"
mode="date"
value="{{checkinEndTime}}"
start="{{checkinStartTime}}"
data-field="checkinEndTime" data-field="checkinEndTime"
bindchange="onPickTime" bind:tap="onOpenDatePicker"
> >
<view class="form-picker">
<view class="picker-value {{checkinEndTime ? '' : 'is-placeholder'}}"> <view class="picker-value {{checkinEndTime ? '' : 'is-placeholder'}}">
{{checkinEndTime || '请选择签到结束时间'}} {{checkinEndTime || '结束时间'}}
</view> </view>
<image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image> <image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image>
</view> </view>
</picker>
</view> </view>
</block> </block>
</view> </view>
@ -463,20 +443,12 @@
<text>议程时间</text> <text>议程时间</text>
<text class="required">*</text> <text class="required">*</text>
</view> </view>
<picker <view class="form-picker" data-index="{{index}}" bind:tap="onOpenAgendaDatePicker">
mode="date"
value="{{item.agendaTime}}"
data-index="{{index}}"
data-field="agendaTime"
bindchange="onAgendaTime"
>
<view class="form-picker">
<view class="picker-value {{item.agendaTime ? '' : 'is-placeholder'}}"> <view class="picker-value {{item.agendaTime ? '' : 'is-placeholder'}}">
{{item.agendaTime || '请选择时间'}} {{item.agendaTime || '请选择时间'}}
</view> </view>
<image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image> <image class="picker-icon" src="{{imageUrl}}icon17.png?t={{Timestamp}}"></image>
</view> </view>
</picker>
</view> </view>
<view class="form-group agenda-group"> <view class="form-group agenda-group">
<view class="form-label"> <view class="form-label">
@ -502,13 +474,13 @@
<textarea <textarea
class="form-textarea" class="form-textarea"
placeholder="请输入议程描述,1-200字" placeholder="请输入议程描述,1-200字"
value="{{item.desc}}" value="{{item.description}}"
data-index="{{index}}" data-index="{{index}}"
data-field="desc" data-field="description"
bindinput="onAgendaInput" bindinput="onAgendaInput"
maxlength="200" maxlength="200"
/> />
<text class="char-count">{{item.desc.length || 0}}/200</text> <text class="char-count">{{item.description.length || 0}}/200</text>
</view> </view>
</view> </view>
</view> </view>
@ -524,3 +496,27 @@
</view> </view>
</block> </block>
</view> </view>
<!-- 时间选择器弹窗 -->
<van-popup show="{{ showDatePicker }}" position="bottom" round bind:close="onCloseDatePicker">
<van-datetime-picker
type="datetime"
value="{{ datePickerValue }}"
min-date="{{ datePickerMinDate }}"
max-date="{{ datePickerMaxDate }}"
bind:confirm="onConfirmDatePicker"
bind:cancel="onCloseDatePicker"
/>
</van-popup>
<!-- 议程时间选择器弹窗 -->
<van-popup show="{{ showAgendaDatePicker }}" position="bottom" round bind:close="onCloseAgendaDatePicker">
<van-datetime-picker
type="datetime"
value="{{ agendaDatePickerValue }}"
min-date="{{ datePickerMinDate }}"
max-date="{{ datePickerMaxDate }}"
bind:confirm="onConfirmAgendaDatePicker"
bind:cancel="onCloseAgendaDatePicker"
/>
</van-popup>

4
src/pages/actAddResult/index.ts

@ -43,8 +43,8 @@ Page({
// 返回活动页 // 返回活动页
handleBack() { handleBack() {
wx.switchTab({ wx.redirectTo({
url: '/pages/act/index', url: `/pages/actDetail/index?id=${this.data.activityId}`,
}) })
}, },
}) })

2
src/pages/actAddResult/index.wxml

@ -3,7 +3,7 @@
<view class="status">申请提交成功</view> <view class="status">申请提交成功</view>
<view class="content">我们将会在 7 个工作日内完成审核,结果将通过公 众号发送给你,请注意查收~</view> <view class="content">我们将会在 7 个工作日内完成审核,结果将通过公 众号发送给你,请注意查收~</view>
<view class="btn" bind:tap="handleContinue">继续发布</view> <view class="btn" bind:tap="handleContinue">继续发布</view>
<view class="back">返回活动页</view> <view class="back" bind:tap="handleBack">返回活动页</view>
<view class="official"> <view class="official">
<view class="title">关注xxx公众号</view> <view class="title">关注xxx公众号</view>
<view class="o-container"> <view class="o-container">

1
src/pages/actDetail/index.scss

@ -221,6 +221,7 @@ page {
} }
} }
.mp-html { .mp-html {
display: block;
margin-top: 30rpx; margin-top: 30rpx;
} }
} }

62
src/pages/actDetail/index.ts

@ -39,7 +39,7 @@ interface IActivityDetail {
categoryIds: number[] categoryIds: number[]
categoryNames: string[] categoryNames: string[]
isRegistered: boolean isRegistered: boolean
isCheckedIn: boolean isChecked: boolean
isFavorited: boolean isFavorited: boolean
isReviewed: boolean isReviewed: boolean
countdownSeconds: number countdownSeconds: number
@ -121,6 +121,9 @@ Page({
totalPages: 0, totalPages: 0,
} as IPagination, } as IPagination,
reviewLoading: false, reviewLoading: false,
// 议程分组
agendaGroups: [] as Array<{ date: string; items: IActivityDetail['agendas'] }>,
}, },
onLoad(options: { id?: string }) { onLoad(options: { id?: string }) {
@ -148,10 +151,13 @@ Page({
data: {}, data: {},
}) })
if (res) { if (res) {
// 对议程按日期分组
const agendaGroups = this.groupAgendasByDate(res.agendas || [])
this.setData({ this.setData({
detail: res, detail: res,
countdownSeconds: res.countdownSeconds || 0, countdownSeconds: res.countdownSeconds || 0,
loading: false, loading: false,
agendaGroups,
}) })
} }
} catch (err) { } catch (err) {
@ -160,6 +166,19 @@ Page({
} }
}, },
// 按日期分组议程
groupAgendasByDate(agendas: IActivityDetail['agendas']) {
const groupMap = new Map<string, IActivityDetail['agendas']>()
agendas.forEach((item) => {
const date = item.agendaDate
if (!groupMap.has(date)) {
groupMap.set(date, [])
}
groupMap.get(date)!.push(item)
})
return Array.from(groupMap.entries()).map(([date, items]) => ({ date, items }))
},
// 获取评价列表 // 获取评价列表
async fetchReviewList(isRefresh = false) { async fetchReviewList(isRefresh = false) {
if (this.data.reviewLoading) return if (this.data.reviewLoading) return
@ -240,17 +259,20 @@ Page({
}) })
// 跳转到报名成功页面 // 跳转到报名成功页面
wx.navigateTo({ wx.navigateTo({
url: `/pages/actResult/index?id=${this.data.activityId}`, url: `/pages/actResult/index?id=${this.data.activityId}&status=success`,
}) })
} }
} catch (err: any) { } catch (err: any) {
wx.hideLoading() wx.hideLoading()
const message = err?.message || '报名失败' const message = err?.message || '报名失败'
wx.showToast({ title: message, icon: 'error' }) // 跳转到报名失败页面
wx.navigateTo({
url: `/pages/actResult/index?id=${this.data.activityId}&status=fail&message=${message}`,
})
} }
}, },
// 签到 // 签到(扫码签到)
async handleCheckin() { async handleCheckin() {
const { detail } = this.data const { detail } = this.data
if (!detail) return if (!detail) return
@ -262,12 +284,26 @@ Page({
return return
} }
// 调用扫码功能
wx.scanCode({
success: async (scanRes) => {
const qrUrl = scanRes.result
// 解析二维码中的 scene 参数
// 格式: https://app.gohighedu.cn/activity/checkin?scene=xxx
const sceneMatch = qrUrl.match(/[?&]scene=([^&]+)/)
if (!sceneMatch) {
wx.showToast({ title: '二维码格式错误', icon: 'error' })
return
}
const token = sceneMatch[1]
try { try {
wx.showLoading({ title: '签到中...' }) wx.showLoading({ title: '签到中...' })
const res = await wx.ajax({ const res = await wx.ajax({
url: `/activity/checkin?id=${this.data.activityId}`, url: `/activity/scan-checkin`,
method: 'POST', method: 'POST',
data: {}, data: { token },
}) })
wx.hideLoading() wx.hideLoading()
@ -275,14 +311,14 @@ Page({
// 显示签到成功弹窗 // 显示签到成功弹窗
this.setData({ this.setData({
popupShow: true, popupShow: true,
popupType: 'checkinSuccess', popupType: 'popup1',
popupParams: { checkedAt: res.checkedAt }, popupParams: { close: true },
}) })
// 更新状态 // 更新状态
this.setData({ this.setData({
detail: { detail: {
...detail, ...detail,
isCheckedIn: true, isChecked: true,
checkinCount: detail.checkinCount + 1, checkinCount: detail.checkinCount + 1,
}, },
}) })
@ -293,6 +329,14 @@ Page({
wx.showToast({ title: message, icon: 'error' }) wx.showToast({ title: message, icon: 'error' })
} }
}, },
fail: (err) => {
console.error('扫码失败:', err)
if (err.errMsg !== 'scanCode:fail cancel') {
wx.showToast({ title: '扫码失败', icon: 'error' })
}
},
})
},
// 分享 // 分享
async handleShare() { async handleShare() {

22
src/pages/actDetail/index.wxml

@ -54,21 +54,21 @@
</view> </view>
<!-- 活动议程 --> <!-- 活动议程 -->
<view class="process-card" wx:if="{{detail.agendas.length > 0}}"> <view class="process-card" wx:if="{{agendaGroups.length > 0}}">
<view class="p-title">活动议程</view> <view class="p-title">活动议程</view>
<view class="p-item" wx:for="{{detail.agendas}}" wx:key="id"> <view class="p-item" wx:for="{{agendaGroups}}" wx:key="date">
<view class="date">{{item.agendaDate}}</view> <view class="date">{{item.date}}</view>
<view class="list"> <view class="list">
<view class="l-item"> <view class="l-item" wx:for="{{item.items}}" wx:for-item="agenda" wx:key="id">
<view class="time">{{item.agendaTime}}</view> <view class="time">{{agenda.agendaTime}}</view>
<view class="aside"> <view class="aside">
<view class="line-top"></view> <view class="line-top"></view>
<view class="circle"></view> <view class="circle"></view>
<view class="line-bottom"></view> <view class="line-bottom"></view>
</view> </view>
<view class="container"> <view class="container">
<view class="title">{{item.title}}</view> <view class="title">{{agenda.title}}</view>
<view class="content">{{item.description}}</view> <view class="content">{{agenda.description}}</view>
</view> </view>
</view> </view>
</view> </view>
@ -82,13 +82,13 @@
</view> </view>
<!-- 精彩评论 --> <!-- 精彩评论 -->
<view class="comment-list"> <view class="comment-list" wx:if="{{detail.commentCount>0}}">
<view class="c-title">精彩评论 ({{detail.commentCount}}条)</view> <view class="c-title">精彩评论 ({{detail.commentCount}}条)</view>
<view class="c-card" wx:for="{{reviewList}}" wx:key="id"> <view class="c-card" wx:for="{{reviewList}}" wx:key="id">
<view class="user"> <view class="user">
<image <image
class="avatar" class="avatar"
src="{{item.isAnonymous ? '{{imageUrl}}avatar-default.png?t={{Timestamp}}' : item.avatarUrl}}" src="{{item.avatarUrl}}"
></image> ></image>
<view class="wrap"> <view class="wrap">
<view class="name">{{item.isAnonymous ? '匿名用户' : item.nickname}}</view> <view class="name">{{item.isAnonymous ? '匿名用户' : item.nickname}}</view>
@ -154,7 +154,7 @@
<!-- 我要报名:需要报名的活动,活动已发布(报名中),未报名的用户 --> <!-- 我要报名:需要报名的活动,活动已发布(报名中),未报名的用户 -->
<view <view
class="btn" class="btn"
wx:if="{{detail.regType === 1 && detail.activityStatus === 5 && !detail.isRegistered}}" wx:if="{{detail.regType === 1 && detail.activityStatus === 5 && !detail.isRegistered && (detail.quota == 0 || detail.quota > detail.regCount)}}"
bind:tap="handleRegister" bind:tap="handleRegister"
> >
我要报名 我要报名
@ -162,7 +162,7 @@
<!-- 签到:已报名用户,活动进行中,未签到,活动需要签到 --> <!-- 签到:已报名用户,活动进行中,未签到,活动需要签到 -->
<view <view
class="btn" class="btn"
wx:elif="{{detail.isRegistered && detail.activityStatus === 6 && !detail.isCheckedIn && detail.checkinType !== 3}}" wx:elif="{{detail.isRegistered && detail.activityStatus === 6 && !detail.isChecked && detail.checkinType !== 3}}"
bind:tap="handleCheckin" bind:tap="handleCheckin"
> >
签到 签到

4
src/pages/actPoster/index.json

@ -1,5 +1,7 @@
{ {
"component": true, "renderer": "skyline",
"componentFramework": "glass-easel",
"navigationStyle": "custom",
"usingComponents": { "usingComponents": {
"van-icon": "@vant/weapp/icon/index" "van-icon": "@vant/weapp/icon/index"
} }

20
src/pages/actPoster/index.scss

@ -1,7 +1,19 @@
.page-back { .page-back {
font-size: 32rpx; padding-top: 50rpx;
color: #fff; width: 48rpx;
height: 34rpx;
.icon{
width: 18rpx;
height: 34rpx;
}
}
// snapshot 组件样式
snapshot {
width: 100%;
display: block;
} }
.page { .page {
height: 1627rpx; height: 1627rpx;
box-sizing: border-box; box-sizing: border-box;
@ -25,7 +37,7 @@
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
} }
.user { .user {
margin-top: 74rpx; margin-top: 70rpx;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20rpx; gap: 20rpx;
@ -49,7 +61,7 @@
} }
} }
.code { .code {
margin-right: 12rpx; margin-right: 10rpx;
flex-shrink: 0; flex-shrink: 0;
width: 104rpx; width: 104rpx;
height: 104rpx; height: 104rpx;

268
src/pages/actPoster/index.ts

@ -1,189 +1,156 @@
Page({ Page({
data: { data: {
posterImage: '', // 生成的海报图片路径 posterImage: '', // 生成的海报图片路径
// Canvas尺寸 - 根据页面实际尺寸计算(rpx转px,750rpx = 375px) activityId: 0, // 活动ID
canvasWidth: 375, activityDetail: null as any, // 活动详情
canvasHeight: 814, // 1627rpx ≈ 813.5px qrcodeUrl: '', // 小程序码URL
userInfo: null as any, // 用户信息
}, },
onLoad(_options: { id?: string }) { onLoad(options: { id?: string }) {
// 可以根据活动ID获取活动信息 const app = getApp()
const activityId = Number(options.id || 0)
if (!activityId) {
wx.showToast({ title: '活动ID不存在', icon: 'error' })
setTimeout(() => wx.navigateBack(), 1500)
return
}
this.setData({ activityId })
// 确保登录状态后获取活动详情、小程序码和用户信息
app.waitLogin({ type: 1 }).then(() => {
this.fetchActivityDetail()
this.fetchActivityQrcode()
this.fetchUserInfo()
})
}, },
onReady() { onReady() {
// 页面渲染完成 // 页面渲染完成
}, },
// 绘制海报 // 获取用户信息
async drawPoster(): Promise<string> { async fetchUserInfo() {
try { try {
wx.showLoading({ title: '生成中...' }) const res = await wx.ajax({
url: '/me/profile',
method: 'GET',
data: {},
})
if (res) {
this.setData({ userInfo: res })
}
} catch (error) {
console.error('获取用户信息失败', error)
}
},
// 等待页面渲染完成 // 获取活动详情
await new Promise(resolve => setTimeout(resolve, 300)) async fetchActivityDetail() {
try {
const res = await wx.ajax({
url: '/activity/detail',
method: 'GET',
data: { id: this.data.activityId },
})
// 获取canvas实例 if (res) {
const canvasNode = await new Promise<any>((resolve, reject) => { // 处理 mainImages JSON字符串
const query = this.createSelectorQuery() let mainImages = res.mainImages || []
query.select('#posterCanvas') if (typeof mainImages === 'string') {
.fields({ node: true, size: true }) try {
.exec((res) => { mainImages = JSON.parse(mainImages)
if (res && res[0] && res[0].node) { } catch {
resolve(res[0]) mainImages = []
} else {
console.error('Canvas查询失败,结果:', res)
reject(new Error('Canvas未找到或未渲染'))
} }
}
this.setData({
activityDetail: {
...res,
mainImages,
},
}) })
}
} catch (error) {
console.error('获取活动详情失败', error)
wx.showToast({ title: '获取活动详情失败', icon: 'error' })
}
},
// 获取活动小程序码
async fetchActivityQrcode() {
try {
const res = await wx.ajax({
url: '/activity/mini-qrcode',
method: 'GET',
data: { activityId: this.data.activityId },
}) })
const canvas = canvasNode.node if (res) {
const ctx = canvas.getContext('2d') this.setData({ qrcodeUrl: res.qrcodeUrl })
}
} catch (error) {
console.error('获取小程序码失败', error)
}
},
// 设置canvas尺寸 // 使用 Snapshot.takeSnapshot 生成海报
const dpr = wx.getSystemInfoSync().pixelRatio async takeSnapshot(): Promise<string> {
canvas.width = this.data.canvasWidth * dpr try {
canvas.height = this.data.canvasHeight * dpr wx.showLoading({ title: '生成中...' })
ctx.scale(dpr, dpr)
// rpx转px计算(750rpx = 375px) // 等待页面渲染完成
const rpx2px = (rpx: number) => rpx * 0.5 await new Promise(resolve => setTimeout(resolve, 300))
// 绘制背景 - 覆盖整个Canvas // 使用 skyline 的 Snapshot.takeSnapshot API
const bgImage = canvas.createImage() // 使用 .node() 方法获取 snapshot 组件节点
bgImage.src = `${this.data.imageUrl}bg7.png?t=${this.data.Timestamp}` const snapshotRes = await new Promise<any>((resolve, reject) => {
await new Promise((resolve, reject) => { this.createSelectorQuery()
bgImage.onload = resolve .select('#posterContainer')
bgImage.onerror = reject .node()
}) .exec((res) => {
ctx.drawImage(bgImage, 0, 0, this.data.canvasWidth, this.data.canvasHeight) if (res && res[0] && res[0].node) {
// 获取到 snapshot 组件节点后调用 takeSnapshot
// 绘制海报图片 res[0].node.takeSnapshot({
// 页面样式:padding: 396rpx 70rpx 0; poster高度458rpx type: 'file',
const posterTop = rpx2px(396) // 198px format: 'png',
const posterLeft = rpx2px(70) // 35px success: (snapshotResult: any) => {
const posterWidth = this.data.canvasWidth - posterLeft * 2 // 305px resolve(snapshotResult)
const posterHeight = rpx2px(458) // 229px },
fail: (err: any) => {
const posterImage = canvas.createImage() console.error('takeSnapshot 失败:', err)
posterImage.src = `${this.data.imageUrl}bg1.png?t=${this.data.Timestamp}` reject(err)
await new Promise((resolve, reject) => { },
posterImage.onload = resolve
posterImage.onerror = reject
}) })
ctx.drawImage(posterImage, posterLeft, posterTop, posterWidth, posterHeight) } else {
console.error('获取节点失败,查询结果:', res)
// 绘制标题 reject(new Error('获取节点失败'))
// 页面样式:margin-top: 32rpx; font-size: 40rpx; line-height: 56rpx; height: 168rpx }
const titleTop = posterTop + posterHeight + rpx2px(32) // 198 + 229 + 16 = 443px
const titleFontSize = rpx2px(40) // 20px
const titleLineHeight = rpx2px(56) // 28px
const titleMaxWidth = posterWidth // 305px
ctx.fillStyle = 'rgba(17, 24, 39, 1)'
ctx.font = `bold ${titleFontSize}px sans-serif`
ctx.textAlign = 'left'
const title = '深职大第十五届校园歌手大赛深职大第十五届校园歌手大赛深职大第十五届校园歌手大赛'
// 文字换行处理
this.drawText(ctx, title, posterLeft, titleTop + titleFontSize, titleMaxWidth, titleLineHeight)
// 绘制用户信息区域
// 页面样式:margin-top: 74rpx; avatar: 92rpx; gap: 20rpx
const userTop = titleTop + rpx2px(168) + rpx2px(74) // 443 + 84 + 37 = 564px
const avatarSize = rpx2px(92) // 46px
const gap = rpx2px(20) // 10px
const avatarImage = canvas.createImage()
avatarImage.src = `${this.data.imageUrl}bg1.png?t=${this.data.Timestamp}`
await new Promise((resolve, reject) => {
avatarImage.onload = resolve
avatarImage.onerror = reject
}) })
// 绘制头像(圆形)
ctx.save()
ctx.beginPath()
ctx.arc(posterLeft + avatarSize / 2, userTop + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI)
ctx.clip()
ctx.drawImage(avatarImage, posterLeft, userTop, avatarSize, avatarSize)
ctx.restore()
// 绘制用户名和日期
// 页面样式:name font-size: 28rpx; date font-size: 24rpx; margin-top: 8rpx
const nameLeft = posterLeft + avatarSize + gap // 35 + 46 + 10 = 91px
const nameFontSize = rpx2px(28) // 14px
const dateFontSize = rpx2px(24) // 12px
const dateMarginTop = rpx2px(8) // 4px
ctx.fillStyle = 'rgba(74, 184, 253, 1)'
ctx.font = `${nameFontSize}px sans-serif`
ctx.textAlign = 'left'
ctx.fillText('亚南邀请您参与活动', nameLeft, userTop + nameFontSize + 5)
ctx.fillStyle = 'rgba(148, 163, 184, 1)'
ctx.font = `${dateFontSize}px sans-serif`
ctx.fillText('2023年7月15日 18:30', nameLeft, userTop + nameFontSize + dateMarginTop + dateFontSize + 5)
// 绘制二维码
// 页面样式:code: 104rpx; margin-right: 12rpx
const qrSize = rpx2px(104) // 52px
const qrRightMargin = rpx2px(12) // 6px
const qrLeft = this.data.canvasWidth - posterLeft - qrSize - qrRightMargin // 375 - 35 - 52 - 6 = 282px
const qrImage = canvas.createImage()
qrImage.src = `${this.data.imageUrl}bg1.png?t=${this.data.Timestamp}`
await new Promise((resolve, reject) => {
qrImage.onload = resolve
qrImage.onerror = reject
}) })
ctx.drawImage(qrImage, qrLeft, userTop, qrSize, qrSize)
wx.hideLoading() wx.hideLoading()
// 将canvas转化为图片 // 保存生成的图片路径
const tempFilePath = await wx.canvasToTempFilePath({ this.setData({ posterImage: snapshotRes.tempFilePath })
canvas, return snapshotRes.tempFilePath
width: this.data.canvasWidth,
height: this.data.canvasHeight,
destWidth: this.data.canvasWidth * dpr,
destHeight: this.data.canvasHeight * dpr,
})
this.setData({ posterImage: tempFilePath.tempFilePath })
return tempFilePath.tempFilePath
} catch (error) { } catch (error) {
wx.hideLoading() wx.hideLoading()
console.error('绘制海报失败', error) console.error('生成海报失败', error)
throw error throw error
} }
}, },
// 文字换行绘制
drawText(ctx: any, text: string, x: number, y: number, maxWidth: number, lineHeight: number) {
const chars = text.split('')
let line = ''
let lineCount = 0
for (let i = 0; i < chars.length; i++) {
const testLine = line + chars[i]
const metrics = ctx.measureText(testLine)
if (metrics.width > maxWidth && i > 0) {
ctx.fillText(line, x, y + lineCount * lineHeight)
line = chars[i]
lineCount++
} else {
line = testLine
}
}
ctx.fillText(line, x, y + lineCount * lineHeight)
},
// 保存海报 // 保存海报
async handleSavePoster() { async handleSavePoster() {
try { try {
const posterPath = await this.drawPoster() const posterPath = await this.takeSnapshot()
// 保存图片到相册 // 保存图片到相册
await wx.saveImageToPhotosAlbum({ filePath: posterPath }) await wx.showShareImageMenu({ path: posterPath })
wx.showToast({ title: '保存成功', icon: 'success' }) wx.showToast({ title: '保存成功', icon: 'success' })
} catch (error: any) { } catch (error: any) {
console.error('保存海报失败', error) console.error('保存海报失败', error)
@ -205,9 +172,10 @@ Page({
// 分享给好友 // 分享给好友
onShareAppMessage() { onShareAppMessage() {
const activityDetail = this.data.activityDetail
return { return {
title: '深职大第十五届校园歌手大赛', title: activityDetail?.name || '活动分享',
path: '/pages/actDetail/index?id=123', path: `/pages/actDetail/index?id=${this.data.activityId}`,
imageUrl: this.data.posterImage || '', imageUrl: this.data.posterImage || '',
} }
}, },

29
src/pages/actPoster/index.wxml

@ -1,29 +1,24 @@
<navbar fixed customStyle="background:{{background}};"> <navbar fixed customStyle="background:{{background}};">
<van-icon class="page-back" name="arrow-left" slot="left" bind:tap="handleBack" /> <view class="page-back" slot="left" bind:tap="handleBack">
<image class="icon" src="/images/icon91.png"></image>
</view>
</navbar> </navbar>
<!-- 海报展示区域 --> <!-- 海报展示区域 - 使用 snapshot 组件包裹 -->
<view class="poster-container" id="posterContainer"> <snapshot id="posterContainer" mode="view">
<view class="page" style="background: url('{{imageUrl}}bg7.png?t={{Timestamp}}') no-repeat top center/100%"> <view class="page" style="background: url('{{imageUrl}}bg7.png?t={{Timestamp}}') no-repeat top center/100%">
<image class="poster" mode="aspectFill" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image> <image class="poster" mode="aspectFill" src="{{activityDetail.mainImages[0]}}"></image>
<view class="title">深职大第十五届校园歌手大赛深职大第十五届校园歌手大赛深职大第十五届校园歌手大赛</view> <view class="title">{{activityDetail.name || '活动标题'}}</view>
<view class="user"> <view class="user">
<image class="avatar" mode="aspectFill" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image> <image class="avatar" mode="aspectFill" src="{{userInfo.avatarUrl || imageUrl + 'bg1.png?t=' + Timestamp}}"></image>
<view class="wrap"> <view class="wrap">
<view class="name">亚南邀请您参与活动</view> <view class="name">{{userInfo.nickname || '用户'}}邀请您参与活动</view>
<view class="date">2023年7月15日 18:30</view> <view class="date">{{activityDetail.startAt || '活动时间'}}</view>
</view>
<image class="code" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image>
</view> </view>
<image class="code" src="{{qrcodeUrl}}"></image>
</view> </view>
</view> </view>
</snapshot>
<!-- Canvas用于绘制海报 - 确保在页面中可见以便正确渲染 -->
<canvas
type="2d"
id="posterCanvas"
style="position: fixed; left: 0; top: 100vh; width: 375px; height: 814px; visibility: hidden;"
></canvas>
<view class="footer"> <view class="footer">
<view class="btn" bind:tap="handleSavePoster">保存海报</view> <view class="btn" bind:tap="handleSavePoster">保存海报</view>

59
src/pages/actResult/index.scss

@ -8,8 +8,11 @@ page {
margin-top: 130rpx; margin-top: 130rpx;
padding: 0 30rpx 56rpx; padding: 0 30rpx 56rpx;
position: relative; position: relative;
background-color: #fff; background: linear-gradient(180deg, #f4fbff 0%, #ffffff 31.05%, #ffffff 100%);
border-radius: 24rpx; border-radius: 24rpx;
&.fail-container {
background: linear-gradient(180deg, #fff4f4 0%, #ffffff 41.67%, #ffffff 100%);
}
.status-icon { .status-icon {
position: absolute; position: absolute;
top: -115rpx; top: -115rpx;
@ -30,6 +33,10 @@ page {
font-size: 28rpx; font-size: 28rpx;
color: rgba(71, 85, 105, 1); color: rgba(71, 85, 105, 1);
line-height: 42rpx; line-height: 42rpx;
text-align: center;
&.fail-content {
color: rgba(253, 91, 89, 1);
}
} }
.code-wrap { .code-wrap {
margin: 32rpx auto 0; margin: 32rpx auto 0;
@ -56,6 +63,11 @@ page {
box-shadow: 0rpx 15rpx 30rpx -6rpx rgba(74, 172, 219, 0.4); box-shadow: 0rpx 15rpx 30rpx -6rpx rgba(74, 172, 219, 0.4);
border-radius: 16rpx 16rpx 16rpx 16rpx; border-radius: 16rpx 16rpx 16rpx 16rpx;
} }
.btn1{
padding-top: 45rpx;
color: rgba(74, 184, 253, 1);
text-align: center;
}
} }
.act { .act {
margin-top: 42rpx; margin-top: 42rpx;
@ -88,9 +100,50 @@ page {
left: 0; left: 0;
padding: 8rpx 16rpx; padding: 8rpx 16rpx;
font-size: 22rpx; font-size: 22rpx;
color: rgba(255, 255, 255, 1);
background: #feb54a;
border-radius: 16rpx 0rpx 16rpx 0rpx; border-radius: 16rpx 0rpx 16rpx 0rpx;
display: flex;
align-items: center;
gap: 8rpx;
.icon {
width: 24rpx;
height: 24rpx;
}
&.status1 {
color: rgba(74, 184, 253, 1);
background: rgba(226, 244, 255, 1);
}
&.status2 {
color: rgba(255, 255, 255, 1);
background: rgba(254, 181, 74, 1);
}
&.status3 {
color: rgba(255, 255, 255, 1);
background: rgba(111, 220, 174, 1);
}
&.status4 {
color: rgba(255, 255, 255, 1);
background: rgba(253, 91, 89, 1);
}
&.status5 {
color: rgba(255, 255, 255, 1);
background: rgba(111, 220, 174, 1);
}
&.status6 {
color: rgba(255, 255, 255, 1);
background: rgba(254, 181, 74, 1);
}
&.status7 {
color: rgba(255, 255, 255, 1);
background: rgba(203, 213, 225, 1);
}
&.status8 {
color: rgba(100, 116, 139, 1);
background: rgba(233, 239, 245, 1);
}
&.status9 {
color: rgba(255, 255, 255, 1);
background: rgba(74, 184, 253, 1);
}
} }
.p-img { .p-img {
border-radius: 16rpx; border-radius: 16rpx;

32
src/pages/actResult/index.ts

@ -8,6 +8,8 @@ interface IActivityItem {
endAt: string endAt: string
location: string location: string
status: string status: string
activityStatus: number
activityStatusName: string
regCount: number regCount: number
} }
@ -18,13 +20,23 @@ Page({
recommendList: [] as IActivityItem[], recommendList: [] as IActivityItem[],
isSubscribed: false, isSubscribed: false,
codeUrl: '', codeUrl: '',
// 报名结果状态
resultStatus: 'success' as 'success' | 'fail',
resultMessage: '',
}, },
onLoad(options: { id?: string }) { onLoad(options: { id?: string; status?: string; message?: string }) {
const activityId = options.id ? Number(options.id) : 0 const activityId = options.id ? Number(options.id) : 0
this.setData({ activityId }) const resultStatus = options.status === 'fail' ? 'fail' : 'success'
const resultMessage = options.message || ''
app.waitLogin({ type: 0 }).then(() => { this.setData({
activityId,
resultStatus,
resultMessage,
})
app.waitLogin({ type: 1 }).then(() => {
this.fetchActivityDetail() this.fetchActivityDetail()
this.fetchRecommendList() this.fetchRecommendList()
this.getCode() this.getCode()
@ -82,6 +94,19 @@ Page({
// 返回活动页 // 返回活动页
handleBack() { handleBack() {
if (this.data.resultStatus === 'fail') {
// 失败时返回活动详情页重新报名
wx.redirectTo({
url: `/pages/actDetail/index?id=${this.data.activityId}`,
})
} else {
// 成功时返回活动列表页
wx.switchTab({
url: '/pages/act/index',
})
}
},
handleActList() {
wx.switchTab({ wx.switchTab({
url: '/pages/act/index', url: '/pages/act/index',
}) })
@ -97,4 +122,3 @@ Page({
}) })
export {} export {}

35
src/pages/actResult/index.wxml

@ -1,20 +1,39 @@
<view class="page"> <view class="page">
<view class="container"> <view class="container {{resultStatus !== 'success' && 'fail-container'}}">
<image class="status-icon" src="{{imageUrl}}icon34.png?t={{Timestamp}}"></image> <image
<view class="status">报名成功</view> class="status-icon"
<view class="content">活动将于{{detail.startAt}}开始,请记得准时参加</view> src="{{imageUrl}}{{resultStatus === 'success' ? 'icon34.png' : 'icon90.png'}}?t={{Timestamp}}"
<view class="code-wrap" wx:if="{{!isSubscribed}}" style="background: url('{{imageUrl}}bg2.png?t={{Timestamp}}') no-repeat center/cover"> ></image>
<view class="status">{{resultStatus === 'success' ? '报名成功' : '报名失败'}}</view>
<view class="content" wx:if="{{resultStatus === 'success'}}">活动将于{{detail.startAt}}开始,请记得准时参加</view>
<view class="content fail-content" wx:else>{{resultMessage || '报名失败,请稍后重试'}}</view>
<view
class="code-wrap"
wx:if="{{resultStatus === 'success' && !isSubscribed}}"
style="background: url('{{imageUrl}}bg2.png?t={{Timestamp}}') no-repeat center/cover"
>
<image class="code" src="{{codeUrl}}"></image> <image class="code" src="{{codeUrl}}"></image>
</view> </view>
<view class="btn" bind:tap="handleBack">返回活动页</view> <view wx:if="{{resultStatus === 'success'}}" class="btn" bind:tap="handleBack">返回活动页</view>
<block wx:else>
<view class="btn" bind:tap="handleActList">查看其他活动</view>
<view class="btn1" bind:tap="handleBack">返回详情</view>
</block>
</view> </view>
<view class="act" wx:if="{{recommendList.length > 0}}"> <view class="act" wx:if="{{recommendList.length > 0}}">
<view class="a-title">你可能感兴趣的活动</view> <view class="a-title">你可能感兴趣的活动</view>
<view class="a-list"> <view class="a-list">
<view class="card" wx:for="{{recommendList}}" wx:key="id" data-id="{{item.id}}" bind:tap="handleDetail"> <view class="card" wx:for="{{recommendList}}" wx:key="id" data-id="{{item.id}}" bind:tap="handleDetail">
<view class="photo"> <view class="photo">
<view class="status">{{item.status === 'registering' ? '报名中' : item.status === 'running' ? '进行中' : '已结束'}}</view> <view class="status status{{item.activityStatus}}" wx:if="{{item.activityStatusName}}">
<image class="p-img" src="{{item.mainImages[0] || '{{imageUrl}}bg1.png?t={{Timestamp}}'}}" mode="aspectFill"></image> <image class="icon" src="{{imageUrl}}icon89.png?t={{Timestamp}}"></image>
{{item.activityStatusName}}
</view>
<image
class="p-img"
src="{{item.mainImages[0]}}"
mode="aspectFill"
></image>
<view class="user">{{item.regCount}}人已报名</view> <view class="user">{{item.regCount}}人已报名</view>
</view> </view>
<view class="wrap"> <view class="wrap">

5
src/pages/feedback/index.json

@ -0,0 +1,5 @@
{
"navigationBarTitleText": "我要反馈",
"navigationStyle": "default",
"usingComponents": {}
}

26
src/pages/feedback/index.scss

@ -0,0 +1,26 @@
page {
background-color: rgba(255, 255, 255, 1);
}
.page {
padding: 32rpx 30rpx;
.tex {
padding: 26rpx 32rpx 0;
min-height: 320rpx;
display: block;
width: 100%;
box-sizing: border-box;
background-color: rgba(247, 248, 250, 1);
border-radius: 16rpx;
}
.btn {
margin-top: 48rpx;
font-size: 32rpx;
color: rgba(255, 255, 255, 1);
height: 96rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, #9ddffd 0%, #4ab8fd 100%);
border-radius: 16rpx 16rpx 16rpx 16rpx;
}
}

8
src/pages/feedback/index.ts

@ -0,0 +1,8 @@
const _app = getApp<IAppOption>();
Page({
data: {},
onLoad() {},
});
export {}

9
src/pages/feedback/index.wxml

@ -0,0 +1,9 @@
<view class="page">
<textarea
class="tex"
placeholder-class="place-tex"
placeholder="请描述具体问题,有助于我们更好的服务"
auto-height="{{true}}"
></textarea>
<view class="btn">提交</view>
</view>

5
src/pages/my/index.ts

@ -27,6 +27,11 @@ Page({
url: '/pages/myComment/index', url: '/pages/myComment/index',
}) })
}, },
handleFeedback() {
wx.navigateTo({
url: '/pages/feedback/index',
})
},
}) })
export {} export {}

16
src/pages/my/index.wxml

@ -16,13 +16,13 @@
</view> </view>
<view class="kkd"> <view class="kkd">
<view class="k-item" bind:tap="handleMyAct"> <view class="k-item" bind:tap="handleMyAct">
<view class="wrap" style="background: url('{{imageUrl}}icon58.png?t={{Timestamp}}') no-repeat top 24rpx right 24rpx/84rpx 84rpx"> <view class="wrap" style="background: url('{{imageUrl}}icon58.png?t={{Timestamp}}') no-repeat center/100%">
<view class="title">我的活动</view> <view class="title">我的活动</view>
<view class="content">已参与10个活动</view> <view class="content">已参与10个活动</view>
</view> </view>
</view> </view>
<view class="k-item" bind:tap="handleMyAgent"> <view class="k-item" bind:tap="handleMyAgent">
<view class="wrap" style="background: url('{{imageUrl}}icon59.png?t={{Timestamp}}') no-repeat top 24rpx right 24rpx/84rpx 84rpx"> <view class="wrap" style="background: url('{{imageUrl}}icon59.png?t={{Timestamp}}') no-repeat center/100%">
<view class="title">我的智能体</view> <view class="title">我的智能体</view>
<view class="content">已启用20个智能体</view> <view class="content">已启用20个智能体</view>
</view> </view>
@ -30,18 +30,18 @@
</view> </view>
<view class="list"> <view class="list">
<view class="list-item" bind:tap="handleMyCommet"> <view class="list-item" bind:tap="handleMyCommet">
<image class="icon" src="{{imageUrl}}icon59.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon60.png?t={{Timestamp}}"></image>
<view class="name">我的评论</view> <view class="name">我的评论</view>
<van-icon name="arrow" /> <van-icon name="arrow" />
</view> </view>
<view class="list-item"> <view class="list-item">
<image class="icon" src="{{imageUrl}}icon60.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon61.png?t={{Timestamp}}"></image>
<view class="name">我的收藏</view> <view class="name">个人信息</view>
<van-icon name="arrow" /> <van-icon name="arrow" />
</view> </view>
<view class="list-item"> <view class="list-item" bind:tap="handleFeedback">
<image class="icon" src="{{imageUrl}}icon61.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon60.png?t={{Timestamp}}"></image>
<view class="name">我的评论</view> <view class="name">我要反馈</view>
<van-icon name="arrow" /> <van-icon name="arrow" />
</view> </view>
</view> </view>

4
src/pages/myAct/index.json

@ -1,9 +1,11 @@
{ {
"navigationBarTitleText": "我的活动", "navigationBarTitleText": "我的活动",
"navigationStyle": "default", "navigationStyle": "default",
"enablePullDownRefresh": true,
"usingComponents": { "usingComponents": {
"van-tab": "@vant/weapp/tab/index", "van-tab": "@vant/weapp/tab/index",
"van-tabs": "@vant/weapp/tabs/index", "van-tabs": "@vant/weapp/tabs/index",
"van-icon": "@vant/weapp/icon/index" "van-icon": "@vant/weapp/icon/index",
"pagination": "/components/pagination/index"
} }
} }

45
src/pages/myAct/index.scss

@ -3,6 +3,7 @@ page {
} }
.page { .page {
padding-bottom: calc(60px + env(safe-area-inset-bottom));
.tabs { .tabs {
.van-tabs__line { .van-tabs__line {
width: 42rpx !important; width: 42rpx !important;
@ -13,6 +14,14 @@ page {
} }
.page0 { .page0 {
padding: 43rpx 30rpx; padding: 43rpx 30rpx;
.empty {
padding: 100rpx 0;
text-align: center;
.empty-text {
font-size: 28rpx;
color: rgba(148, 163, 184, 1);
}
}
.card { .card {
margin-bottom: 24rpx; margin-bottom: 24rpx;
padding: 24rpx; padding: 24rpx;
@ -40,19 +49,49 @@ page {
padding: 8rpx 16rpx; padding: 8rpx 16rpx;
font-size: 22rpx; font-size: 22rpx;
border-radius: 16rpx 0rpx 16rpx 0rpx; border-radius: 16rpx 0rpx 16rpx 0rpx;
color: rgba(255, 255, 255, 1); display: flex;
align-items: center;
gap: 8rpx;
.icon {
width: 24rpx;
height: 24rpx;
}
&.status1 { &.status1 {
background: #feb54a; color: rgba(74, 184, 253, 1);
background: rgba(226, 244, 255, 1);
} }
&.status2 { &.status2 {
background: rgba(74, 184, 253, 1); color: rgba(255, 255, 255, 1);
background: rgba(254, 181, 74, 1);
} }
&.status3 { &.status3 {
color: rgba(255, 255, 255, 1);
background: rgba(111, 220, 174, 1); background: rgba(111, 220, 174, 1);
} }
&.status4 { &.status4 {
color: rgba(255, 255, 255, 1);
background: rgba(253, 91, 89, 1);
}
&.status5 {
color: rgba(255, 255, 255, 1);
background: rgba(111, 220, 174, 1);
}
&.status6 {
color: rgba(255, 255, 255, 1);
background: rgba(254, 181, 74, 1);
}
&.status7 {
color: rgba(255, 255, 255, 1);
background: rgba(203, 213, 225, 1); background: rgba(203, 213, 225, 1);
} }
&.status8 {
color: rgba(100, 116, 139, 1);
background: rgba(233, 239, 245, 1);
}
&.status9 {
color: rgba(255, 255, 255, 1);
background: rgba(74, 184, 253, 1);
}
} }
.p-img { .p-img {
border-radius: 16rpx; border-radius: 16rpx;

232
src/pages/myAct/index.ts

@ -1,8 +1,232 @@
const _app = getApp<IAppOption>(); const app = getApp<IAppOption>()
interface IActivityItem {
id: number
name: string
mainImages: string[]
startAt: string
endAt: string
status: string
activityStatus: number
activityStatusName: string
checkinType: string
regCount: number
checkinCount: number
isRegistered: boolean
isChecked: boolean
isReviewed: boolean
location?: string
}
interface IPagination {
page: number
pageSize: number
pages: number
count: number
}
Page({ Page({
data: {}, data: {
onLoad() {}, active: 0,
}); imageUrl: app.globalData.imageUrl,
Timestamp: app.globalData.Timestamp,
// 我发布的
createdList: [] as IActivityItem[],
createdPagination: { page: 1, pageSize: 10, pages: 0, count: 0 } as IPagination,
createdLoading: false,
// 我参与的
registeredList: [] as IActivityItem[],
registeredPagination: { page: 1, pageSize: 10, pages: 0, count: 0 } as IPagination,
registeredLoading: false,
},
onLoad() {
app.waitLogin({ type: 1 }).then(() => {
this.fetchCreatedList()
})
},
// 切换 tab
onChange(e: WechatMiniprogram.CustomEvent) {
const active = e.detail.name || e.detail.index
this.setData({ active })
if (active === 0 && this.data.createdList.length === 0) {
this.fetchCreatedList()
} else if (active === 1 && this.data.registeredList.length === 0) {
this.fetchRegisteredList()
}
},
// 获取我发布的活动
async fetchCreatedList(isRefresh = false) {
if (this.data.createdLoading) return
const { createdPagination } = this.data
const page = isRefresh ? 1 : createdPagination.page
this.setData({ createdLoading: true })
try {
const res = await wx.ajax({
url: '/me/my-activities',
method: 'GET',
data: { type: 'created', page, pageSize: createdPagination.pageSize },
})
if (res) {
const newList = isRefresh ? res.list : [...this.data.createdList, ...res.list]
this.setData({
createdList: newList,
createdPagination: {
page: res.pagination?.page || page,
pageSize: res.pagination?.pageSize || createdPagination.pageSize,
pages: res.pagination?.totalPages || 0,
count: res.pagination?.total || 0,
},
})
}
} catch (err) {
console.error('获取我发布的活动失败:', err)
} finally {
this.setData({ createdLoading: false })
}
},
// 获取我参与的活动
async fetchRegisteredList(isRefresh = false) {
if (this.data.registeredLoading) return
const { registeredPagination } = this.data
const page = isRefresh ? 1 : registeredPagination.page
this.setData({ registeredLoading: true })
try {
const res = await wx.ajax({
url: '/me/my-activities',
method: 'GET',
data: { type: 'registered', page, pageSize: registeredPagination.pageSize },
})
if (res) {
const newList = isRefresh ? res.list : [...this.data.registeredList, ...res.list]
this.setData({
registeredList: newList,
registeredPagination: {
page: res.pagination?.page || page,
pageSize: res.pagination?.pageSize || registeredPagination.pageSize,
pages: res.pagination?.totalPages || 0,
count: res.pagination?.total || 0,
},
})
}
} catch (err) {
console.error('获取我参与的活动失败:', err)
} finally {
this.setData({ registeredLoading: false })
}
},
// 删除活动
async handleDelete(e: WechatMiniprogram.TouchEvent) {
const { id } = e.currentTarget.dataset
wx.showModal({
title: '提示',
content: '确定要删除该活动吗?',
success: async (res) => {
if (res.confirm) {
try {
await wx.ajax({
url: `/activity/delete?id=${id}`,
method: 'POST',
})
wx.showToast({ title: '删除成功', icon: 'success' })
this.fetchCreatedList(true)
} catch (err: any) {
wx.showToast({ title: err?.message || '删除失败', icon: 'error' })
}
}
},
})
},
// 编辑活动
handleEdit(e: WechatMiniprogram.TouchEvent) {
const { id } = e.currentTarget.dataset
wx.navigateTo({ url: `/pages/actEdit/index?id=${id}` })
},
// 查看详情
handleDetail(e: WechatMiniprogram.TouchEvent) {
const { id } = e.currentTarget.dataset
wx.navigateTo({ url: `/pages/actDetail/index?id=${id}` })
},
// 取消活动
async handleCancel(e: WechatMiniprogram.TouchEvent) {
const { id } = e.currentTarget.dataset
wx.showModal({
title: '提示',
content: '确定要取消该活动吗?取消后已报名用户将收到通知。',
success: async (res) => {
if (res.confirm) {
try {
await wx.ajax({
url: `/activity/cancel?id=${id}`,
method: 'POST',
})
wx.showToast({ title: '取消成功', icon: 'success' })
this.fetchCreatedList(true)
} catch (err: any) {
wx.showToast({ title: err?.message || '取消失败', icon: 'error' })
}
}
},
})
},
// 签到二维码
handleQrcode(e: WechatMiniprogram.TouchEvent) {
const { id } = e.currentTarget.dataset
wx.navigateTo({ url: `/pages/actQrcode/index?id=${id}` })
},
// 下拉刷新
onPullDownRefresh() {
const { active } = this.data
if (active === 0) {
this.fetchCreatedList(true)
} else {
this.fetchRegisteredList(true)
}
wx.stopPullDownRefresh()
},
// 上拉加载更多
onReachBottom() {
const { active, createdPagination, registeredPagination } = this.data
if (active === 0) {
if (createdPagination.page < createdPagination.pages) {
this.setData({
createdPagination: { ...createdPagination, page: createdPagination.page + 1 },
})
this.fetchCreatedList()
}
} else {
if (registeredPagination.page < registeredPagination.pages) {
this.setData({
registeredPagination: { ...registeredPagination, page: registeredPagination.page + 1 },
})
this.fetchRegisteredList()
}
}
},
})
export {} export {}

103
src/pages/myAct/index.wxml

@ -9,41 +9,102 @@
> >
<van-tab title="我发布的"> <van-tab title="我发布的">
<view class="page0"> <view class="page0">
<view class="card" wx:for="{{10}}" wx:key="index"> <view class="card" wx:for="{{createdList}}" wx:key="id">
<view class="c-body"> <view class="c-body" bind:tap="handleDetail" data-id="{{item.id}}">
<view class="photo"> <view class="photo">
<view class="status status1">进行中</view> <view class="status status{{item.activityStatus}}" wx:if="{{item.activityStatusName}}">
<view class="status status2">已发布</view> <image class="icon" src="{{imageUrl}}icon89.png?t={{Timestamp}}"></image>
<view class="status status3">报名中</view> {{item.activityStatusName}}
<view class="status status4">草稿</view> </view>
<view class="status status4">已取消</view> <image class="p-img" src="{{item.mainImages[0] || imageUrl + 'bg1.png?t=' + Timestamp}}" mode="aspectFill"></image>
<view class="status status4">已结束</view> <view class="user">{{item.regCount}}人已报名</view>
<image class="p-img" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image>
<view class="user">128人已报名</view>
</view> </view>
<view class="wrap"> <view class="wrap">
<view class="title">深职大第十五届校园歌手大赛</view> <view class="title">{{item.name}}</view>
<view class="date"> <view class="date">
<image class="icon" src="{{imageUrl}}icon3.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon3.png?t={{Timestamp}}"></image>
<view class="content">2026.04.01-2026.05.30</view> <view class="content">{{item.startAt}} - {{item.endAt}}</view>
</view> </view>
<view class="site"> <view class="site" wx:if="{{item.location}}">
<image class="icon" src="{{imageUrl}}icon15.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon15.png?t={{Timestamp}}"></image>
<view class="content">留仙洞校区音乐厅</view> <view class="content">{{item.location}}</view>
</view> </view>
</view> </view>
</view> </view>
<view class="c-footer"> <view class="c-footer">
<view class="stat">86人已报名</view> <view class="stat">{{item.regCount}}人已报名</view>
<view class="btn1">删除</view> <!-- 草稿状态 -->
<!-- <view class="btn1">签到二维码</view> --> <block wx:if="{{item.activityStatus === 0}}">
<!-- <view class="btn1">取消</view> --> <view class="btn1" data-id="{{item.id}}" bind:tap="handleDelete">删除</view>
<!-- <view class="btn1">详情</view> --> <view class="btn2" data-id="{{item.id}}" bind:tap="handleEdit">编辑</view>
<view class="btn2">编辑</view> </block>
<!-- 已发布/报名中状态 -->
<block wx:elif="{{item.activityStatus === 5}}">
<view class="btn1" data-id="{{item.id}}" bind:tap="handleQrcode">签到二维码</view>
<view class="btn1" data-id="{{item.id}}" bind:tap="handleCancel">取消</view>
<view class="btn2" data-id="{{item.id}}" bind:tap="handleEdit">编辑</view>
</block>
<!-- 进行中状态 -->
<block wx:elif="{{item.activityStatus === 6}}">
<view class="btn1" data-id="{{item.id}}" bind:tap="handleQrcode">签到二维码</view>
<view class="btn1" data-id="{{item.id}}" bind:tap="handleDetail">详情</view>
</block>
<!-- 已结束状态 -->
<block wx:elif="{{item.activityStatus === 7}}">
<view class="btn1" data-id="{{item.id}}" bind:tap="handleDelete">删除</view>
<view class="btn1" data-id="{{item.id}}" bind:tap="handleDetail">详情</view>
</block>
</view>
</view>
<pagination pagination="{{createdPagination}}" />
</view>
</van-tab>
<van-tab title="我参与的">
<view class="page0">
<view class="card" wx:for="{{registeredList}}" wx:key="id">
<view class="c-body" bind:tap="handleDetail" data-id="{{item.id}}">
<view class="photo">
<view class="status status{{item.activityStatus}}" wx:if="{{item.activityStatusName}}">
<image class="icon" src="{{imageUrl}}icon89.png?t={{Timestamp}}"></image>
{{item.activityStatusName}}
</view>
<image class="p-img" src="{{item.mainImages[0] || imageUrl + 'bg1.png?t=' + Timestamp}}" mode="aspectFill"></image>
<view class="user">{{item.regCount}}人已报名</view>
</view>
<view class="wrap">
<view class="title">{{item.name}}</view>
<view class="date">
<image class="icon" src="{{imageUrl}}icon3.png?t={{Timestamp}}"></image>
<view class="content">{{item.startAt}} - {{item.endAt}}</view>
</view>
<view class="site" wx:if="{{item.location}}">
<image class="icon" src="{{imageUrl}}icon15.png?t={{Timestamp}}"></image>
<view class="content">{{item.location}}</view>
</view>
</view>
</view>
<view class="c-footer">
<view class="stat">
<text wx:if="{{item.isChecked}}">已签到</text>
<text wx:elif="{{item.isReviewed}}">已评价</text>
<text wx:else>已报名</text>
</view>
<!-- 进行中且未签到 -->
<block wx:if="{{item.activityStatus === 6 && !item.isChecked}}">
<view class="btn2" data-id="{{item.id}}" bind:tap="handleDetail">去签到</view>
</block>
<!-- 已结束且未评价 -->
<block wx:elif="{{item.activityStatus === 7 && !item.isReviewed}}">
<view class="btn2" data-id="{{item.id}}" bind:tap="handleDetail">去评价</view>
</block>
<!-- 其他状态 -->
<block wx:else>
<view class="btn1" data-id="{{item.id}}" bind:tap="handleDetail">详情</view>
</block>
</view> </view>
</view> </view>
<pagination pagination="{{registeredPagination}}" />
</view> </view>
</van-tab> </van-tab>
<van-tab title="我参与的">内容 2</van-tab>
</van-tabs> </van-tabs>
</view> </view>

5
src/pages/myComment/index.wxml

@ -7,7 +7,7 @@
active="{{ active }}" active="{{ active }}"
bind:change="onChange" bind:change="onChange"
> >
<van-tab title="我发布的"> <van-tab title="活动">
<view class="page0"> <view class="page0">
<view class="card"> <view class="card">
<view class="c-header"> <view class="c-header">
@ -52,7 +52,6 @@
</view> </view>
</view> </view>
</van-tab> </van-tab>
<van-tab title="历史使用">内容 2</van-tab> <van-tab title="我的评论">内容 2</van-tab>
<van-tab title="我收藏的">内容 2</van-tab>
</van-tabs> </van-tabs>
</view> </view>

5
src/pages/notice/index.json

@ -1,9 +1,12 @@
{ {
"navigationBarTitleText": "通知", "navigationBarTitleText": "通知",
"navigationStyle": "default", "navigationStyle": "default",
"enablePullDownRefresh": true,
"usingComponents": { "usingComponents": {
"van-tab": "@vant/weapp/tab/index", "van-tab": "@vant/weapp/tab/index",
"van-tabs": "@vant/weapp/tabs/index", "van-tabs": "@vant/weapp/tabs/index",
"van-icon": "@vant/weapp/icon/index" "van-icon": "@vant/weapp/icon/index",
"pagination": "/components/pagination/index",
"mp-html": "mp-html"
} }
} }

7
src/pages/notice/index.scss

@ -11,6 +11,12 @@ page {
--tab-font-size: 32rpx; --tab-font-size: 32rpx;
} }
} }
.page-empty {
padding: 100rpx 0;
text-align: center;
font-size: 28rpx;
color: rgba(148, 163, 184, 1);
}
.page0 { .page0 {
padding: 0 32rpx; padding: 0 32rpx;
.module { .module {
@ -31,6 +37,7 @@ page {
font-weight: bold; font-weight: bold;
} }
.content { .content {
display: block;
margin-top: 24rpx; margin-top: 24rpx;
font-size: 28rpx; font-size: 28rpx;
color: rgba(100, 116, 139, 1); color: rgba(100, 116, 139, 1);

220
src/pages/notice/index.ts

@ -1,14 +1,224 @@
const _app = getApp<IAppOption>() const app = getApp<IAppOption>()
// 公告列表项
interface IAnnouncementItem {
id: number
title: string
content: string
displayPosition: number
priority: number
redDot: boolean
jumpType: number
jumpTarget: string
buttonText: string
startAt: string
endAt: string
createdAt: string
isRead: boolean
}
// 通知列表项
interface INoticeItem {
id: number
taskId: number
title: string
subtitle: string
subType: string
subTypeName: string
coverImage: string
jumpType: number
jumpTarget: string
buttonText: string
isRead: boolean
scheduleAt: string
}
interface IPagination {
page: number
pageSize: number
pages: number
count: number
}
Page({ Page({
data: { data: {
active: 2, active: 0,
imageUrl: app.globalData.imageUrl,
Timestamp: app.globalData.Timestamp,
// 公告
announcementList: [] as IAnnouncementItem[],
announcementPagination: { page: 1, pageSize: 20, pages: 0, count: 0 } as IPagination,
announcementLoading: false,
// 重要通知
importantList: [] as INoticeItem[],
importantPagination: { page: 1, pageSize: 20, pages: 0, count: 0 } as IPagination,
importantLoading: false,
// 活动通知
activityList: [] as INoticeItem[],
activityPagination: { page: 1, pageSize: 20, pages: 0, count: 0 } as IPagination,
activityLoading: false,
},
onLoad() {
app.waitLogin({ type: 1 }).then(() => {
this.fetchAnnouncementList()
})
},
onShow() {
// 刷新 tabbar 红点
const tabBar = this.getTabBar()
if (tabBar && typeof tabBar.fetchUnreadCount === 'function') {
tabBar.fetchUnreadCount()
}
},
// 切换 tab
onChange(e: WechatMiniprogram.CustomEvent) {
const active = e.detail.index ?? 0
this.setData({ active })
if (active === 0 && this.data.announcementList.length === 0) {
this.fetchAnnouncementList()
} else if (active === 1 && this.data.importantList.length === 0) {
this.fetchNoticeList('important')
} else if (active === 2 && this.data.activityList.length === 0) {
this.fetchNoticeList('activity')
}
},
// 获取公告列表(专用接口)
async fetchAnnouncementList(isRefresh = false) {
if (this.data.announcementLoading) return
const { announcementPagination } = this.data
const page = isRefresh ? 1 : announcementPagination.page
this.setData({ announcementLoading: true })
try {
const res = await wx.ajax({
url: '/notification/announcement-list',
method: 'GET',
data: { page, pageSize: announcementPagination.pageSize },
})
if (res) {
const newList = isRefresh ? res.list : [...this.data.announcementList, ...res.list]
this.setData({
announcementList: newList,
announcementPagination: {
page: res.pagination?.page || page,
pageSize: res.pagination?.pageSize || announcementPagination.pageSize,
pages: res.pagination?.totalPages || 0,
count: res.pagination?.total || 0,
},
})
}
} catch (err) {
console.error('获取公告列表失败:', err)
} finally {
this.setData({ announcementLoading: false })
}
}, },
onLoad() {},
handleDetail() { // 获取通知列表(重要通知/活动通知)
async fetchNoticeList(type: 'important' | 'activity', isRefresh = false) {
const loadingKey = `${type}Loading` as const
const listKey = `${type}List` as const
const paginationKey = `${type}Pagination` as const
if (this.data[loadingKey]) return
const pagination = this.data[paginationKey]
const page = isRefresh ? 1 : pagination.page
this.setData({ [loadingKey]: true } as any)
try {
const res = await wx.ajax({
url: '/notification/list',
method: 'GET',
data: { type, page, pageSize: pagination.pageSize },
})
if (res) {
const newList = isRefresh ? res.list : [...this.data[listKey], ...res.list]
this.setData({
[listKey]: newList,
[paginationKey]: {
page: res.pagination?.page || page,
pageSize: res.pagination?.pageSize || pagination.pageSize,
pages: res.pagination?.totalPages || 0,
count: res.pagination?.total || 0,
},
} as any)
}
} catch (err) {
console.error('获取通知列表失败:', err)
} finally {
this.setData({ [loadingKey]: false } as any)
}
},
// 查看公告详情
handleAnnouncementDetail(e: WechatMiniprogram.TouchEvent) {
const { id } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/noticeDetail/index?id=${id}&type=announcement`,
})
},
// 查看通知详情
handleNoticeDetail(e: WechatMiniprogram.TouchEvent) {
const { taskId } = e.currentTarget.dataset
wx.navigateTo({ wx.navigateTo({
url: '/pages/noticeDetail/index', url: `/pages/noticeDetail/index?taskId=${taskId}&type=notice`,
})
},
// 下拉刷新
onPullDownRefresh() {
const { active } = this.data
if (active === 0) {
this.fetchAnnouncementList(true)
} else if (active === 1) {
this.fetchNoticeList('important', true)
} else if (active === 2) {
this.fetchNoticeList('activity', true)
}
wx.stopPullDownRefresh()
},
// 上拉加载更多
onReachBottom() {
const { active, announcementPagination, importantPagination, activityPagination } = this.data
if (active === 0) {
if (announcementPagination.page < announcementPagination.pages) {
this.setData({
announcementPagination: { ...announcementPagination, page: announcementPagination.page + 1 },
})
this.fetchAnnouncementList()
}
} else if (active === 1) {
if (importantPagination.page < importantPagination.pages) {
this.setData({
importantPagination: { ...importantPagination, page: importantPagination.page + 1 },
})
this.fetchNoticeList('important')
}
} else if (active === 2) {
if (activityPagination.page < activityPagination.pages) {
this.setData({
activityPagination: { ...activityPagination, page: activityPagination.page + 1 },
}) })
this.fetchNoticeList('activity')
}
}
}, },
}) })

79
src/pages/notice/index.wxml

@ -7,15 +7,47 @@
active="{{ active }}" active="{{ active }}"
bind:change="onChange" bind:change="onChange"
> >
<!-- 公告 -->
<van-tab title="公告"> <van-tab title="公告">
<view class="page0"> <view class="page0">
<view class="module" wx:for="{{4}}" wx:key="index" bind:tap="handleDetail"> <view
<view class="date">03-01 13:01</view> class="module"
wx:for="{{announcementList}}"
wx:key="id"
data-id="{{item.id}}"
bind:tap="handleAnnouncementDetail"
>
<view class="date">{{item.createdAt}}</view>
<view class="card"> <view class="card">
<view class="title">2025-2026学年暑期社会实践报名启动</view> <view class="title">{{item.title}}</view>
<view class="content"> <mp-html class="content" content="{{item.content}}"></mp-html>
本次活动聚焦社会服务、乡村振兴、红色教育、志愿服务等方向,引导青年以实干践行使命,用行... <view class="c-footer">
<view class="tag">公告</view>
<view class="detail">
查看详情
<van-icon name="arrow" />
</view>
</view>
</view>
</view>
<pagination pagination="{{announcementPagination}}"></pagination>
</view> </view>
</van-tab>
<!-- 重要通知 -->
<van-tab title="重要通知">
<view class="page0">
<view
class="module"
wx:for="{{importantList}}"
wx:key="id"
data-task-id="{{item.taskId}}"
bind:tap="handleNoticeDetail"
>
<view class="date">{{item.scheduleAt}}</view>
<view class="card">
<view class="title">{{item.title}}</view>
<mp-html class="content" content="{{item.content}}"></mp-html>
<view class="c-footer"> <view class="c-footer">
<view class="tag">重要通知</view> <view class="tag">重要通知</view>
<view class="detail"> <view class="detail">
@ -25,26 +57,28 @@
</view> </view>
</view> </view>
</view> </view>
<pagination pagination="{{importantPagination}}"></pagination>
</view> </view>
</van-tab> </van-tab>
<van-tab title="重要通知">内容 2</van-tab>
<!-- 活动通知 -->
<van-tab title="活动通知"> <van-tab title="活动通知">
<view class="page2"> <view class="page2">
<view class="module" wx:for="{{4}}" wx:key="index"> <view
<view class="date">03-01 13:01</view> class="module"
wx:for="{{activityList}}"
wx:key="id"
data-task-id="{{item.taskId}}"
bind:tap="handleNoticeDetail"
>
<view class="date">{{item.scheduleAt}}</view>
<view class="card"> <view class="card">
<view class="title">2025-2026学年暑期社会实践报名启动</view> <view class="title">{{item.title}}</view>
<view class="content"> <view class="content">{{item.subtitle}}</view>
本次活动聚焦社会服务、乡村振兴、红色教育、志愿服务等方向,引导青年以实干践行使命,用行...
</view>
<!-- <view class="reason">驳回原因:</view> -->
<view class="c-footer"> <view class="c-footer">
<!-- tag1 活动审核 --> <view class="tag {{item.subType === 'approved' ? 'tag1' : item.subType === 'rejected' ? 'tag2' : item.subType === 'registered' ? 'tag3' : item.subType === 'checked_in' ? 'tag4' : item.subType === 'review_approved' ? 'tag5' : item.subType === 'review_rejected' ? 'tag2' : 'tag3'}}">
<!-- tag2 活动驳回 --> {{item.subTypeName}}
<!-- tag3 报名通知 --> </view>
<!-- tag4 签到通知 -->
<!-- tag5 评论审核 -->
<view class="tag tag1">重要通知</view>
<view class="detail"> <view class="detail">
查看详情 查看详情
<van-icon name="arrow" /> <van-icon name="arrow" />
@ -52,8 +86,13 @@
</view> </view>
</view> </view>
</view> </view>
<pagination pagination="{{activityPagination}}"></pagination>
</view> </view>
</van-tab> </van-tab>
<van-tab title="智能体通知">内容 2</van-tab>
<!-- 智能体通知 -->
<van-tab title="智能体通知">
<view class="page-empty">暂未开放</view>
</van-tab>
</van-tabs> </van-tabs>
</view> </view>

119
src/pages/noticeDetail/index.ts

@ -1,8 +1,119 @@
const _app = getApp<IAppOption>(); const app = getApp<IAppOption>()
// 分类名称映射
const categoryNameMap: Record<string, string> = {
announcement: '公告',
important: '重要通知',
activity: '活动通知',
agent: '智能体通知',
}
Page({ Page({
data: {}, data: {
onLoad() {}, imageUrl: app.globalData.imageUrl,
}); Timestamp: app.globalData.Timestamp,
type: '', // announcement | notice
id: 0,
taskId: 0,
detail: null as any,
categoryName: '',
},
onLoad(options: { id?: string, taskId?: string, type?: string }) {
const type = options.type || 'notice'
const id = Number(options.id || 0)
const taskId = Number(options.taskId || 0)
if (type === 'announcement' && !id) {
wx.showToast({ title: '参数错误', icon: 'error' })
setTimeout(() => wx.navigateBack(), 1500)
return
}
if (type === 'notice' && !taskId) {
wx.showToast({ title: '参数错误', icon: 'error' })
setTimeout(() => wx.navigateBack(), 1500)
return
}
this.setData({ type, id, taskId })
app.waitLogin({ type: 1 }).then(() => {
if (type === 'announcement') {
this.fetchAnnouncementDetail()
} else {
this.fetchNoticeDetail()
}
})
},
// 获取公告详情(专用接口)
async fetchAnnouncementDetail() {
try {
const res = await wx.ajax({
url: '/notification/announcement-detail',
method: 'GET',
data: { id: this.data.id },
})
if (res) {
this.setData({
detail: res,
categoryName: '公告',
})
}
} catch (err) {
console.error('获取公告详情失败:', err)
wx.showToast({ title: '获取详情失败', icon: 'error' })
}
},
// 获取通知详情
async fetchNoticeDetail() {
try {
const res = await wx.ajax({
url: '/notification/detail',
method: 'GET',
data: { taskId: this.data.taskId },
})
if (res) {
this.setData({
detail: res,
categoryName: categoryNameMap[res.category] || '通知',
})
}
} catch (err) {
console.error('获取通知详情失败:', err)
wx.showToast({ title: '获取详情失败', icon: 'error' })
}
},
// 跳转
handleJump() {
const { detail } = this.data
if (!detail || detail.jumpType === 0) return
if (detail.jumpType === 1) {
// 不跳转
} else if (detail.jumpType === 2) {
// 跳转活动
wx.navigateTo({ url: `/pages/actDetail/index?id=${detail.jumpTarget}` })
} else if (detail.jumpType === 3) {
// 跳转小程序页面
wx.navigateTo({ url: detail.jumpTarget })
} else if (detail.jumpType === 4) {
// 跳转H5链接
wx.navigateTo({
url: `/pages/webview/index?url=${encodeURIComponent(detail.jumpTarget)}`,
})
}
},
handleBack() {
wx.navigateBack()
},
})
export {} export {}

29
src/pages/noticeDetail/index.wxml

@ -1,38 +1,23 @@
<view class="page"> <view class="page" wx:if="{{detail}}">
<view class="info"> <view class="info">
<view class="row"> <view class="row">
<view class="type"> <view class="type">
<image class="icon" src="{{imageUrl}}icon23.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon23.png?t={{Timestamp}}"></image>
<view class="content">通知</view> <view class="content">{{categoryName}}</view>
</view> </view>
<view class="status status1">已发布</view>
</view>
<view class="title">关于开展校园歌手大赛的通知</view>
<view class="tag-wrap">
<view class="tag tag1">校园活动</view>
</view> </view>
<view class="title">{{detail.title}}</view>
<view class="stat"> <view class="stat">
<view class="date"> <view class="date">
<image class="icon" src="{{imageUrl}}icon24.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon24.png?t={{Timestamp}}"></image>
<view class="content">2026-05-20 14:30:00</view> <view class="content">{{type === 'announcement' ? detail.createdAt : detail.scheduleAt}}</view>
</view>
<view class="user">
<image class="icon" src="{{imageUrl}}icon25.png?t={{Timestamp}}"></image>
<view class="content">浏览人数:128</view>
</view> </view>
</view> </view>
</view> </view>
<view class="mp-html"> <view class="mp-html">
<mp-html content="{{content}}"></mp-html> <mp-html content="{{type === 'announcement' ? detail.content : detail.richContent}}"></mp-html>
</view>
<view class="tip">
<image class="icon" src="{{imageUrl}}icon26.png?t={{Timestamp}}"></image>
<view class="wrap">
<view class="w-title">温馨提示</view>
<view class="w-content">未尽事宜,请联系校团委文艺部,联系电话:0755-12345678。</view>
</view>
</view> </view>
<view class="footer"> <view class="footer" wx:if="{{detail.jumpType !== 1 && detail.buttonText}}">
<view class="btn">查看活动详情</view> <view class="btn" bind:tap="handleJump">{{detail.buttonText}}</view>
</view> </view>
</view> </view>

4
typings/index.d.ts vendored

@ -33,11 +33,13 @@ interface IAppOption {
openidSession?: string // 临时凭证(用于绑定) openidSession?: string // 临时凭证(用于绑定)
userInfo?: UserInfo | {} userInfo?: UserInfo | {}
loginRedirectUrl?: string // 登录后返回的页面路径 loginRedirectUrl?: string // 登录后返回的页面路径
needBind: boolean
loginPromise: Promise<void>
[propName: string]: any [propName: string]: any
} }
getUserInfo: (type?: 0 | 1 | 2) => Promise<any> getUserInfo: (type?: 0 | 1 | 2) => Promise<any>
startLogin: (callback?: () => void) => void startLogin: (callback?: () => void) => Promise<void>
waitLogin: (params?: { type?: 0 | 1 }) => Promise<void> waitLogin: (params?: { type?: 0 | 1 }) => Promise<void>
[propName: string]: any [propName: string]: any
} }

Loading…
Cancel
Save