Browse Source

feat: 新增活动分享海报页,优化活动列表与详情功能

1. 新增actPoster页面用于生成和分享活动海报
2. 修复活动分类筛选参数格式问题,改为用逗号连接的字符串
3. 优化活动列表和详情页的活动状态标签样式,新增状态分类配色
4. 完善活动报名、签到的逻辑判断条件
5. 优化首页布局,替换静态数据为接口请求的动态数据
6. 优化活动详情页的评论点赞逻辑和分享跳转逻辑
7. 新增活动报名成功页的用户订阅状态判断和二维码展示
master
kola-web 1 week ago
parent
commit
5a4359dd27
  1. 18
      project.private.config.json
  2. 1
      src/app.json
  3. BIN
      src/images/bg7.png
  4. BIN
      src/images/icon89.png
  5. 55
      src/pages/act/index.scss
  6. 2
      src/pages/act/index.ts
  7. 19
      src/pages/act/index.wxml
  8. 7
      src/pages/actDetail/index.scss
  9. 35
      src/pages/actDetail/index.ts
  10. 26
      src/pages/actDetail/index.wxml
  11. 6
      src/pages/actPoster/index.json
  12. 91
      src/pages/actPoster/index.scss
  13. 220
      src/pages/actPoster/index.ts
  14. 31
      src/pages/actPoster/index.wxml
  15. 17
      src/pages/actResult/index.ts
  16. 4
      src/pages/actResult/index.wxml
  17. 3
      src/pages/index/index.json
  18. 67
      src/pages/index/index.scss
  19. 227
      src/pages/index/index.ts
  20. 103
      src/pages/index/index.wxml

18
project.private.config.json

@ -4,11 +4,25 @@ @@ -4,11 +4,25 @@
"miniprogram": {
"list": [
{
"name": "活动报名成功页",
"pathName": "pages/actResult/index",
"query": "id=59",
"scene": null,
"launchMode": "default"
},
{
"name": "活动分享页",
"pathName": "pages/actPoster/index",
"query": "id=45",
"launchMode": "default",
"scene": null
},
{
"name": "课表",
"pathName": "pages/schedule/index",
"query": "",
"scene": null,
"launchMode": "default"
"launchMode": "default",
"scene": null
},
{
"name": "登录",

1
src/app.json

@ -9,6 +9,7 @@ @@ -9,6 +9,7 @@
"pages/notice/index",
"pages/actDetail/index",
"pages/actResult/index",
"pages/actPoster/index",
"pages/actAdd/index",
"pages/actAddResult/index",
"pages/noticeDetail/index",

BIN
src/images/bg7.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
src/images/icon89.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

55
src/pages/act/index.scss

@ -163,9 +163,60 @@ page { @@ -163,9 +163,60 @@ page {
left: 0;
padding: 8rpx 16rpx;
font-size: 22rpx;
color: rgba(255, 255, 255, 1);
background: #feb54a;
border-radius: 16rpx 0rpx 16rpx 0rpx;
display: flex;
align-items: center;
gap: 8rpx;
.icon {
display: none;
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);
.icon {
display: block;
}
}
&.status3 {
color: rgba(255, 255, 255, 1);
background: rgba(111, 220, 174, 1);
.icon {
display: block;
}
}
&.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 {
.icon {
display: block;
}
color: rgba(255, 255, 255, 1);
background: rgba(74, 184, 253, 1);
}
}
.p-img {
border-radius: 16rpx;

2
src/pages/act/index.ts

@ -242,7 +242,7 @@ Page({ @@ -242,7 +242,7 @@ Page({
// 分类筛选:选中的分类 ID 数组(非空时传值)
if (selectedCategoryIds.length > 0) {
params.categoryIds = selectedCategoryIds
params.categoryIds = selectedCategoryIds.join(',')
}
const res = await wx.ajax({

19
src/pages/act/index.wxml

@ -47,16 +47,17 @@ @@ -47,16 +47,17 @@
<!-- 活动列表 -->
<view class="list">
<view
class="card"
wx:for="{{activityList}}"
wx:key="id"
data-id="{{item.id}}"
bind:tap="handleDetail"
>
<view class="card" wx:for="{{activityList}}" wx:key="id" data-id="{{item.id}}" bind:tap="handleDetail">
<view class="photo">
<view class="status" wx:if="{{item.activityStatusName}}">{{item.activityStatusName}}</view>
<image class="p-img" src="{{item.mainImages[0] || '{{imageUrl}}bg1.png?t={{Timestamp}}'}}" mode="aspectFill"></image>
<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" wx:if="{{item.regCount}}">{{item.regCount}}人已报名</view>
</view>
<view class="wrap">

7
src/pages/actDetail/index.scss

@ -309,8 +309,8 @@ page { @@ -309,8 +309,8 @@ page {
font-size: 28rpx;
color: rgba(100, 116, 139, 1);
}
&.active{
.s-content{
&.active {
.s-content {
color: rgba(254, 181, 74, 1);
}
}
@ -359,6 +359,9 @@ page { @@ -359,6 +359,9 @@ page {
display: flex;
align-items: center;
gap: 26rpx;
&:empty {
display: none;
}
.com,
.btn {
flex: 1;

35
src/pages/actDetail/index.ts

@ -20,6 +20,8 @@ interface IActivityDetail { @@ -20,6 +20,8 @@ interface IActivityDetail {
location: string
organizer: string
status: string
activityStatus: number
activityStatusName: string
checkinType: number
checkinStartAt: string
checkinEndAt: string
@ -218,9 +220,11 @@ Page({ @@ -218,9 +220,11 @@ Page({
try {
wx.showLoading({ title: '报名中...' })
const res = await wx.ajax({
url: `/activity/register?id=${this.data.activityId}`,
url: `/activity/register`,
method: 'POST',
data: {},
data: {
activityId: this.data.activityId,
},
})
wx.hideLoading()
@ -298,9 +302,12 @@ Page({ @@ -298,9 +302,12 @@ Page({
// 上报分享
try {
await wx.ajax({
url: `/activity/share?id=${this.data.activityId}`,
url: `/activity/share`,
method: 'POST',
data: { channel: 'friend' },
data: { channel: 'friend', id: this.data.activityId },
})
wx.navigateTo({
url: `/pages/actPoster/index?id=${this.data.activityId}`,
})
} catch (err) {
console.error('上报分享失败:', err)
@ -409,7 +416,7 @@ Page({ @@ -409,7 +416,7 @@ Page({
// 点赞评价
async handleLikeReview(e: WechatMiniprogram.TouchEvent) {
const { reviewId, islike } = e.currentTarget.dataset
const { id, islike } = e.currentTarget.dataset
if (islike) return
const { reviewList } = this.data
@ -426,18 +433,18 @@ Page({ @@ -426,18 +433,18 @@ Page({
url: `/activity/toggle-review-like`,
method: 'POST',
data: {
reviewId,
reviewId: id,
},
})
if (res) {
// 更新评价列表中的点赞状态
const updatedList = reviewList.map((item) => {
if (item.id === reviewId) {
if (item.id === id) {
return {
...item,
isLiked: res.isLiked,
likeCount: res.likeCount,
likeCount: item.likeCount + 1,
}
}
return item
@ -482,7 +489,17 @@ Page({ @@ -482,7 +489,17 @@ Page({
},
// 获取活动状态文本
getStatusText(status: string): string {
getStatusText(status: string | number): string {
// 如果是数字类型,处理 activityStatus
if (typeof status === 'number') {
const statusMap: Record<number, string> = {
5: '已发布',
6: '进行中',
7: '已结束',
}
return statusMap[status] || '未知状态'
}
// 如果是字符串类型,处理 status
const statusMap: Record<string, string> = {
draft: '草稿',
pending: '待审核',

26
src/pages/actDetail/index.wxml

@ -107,7 +107,12 @@ @@ -107,7 +107,12 @@
<view class="c-footer">
<view class="date">{{item.createdAt}}</view>
<view class="stat">
<view class="s-item {{item.isLiked && 'active'}}" data-id="{{item.id}}" data-islike="{{item.isLiked}}" bind:tap="handleLikeReview">
<view
class="s-item {{item.isLiked && 'active'}}"
data-id="{{item.id}}"
data-islike="{{item.isLiked}}"
bind:tap="handleLikeReview"
>
<image class="icon" src="{{imageUrl}}{{item.isLiked ? 'icon53' : 'icon31'}}.png?t={{Timestamp}}"></image>
<view class="s-content">{{item.likeCount}}</view>
</view>
@ -146,21 +151,30 @@ @@ -146,21 +151,30 @@
</van-count-down>
<view class="options">
<view wx:if="{{!detail.isReviewed}}" class="com" bind:tap="handleOpenComment">去评论</view>
<view class="btn" wx:if="{{!detail.isRegistered}}" bind:tap="handleRegister">我要报名</view>
<!-- 我要报名:需要报名的活动,活动已发布(报名中),未报名的用户 -->
<view
class="btn"
wx:if="{{detail.regType === 1 && detail.activityStatus === 5 && !detail.isRegistered}}"
bind:tap="handleRegister"
>
我要报名
</view>
<!-- 签到:已报名用户,活动进行中,未签到,活动需要签到 -->
<view
class="btn disabled"
wx:elif="{{detail.isRegistered && !detail.isCheckedIn && detail.status === 'running'}}"
class="btn"
wx:elif="{{detail.isRegistered && detail.activityStatus === 6 && !detail.isCheckedIn && detail.checkinType !== 3}}"
bind:tap="handleCheckin"
>
签到
</view>
<view class="btn disabled" wx:else>已报名</view>
<!-- 已报名:活动已发布(报名中),已报名的用户 -->
<view class="btn disabled" wx:elif="{{detail.activityStatus === 5 && detail.isRegistered}}">已报名</view>
</view>
</view>
</view>
<!-- 分享按钮 -->
<view class="slidebar-share">
<view class="slidebar-share" bind:tap="handleShare">
<image class="icon" src="{{imageUrl}}icon42.png?t={{Timestamp}}"></image>
</view>

6
src/pages/actPoster/index.json

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"van-icon": "@vant/weapp/icon/index"
}
}

91
src/pages/actPoster/index.scss

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
.page-back {
font-size: 32rpx;
color: #fff;
}
.page {
height: 1627rpx;
box-sizing: border-box;
padding: 396rpx 70rpx 0;
.poster {
display: block;
height: 458rpx;
border-radius: 32rpx;
}
.title {
margin-top: 32rpx;
font-size: 40rpx;
line-height: 56rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
height: 168rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.user {
margin-top: 74rpx;
display: flex;
align-items: center;
gap: 20rpx;
.avatar {
flex-shrink: 0;
width: 92rpx;
height: 92rpx;
border-radius: 50%;
}
.wrap {
flex: 1;
.name {
font-size: 28rpx;
color: rgba(74, 184, 253, 1);
line-height: 38rpx;
}
.date {
margin-top: 8rpx;
font-size: 24rpx;
color: rgba(148, 163, 184, 1);
}
}
.code {
margin-right: 12rpx;
flex-shrink: 0;
width: 104rpx;
height: 104rpx;
}
}
}
.footer {
position: fixed;
top: 1330rpx;
left: 0;
width: 100%;
box-sizing: border-box;
padding: 0 40rpx;
display: flex;
align-items: center;
gap: 30rpx;
.btn {
flex: 1;
height: 96rpx;
background: #ffffff;
border-radius: 16rpx 16rpx 16rpx 16rpx;
font-size: 32rpx;
color: rgba(74, 184, 253, 1);
display: flex;
align-items: center;
justify-content: center;
}
.share-btn {
// 重置button默认样式
padding: 0;
margin: 0;
border: none;
line-height: normal;
background: #ffffff;
&::after {
border: none;
}
}
}

220
src/pages/actPoster/index.ts

@ -0,0 +1,220 @@ @@ -0,0 +1,220 @@
Page({
data: {
posterImage: '', // 生成的海报图片路径
// Canvas尺寸 - 根据页面实际尺寸计算(rpx转px,750rpx = 375px)
canvasWidth: 375,
canvasHeight: 814, // 1627rpx ≈ 813.5px
},
onLoad(_options: { id?: string }) {
// 可以根据活动ID获取活动信息
},
onReady() {
// 页面渲染完成
},
// 绘制海报
async drawPoster(): Promise<string> {
try {
wx.showLoading({ title: '生成中...' })
// 等待页面渲染完成
await new Promise(resolve => setTimeout(resolve, 300))
// 获取canvas实例
const canvasNode = await new Promise<any>((resolve, reject) => {
const query = this.createSelectorQuery()
query.select('#posterCanvas')
.fields({ node: true, size: true })
.exec((res) => {
if (res && res[0] && res[0].node) {
resolve(res[0])
} else {
console.error('Canvas查询失败,结果:', res)
reject(new Error('Canvas未找到或未渲染'))
}
})
})
const canvas = canvasNode.node
const ctx = canvas.getContext('2d')
// 设置canvas尺寸
const dpr = wx.getSystemInfoSync().pixelRatio
canvas.width = this.data.canvasWidth * dpr
canvas.height = this.data.canvasHeight * dpr
ctx.scale(dpr, dpr)
// rpx转px计算(750rpx = 375px)
const rpx2px = (rpx: number) => rpx * 0.5
// 绘制背景 - 覆盖整个Canvas
const bgImage = canvas.createImage()
bgImage.src = `${this.data.imageUrl}bg7.png?t=${this.data.Timestamp}`
await new Promise((resolve, reject) => {
bgImage.onload = resolve
bgImage.onerror = reject
})
ctx.drawImage(bgImage, 0, 0, this.data.canvasWidth, this.data.canvasHeight)
// 绘制海报图片
// 页面样式:padding: 396rpx 70rpx 0; poster高度458rpx
const posterTop = rpx2px(396) // 198px
const posterLeft = rpx2px(70) // 35px
const posterWidth = this.data.canvasWidth - posterLeft * 2 // 305px
const posterHeight = rpx2px(458) // 229px
const posterImage = canvas.createImage()
posterImage.src = `${this.data.imageUrl}bg1.png?t=${this.data.Timestamp}`
await new Promise((resolve, reject) => {
posterImage.onload = resolve
posterImage.onerror = reject
})
ctx.drawImage(posterImage, posterLeft, posterTop, posterWidth, posterHeight)
// 绘制标题
// 页面样式: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()
// 将canvas转化为图片
const tempFilePath = await wx.canvasToTempFilePath({
canvas,
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) {
wx.hideLoading()
console.error('绘制海报失败', 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() {
try {
const posterPath = await this.drawPoster()
// 保存图片到相册
await wx.saveImageToPhotosAlbum({ filePath: posterPath })
wx.showToast({ title: '保存成功', icon: 'success' })
} catch (error: any) {
console.error('保存海报失败', error)
if (error.errMsg && error.errMsg.includes('auth deny')) {
wx.showModal({
title: '提示',
content: '需要授权保存图片到相册',
success: (res) => {
if (res.confirm) {
wx.openSetting()
}
},
})
} else {
wx.showToast({ title: '保存失败', icon: 'error' })
}
}
},
// 分享给好友
onShareAppMessage() {
return {
title: '深职大第十五届校园歌手大赛',
path: '/pages/actDetail/index?id=123',
imageUrl: this.data.posterImage || '',
}
},
handleBack() {
wx.navigateBack()
},
})
export {}

31
src/pages/actPoster/index.wxml

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
<navbar fixed customStyle="background:{{background}};">
<van-icon class="page-back" name="arrow-left" slot="left" bind:tap="handleBack" />
</navbar>
<!-- 海报展示区域 -->
<view class="poster-container" id="posterContainer">
<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>
<view class="title">深职大第十五届校园歌手大赛深职大第十五届校园歌手大赛深职大第十五届校园歌手大赛</view>
<view class="user">
<image class="avatar" mode="aspectFill" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image>
<view class="wrap">
<view class="name">亚南邀请您参与活动</view>
<view class="date">2023年7月15日 18:30</view>
</view>
<image class="code" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image>
</view>
</view>
</view>
<!-- 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="btn" bind:tap="handleSavePoster">保存海报</view>
<button class="btn share-btn" open-type="share">分享给好友</button>
</view>

17
src/pages/actResult/index.ts

@ -16,6 +16,8 @@ Page({ @@ -16,6 +16,8 @@ Page({
activityId: 0,
detail: null as any,
recommendList: [] as IActivityItem[],
isSubscribed: false,
codeUrl: '',
},
onLoad(options: { id?: string }) {
@ -25,6 +27,20 @@ Page({ @@ -25,6 +27,20 @@ Page({
app.waitLogin({ type: 0 }).then(() => {
this.fetchActivityDetail()
this.fetchRecommendList()
this.getCode()
})
},
getCode() {
wx.ajax({
method: 'GET',
url: '/me/profile',
data: {},
}).then((res) => {
this.setData({
isSubscribed: res.wechatSubscribe.isSubscribed,
codeUrl: res.wechatSubscribe.qrCodeUrl,
})
})
},
@ -81,3 +97,4 @@ Page({ @@ -81,3 +97,4 @@ Page({
})
export {}

4
src/pages/actResult/index.wxml

@ -3,8 +3,8 @@ @@ -3,8 +3,8 @@
<image class="status-icon" src="{{imageUrl}}icon34.png?t={{Timestamp}}"></image>
<view class="status">报名成功</view>
<view class="content">活动将于{{detail.startAt}}开始,请记得准时参加</view>
<view class="code-wrap" style="background: url('{{imageUrl}}bg2.png?t={{Timestamp}}') no-repeat center/cover">
<image class="code" src="{{detail.mainImages[0] || '{{imageUrl}}bg1.png?t={{Timestamp}}'}}"></image>
<view class="code-wrap" wx:if="{{!isSubscribed}}" style="background: url('{{imageUrl}}bg2.png?t={{Timestamp}}') no-repeat center/cover">
<image class="code" src="{{codeUrl}}"></image>
</view>
<view class="btn" bind:tap="handleBack">返回活动页</view>
</view>

3
src/pages/index/index.json

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
"van-icon": "@vant/weapp/icon/index",
"van-notice-bar": "@vant/weapp/notice-bar/index",
"van-tab": "@vant/weapp/tab/index",
"van-tabs": "@vant/weapp/tabs/index"
"van-tabs": "@vant/weapp/tabs/index",
"pagination": "/components/pagination/index"
}
}

67
src/pages/index/index.scss

@ -43,6 +43,18 @@ page { @@ -43,6 +43,18 @@ page {
--notice-bar-padding: 0;
--notice-bar-line-height: 40rpx;
--notice-bar-height: 40rpx;
.notice-swiper {
height: 40rpx;
line-height: 40rpx;
}
.notice-text {
font-size: 28rpx;
color: rgba(71, 85, 105, 1);
line-height: 40rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.more {
color: rgba(148, 163, 184, 1);
@ -202,9 +214,60 @@ page { @@ -202,9 +214,60 @@ page {
left: 0;
padding: 8rpx 16rpx;
font-size: 22rpx;
color: rgba(255, 255, 255, 1);
background: #feb54a;
border-radius: 16rpx 0rpx 16rpx 0rpx;
display: flex;
align-items: center;
gap: 8rpx;
.icon {
display: none;
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);
.icon {
display: block;
}
}
&.status3 {
color: rgba(255, 255, 255, 1);
background: rgba(111, 220, 174, 1);
.icon {
display: block;
}
}
&.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 {
.icon {
display: block;
}
color: rgba(255, 255, 255, 1);
background: rgba(74, 184, 253, 1);
}
}
.p-img {
border-radius: 16rpx;

227
src/pages/index/index.ts

@ -4,60 +4,189 @@ Page({ @@ -4,60 +4,189 @@ Page({
data: {
statusBarHeight: 44,
activeTab: 0,
navItems: [
{ type: 'schedule', label: '智能课表', icon: '/images/icon_schedule.png' },
{ type: 'bus', label: '校园巴士', icon: '/images/icon_bus.png' },
{ type: 'race', label: '活动报名', icon: '/images/icon_race.png' },
{ type: 'ai', label: 'AI辅导员', icon: '/images/icon_ai.png' },
],
tabList: [
{ label: '首页', icon: '/images/icon_home_active.png' },
{ label: '活动', icon: '/images/icon_calendar.png' },
{ label: '智能体', icon: '/images/icon_ai.png' },
{ label: '通知', icon: '/images/icon_bell.png' },
{ label: '我的', icon: '/images/icon_user.png' },
],
recommendAgents: [
{ id: 'ppt', name: 'PPT小助手', icon: '/images/agent_ppt.png', bgImage: '/images/activity_1.png' },
{ id: 'ielts', name: '雅思模拟考官', icon: '/images/agent_ielts.png', bgImage: '/images/activity_2.png' },
{ id: 'pdf', name: 'PDF翻译', icon: '/images/agent_pdf.png', bgImage: '/images/activity_3.png' },
],
recommendActivities: [
{
id: '3',
title: '英语角交流活动',
desc: '提升口语能力',
time: '活动时间:2026-04-01~2026-05-30',
image: '/images/activity_1.png',
},
{
id: '4',
title: '大学生创业项目大赛',
desc: '提升创新能力',
time: '活动时间:2026-04-01~2026-05-30',
image: '/images/activity_2.png',
},
],
hotActivities: [
{
id: '1',
title: '计算机学院编程大赛',
image: '/images/activity_1.png',
deadline: '截止日期:2026年5月30日',
},
{
id: '2',
title: '创新创业项目路演',
image: '/images/activity_2.png',
deadline: '截止日期:2026年6月15日',
},
],
// 首页聚合数据
searchPlaceholder: '请搜索你想要的内容',
notifications: [] as any[],
quickEntries: [] as any[],
hotActivities: [] as any[],
recommendAgents: [] as any[],
bannerItems: [] as any[],
currentBannerIndex: 0,
currentBannerImage: '',
// 推荐活动分页数据
latestActivities: [] as any[],
activityPagination: {
page: 1,
pages: 1,
count: 0,
},
activityLoading: false,
// 智能体数据
agentList: [] as any[],
},
bannerTimer: null as number | null,
onLoad() {
const sysInfo = wx.getSystemInfoSync()
this.setData({ statusBarHeight: sysInfo.statusBarHeight || 44 })
app.waitLogin()
app.waitLogin().then(() => {
this.fetchHomeData()
this.fetchRecommendedActivities(1)
// 智能体推荐列表暂不联调
// this.fetchAgentList()
})
},
onUnload() {
if (this.bannerTimer) {
clearInterval(this.bannerTimer)
this.bannerTimer = null
}
},
onReachBottom() {
// 触底加载更多推荐活动
if (this.data.activeTab === 0) {
this.loadMoreActivities()
}
},
fetchHomeData() {
wx.ajax({
url: '/home/index',
method: 'GET',
}).then((res: any) => {
const data: any = {}
// 搜索模块
if (res.searchModule?.config?.placeholder) {
data.searchPlaceholder = res.searchModule.config.placeholder
}
// Banner 模块
if (res.bannerModule?.items?.length) {
data.bannerItems = res.bannerModule.items
data.currentBannerImage = res.bannerModule.items[0].imageUrl
this.startBannerRotation(res.bannerModule.items)
}
// 快捷入口模块
if (res.quickEntryModule?.items?.length) {
data.quickEntries = res.quickEntryModule.items
}
// 通知栏模块
if (res.notificationModule?.data?.length) {
data.notifications = res.notificationModule.data
}
// 热门活动模块 - 处理mainImages JSON字符串解析
if (res.hotActivityModule?.data?.length) {
const hotActivities = res.hotActivityModule.data.map((item: any) => {
let mainImages = item.mainImages
// 如果mainImages是字符串,解析为数组
if (typeof mainImages === 'string') {
try {
mainImages = JSON.parse(mainImages)
} catch (e) {
console.error('解析mainImages失败', e)
mainImages = []
}
}
return { ...item, mainImages }
})
data.hotActivities = hotActivities
}
// 推荐智能体模块
if (res.recommendAgentModule?.data?.length) {
data.recommendAgents = res.recommendAgentModule.data
}
this.setData(data)
}).catch((err) => {
console.error('获取首页数据失败', err)
})
},
fetchRecommendedActivities(page: number) {
if (this.data.activityLoading) return
this.setData({ activityLoading: true })
wx.ajax({
url: '/activity/list',
method: 'GET',
data: { getRecommended: 1, page, pageSize: 10 },
}).then((res: any) => {
const list = res.list || []
const pagination = res.pagination || {}
// 处理mainImages JSON字符串解析
const activities = list.map((item: any) => {
let mainImages = item.mainImages
if (typeof mainImages === 'string') {
try {
mainImages = JSON.parse(mainImages)
} catch (e) {
console.error('解析mainImages失败', e)
mainImages = []
}
}
return { ...item, mainImages }
})
// 如果是第一页,直接替换;否则追加
const latestActivities = page === 1 ? activities : [...this.data.latestActivities, ...activities]
this.setData({
latestActivities,
activityPagination: {
page: pagination.page || page,
pages: pagination.totalPages || 1,
count: pagination.total || 0,
},
activityLoading: false,
})
}).catch((err) => {
console.error('获取推荐活动失败', err)
this.setData({ activityLoading: false })
})
},
loadMoreActivities() {
const { activityPagination, activityLoading } = this.data
if (activityLoading || activityPagination.page >= activityPagination.pages) return
this.fetchRecommendedActivities(activityPagination.page + 1)
},
fetchAgentList() {
wx.ajax({
url: '/agent/list',
method: 'GET',
data: { page: 1, pageSize: 10 },
}).then((res: any) => {
if (res.list?.length) {
this.setData({ agentList: res.list })
}
}).catch((err) => {
console.error('获取智能体列表失败', err)
})
},
startBannerRotation(bannerItems: any[]) {
if (bannerItems.length <= 1) return
this.bannerTimer = setInterval(() => {
const currentIndex = this.data.currentBannerIndex
const nextIndex = (currentIndex + 1) % bannerItems.length
this.setData({
currentBannerIndex: nextIndex,
currentBannerImage: bannerItems[nextIndex].imageUrl,
})
}, 3000)
},
onSearchTap() {

103
src/pages/index/index.wxml

@ -1,42 +1,35 @@ @@ -1,42 +1,35 @@
<view
class="page"
style="background: url('{{imageUrl}}bg1.png?t={{Timestamp}}') no-repeat top center/100% 655rpx;padding-top: {{pageTop}}px;"
style="background: url('{{currentBannerImage || imageUrl + 'bg1.png?t=' + Timestamp}}') no-repeat top center/100% 655rpx;padding-top: {{pageTop}}px;"
>
<view class="search">
<image class="icon" src="{{imageUrl}}icon1.png?t={{Timestamp}}"></image>
<view class="content">请搜索你想要的内容</view>
<view class="content">{{searchPlaceholder}}</view>
</view>
<view class="notice">
<view class="notice" wx:if="{{notifications.length > 0}}">
<image class="icon" src="{{imageUrl}}icon2.png?t={{Timestamp}}"></image>
<view class="content">
<van-notice-bar
color="rgba(71, 85, 105, 1)"
background="transparent"
speed="{{3}}"
text="技术是开发它的人的共同灵魂。"
/>
<swiper
class="notice-swiper"
vertical="{{true}}"
autoplay="{{true}}"
circular="{{true}}"
interval="{{3000}}"
>
<swiper-item wx:for="{{notifications}}" wx:key="id">
<view class="notice-text">{{item.title}}</view>
</swiper-item>
</swiper>
</view>
<van-icon class="more" name="arrow" />
</view>
<view class="kkd">
<view class="k-item">
<image class="icon" src="{{imageUrl}}temporary/t1.png?t={{Timestamp}}"></image>
<view class="name">智能课表</view>
</view>
<view class="k-item">
<image class="icon" src="{{imageUrl}}temporary/t2.png?t={{Timestamp}}"></image>
<view class="name">校园巴士</view>
</view>
<view class="k-item">
<image class="icon" src="{{imageUrl}}temporary/t3.png?t={{Timestamp}}"></image>
<view class="name">活动报名</view>
</view>
<view class="k-item">
<image class="icon" src="{{imageUrl}}temporary/t4.png?t={{Timestamp}}"></image>
<view class="name">AI辅导员</view>
<view class="kkd" wx:if="{{quickEntries.length > 0}}">
<view class="k-item" wx:for="{{quickEntries}}" wx:key="id">
<image class="icon" src="{{item.imageUrl}}"></image>
<view class="name">{{item.name}}</view>
</view>
</view>
<view class="module">
<view class="module" wx:if="{{hotActivities.length > 0}}">
<view class="m-header">
<view class="title">热门活动</view>
<view class="more">
@ -45,12 +38,12 @@ @@ -45,12 +38,12 @@
</view>
</view>
<view class="activity-list">
<view class="a-card" wx:for="{{4}}" wx:key="index">
<image class="a-img" mode="aspectFill" src="{{imageUrl}}temporary/t5.png?t={{Timestamp}}"></image>
<view class="a-card" wx:for="{{hotActivities}}" wx:key="id">
<image class="a-img" mode="aspectFill" src="{{item.mainImages[0]}}"></image>
</view>
</view>
</view>
<view class="module">
<view class="module" wx:if="{{recommendAgents.length > 0}}">
<view class="m-header">
<view class="title">推荐智能体</view>
<view class="more">
@ -59,21 +52,9 @@ @@ -59,21 +52,9 @@
</view>
</view>
<view class="agent-list">
<view class="a-card">
<image class="icon" src="{{imageUrl}}temporary/t6.png?t={{Timestamp}}"></image>
<view class="name">PPT小助手</view>
</view>
<view class="a-card">
<image class="icon" src="{{imageUrl}}temporary/t7.png?t={{Timestamp}}"></image>
<view class="name">雅思模拟考官</view>
</view>
<view class="a-card">
<image class="icon" src="{{imageUrl}}temporary/t8.png?t={{Timestamp}}"></image>
<view class="name">PDF翻译</view>
</view>
<view class="a-card">
<image class="icon" src="{{imageUrl}}temporary/t7.png?t={{Timestamp}}"></image>
<view class="name">雅思模拟考官</view>
<view class="a-card" wx:for="{{recommendAgents}}" wx:key="id">
<image class="icon" src="{{item.icon}}"></image>
<view class="name">{{item.name}}</view>
</view>
</view>
</view>
@ -88,23 +69,43 @@ @@ -88,23 +69,43 @@
>
<van-tab title="推荐活动">
<view class="r-activity-list">
<view class="card" wx:for="{{2}}" wx:key="index">
<view class="card" wx:for="{{latestActivities}}" wx:key="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]}}"></image>
</view>
<view class="wrap">
<view class="title">{{item.name}}</view>
<view class="user">{{item.regCount}}人已报名</view>
<view class="date">
<image class="icon" src="{{imageUrl}}icon3.png?t={{Timestamp}}"></image>
<view class="content">{{item.startAt}}</view>
</view>
</view>
</view>
<pagination pagination="{{activityPagination}}" />
</view>
</van-tab>
<van-tab title="智能体">
<view class="r-activity-list">
<view class="card" wx:for="{{agentList}}" wx:key="id">
<view class="photo">
<view class="status">进行中</view>
<image class="p-img" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image>
<image class="p-img" src="{{item.icon}}"></image>
</view>
<view class="wrap">
<view class="title">深职大第十五届校园歌手大赛</view>
<view class="user">128人已报名</view>
<view class="title">{{item.name}}</view>
<view class="user">{{item.brief}}</view>
<view class="date">
<image class="icon" src="{{imageUrl}}icon3.png?t={{Timestamp}}"></image>
<view class="content">2026.04.01-2026.05.30</view>
<view class="content">{{item.usageCount}}次使用</view>
</view>
</view>
</view>
</view>
</van-tab>
<van-tab title="智能体">内容 2</van-tab>
</van-tabs>
</view>
</view>

Loading…
Cancel
Save