Browse Source

feat: 新增分页组件,优化活动列表与页面细节

- 新增通用分页组件pagination,支持空状态、加载中、无更多数据状态
- 重构活动列表页面,添加分页逻辑、分类筛选、动态数据渲染
- 完善活动详情页面,支持动态数据展示、签到报名逻辑
- 优化活动创建页面,调整上传组件与表单逻辑
- 重构登录与请求逻辑,统一鉴权处理
- 简化课表页面代码,移除旧的双向滚动逻辑
- 优化上传组件,移除预览删除功能,仅保留上传核心逻辑
- 新增活动结果页面,完善报名成功后的展示与推荐活动
master
kola-web 1 week ago
parent
commit
5b402c3532
  1. 2
      AGENTS.md
  2. 5
      src/api/request.ts
  3. 145
      src/app.ts
  4. 26
      src/components/pagination/index.js
  5. 6
      src/components/pagination/index.json
  6. 6
      src/components/pagination/index.scss
  7. 9
      src/components/pagination/index.wxml
  8. 34
      src/components/uploadFile/README.md
  9. 206
      src/components/uploadFile/index.scss
  10. 179
      src/components/uploadFile/index.ts
  11. 98
      src/components/uploadFile/index.wxml
  12. BIN
      src/images/none.png
  13. 3
      src/pages/act/index.json
  14. 17
      src/pages/act/index.scss
  15. 485
      src/pages/act/index.ts
  16. 87
      src/pages/act/index.wxml
  17. 3
      src/pages/actAdd/index.json
  18. 30
      src/pages/actAdd/index.scss
  19. 618
      src/pages/actAdd/index.ts
  20. 75
      src/pages/actAdd/index.wxml
  21. 52
      src/pages/actAddResult/index.ts
  22. 4
      src/pages/actAddResult/index.wxml
  23. 33
      src/pages/actDetail/index.scss
  24. 501
      src/pages/actDetail/index.ts
  25. 153
      src/pages/actDetail/index.wxml
  26. 85
      src/pages/actResult/index.ts
  27. 26
      src/pages/actResult/index.wxml
  28. 12
      src/pages/login/index.ts
  29. 220
      src/pages/schedule/index.scss
  30. 26
      src/pages/schedule/index.ts
  31. 47
      src/pages/schedule/index.wxml
  32. 12
      typings/index.d.ts

2
AGENTS.md

@ -15,6 +15,7 @@ WeChat Mini Program (微信小程序) — school campus app. Source root is `src
- **Custom Page wrapper** (`src/utils/page.ts`): The global `Page` constructor is replaced in `app.ts` `onLaunch`. Every page automatically gets navbar setup, image params, scroll-based nav background, and a default `onShareAppMessage`. When adding page lifecycle hooks, be aware these wrappers run before your own handlers. - **Custom Page wrapper** (`src/utils/page.ts`): The global `Page` constructor is replaced in `app.ts` `onLaunch`. Every page automatically gets navbar setup, image params, scroll-based nav background, and a default `onShareAppMessage`. When adding page lifecycle hooks, be aware these wrappers run before your own handlers.
- **`wx.ajax`**: Custom property added to `wx` in `app.ts` `onLaunch`. It is a curried version of `src/api/request.ts` with the base URL baked in. Use `wx.ajax(...)` for network requests — do not call `request` directly from pages. - **`wx.ajax`**: Custom property added to `wx` in `app.ts` `onLaunch`. It is a curried version of `src/api/request.ts` with the base URL baked in. Use `wx.ajax(...)` for network requests — do not call `request` directly from pages.
- **Login flow**: Managed in `App` via `globalData.loginState` / `globalData.initLoginInfo`. Pages call `getApp().waitLogin()` before making authenticated requests. - **Login flow**: Managed in `App` via `globalData.loginState` / `globalData.initLoginInfo`. Pages call `getApp().waitLogin()` before making authenticated requests.
- **API request pattern**: All page-level API requests should be placed inside `app.waitLogin()` callback in `onLoad` to ensure login state is ready before making authenticated requests. Example: `app.waitLogin({ type: 1 }).then(() => { this.fetchData() })`.
- **Custom tabbar**: `app.json` has `"custom": true`; the tabbar UI is implemented in components. - **Custom tabbar**: `app.json` has `"custom": true`; the tabbar UI is implemented in components.
## Path Aliases ## Path Aliases
@ -26,6 +27,7 @@ WeChat Mini Program (微信小程序) — school campus app. Source root is `src
- ESLint: `@antfu/eslint-config` + `eslint-config-prettier` (stylistic rules off). Globals `wx`, `App`, `Page`, `Component`, `getApp`, `getCurrentPages`, `requirePlugin`, `requireMiniProgram` are declared. - ESLint: `@antfu/eslint-config` + `eslint-config-prettier` (stylistic rules off). Globals `wx`, `App`, `Page`, `Component`, `getApp`, `getCurrentPages`, `requirePlugin`, `requireMiniProgram` are declared.
- Prettier: no semicolons, single quotes, trailing commas, printWidth 120. Uses `html` parser for `.wxml`, `css` for `.wxss`, `babel` for `.wxs`. - Prettier: no semicolons, single quotes, trailing commas, printWidth 120. Uses `html` parser for `.wxml`, `css` for `.wxss`, `babel` for `.wxs`.
- TypeScript: strict mode (but `noImplicitAny: false`), target ES5, CommonJS modules. Typings in `typings/index.d.ts` declare `IAppOption`, `IAgaxParams`, and extend `WechatMiniprogram.Wx` with `ajax`. - TypeScript: strict mode (but `noImplicitAny: false`), target ES5, CommonJS modules. Typings in `typings/index.d.ts` declare `IAppOption`, `IAgaxParams`, and extend `WechatMiniprogram.Wx` with `ajax`.
- **Interface usage**: Minimize interface definitions. Only add interfaces when absolutely necessary (e.g., complex API response structures, reusable data models). Prefer inline type annotations or `any` for simple cases. Avoid over-engineering type safety.
## File Structure ## File Structure

5
src/api/request.ts

@ -25,10 +25,13 @@ export const request = function (
}) })
} }
const app = getApp<IAppOption>() // 获取 accessToken 并构建 Authorization 头
const accessToken = getApp<IAppOption>().globalData.accessToken
const authHeader = accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
wx.request({ wx.request({
header: { header: {
...authHeader,
...header, ...header,
}, },
url: gUrl + url, url: gUrl + url,

145
src/app.ts

@ -13,7 +13,7 @@ dayjs.extend(relativeTime)
App<IAppOption>({ App<IAppOption>({
globalData: { globalData: {
url: 'https://app.gohighedu.cn', url: 'https://app.gohighedu.cn',
upFileUrl: 'https://app.gohighedu.cn', upFileUrl: 'https://app.gohighedu.cn/upload/index',
imageUrl: 'https://app.gohighedu.cn/images', imageUrl: 'https://app.gohighedu.cn/images',
Timestamp: new Date().getTime(), Timestamp: new Date().getTime(),
@ -23,6 +23,9 @@ App<IAppOption>({
initLoginInfo: {}, initLoginInfo: {},
userInfo: {}, userInfo: {},
accessToken: '',
needBind: true,
}, },
onLaunch() { onLaunch() {
this.autoUpdate() this.autoUpdate()
@ -40,40 +43,35 @@ App<IAppOption>({
startLogin(callback?: () => void) { startLogin(callback?: () => void) {
wx.login({ wx.login({
success: (res) => { success: (res) => {
console.log("DEBUGPRINT[244]: app.ts:42: res=", res) // 调用静默登录接口
// // 调用静默登录接口 wx.ajax({
// wx.ajax({ method: 'POST',
// method: 'POST', url: '/auth/silent-login',
// url: '/auth/silent-login', showMsg: false, // 隐藏错误提示
// showMsg: false, // 隐藏错误提示 data: {
// data: { code: res.code,
// code: res.code, },
// }, })
// }) .then((response: any) => {
// .then((response: any) => { const { accessToken, user, needBind } = response
// const { accessToken, user } = response
// // 存储 accessToken
// // 存储 accessToken this.globalData.accessToken = accessToken
// this.globalData.accessToken = accessToken this.globalData.needBind = needBind
//
// // 存储用户信息 // 存储用户信息
// if (user) { if (user) {
// this.globalData.userInfo = user this.globalData.userInfo = user
// } }
//
// // 更新 initLoginInfo if (callback) {
// this.globalData.initLoginInfo = { callback()
// user, }
// } })
// .catch((err: any) => {
// if (callback) { // 静默失败,不提示用户
// callback() console.error('静默登录请求失败:', err)
// } })
// })
// .catch((err: any) => {
// // 静默失败,不提示用户
// console.error('静默登录请求失败:', err)
// })
}, },
fail: (err) => { fail: (err) => {
// 静默失败,不提示用户 // 静默失败,不提示用户
@ -81,52 +79,16 @@ App<IAppOption>({
}, },
}) })
}, },
updateLoginInfo(callback?: () => void) { waitLogin({ type }: { type?: 0 | 1 } = { type: 0 }) {
wx.ajax({ return new Promise<void>((resolve) => {
method: 'GET',
url: '?r=wtx/user/init-info',
data: {},
}).then((res: any) => {
this.globalData.initLoginInfo = res
if (callback) {
callback()
}
})
},
waitLogin({ type }: { type?: 0 | 1 | 2 | 'any' } = { type: 'any' }) {
return new Promise<void>((resolve, reject) => {
const checkLogin = () => { const checkLogin = () => {
// type = 0:不需要登录即可访问 // type = 0:不需要登录即可访问
if (type === 0) { if (type === 0) {
resolve() resolve()
return return
} }
if (type === 1 && this.globalData.needBind) {
// type = 'any':不检查登录状态 this.redirectToLogin()
if (type === 'any') {
resolve()
return
}
// type = 1 或 2:需要登录
if (type === 1 || type === 2) {
// 检查是否有 accessToken
if (this.globalData.accessToken) {
// 已登录,检查是否需要绑定
if (this.globalData.initLoginInfo?.needBind) {
// 需要绑定,跳转到登录页
this.redirectToLogin(type)
reject(new Error('need_bind'))
return
}
resolve()
return
}
// 未登录,跳转到登录页
this.redirectToLogin(type)
reject(new Error('not_logged_in'))
return
} }
resolve() resolve()
@ -138,7 +100,7 @@ App<IAppOption>({
/** /**
* *
*/ */
redirectToLogin(_type: 1 | 2) { redirectToLogin() {
// 获取当前页面路径 // 获取当前页面路径
const pages = getCurrentPages() const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] const currentPage = pages[pages.length - 1]
@ -153,37 +115,6 @@ App<IAppOption>({
url: '/pages/login/index', url: '/pages/login/index',
}) })
}, },
checkLoginType(type: 0 | 1 | 2 | 'any') {
// type = 0:不需要登录
if (type === 0) {
return true
}
// type = 'any':不检查
if (type === 'any') {
return true
}
// type = 1 或 2:需要登录
if (type === 1 || type === 2) {
// 检查是否有 accessToken
if (!this.globalData.accessToken) {
this.redirectToLogin(type as 1 | 2)
return false
}
// 检查是否需要绑定
if (this.globalData.initLoginInfo?.needBind) {
this.redirectToLogin(type as 1 | 2)
return false
}
return true
}
return true
},
getUserInfo(type: 0 | 1 | 2 = 0) { getUserInfo(type: 0 | 1 | 2 = 0) {
const url: Record<number, string> = { const url: Record<number, string> = {
0: '?r=wtx/user/userinfo', 0: '?r=wtx/user/userinfo',

26
src/components/pagination/index.js

@ -0,0 +1,26 @@
/* global getApp, Component */
const app = getApp()
Component({
externalClasses: ['external-class'],
properties: {
pagination: {
type: Object,
value() {
return {}
},
},
customEmpty: {
tyep: Boolean,
value: false,
},
},
data: {
imageUrl: app.globalData.imageUrl,
},
methods: {
handleTouchmove() {
return false
},
},
})

6
src/components/pagination/index.json

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

6
src/components/pagination/index.scss

@ -0,0 +1,6 @@
/* components/pagination/index.wxss */
.none {
display: block;
margin: 30rpx auto;
width: 80%;
}

9
src/components/pagination/index.wxml

@ -0,0 +1,9 @@
<block wx:if="{{pagination.count==0}}">
<slot wx:if="{{customEmpty}}"></slot>
<image wx:else class="none external-class" src="{{imageUrl}}/none.png?t={{Timestamp}}"></image>
</block>
<van-divider contentPosition="center" wx:elif="{{pagination.page<pagination.pages}}">
<van-loading />
加载中...
</van-divider>
<van-divider contentPosition="center" wx:elif="{{pagination.page>=pagination.pages}}">没有更多了</van-divider>

34
src/components/uploadFile/README.md

@ -8,6 +8,10 @@
上传接口地址固定为 `app.globalData.upFileUrl`,组件内部自动附加 `loginState`,无需外部传入。 上传接口地址固定为 `app.globalData.upFileUrl`,组件内部自动附加 `loginState`,无需外部传入。
组件仅提供上传按钮,不维护文件列表状态,不显示文件列表,不包含文件预览功能。父页面需要自行处理文件列表的显示和管理。
组件通过事件通知父页面文件选择、上传进度、上传成功、上传失败等状态,父页面需要监听这些事件并更新自己的文件列表。
## Properties 入参 ## Properties 入参
| 参数 | 说明 | 类型 | 默认值 | 必填 | | 参数 | 说明 | 类型 | 默认值 | 必填 |
@ -16,9 +20,9 @@
| `maxSize` | 单文件最大限制(byte) | `Number` | `10485760`(10MB) | 否 | | `maxSize` | 单文件最大限制(byte) | `Number` | `10485760`(10MB) | 否 |
| `accept` | 允许的文件类型数组,可选值:`'image'` \| `'video'` \| `'file'` | `Array` | `['image']` | 否 | | `accept` | 允许的文件类型数组,可选值:`'image'` \| `'video'` \| `'file'` | `Array` | `['image']` | 否 |
| `extensions` | 自定义文件后缀(仅 accept 含 `'file'` 时生效),如 `['.pdf', '.doc']` | `Array` | `[]` | 否 | | `extensions` | 自定义文件后缀(仅 accept 含 `'file'` 时生效),如 `['.pdf', '.doc']` | `Array` | `[]` | 否 |
| `readonly` | 只读模式(不显示上传和删除按钮) | `Boolean` | `false` | 否 | | `readonly` | 只读模式(不显示上传按钮) | `Boolean` | `false` | 否 |
| `useSlot` | 是否使用自定义上传区域插槽 | `Boolean` | `false` | 否 | | `useSlot` | 是否使用自定义上传区域插槽 | `Boolean` | `false` | 否 |
| `fileList` | 已有文件列表(用于回显) | `Array` | `[]` | 否 | | `fileList` | 已有文件列表(用于判断是否还能上传) | `Array` | `[]` | 否 |
### fileList 数据结构 ### fileList 数据结构
@ -36,10 +40,8 @@
| 事件名 | 说明 | 回调参数 | | 事件名 | 说明 | 回调参数 |
| --- | --- | --- | | --- | --- | --- |
| `bind:select` | 选中本地文件后触发 | `{ files: UploadFile[] }` |
| `bind:success` | 单文件上传完成 | `{ file: UploadFile, response: any }` | | `bind:success` | 单文件上传完成 | `{ file: UploadFile, response: any }` |
| `bind:error` | 上传失败 | `{ file: UploadFile, error: Error }` | | `bind:error` | 上传失败 | `{ file: UploadFile, error: Error }` |
| `bind:remove` | 删除文件 | `{ file: UploadFile, fileList: UploadFile[] }` |
### 事件返回的 UploadFile 结构 ### 事件返回的 UploadFile 结构
@ -67,13 +69,9 @@
| --- | --- | --- | | --- | --- | --- |
| `--upload-bg` | 上传区域背景色 | `#f7f8fa` | | `--upload-bg` | 上传区域背景色 | `#f7f8fa` |
| `--upload-border` | 边框颜色 | `#e5e7eb` | | `--upload-border` | 边框颜色 | `#e5e7eb` |
| `--upload-text` | 主文字颜色 | `#1f2937` |
| `--upload-text-secondary` | 次要文字颜色 | `#9ca3af` | | `--upload-text-secondary` | 次要文字颜色 | `#9ca3af` |
| `--upload-primary` | 主题色 | `#3b82f6` |
| `--upload-error` | 错误色 | `#ef4444` |
| `--upload-mask` | 遮罩颜色 | `rgba(0, 0, 0, 0.5)` |
| `--upload-radius` | 圆角大小 | `16rpx` | | `--upload-radius` | 圆角大小 | `16rpx` |
| `--upload-size` | 文件项尺寸(仅默认上传框生效,slot 模式自适应) | `160rpx` | | `--upload-size` | 上传按钮尺寸(仅默认上传框生效,slot 模式自适应) | `160rpx` |
## 使用案例 ## 使用案例
@ -94,27 +92,30 @@
maxCount="{{3}}" maxCount="{{3}}"
maxSize="{{5242880}}" maxSize="{{5242880}}"
accept="{{['image']}}" accept="{{['image']}}"
bind:select="onUploadSelect" fileList="{{fileList}}"
bind:success="onUploadSuccess" bind:success="onUploadSuccess"
bind:error="onUploadError" bind:error="onUploadError"
bind:remove="onUploadRemove"
/> />
``` ```
```ts ```ts
// page.ts // page.ts
Page({ Page({
onUploadSelect(e: WechatMiniprogram.CustomEvent) { data: {
console.log('选中文件', e.detail.files) fileList: [] as UploadFile[],
}, },
onUploadSuccess(e: WechatMiniprogram.CustomEvent) { onUploadSuccess(e: WechatMiniprogram.CustomEvent) {
console.log('上传成功', e.detail.file, e.detail.response) console.log('上传成功', e.detail.file, e.detail.response)
// 添加上传成功的文件到列表
this.setData({
fileList: [...this.data.fileList, e.detail.file],
})
}, },
onUploadError(e: WechatMiniprogram.CustomEvent) { onUploadError(e: WechatMiniprogram.CustomEvent) {
console.log('上传失败', e.detail.file, e.detail.error) console.log('上传失败', e.detail.file, e.detail.error)
}, wx.showToast({ title: '文件上传失败', icon: 'none' })
onUploadRemove(e: WechatMiniprogram.CustomEvent) {
console.log('删除文件', e.detail.file, e.detail.fileList)
}, },
}) })
``` ```
@ -127,6 +128,7 @@ Page({
useSlot="{{true}}" useSlot="{{true}}"
maxCount="{{5}}" maxCount="{{5}}"
accept="{{['image', 'video']}}" accept="{{['image', 'video']}}"
fileList="{{fileList}}"
bind:success="onUploadSuccess" bind:success="onUploadSuccess"
> >
<view slot="upload-area" class="custom-upload-area"> <view slot="upload-area" class="custom-upload-area">

206
src/components/uploadFile/index.scss

@ -7,218 +7,14 @@
page { page {
--upload-bg: #f7f8fa; --upload-bg: #f7f8fa;
--upload-border: #e5e7eb; --upload-border: #e5e7eb;
--upload-text: #1f2937;
--upload-text-secondary: #9ca3af; --upload-text-secondary: #9ca3af;
--upload-primary: #3b82f6;
--upload-error: #ef4444;
--upload-mask: rgba(0, 0, 0, 0.5);
--upload-radius: 16rpx; --upload-radius: 16rpx;
--upload-size: 160rpx; --upload-size: 160rpx;
--upload-preview-height: 160rpx; // 预览项高度独立控制
} }
.upload { .upload {
width: 100%; width: 100%;
.upload-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.upload-item {
position: relative;
width: 100%;
height: var(--upload-preview-height);
border-radius: var(--upload-radius);
overflow: hidden;
&--file {
width: 100%;
height: auto;
min-height: var(--upload-preview-height);
}
}
.upload-preview {
width: 100%;
height: 100%;
position: relative;
&--file {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx;
background: var(--upload-bg);
border-radius: var(--upload-radius);
}
.upload-preview-media {
width: 100%;
height: 100%;
display: block;
}
.upload-preview-play {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--upload-mask);
.upload-preview-play-icon {
width: 0;
height: 0;
border-style: solid;
border-width: 16rpx 0 16rpx 28rpx;
border-color: transparent transparent transparent #fff;
margin-left: 8rpx;
}
}
}
/* 文件类型预览 */
.upload-file-icon {
width: 80rpx;
height: 96rpx;
background: #fff;
border: 2rpx solid var(--upload-border);
border-radius: 8rpx;
position: relative;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 12rpx;
flex-shrink: 0;
.upload-file-icon-corner {
position: absolute;
top: 0;
right: 0;
width: 24rpx;
height: 24rpx;
background: var(--upload-border);
border-radius: 0 6rpx 0 8rpx;
}
.upload-file-icon-text {
font-size: 16rpx;
color: var(--upload-text-secondary);
max-width: 60rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.upload-file-name {
flex: 1;
font-size: 26rpx;
color: var(--upload-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 上传中遮罩 */
.upload-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--upload-mask);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12rpx;
.upload-progress {
width: 70%;
height: 6rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 3rpx;
overflow: hidden;
.upload-progress-bar {
height: 100%;
background: #fff;
border-radius: 3rpx;
transition: width 0.2s ease;
}
}
.upload-progress-text {
font-size: 24rpx;
color: #fff;
}
&--error {
.upload-error-text {
font-size: 24rpx;
color: #fff;
}
.upload-retry {
padding: 8rpx 24rpx;
border: 2rpx solid #fff;
border-radius: 24rpx;
.upload-retry-text {
font-size: 24rpx;
color: #fff;
}
}
}
}
/* 删除按钮 */
.upload-remove {
position: absolute;
top: -8rpx;
right: -8rpx;
width: 36rpx;
height: 36rpx;
background: var(--upload-mask);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
.upload-remove-icon {
position: relative;
width: 18rpx;
height: 18rpx;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 2rpx;
background: #fff;
transform-origin: center;
}
&::before {
transform: translateY(-50%) rotate(45deg);
}
&::after {
transform: translateY(-50%) rotate(-45deg);
}
}
}
/* 上传触发器 */ /* 上传触发器 */
.upload-trigger { .upload-trigger {
width: var(--upload-size); width: var(--upload-size);
@ -226,7 +22,7 @@ page {
&--slot { &--slot {
width: 100%; // slot 模式下宽度撑满父容器 width: 100%; // slot 模式下宽度撑满父容器
height: auto; // 高度自适应 slot 内容 height: auto; // slot 模式下高度自适应内容
} }
.upload-trigger-default { .upload-trigger-default {

179
src/components/uploadFile/index.ts

@ -1,6 +1,7 @@
/** /**
* Upload * Upload
* // *
*
*/ */
/** 单个文件对象 */ /** 单个文件对象 */
@ -57,7 +58,7 @@ Component({
type: Array, type: Array,
value: [], value: [],
}, },
/** 只读模式(不显示上传和删除按钮) */ /** 只读模式(不显示上传按钮) */
readonly: { readonly: {
type: Boolean, type: Boolean,
value: false, value: false,
@ -67,45 +68,24 @@ Component({
type: Boolean, type: Boolean,
value: false, value: false,
}, },
/** 已有文件列表(用于回显) */ /** 已有文件列表(用于判断是否还能上传) */
fileList: { fileList: {
type: Array, type: Array,
value: [], value: [],
}, },
}, },
data: {
/** 内部文件列表 */
_fileList: [] as UploadFile[],
},
lifetimes: {
attached() {
// 初始化已有文件列表
if (this.properties.fileList.length > 0) {
this.setData({
_fileList: this.properties.fileList.map((item: any) => ({
uid: item.uid || `init_${Date.now()}_${Math.random()}`,
url: item.url,
type: item.type || 'image',
name: item.name || '',
size: item.size || 0,
status: 'success',
progress: 100,
})),
})
}
},
},
methods: { methods: {
/** /**
* *
*/ */
onChooseFile() { onChooseFile() {
if (this.data.readonly) return if (this.properties.readonly) return
const { _fileList, maxCount } = this.data
const remaining = maxCount - _fileList.length const fileList = this.properties.fileList as any[]
const maxCount = this.properties.maxCount
const remaining = maxCount - fileList.length
if (remaining <= 0) { if (remaining <= 0) {
wx.showToast({ title: `最多上传${maxCount}个文件`, icon: 'none' }) wx.showToast({ title: `最多上传${maxCount}个文件`, icon: 'none' })
return return
@ -217,13 +197,6 @@ Component({
if (validFiles.length === 0) return if (validFiles.length === 0) return
// 触发 onSelect 事件
this.triggerEvent('select', { files: validFiles })
// 更新文件列表
const newList = [...this.data._fileList, ...validFiles]
this.setData({ _fileList: newList })
// 逐个上传 // 逐个上传
validFiles.forEach((file) => { validFiles.forEach((file) => {
this.uploadFile(file) this.uploadFile(file)
@ -236,128 +209,50 @@ Component({
uploadFile(file: UploadFile) { uploadFile(file: UploadFile) {
const app = getApp<IAppOption>() const app = getApp<IAppOption>()
const action = app.globalData.upFileUrl const action = app.globalData.upFileUrl
const loginState = app.globalData.loginState const accessToken = app.globalData.accessToken
this.updateFile(file.uid, { status: 'uploading', progress: 0 }) const fileType = file.type === 'file' ? 'document' : file.type
const task = wx.uploadFile({ const task = wx.uploadFile({
url: action, url: action,
filePath: file.url, filePath: file.url,
name: 'file', name: 'file',
header: { loginState }, header: {
formData: { loginState }, Authorization: accessToken ? `Bearer ${accessToken}` : '',
},
formData: { type: fileType },
success: (res) => { success: (res) => {
// 尝试解析返回数据
let data: any let data: any
try { try {
data = JSON.parse(res.data) data = JSON.parse(res.data)
} catch { } catch {
data = res.data data = res.data
} }
this.updateFile(file.uid, { if (data.code === 0 && data.data?.url) {
status: 'success', // 上传成功,更新文件信息
progress: 100, const successFile = {
url: data.url || data.data?.url || file.url, ...file,
}) status: 'success',
this.triggerEvent('success', { file: this.getFile(file.uid), response: data }) progress: 100,
url: data.data.url,
name: data.data.fileName || file.name,
size: data.data.fileSize || file.size,
}
this.triggerEvent('success', { file: successFile, response: data })
} else {
// 上传失败
const errorFile = { ...file, status: 'error', progress: 0 }
this.triggerEvent('error', { file: errorFile, error: data.message || '上传失败' })
wx.showToast({ title: data.message || '上传失败', icon: 'none' })
}
}, },
fail: (err) => { fail: (err) => {
this.updateFile(file.uid, { status: 'error', progress: 0 }) // 上传失败
this.triggerEvent('error', { file: this.getFile(file.uid), error: err }) const errorFile = { ...file, status: 'error', progress: 0 }
this.triggerEvent('error', { file: errorFile, error: err })
wx.showToast({ title: '上传失败', icon: 'none' })
}, },
}) })
// 监听上传进度
task.onProgressUpdate((res) => {
this.updateFile(file.uid, { progress: res.progress })
})
// 保存 task 引用
this.updateFile(file.uid, { _task: task })
},
/**
*
*/
onRetry(e: WechatMiniprogram.BaseEvent) {
const { uid } = e.currentTarget.dataset
const file = this.getFile(uid)
if (file && file.status === 'error') {
this.uploadFile(file)
}
},
/**
*
*/
onRemove(e: WechatMiniprogram.BaseEvent) {
const { uid } = e.currentTarget.dataset
const file = this.getFile(uid)
if (!file) return
// 取消上传任务
if (file._task) {
file._task.abort()
}
const newList = this.data._fileList.filter((f) => f.uid !== uid)
this.setData({ _fileList: newList })
this.triggerEvent('remove', { file, fileList: newList })
},
/**
*
*/
onPreviewImage(e: WechatMiniprogram.BaseEvent) {
const { uid } = e.currentTarget.dataset
const file = this.getFile(uid)
if (!file) return
const imageList = this.data._fileList.filter((f) => f.type === 'image').map((f) => f.url)
wx.previewImage({
current: file.url,
urls: imageList,
})
},
/**
*
*/
onPreviewVideo(e: WechatMiniprogram.BaseEvent) {
const { uid } = e.currentTarget.dataset
const file = this.getFile(uid)
if (!file) return
wx.previewMedia({
sources: [{ url: file.url, type: 'video' }],
current: 0,
})
},
/**
*
*/
updateFile(uid: string, patch: Partial<UploadFile>) {
const list = this.data._fileList.map((f) => {
if (f.uid === uid) {
return { ...f, ...patch }
}
return f
})
this.setData({ _fileList: list })
},
/**
*
*/
getFile(uid: string): UploadFile | undefined {
return this.data._fileList.find((f) => f.uid === uid)
},
/**
*
*/
getFileList(): UploadFile[] {
return this.data._fileList
}, },
}, },
}) })

98
src/components/uploadFile/index.wxml

@ -1,92 +1,20 @@
<!-- <!--
Upload 上传组件 Upload 上传组件
支持图片/视频/文件上传,进度条,预览,删除,重试 支持图片/视频/文件上传
仅保留上传功能,不显示文件列表
--> -->
<view class="upload"> <view class="upload">
<!-- 文件列表 --> <!-- 默认上传按钮 -->
<view class="upload-list"> <view
<!-- 单个文件项 --> wx:if="{{!readonly && fileList.length < maxCount}}"
<view class="upload-trigger {{useSlot ? 'upload-trigger--slot' : ''}}"
wx:for="{{_fileList}}" bindtap="onChooseFile"
wx:key="uid" >
class="upload-item upload-item--{{item.type}}" <!-- 具名插槽:自定义上传区域 -->
> <slot name="upload-area"></slot>
<!-- 图片预览 --> <!-- 默认上传占位 -->
<view <view wx:if="{{!useSlot}}" class="upload-trigger-default">
wx:if="{{item.type === 'image'}}" <view class="upload-trigger-icon"></view>
class="upload-preview"
data-uid="{{item.uid}}"
bindtap="onPreviewImage"
>
<image class="upload-preview-media" src="{{item.url}}" mode="aspectFill" />
</view>
<!-- 视频预览 -->
<view
wx:elif="{{item.type === 'video'}}"
class="upload-preview"
data-uid="{{item.uid}}"
bindtap="onPreviewVideo"
>
<video
class="upload-preview-media"
src="{{item.url}}"
controls="{{false}}"
object-fit="cover"
/>
<view class="upload-preview-play">
<view class="upload-preview-play-icon"></view>
</view>
</view>
<!-- 文件预览 -->
<view wx:elif="{{item.type === 'file'}}" class="upload-preview upload-preview--file">
<view class="upload-file-icon">
<view class="upload-file-icon-corner"></view>
<text class="upload-file-icon-text">{{item.name}}</text>
</view>
<text class="upload-file-name">{{item.name}}</text>
</view>
<!-- 上传中遮罩 + 进度条 -->
<view wx:if="{{item.status === 'uploading'}}" class="upload-mask">
<view class="upload-progress">
<view class="upload-progress-bar" style="width: {{item.progress}}%"></view>
</view>
<text class="upload-progress-text">{{item.progress}}%</text>
</view>
<!-- 上传失败遮罩 + 重试 -->
<view wx:if="{{item.status === 'error'}}" class="upload-mask upload-mask--error">
<text class="upload-error-text">上传失败</text>
<view class="upload-retry" data-uid="{{item.uid}}" catchtap="onRetry">
<text class="upload-retry-text">重试</text>
</view>
</view>
<!-- 删除按钮 -->
<view
wx:if="{{!readonly}}"
class="upload-remove"
data-uid="{{item.uid}}"
catchtap="onRemove"
>
<view class="upload-remove-icon"></view>
</view>
</view>
<!-- 默认上传按钮(未达到最大数量且非只读时显示) -->
<view
wx:if="{{!readonly && _fileList.length < maxCount}}"
class="upload-trigger {{useSlot ? 'upload-trigger--slot' : ''}}"
bindtap="onChooseFile"
>
<!-- 具名插槽:自定义上传区域 -->
<slot name="upload-area"></slot>
<!-- 默认上传占位 -->
<view wx:if="{{!useSlot}}" class="upload-trigger-default">
<view class="upload-trigger-icon"></view>
</view>
</view> </view>
</view> </view>
</view> </view>

BIN
src/images/none.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

3
src/pages/act/index.json

@ -2,6 +2,7 @@
"navigationStyle": "default", "navigationStyle": "default",
"navigationBarTitleText": "活动", "navigationBarTitleText": "活动",
"usingComponents": { "usingComponents": {
"van-popup": "@vant/weapp/popup/index" "van-popup": "@vant/weapp/popup/index",
"pagination": "/components/pagination/index"
} }
} }

17
src/pages/act/index.scss

@ -7,7 +7,7 @@ page {
padding: 30rpx 30rpx 0; padding: 30rpx 30rpx 0;
background-color: #fff; background-color: #fff;
.search { .search {
margin: 30rpx; margin: 30rpx 0;
padding: 12rpx 24rpx; padding: 12rpx 24rpx;
display: flex; display: flex;
align-items: center; align-items: center;
@ -27,6 +27,7 @@ page {
.options { .options {
display: flex; display: flex;
.tabs { .tabs {
margin: 0 -16rpx;
flex: 1; flex: 1;
overflow-x: auto; overflow-x: auto;
display: flex; display: flex;
@ -38,7 +39,7 @@ page {
} }
.tab { .tab {
flex-shrink: 0; flex-shrink: 0;
padding: 10rpx 18rpx 16rpx; padding: 10rpx 32rpx 16rpx;
font-size: 28rpx; font-size: 28rpx;
color: rgba(71, 85, 105, 1); color: rgba(71, 85, 105, 1);
&.active { &.active {
@ -128,12 +129,13 @@ page {
} }
} }
.banner { .banner {
margin: 30rpx 30rpx 0; margin: 0 30rpx 0;
height: 230rpx; height: 230rpx;
.b-img { .b-img {
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 24rpx;
} }
} }
.list { .list {
@ -207,9 +209,10 @@ page {
font-size: 28rpx; font-size: 28rpx;
color: rgba(100, 116, 139, 1); color: rgba(100, 116, 139, 1);
display: flex; display: flex;
align-items: center;
gap: 8rpx; gap: 8rpx;
.icon { .icon {
margin-top: 8rpx;
flex-shrink: 0;
width: 22rpx; width: 22rpx;
height: 22rpx; height: 22rpx;
} }
@ -271,6 +274,12 @@ page {
padding: 15rpx 30rpx; padding: 15rpx 30rpx;
background: #f7f8fa; background: #f7f8fa;
border-radius: 15rpx 15rpx 15rpx 15rpx; border-radius: 15rpx 15rpx 15rpx 15rpx;
border: 1px solid #f7f8fa;
&.active{
background-color: rgba(157, 223, 253, 0.16);
border: 1px solid rgba(74, 184, 253, 0.32);
color: rgba(74, 184, 253, 1);
}
} }
} }
.date-raneg { .date-raneg {

485
src/pages/act/index.ts

@ -1,52 +1,489 @@
const _app = getApp<IAppOption>() const app = getApp<IAppOption>()
interface ILevelItem {
id: number
name: string
code: string
sort: number
}
interface ICategoryItem {
id: number
name: string
code: string
icon: string
sort: number
isEnabled: number
}
interface IActivityItem {
id: number
name: string
type: number
typeOther: string
mainImages: string[]
summary: string
description: string
regType: number
regCondition: string
contactName: string
contactPhone: string
startAt: string
endAt: string
location: string
status: string
}
interface IPagination {
page: number
pageSize: number
pages: number
count: number
}
Page({ Page({
data: { data: {
filterShow: true, filterShow: false,
// 活动等级列表
levelList: [] as ILevelItem[],
// 等级 Tab 列表(包含"全部等级")
levelTabs: [
{
id: 0,
name: '全部',
},
] as Array<{ id: number; name: string }>,
// 当前选中的等级索引
currentLevelIndex: 0,
// 活动分类列表
categoryList: [] as ICategoryItem[],
// 分类 Tab 列表(包含"全部分类")
typeList: [ typeList: [
{ {
id: 0,
name: '全部分类', name: '全部分类',
icon: '5', icon: '/images/icon5.png',
iconActive: '6', iconActive: '/images/icon6.png',
isSelected: true,
}, },
{ ] as Array<{ id: number; name: string; icon: string; iconActive: string; isSelected: boolean }>,
name: '学术科技', // 当前选中的分类 ID 数组(支持多选)
icon: '7', selectedCategoryIds: [] as number[],
iconActive: '8', // 活动列表
activityList: [] as IActivityItem[],
// 分页信息
pagination: {
page: 1,
pageSize: 20,
pages: 0,
count: 0,
} as IPagination,
// 加载状态
loading: false,
// 筛选参数
filters: {
status: '',
keyword: '',
levelId: 0,
categoryIds: [] as number[],
startTime: '',
endTime: '',
},
// 时间快捷选项
timeOptions: [
{ id: 0, name: '全部时间' },
{ id: 1, name: '今天' },
{ id: 2, name: '本周' },
{ id: 3, name: '本月' },
{ id: 4, name: '自定义时间' },
],
// 当前选中的时间选项索引
selectedTimeIndex: 0,
// 自定义时间范围(用于 picker 显示)
customStartTime: '',
customEndTime: '',
},
onLoad() {
// 在 waitLogin 回调中请求接口
app.waitLogin({ type: 1 }).then(() => {
this.fetchLevelList()
this.fetchCategoryList()
this.fetchActivityList()
})
},
// 获取活动等级列表
async fetchLevelList() {
try {
const res = await wx.ajax({
url: '/activity-level/list',
method: 'GET',
data: {},
})
if (res && res.list) {
// 构建 levelTabs,在开头添加"全部"
const levelTabs = [
{
id: 0,
name: '全部',
},
...res.list.map((item: ILevelItem) => ({
id: item.id,
name: item.name,
})),
]
this.setData({
levelList: res.list,
levelTabs,
})
}
} catch (err) {
console.error('获取活动等级列表失败:', err)
}
},
// 获取活动分类列表
async fetchCategoryList() {
try {
const res = await wx.ajax({
url: '/activity-category/list',
method: 'GET',
data: {},
})
if (res && res.list) {
const { selectedCategoryIds } = this.data
// 构建 typeList,在开头添加"全部分类",并计算选中状态
const typeList = [
{
id: 0,
name: '全部分类',
icon: '/images/icon5.png',
iconActive: '/images/icon6.png',
isSelected: selectedCategoryIds.length === 0,
},
...res.list.map((item: ICategoryItem) => ({
id: item.id,
name: item.name,
icon: item.icon || '/images/icon5.png',
iconActive: item.icon || '/images/icon6.png',
isSelected: selectedCategoryIds.includes(item.id),
})),
]
this.setData({
categoryList: res.list,
typeList,
})
}
} catch (err) {
console.error('获取活动分类列表失败:', err)
}
},
// 获取活动列表
async fetchActivityList(isRefresh = false) {
if (this.data.loading) return
const { pagination, filters, selectedCategoryIds, currentLevelIndex, levelTabs } = this.data
const page = isRefresh ? 1 : pagination.page
this.setData({ loading: true })
try {
// 构建请求参数
const params: Record<string, any> = {
page,
pageSize: pagination.pageSize,
}
// 添加筛选参数
if (filters.status) params.status = filters.status
if (filters.keyword) params.keyword = filters.keyword
if (filters.startTime) params.startTime = filters.startTime
if (filters.endTime) params.endTime = filters.endTime
// 等级筛选:当前选中的等级(非"全部等级")
if (currentLevelIndex > 0 && levelTabs[currentLevelIndex]) {
params.levelId = levelTabs[currentLevelIndex].id
}
// 分类筛选:选中的分类 ID 数组(非空时传值)
if (selectedCategoryIds.length > 0) {
params.categoryIds = selectedCategoryIds
}
const res = await wx.ajax({
url: '/activity/list',
method: 'GET',
data: params,
})
if (res) {
const newList = isRefresh ? res.list : [...this.data.activityList, ...res.list]
this.setData({
activityList: newList,
pagination: {
page: res.page || page,
pageSize: res.pageSize || pagination.pageSize,
pages: res.pages || 0,
count: res.count || 0,
},
})
}
} catch (err) {
console.error('获取活动列表失败:', err)
} finally {
this.setData({ loading: false })
}
},
// 切换分类(支持多选)
handleTypeChange(e: WechatMiniprogram.TouchEvent) {
const id = e.currentTarget.dataset.id
let { selectedCategoryIds, typeList } = this.data
// 点击"全部分类"(id=0)时,清空所有选中
if (id === 0) {
selectedCategoryIds = []
} else {
// 点击其他分类时
const index = selectedCategoryIds.indexOf(id)
if (index > -1) {
// 已选中,取消选中
selectedCategoryIds = selectedCategoryIds.filter((item) => item !== id)
} else {
// 未选中,添加选中
selectedCategoryIds = [...selectedCategoryIds, id]
}
}
// 更新 typeList 的选中状态
typeList = typeList.map((item) => ({
...item,
isSelected: item.id === 0 ? selectedCategoryIds.length === 0 : selectedCategoryIds.includes(item.id),
}))
this.setData({
selectedCategoryIds,
typeList,
activityList: [],
pagination: {
page: 1,
pageSize: 20,
pages: 0,
count: 0,
}, },
{ })
name: '文体艺术', this.fetchActivityList(true)
icon: '9', },
iconActive: '10',
// 切换等级 Tab
handleLevelChange(e: WechatMiniprogram.TouchEvent) {
const index = e.currentTarget.dataset.index
if (index === this.data.currentLevelIndex) return
this.setData({
currentLevelIndex: index,
activityList: [],
pagination: {
page: 1,
pageSize: 20,
pages: 0,
count: 0,
}, },
{ })
name: '志愿公益', this.fetchActivityList(true)
icon: '11', },
iconActive: '12',
// 下拉刷新
onPullDownRefresh() {
this.setData({
activityList: [],
pagination: {
page: 1,
pageSize: 20,
pages: 0,
count: 0,
}, },
{ })
name: '创新创业', this.fetchActivityList(true).then(() => {
icon: '13', wx.stopPullDownRefresh()
iconActive: '14', })
},
// 上拉加载更多
onReachBottom() {
const { pagination, loading } = this.data
if (loading || pagination.page >= pagination.pages) return
this.setData({
pagination: {
...pagination,
page: pagination.page + 1,
}, },
], })
this.fetchActivityList()
}, },
onLoad() {},
// 关闭筛选弹窗
handlePopupClose() { handlePopupClose() {
this.setData({ this.setData({
filterShow: false, filterShow: false,
}) })
}, },
// 打开筛选弹窗
handleFilterOpen() {
this.setData({
filterShow: true,
})
},
// 选择时间快捷选项
handleTimeOptionChange(e: WechatMiniprogram.TouchEvent) {
const index = e.currentTarget.dataset.index
this.setData({
selectedTimeIndex: index,
})
// 根据选项设置时间范围
const today = new Date()
const formatDate = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
let startTime = ''
let endTime = ''
switch (index) {
case 0: // 全部时间
startTime = ''
endTime = ''
break
case 1: // 今天
startTime = formatDate(today)
endTime = formatDate(today)
break
case 2: { // 本周
const weekStart = new Date(today)
weekStart.setDate(today.getDate() - today.getDay() + 1)
const weekEnd = new Date(weekStart)
weekEnd.setDate(weekStart.getDate() + 6)
startTime = formatDate(weekStart)
endTime = formatDate(weekEnd)
break
}
case 3: { // 本月
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1)
const monthEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0)
startTime = formatDate(monthStart)
endTime = formatDate(monthEnd)
break
}
case 4: // 自定义时间
// 使用已选择的自定义时间,或清空
startTime = this.data.customStartTime
endTime = this.data.customEndTime
break
}
this.setData({
filters: {
...this.data.filters,
startTime,
endTime,
},
})
},
// 选择开始时间
handleStartTimeChange(e: WechatMiniprogram.PickerChange) {
const date = e.detail.value as string
this.setData({
customStartTime: date,
selectedTimeIndex: 4, // 自动切换到自定义时间
filters: {
...this.data.filters,
startTime: date,
},
})
},
// 选择结束时间
handleEndTimeChange(e: WechatMiniprogram.PickerChange) {
const date = e.detail.value as string
this.setData({
customEndTime: date,
selectedTimeIndex: 4, // 自动切换到自定义时间
filters: {
...this.data.filters,
endTime: date,
},
})
},
// 取消筛选
handleFilterCancel() {
// 重置筛选条件
this.setData({
filterShow: false,
selectedTimeIndex: 0,
customStartTime: '',
customEndTime: '',
filters: {
status: '',
keyword: '',
levelId: 0,
categoryIds: [],
startTime: '',
endTime: '',
},
})
this.fetchActivityList(true)
},
// 确定筛选
handleFilterConfirm() {
this.setData({
filterShow: false,
activityList: [],
pagination: {
page: 1,
pageSize: 20,
pages: 0,
count: 0,
},
})
this.fetchActivityList(true)
},
// 申请活动
handleApply() { handleApply() {
wx.navigateTo({ wx.navigateTo({
url: '/pages/actAdd/index', url: '/pages/actAdd/index',
}) })
}, },
handleDetail() {
// 查看活动详情
handleDetail(e: WechatMiniprogram.TouchEvent) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ wx.navigateTo({
url: '/pages/actDetail/index', url: `/pages/actDetail/index?id=${id}`,
}) })
}, },
// 获取活动状态文本
getStatusText(status: string): string {
const statusMap: Record<string, string> = {
registering: '报名中',
running: '进行中',
ended: '已结束',
}
return statusMap[status] || status
},
}) })
export {} export {}

87
src/pages/act/index.wxml

@ -6,89 +6,122 @@
</view> </view>
<view class="options"> <view class="options">
<view class="tabs"> <view class="tabs">
<view class="tab {{index==0 && 'active'}}" wx:for="{{10}}" wx:key="index">全部</view> <view
class="tab {{index === currentLevelIndex && 'active'}}"
wx:for="{{levelTabs}}"
wx:key="id"
data-index="{{index}}"
bind:tap="handleLevelChange"
>
{{item.name}}
</view>
</view> </view>
<view class="filter-btn"> <view class="filter-btn" bind:tap="handleFilterOpen">
<image class="icon" src="/images/icon4.png"></image> <image class="icon" src="/images/icon4.png"></image>
筛选 筛选
</view> </view>
</view> </view>
</view> </view>
<!-- 分类列表 -->
<view class="type-list"> <view class="type-list">
<view class="type {{index==0 && 'active'}}" wx:for="{{typeList}}" wx:key="index"> <view
<image class="icon" src="/images/icon5.png"></image> class="type {{item.isSelected && 'active'}}"
<image class="icon-active" src="/images/icon6.png"></image> wx:for="{{typeList}}"
<view class="name">全部分类</view> wx:key="id"
data-id="{{item.id}}"
bind:tap="handleTypeChange"
>
<image class="icon" src="{{item.icon}}"></image>
<image class="icon-active" src="{{item.iconActive}}"></image>
<view class="name">{{item.name}}</view>
</view> </view>
</view> </view>
<!-- Banner -->
<swiper class="banner"> <swiper class="banner">
<swiper-item> <swiper-item>
<image class="b-img" mode="aspectFill" src="/images/bg1.png"></image> <image class="b-img" mode="aspectFill" src="/images/bg1.png"></image>
</swiper-item> </swiper-item>
</swiper> </swiper>
<!-- 活动列表 -->
<view class="list"> <view class="list">
<view class="card" wx:for="{{2}}" wx:key="index" bind:tap="handleDetail"> <view
class="card"
wx:for="{{activityList}}"
wx:key="id"
data-id="{{item.id}}"
bind:tap="handleDetail"
>
<view class="photo"> <view class="photo">
<view class="status">进行中</view> <view class="status" wx:if="{{item.activityStatusName}}">{{item.activityStatusName}}</view>
<image class="p-img" src="/images/bg1.png"></image> <image class="p-img" src="{{item.mainImages[0] || '/images/bg1.png'}}" mode="aspectFill"></image>
<view class="user">128人已报名</view> <view class="user" wx:if="{{item.regCount}}">{{item.regCount}}人已报名</view>
</view> </view>
<view class="wrap"> <view class="wrap">
<view class="title"> <view class="title">{{item.name}}</view>
深职大第十五届校园歌手大赛深职大第十五届校园歌手大深职大第十五届校园歌手大深职大第十五届校园歌手大赛赛赛
</view>
<view class="date"> <view class="date">
<image class="icon" src="/images/icon3.png"></image> <image class="icon" src="/images/icon3.png"></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">
<image class="icon" src="/images/icon15.png"></image> <image class="icon" src="/images/icon15.png"></image>
<view class="content">留仙洞校区音乐厅</view> <view class="content">{{item.location}}</view>
</view> </view>
</view> </view>
</view> </view>
<!-- 分页组件 -->
<pagination pagination="{{pagination}}" />
</view> </view>
</view> </view>
<!-- 活动申请按钮 -->
<view class="aside-bar" bind:tap="handleApply"> <view class="aside-bar" bind:tap="handleApply">
<image class="icon" src="/images/icon16.png"></image> <image class="icon" src="/images/icon16.png"></image>
<view class="name">活动申请</view> <view class="name">活动申请</view>
</view> </view>
<!-- 筛选弹窗 -->
<van-popup show="{{ filterShow }}" position="bottom" round bind:close="handlePopupClose"> <van-popup show="{{ filterShow }}" position="bottom" round bind:close="handlePopupClose">
<view class="popup"> <view class="popup">
<view class="popup-line"></view> <view class="popup-line"></view>
<view class="row"> <view class="row">
<view class="r-title">活动开始时间</view> <view class="r-title">活动开始时间</view>
<view class="select"> <view class="select">
<view class="s-item">全部时间</view> <view
<view class="s-item">今天</view> class="s-item {{index === selectedTimeIndex && 'active'}}"
<view class="s-item">本周</view> wx:for="{{timeOptions}}"
<view class="s-item">本月</view> wx:key="id"
<view class="s-item">自定义时间</view> data-index="{{index}}"
bind:tap="handleTimeOptionChange"
>
{{item.name}}
</view>
</view> </view>
</view> </view>
<view class="row"> <view class="row" wx:if="{{selectedTimeIndex === 4}}">
<view class="r-title">时间范围</view> <view class="r-title">时间范围</view>
<view class="date-raneg"> <view class="date-raneg">
<picker class="date" mode="date"> <picker class="date" mode="date" value="{{customStartTime}}" bindchange="handleStartTimeChange">
<view class="content"> <view class="content">
2026-05-28 {{customStartTime || '请选择'}}
<image class="icon" src="/images/icon17.png"></image> <image class="icon" src="/images/icon17.png"></image>
</view> </view>
</picker> </picker>
<view class="line">-</view> <view class="line">-</view>
<picker class="date" mode="date"> <picker class="date" mode="date" value="{{customEndTime}}" bindchange="handleEndTimeChange">
<view class="content"> <view class="content">
2026-05-28 {{customEndTime || '请选择'}}
<image class="icon" src="/images/icon17.png"></image> <image class="icon" src="/images/icon17.png"></image>
</view> </view>
</picker> </picker>
</view> </view>
</view> </view>
<view class="popup-footer"> <view class="popup-footer">
<view class="cancel">取消</view> <view class="cancel" bind:tap="handleFilterCancel">取消</view>
<view class="sure">确定</view> <view class="sure" bind:tap="handleFilterConfirm">确定</view>
</view> </view>
</view> </view>
</van-popup> </van-popup>

3
src/pages/actAdd/index.json

@ -2,6 +2,7 @@
"navigationBarTitleText": "创建活动", "navigationBarTitleText": "创建活动",
"navigationStyle": "default", "navigationStyle": "default",
"usingComponents": { "usingComponents": {
"upload-file": "/components/uploadFile/index" "upload-file": "/components/uploadFile/index",
"van-icon": "@vant/weapp/icon/index"
} }
} }

30
src/pages/actAdd/index.scss

@ -15,6 +15,13 @@ page {
background: #f5f7fa; background: #f5f7fa;
} }
/* ========== 自定义预览样式 ========== */
.upload-preview-image {
width: 100%;
height: 100%;
display: block;
}
/* ========== 步骤条 ========== */ /* ========== 步骤条 ========== */
.step-bar { .step-bar {
flex-shrink: 0; flex-shrink: 0;
@ -284,6 +291,29 @@ page {
} }
} }
.upload-preview {
position: relative;
width: 100%;
height: 350rpx;
border-radius: 16rpx;
.p-img {
display: block;
width: 100%;
height: 100%;
border-radius: 16rpx;
}
.close {
padding: 10rpx;
position: absolute;
top: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 20rpx;
border-radius: 0 16rpx 0 16rpx;
}
}
/* ========== 标签选择 ========== */ /* ========== 标签选择 ========== */
.tag-list { .tag-list {
display: grid; display: grid;

618
src/pages/actAdd/index.ts

@ -1,10 +1,36 @@
const app = getApp<IAppOption>() const app = getApp<IAppOption>()
interface AgendaItem { const DRAFT_KEY = 'actAdd:draft'
interface ILevelItem {
id: number
name: string
code: string
sort: number
}
interface ICategoryItem {
id: number
name: string
code: string
icon: string
sort: number
isEnabled: number
}
interface ITagItem {
id: number id: number
time: string name: string
sort: number
activityCount: number
}
interface AgendaItem {
agendaDate: string
agendaTime: string
title: string title: string
desc: string description: string
sort: number
} }
Page({ Page({
@ -17,25 +43,43 @@ Page({
], ],
// 步骤1 基本信息 // 步骤1 基本信息
coverImageList: [] as Array<{ uid: string, url: string, type: string, name: string, size: number }>, coverImageList: [] as Array<{ uid: string; url: string; type: string; name: string; size: number }>,
title: '', title: '',
type: 1,
typeOptions: [
{ id: 1, name: '讲座' },
{ id: 2, name: '比赛' },
{ id: 3, name: '社团' },
{ id: 4, name: '志愿' },
{ id: 5, name: '文体' },
{ id: 6, name: '其他' },
],
typeOther: '',
summary: '',
startTime: '', startTime: '',
endTime: '', endTime: '',
detail: '', detail: '',
level: '', detailImages: [] as string[],
levels: ['校级', '院级', '系级', '班级', '其他'],
category: '',
categories: ['讲座', '比赛', '社团活动', '志愿服务', '文体活动', '学术交流', '其他'],
location: '', location: '',
organizer: '', organizer: '',
contactName: '', contactName: '',
contactPhone: '', contactPhone: '',
// 步骤2 活动议程 // 活动等级
agendas: [{ id: 1, time: '', title: '', desc: '' }] as AgendaItem[], levelList: [] as ILevelItem[],
nextAgendaId: 2, levelId: 0,
// 活动分类
categoryList: [] as ICategoryItem[],
selectedCategoryIds: [] as number[],
categoryTags: [] as Array<{ id: number; name: string; isSelected: boolean }>, // 用于显示选中状态
// 活动标签
tagList: [] as ITagItem[],
selectedTagIds: [] as number[],
tagTags: [] as Array<{ id: number; name: string; isSelected: boolean }>, // 用于显示选中状态
// 步骤3 报名签到设置 // 步骤2 报名签到设置
needRegister: true, needRegister: true,
registerStartTime: '', registerStartTime: '',
registerEndTime: '', registerEndTime: '',
@ -46,120 +90,446 @@ Page({
checkinWay: 'dynamic', checkinWay: 'dynamic',
checkinStartTime: '', checkinStartTime: '',
checkinEndTime: '', checkinEndTime: '',
// 步骤3 活动议程
agendas: [{ agendaDate: '', agendaTime: '', title: '', description: '', sort: 0 }] as AgendaItem[],
nextAgendaId: 2,
// 提交状态
submitting: false,
}, },
onLoad() { async onLoad() {
app.waitLogin() await app.waitLogin({ type: 1 })
try {
await Promise.all([this.fetchLevelList(), this.fetchCategoryList(), this.fetchTagList()])
} catch (err) {
console.error('初始化数据失败:', err)
}
// 检查是否有本地草稿
try {
const draft = wx.getStorageSync(DRAFT_KEY) as any
if (draft && Object.keys(draft).length) {
wx.showModal({
title: '检测到未完成的活动',
content: '上次未完成的表单已保存,是否继续编辑?',
confirmText: '继续编辑',
cancelText: '重新开始',
success: (res) => {
if (res.confirm) {
this.restoreDraft(draft)
} else {
// 用户选择重新开始,清除草稿
this.clearDraft()
}
},
})
}
} catch (err) {
console.error('读取本地草稿失败:', err)
}
},
// ========== 草稿管理 ==========
buildDraft(partial: Record<string, any> = {}) {
const keys = [
'currentStep',
'coverImageList',
'title',
'type',
'typeOther',
'summary',
'startTime',
'endTime',
'detail',
'detailImages',
'location',
'organizer',
'contactName',
'contactPhone',
'levelId',
'selectedCategoryIds',
'selectedTagIds',
'needRegister',
'registerStartTime',
'registerEndTime',
'registerLimit',
'registerLimitCount',
'registerCondition',
'checkinWay',
'checkinStartTime',
'checkinEndTime',
'agendas',
]
const draft: Record<string, any> = {}
keys.forEach((k) => {
if (Object.prototype.hasOwnProperty.call(partial, k)) {
draft[k] = partial[k]
} else {
draft[k] = (this.data as any)[k]
}
})
return draft
},
saveDraft(partial: Record<string, any> = {}) {
try {
const draft = this.buildDraft(partial)
wx.setStorageSync(DRAFT_KEY, draft)
} catch (err) {
console.error('保存草稿失败:', err)
}
},
clearDraft() {
try {
wx.removeStorageSync(DRAFT_KEY)
} catch {
/* ignore */
}
},
restoreDraft(draft: Record<string, any>) {
if (!draft) return
const safeDraft = { ...draft }
safeDraft.coverImageList = safeDraft.coverImageList || []
safeDraft.detailImages = safeDraft.detailImages || []
safeDraft.agendas = safeDraft.agendas || []
safeDraft.selectedCategoryIds = safeDraft.selectedCategoryIds || []
safeDraft.selectedTagIds = safeDraft.selectedTagIds || []
this.setData(safeDraft)
// 重建标签选中状态显示
const categoryTags = (this.data.categoryList || []).map((item: ICategoryItem) => ({
id: item.id,
name: item.name,
isSelected: safeDraft.selectedCategoryIds.includes(item.id),
}))
const tagTags = (this.data.tagList || []).map((item: ITagItem) => ({
id: item.id,
name: item.name,
isSelected: safeDraft.selectedTagIds.includes(item.id),
}))
this.setData({ categoryTags, tagTags })
},
setAndSave(patch: Record<string, any>) {
this.setData(patch)
this.saveDraft(patch)
},
// 获取活动等级列表
async fetchLevelList() {
try {
const res = await wx.ajax({
url: '/activity-level/list',
method: 'GET',
data: {},
})
if (res && res.list) {
this.setData({ levelList: res.list })
}
} catch (err) {
console.error('获取活动等级列表失败:', err)
}
},
// 获取活动分类列表
async fetchCategoryList() {
try {
const res = await wx.ajax({
url: '/activity-category/list',
method: 'GET',
data: {},
})
if (res && res.list) {
const { selectedCategoryIds } = this.data
const categoryTags = res.list.map((item: ICategoryItem) => ({
id: item.id,
name: item.name,
isSelected: selectedCategoryIds.includes(item.id),
}))
this.setData({
categoryList: res.list,
categoryTags,
})
}
} catch (err) {
console.error('获取活动分类列表失败:', err)
}
},
// 获取活动标签列表
async fetchTagList() {
try {
const res = await wx.ajax({
url: '/activity-tag/list',
method: 'GET',
data: {},
})
if (res && res.list) {
const { selectedTagIds } = this.data
const tagTags = res.list.map((item: ITagItem) => ({
id: item.id,
name: item.name,
isSelected: selectedTagIds.includes(item.id),
}))
this.setData({
tagList: res.list,
tagTags,
})
}
} catch (err) {
console.error('获取活动标签列表失败:', err)
}
}, },
// ========== 步骤切换 ========== // ========== 步骤切换 ==========
goStep(step: number) { goStep(step: number) {
if (step < 1 || step > 4) return if (step < 1 || step > 4) return
this.setData({ currentStep: step }) this.setAndSave({ currentStep: step })
},
// 验证当前步骤的必填项
validateCurrentStep(): boolean {
const {
currentStep,
coverImageList,
title,
startTime,
endTime,
location,
needRegister,
registerStartTime,
registerEndTime,
checkinWay,
checkinStartTime,
checkinEndTime,
agendas,
} = this.data
// 步骤1:基本信息
if (currentStep === 1) {
if (!coverImageList.length) {
wx.showToast({ title: '请上传活动头图', icon: 'none' })
return false
}
if (!title.trim()) {
wx.showToast({ title: '请输入活动标题', icon: 'none' })
return false
}
if (!startTime) {
wx.showToast({ title: '请选择活动开始时间', icon: 'none' })
return false
}
if (!endTime) {
wx.showToast({ title: '请选择活动结束时间', icon: 'none' })
return false
}
if (!location.trim()) {
wx.showToast({ title: '请输入活动地点', icon: 'none' })
return false
}
}
// 步骤2:报名签到设置
if (currentStep === 2) {
if (needRegister) {
if (!registerStartTime) {
wx.showToast({ title: '请选择报名开始时间', icon: 'none' })
return false
}
if (!registerEndTime) {
wx.showToast({ title: '请选择报名截止时间', icon: 'none' })
return false
}
}
// 签到时间校验(动态二维码或固定二维码时必填)
if (checkinWay !== 'none') {
if (!checkinStartTime) {
wx.showToast({ title: '请选择签到开始时间', icon: 'none' })
return false
}
if (!checkinEndTime) {
wx.showToast({ title: '请选择签到结束时间', icon: 'none' })
return false
}
}
}
// 步骤3:活动议程
if (currentStep === 3) {
if (!agendas.length || !agendas[0].title.trim()) {
wx.showToast({ title: '请添加活动议程', icon: 'none' })
return false
}
}
return true
}, },
onNextStep() { onNextStep() {
// 先验证当前步骤
if (!this.validateCurrentStep()) {
return
}
const next = this.data.currentStep + 1 const next = this.data.currentStep + 1
if (next <= 4) this.setData({ currentStep: next }) if (next <= 4) this.setAndSave({ currentStep: next })
}, },
onPrevStep() { onPrevStep() {
const prev = this.data.currentStep - 1 const prev = this.data.currentStep - 1
if (prev >= 1) this.setData({ currentStep: prev }) if (prev >= 1) this.setAndSave({ currentStep: prev })
}, },
// ========== 图片上传 ========== // ========== 图片上传 ==========
onCoverUploadSuccess(e: WechatMiniprogram.CustomEvent) { // 上传成功后,直接添加到列表(maxCount=1,只保留一个)
onCoverSuccess(e: WechatMiniprogram.CustomEvent) {
const { file } = e.detail const { file } = e.detail
this.setData({ coverImageList: [file] }) this.setAndSave({ coverImageList: [file] })
},
// 上传失败后,显示错误
onCoverError(_e: WechatMiniprogram.CustomEvent) {
wx.showToast({ title: '上传失败,请重试', icon: 'none' })
},
// 删除封面图片
handleDelCover() {
this.setAndSave({ coverImageList: [] })
}, },
onCoverRemove(_e: WechatMiniprogram.CustomEvent) { onDetailImageSuccess(e: WechatMiniprogram.CustomEvent) {
this.setData({ coverImageList: [] }) const { urls } = e.detail
this.setAndSave({ detailImages: urls })
}, },
// ========== 输入绑定 ========== // ========== 输入绑定 ==========
onInputChange(e: WechatMiniprogram.Input) { onInputChange(e: WechatMiniprogram.Input) {
const { field } = e.currentTarget.dataset const { field } = e.currentTarget.dataset
this.setData({ [field]: e.detail.value }) this.setAndSave({ [field]: e.detail.value })
}, },
onTextareaChange(e: WechatMiniprogram.TextareaInput) { onTextareaChange(e: WechatMiniprogram.TextareaInput) {
const { field } = e.currentTarget.dataset const { field } = e.currentTarget.dataset
this.setData({ [field]: e.detail.value }) this.setAndSave({ [field]: e.detail.value })
}, },
// ========== 时间选择 ========== // ========== 时间选择 ==========
onPickTime(e: WechatMiniprogram.PickerChange) { onPickTime(e: WechatMiniprogram.PickerChange) {
const { field } = e.currentTarget.dataset const { field } = e.currentTarget.dataset
this.setData({ [field]: e.detail.value }) this.setAndSave({ [field]: e.detail.value })
}, },
// ========== 标签选择 ========== // ========== 活动类型选择 ==========
onSelectLevel(e: WechatMiniprogram.TouchEvent) { onSelectType(e: WechatMiniprogram.TouchEvent) {
const { value } = e.currentTarget.dataset const { value } = e.currentTarget.dataset
this.setData({ level: value }) this.setAndSave({ type: value })
},
// ========== 活动等级选择 ==========
onSelectLevel(e: WechatMiniprogram.TouchEvent) {
const { id } = e.currentTarget.dataset
this.setAndSave({ levelId: id })
}, },
// ========== 活动分类选择(多选) ==========
onSelectCategory(e: WechatMiniprogram.TouchEvent) { onSelectCategory(e: WechatMiniprogram.TouchEvent) {
const { value } = e.currentTarget.dataset const { id } = e.currentTarget.dataset
this.setData({ category: value }) let { selectedCategoryIds, categoryTags } = this.data
const index = selectedCategoryIds.indexOf(id)
if (index > -1) {
selectedCategoryIds = selectedCategoryIds.filter((item) => item !== id)
} else {
selectedCategoryIds = [...selectedCategoryIds, id]
}
categoryTags = categoryTags.map((item) => ({
...item,
isSelected: selectedCategoryIds.includes(item.id),
}))
this.setAndSave({ selectedCategoryIds, categoryTags })
},
// ========== 活动标签选择(多选) ==========
onSelectTag(e: WechatMiniprogram.TouchEvent) {
const { id } = e.currentTarget.dataset
let { selectedTagIds, tagTags } = this.data
const index = selectedTagIds.indexOf(id)
if (index > -1) {
selectedTagIds = selectedTagIds.filter((item) => item !== id)
} else {
selectedTagIds = [...selectedTagIds, id]
}
tagTags = tagTags.map((item) => ({
...item,
isSelected: selectedTagIds.includes(item.id),
}))
this.setAndSave({ selectedTagIds, tagTags })
}, },
// ========== 议程管理 ========== // ========== 议程管理 ==========
onAddAgenda() { onAddAgenda() {
const agendas = this.data.agendas const agendas = this.data.agendas
agendas.push({ agendas.push({
id: this.data.nextAgendaId, agendaDate: '',
time: '', agendaTime: '',
title: '', title: '',
desc: '', description: '',
}) sort: agendas.length,
this.setData({
agendas,
nextAgendaId: this.data.nextAgendaId + 1,
}) })
this.setAndSave({ agendas })
}, },
onRemoveAgenda(e: WechatMiniprogram.TouchEvent) { onRemoveAgenda(e: WechatMiniprogram.TouchEvent) {
const { index } = e.currentTarget.dataset const { index } = e.currentTarget.dataset
const agendas = this.data.agendas.filter((_, i) => i !== index) const agendas = this.data.agendas.filter((_, i) => i !== index)
this.setData({ agendas }) this.setAndSave({ agendas })
}, },
onAgendaInput(e: WechatMiniprogram.Input | WechatMiniprogram.TextareaInput) { onAgendaInput(e: WechatMiniprogram.Input | WechatMiniprogram.TextareaInput) {
const { index, field } = e.currentTarget.dataset const { index, field } = e.currentTarget.dataset
const agendas = this.data.agendas const agendas = this.data.agendas
agendas[index][field] = e.detail.value agendas[index][field] = e.detail.value
this.setData({ agendas }) this.setAndSave({ agendas })
}, },
onAgendaTime(e: WechatMiniprogram.PickerChange) { onAgendaTime(e: WechatMiniprogram.PickerChange) {
const { index } = e.currentTarget.dataset const { index, field } = e.currentTarget.dataset
const agendas = this.data.agendas const agendas = this.data.agendas
agendas[index].time = e.detail.value as string agendas[index][field] = e.detail.value as string
this.setData({ agendas }) this.setAndSave({ agendas })
}, },
// ========== 报名签到设置 ========== // ========== 报名签到设置 ==========
onToggleRegister(e: WechatMiniprogram.TouchEvent) { onToggleRegister(e: WechatMiniprogram.TouchEvent) {
const { value } = e.currentTarget.dataset const { value } = e.currentTarget.dataset
this.setData({ needRegister: value === 'yes' }) this.setAndSave({ needRegister: value === 'yes' })
}, },
onToggleRegisterLimit(e: WechatMiniprogram.TouchEvent) { onToggleRegisterLimit(e: WechatMiniprogram.TouchEvent) {
const { value } = e.currentTarget.dataset const { value } = e.currentTarget.dataset
this.setData({ registerLimit: value }) this.setAndSave({ registerLimit: value })
}, },
onSelectCheckinWay(e: WechatMiniprogram.TouchEvent) { onSelectCheckinWay(e: WechatMiniprogram.TouchEvent) {
const { value } = e.currentTarget.dataset const { value } = e.currentTarget.dataset
this.setData({ checkinWay: value }) this.setAndSave({ checkinWay: value })
}, },
// ========== 底部操作 ========== // ========== 底部操作 ==========
onSaveDraft() { onSaveDraft() {
wx.showToast({ title: '已保存草稿', icon: 'success' }) this.submitActivity(1) // activityStatus = 1 (草稿)
}, },
onSubmit() { onSubmit() {
@ -168,13 +538,169 @@ Page({
content: '提交后将进入审核流程,是否继续?', content: '提交后将进入审核流程,是否继续?',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
this.setData({ currentStep: 4 }) this.submitActivity(2) // activityStatus = 2 (待审核)
} }
}, },
}) })
}, },
// 提交活动申请
async submitActivity(activityStatus: number) {
const {
coverImageList,
title,
type,
typeOther,
summary,
startTime,
endTime,
location,
organizer,
contactName,
contactPhone,
levelId,
selectedCategoryIds,
selectedTagIds,
needRegister,
registerStartTime,
registerEndTime,
registerLimit,
registerLimitCount,
registerCondition,
checkinWay,
checkinStartTime,
checkinEndTime,
agendas,
detailImages,
submitting,
} = this.data
if (submitting) return
// 校验必填字段
if (!coverImageList.length) {
wx.showToast({ title: '请上传活动头图', icon: 'error' })
return
}
if (!title.trim()) {
wx.showToast({ title: '请输入活动标题', icon: 'error' })
return
}
if (!startTime) {
wx.showToast({ title: '请选择活动开始时间', icon: 'error' })
return
}
if (!endTime) {
wx.showToast({ title: '请选择活动结束时间', icon: 'error' })
return
}
if (!location.trim()) {
wx.showToast({ title: '请输入活动地点', icon: 'error' })
return
}
// 校验报名设置
if (needRegister) {
if (!registerStartTime) {
wx.showToast({ title: '请选择报名开始时间', icon: 'error' })
return
}
if (!registerEndTime) {
wx.showToast({ title: '请选择报名截止时间', icon: 'error' })
return
}
}
// 校验签到设置
if (checkinWay !== 'none') {
if (!checkinStartTime) {
wx.showToast({ title: '请选择签到开始时间', icon: 'error' })
return
}
if (!checkinEndTime) {
wx.showToast({ title: '请选择签到结束时间', icon: 'error' })
return
}
}
// 校验议程
if (!agendas.length || !agendas[0].title.trim()) {
wx.showToast({ title: '请添加活动议程', icon: 'error' })
return
}
this.setData({ submitting: true })
wx.showLoading({ title: activityStatus === 1 ? '保存中...' : '提交中...' })
try {
const checkinTypeMap: Record<string, number> = {
dynamic: 1,
fixed: 2,
none: 3,
}
const params: Record<string, any> = {
mainImages: coverImageList.map((item) => item.url),
name: title,
type,
typeOther: type === 6 ? typeOther : '',
summary,
detailImages,
checkinType: checkinTypeMap[checkinWay],
regType: needRegister ? 1 : 2,
regCondition: registerCondition,
quota: registerLimit === 'limited' ? Number(registerLimitCount) : 0,
regStartAt: needRegister ? registerStartTime : '',
regEndAt: needRegister ? registerEndTime : '',
startAt: startTime,
endAt: endTime,
location,
organizer,
contactName,
contactPhone,
tagIds: selectedTagIds,
categoryIds: selectedCategoryIds,
levelId,
agendas: agendas.map((item, index) => ({
...item,
sort: index,
})),
activityStatus,
}
const res = await wx.ajax({
url: '/activity/apply',
method: 'POST',
data: params,
})
wx.hideLoading()
if (res) {
wx.showToast({
title: activityStatus === 1 ? '已保存草稿' : '提交成功',
icon: 'success',
})
// 清理本地草稿
this.clearDraft()
// 跳转到结果页面
wx.redirectTo({
url: `/pages/actAddResult/index?id=${res.activityId}&status=${res.status}`,
})
}
} catch (err: any) {
wx.hideLoading()
const message = err?.message || '提交失败'
wx.showToast({ title: message, icon: 'error' })
} finally {
this.setData({ submitting: false })
}
},
onGoHome() { onGoHome() {
this.clearDraft()
wx.switchTab({ url: '/pages/index/index' }) wx.switchTab({ url: '/pages/index/index' })
}, },
}) })

75
src/pages/actAdd/index.wxml

@ -26,19 +26,18 @@
<text>活动头图</text> <text>活动头图</text>
<text class="required">*</text> <text class="required">*</text>
</view> </view>
<upload-file <upload-file maxCount="{{1}}" accept="{{['image']}}" useSlot="{{true}}" fileList="{{coverImageList}}" bind:success="onCoverSuccess" bind:error="onCoverError">
maxCount="{{1}}"
accept="{{['image']}}"
useSlot="{{true}}"
fileList="{{coverImageList}}"
bind:success="onCoverUploadSuccess"
bind:remove="onCoverRemove"
>
<view slot="upload-area" class="upload-inner"> <view slot="upload-area" class="upload-inner">
<image class="upload-camera" src="/images/icon35.png"></image> <image class="upload-camera" src="/images/icon35.png"></image>
<text class="upload-text">点击上传</text> <text class="upload-text">点击上传</text>
</view> </view>
</upload-file> </upload-file>
<view class="upload-preview" wx:if="{{coverImageList.length}}" bind:tap="handleDelCover">
<image class="p-img" src="{{coverImageList[0].url}}" mode="aspectFill"></image>
<view class="close">
<van-icon name="cross" />
</view>
</view>
</view> </view>
<view class="form-field"> <view class="form-field">
@ -65,13 +64,13 @@
<text>活动时间</text> <text>活动时间</text>
<text class="required">*</text> <text class="required">*</text>
</view> </view>
<picker mode="date" value="{{startTime}}" data-field="startTime" bindchange="onPickTime"> <picker mode="date" value="{{startTime}}" end="{{endTime}}" data-field="startTime" bindchange="onPickTime">
<view class="form-picker"> <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="/images/icon17.png"></image> <image class="picker-icon" src="/images/icon17.png"></image>
</view> </view>
</picker> </picker>
<picker mode="date" value="{{endTime}}" data-field="endTime" bindchange="onPickTime"> <picker mode="date" value="{{endTime}}" start="{{startTime}}" data-field="endTime" bindchange="onPickTime">
<view class="form-picker"> <view class="form-picker">
<text class="picker-value {{endTime ? '' : 'is-placeholder'}}">{{endTime || '请选择结束时间'}}</text> <text class="picker-value {{endTime ? '' : 'is-placeholder'}}">{{endTime || '请选择结束时间'}}</text>
<image class="picker-icon" src="/images/icon17.png"></image> <image class="picker-icon" src="/images/icon17.png"></image>
@ -105,13 +104,13 @@
<view class="form-label">活动等级</view> <view class="form-label">活动等级</view>
<view class="tag-list"> <view class="tag-list">
<view <view
class="tag {{level === item ? 'is-active' : ''}}" class="tag {{levelId === item.id ? 'is-active' : ''}}"
wx:for="{{levels}}" wx:for="{{levelList}}"
wx:key="*this" wx:key="id"
bindtap="onSelectLevel" bindtap="onSelectLevel"
data-value="{{item}}" data-id="{{item.id}}"
> >
{{item}} {{item.name}}
</view> </view>
</view> </view>
</view> </view>
@ -120,19 +119,22 @@
<view class="form-label">活动分类</view> <view class="form-label">活动分类</view>
<view class="tag-list"> <view class="tag-list">
<view <view
class="tag {{category === item ? 'is-active' : ''}}" class="tag {{item.isSelected ? 'is-active' : ''}}"
wx:for="{{categories}}" wx:for="{{categoryTags}}"
wx:key="*this" wx:key="id"
bindtap="onSelectCategory" bindtap="onSelectCategory"
data-value="{{item}}" data-id="{{item.id}}"
> >
{{item}} {{item.name}}
</view> </view>
</view> </view>
</view> </view>
<view class="form-field"> <view class="form-field">
<view class="form-label">活动地点</view> <view class="form-label">
活动地点
<text class="required">*</text>
</view>
<view class="input-wrap"> <view class="input-wrap">
<input <input
class="form-input" class="form-input"
@ -237,7 +239,7 @@
<text>报名时间</text> <text>报名时间</text>
<text class="required">*</text> <text class="required">*</text>
</view> </view>
<picker mode="date" value="{{registerStartTime}}" data-field="registerStartTime" bindchange="onPickTime"> <picker mode="date" value="{{registerStartTime}}" end="{{registerEndTime}}" data-field="registerStartTime" bindchange="onPickTime">
<view class="form-picker"> <view class="form-picker">
<view class="picker-value {{registerStartTime ? '' : 'is-placeholder'}}"> <view class="picker-value {{registerStartTime ? '' : 'is-placeholder'}}">
{{registerStartTime || '请选择开始时间'}} {{registerStartTime || '请选择开始时间'}}
@ -245,7 +247,7 @@
<image class="picker-icon" src="/images/icon17.png"></image> <image class="picker-icon" src="/images/icon17.png"></image>
</view> </view>
</picker> </picker>
<picker mode="date" value="{{registerEndTime}}" data-field="registerEndTime" bindchange="onPickTime"> <picker mode="date" value="{{registerEndTime}}" start="{{registerStartTime}}" data-field="registerEndTime" bindchange="onPickTime">
<view class="form-picker"> <view class="form-picker">
<view class="picker-value {{registerEndTime ? '' : 'is-placeholder'}}"> <view class="picker-value {{registerEndTime ? '' : 'is-placeholder'}}">
{{registerEndTime || '请选择结束时间'}} {{registerEndTime || '请选择结束时间'}}
@ -361,7 +363,7 @@
<text>签到时间</text> <text>签到时间</text>
<text class="required">*</text> <text class="required">*</text>
</view> </view>
<picker mode="date" value="{{checkinStartTime}}" data-field="checkinStartTime" bindchange="onPickTime"> <picker mode="date" value="{{checkinStartTime}}" end="{{checkinEndTime}}" data-field="checkinStartTime" bindchange="onPickTime">
<view class="form-picker"> <view class="form-picker">
<view class="picker-value {{checkinStartTime ? '' : 'is-placeholder'}}"> <view class="picker-value {{checkinStartTime ? '' : 'is-placeholder'}}">
{{checkinStartTime || '请选择签到开始时间'}} {{checkinStartTime || '请选择签到开始时间'}}
@ -369,7 +371,7 @@
<image class="picker-icon" src="/images/icon17.png"></image> <image class="picker-icon" src="/images/icon17.png"></image>
</view> </view>
</picker> </picker>
<picker mode="date" value="{{checkinEndTime}}" data-field="checkinEndTime" bindchange="onPickTime"> <picker mode="date" value="{{checkinEndTime}}" start="{{checkinStartTime}}" data-field="checkinEndTime" bindchange="onPickTime">
<view class="form-picker"> <view class="form-picker">
<view class="picker-value {{checkinEndTime ? '' : 'is-placeholder'}}"> <view class="picker-value {{checkinEndTime ? '' : 'is-placeholder'}}">
{{checkinEndTime || '请选择签到结束时间'}} {{checkinEndTime || '请选择签到结束时间'}}
@ -424,9 +426,17 @@
<text>议程时间</text> <text>议程时间</text>
<text class="required">*</text> <text class="required">*</text>
</view> </view>
<picker mode="date" value="{{item.time}}" data-index="{{index}}" bindchange="onAgendaTime"> <picker
mode="date"
value="{{item.agendaTime}}"
data-index="{{index}}"
data-field="agendaTime"
bindchange="onAgendaTime"
>
<view class="form-picker"> <view class="form-picker">
<text class="picker-value {{item.time ? '' : 'is-placeholder'}}">{{item.time || '请选择时间'}}</text> <view class="picker-value {{item.agendaTime ? '' : 'is-placeholder'}}">
{{item.agendaTime || '请选择时间'}}
</view>
<image class="picker-icon" src="/images/icon17.png"></image> <image class="picker-icon" src="/images/icon17.png"></image>
</view> </view>
</picker> </picker>
@ -477,15 +487,4 @@
</view> </view>
</block> </block>
<!-- ========== 步骤4 完成 ========== -->
<block wx:if="{{currentStep === 4}}">
<view class="result-page">
<view class="result-circle">
<text class="result-check">✓</text>
</view>
<text class="result-title">提交成功</text>
<text class="result-desc">活动已提交审核,请耐心等待</text>
<view class="btn btn-primary btn--large" bindtap="onGoHome">返回首页</view>
</view>
</block>
</view> </view>

52
src/pages/actAddResult/index.ts

@ -1,8 +1,52 @@
const _app = getApp<IAppOption>(); const app = getApp<IAppOption>()
Page({ Page({
data: {}, data: {
onLoad() {}, activityId: 0,
}); status: '', // pending | draft
qrCodeUrl: '', // 公众号二维码 URL
},
onLoad(options: { id?: string; status?: string }) {
const activityId = options.id ? Number(options.id) : 0
const status = options.status || ''
this.setData({
activityId,
status,
})
app.waitLogin({ type: 1 }).then(() => {
// 获取用户信息,包括公众号二维码
this.getUserProfile()
})
},
// 获取用户信息
async getUserProfile() {
const res = await wx.ajax({
url: '/me/profile',
method: 'GET',
})
this.setData({
qrCodeUrl: res.wechatSubscribe.qrCodeUrl,
})
},
// 继续发布
handleContinue() {
wx.redirectTo({
url: '/pages/actAdd/index',
})
},
// 返回活动页
handleBack() {
wx.switchTab({
url: '/pages/act/index',
})
},
})
export {} export {}

4
src/pages/actAddResult/index.wxml

@ -2,12 +2,12 @@
<image class="status-icon" src="/images/icon33.png"></image> <image class="status-icon" src="/images/icon33.png"></image>
<view class="status">申请提交成功</view> <view class="status">申请提交成功</view>
<view class="content">我们将会在 7 个工作日内完成审核,结果将通过公 众号发送给你,请注意查收~</view> <view class="content">我们将会在 7 个工作日内完成审核,结果将通过公 众号发送给你,请注意查收~</view>
<view class="btn">继续发布</view> <view class="btn" bind:tap="handleContinue">继续发布</view>
<view class="back">返回活动页</view> <view class="back">返回活动页</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">
<image class="code" src="/images/bg1.png" show-menu-by-longpress></image> <image class="code" src="{{qrCodeUrl}}" show-menu-by-longpress></image>
<view class="tip">长按识别二维码</view> <view class="tip">长按识别二维码</view>
</view> </view>
</view> </view>

33
src/pages/actDetail/index.scss

@ -13,7 +13,7 @@ page {
.page { .page {
min-height: 100vh; min-height: 100vh;
box-sizing: border-box; box-sizing: border-box;
padding-bottom: 280rpx; padding-bottom: 320rpx;
.info-card { .info-card {
margin: 349rpx 30rpx 0; margin: 349rpx 30rpx 0;
padding: 48rpx 30rpx 40rpx; padding: 48rpx 30rpx 40rpx;
@ -83,9 +83,9 @@ page {
.row { .row {
margin-top: 34rpx; margin-top: 34rpx;
display: flex; display: flex;
align-items: center;
gap: 12rpx; gap: 12rpx;
.icon { .icon {
margin-top: 4rpx;
flex-shrink: 0; flex-shrink: 0;
width: 36rpx; width: 36rpx;
height: 36rpx; height: 36rpx;
@ -418,6 +418,35 @@ page {
} }
.upload-list { .upload-list {
margin-top: 20rpx; margin-top: 20rpx;
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.preview-list {
display: contents;
.preview-item {
position: relative;
width: 108rpx;
height: 108rpx;
border-radius: 16rpx;
overflow: hidden;
.p-img {
display: block;
width: 100%;
height: 100%;
border-radius: 16rpx;
}
.close {
padding: 10rpx;
position: absolute;
top: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 20rpx;
border-radius: 0 16rpx 0 16rpx;
}
}
}
.upload { .upload {
width: 108rpx; width: 108rpx;
height: 108rpx; height: 108rpx;

501
src/pages/actDetail/index.ts

@ -1,33 +1,510 @@
const _app = getApp<IAppOption>() const app = getApp<IAppOption>()
interface IActivityDetail {
id: number
name: string
type: number
typeOther: string
summary: string
description: string
regType: number
regCondition: string
contactName: string
contactPhone: string
mainImages: string[]
detailImages: string[]
regStartAt: string
regEndAt: string
startAt: string
endAt: string
location: string
organizer: string
status: string
checkinType: number
checkinStartAt: string
checkinEndAt: string
quota: number
regCount: number
viewUserCount: number
viewCount: number
checkinCount: number
commentCount: number
shareCount: number
collectCount: number
tags: string[]
levelId: number
levelName: string
categoryIds: number[]
categoryNames: string[]
isRegistered: boolean
isCheckedIn: boolean
isFavorited: boolean
isReviewed: boolean
countdownSeconds: number
registrationList: Array<{
userId: number
avatarUrl: string
nickname: string
realName: string
registeredAt: string
}>
agendas: Array<{
id: number
agendaDate: string
agendaTime: string
title: string
description: string
sort: number
}>
}
interface IReviewItem {
id: number
userId: number
nickname: string
avatarUrl: string
rating: string
content: string
images: string[]
isAnonymous: boolean
likeCount: number
isLiked: boolean
createdAt: string
auditStatus: string
}
interface IPagination {
page: number
pageSize: number
total: number
totalPages: number
}
Page({ Page({
data: { data: {
activityId: 0,
detail: null as IActivityDetail | null,
loading: true,
// 倒计时
countdownSeconds: 0,
timeData: { days: 0, hours: 0, minutes: 0, seconds: 0 },
// 弹窗
popupShow: false, popupShow: false,
popupType: 'popup1', // 签到成功弹窗 popupType: 'popup1',
popupParams: {} as any, popupParams: {} as any,
// 评论
commentShow: false, commentShow: false,
commentRating: 5,
commentContent: '',
commentImages: [] as Array<{
uid: string
url: string
type: string
name: string
size: number
status: 'pending' | 'uploading' | 'success' | 'error'
progress: number
}>,
commentAnonymous: false,
// 评价列表
reviewList: [] as IReviewItem[],
reviewPagination: {
page: 1,
pageSize: 10,
total: 0,
totalPages: 0,
} as IPagination,
reviewLoading: false,
}, },
onLoad() {},
handlePopupOk() { onLoad(options: { id?: string }) {
const { popupType } = this.data const activityId = options.id ? Number(options.id) : 0
if (popupType === 'argument') { if (!activityId) {
wx.showToast({ title: '活动不存在', icon: 'error' })
setTimeout(() => wx.navigateBack(), 1500)
return
} }
this.setData({ activityId })
app.waitLogin({ type: 1 }).then(() => {
this.fetchActivityDetail()
this.fetchReviewList()
})
}, },
handlePopupCancel() {
const { popupType } = this.data // 获取活动详情
if (popupType === 'conformBindDoctorConform') { async fetchActivityDetail() {
try {
const res = await wx.ajax({
url: `/activity/detail?id=${this.data.activityId}`,
method: 'GET',
data: {},
})
if (res) {
this.setData({
detail: res,
countdownSeconds: res.countdownSeconds || 0,
loading: false,
})
}
} catch (err) {
console.error('获取活动详情失败:', err)
this.setData({ loading: false })
}
},
// 获取评价列表
async fetchReviewList(isRefresh = false) {
if (this.data.reviewLoading) return
const { reviewPagination } = this.data
const page = isRefresh ? 1 : reviewPagination.page
this.setData({ reviewLoading: true })
try {
const res = await wx.ajax({
url: `/activity/review-list?activityId=${this.data.activityId}`,
method: 'GET',
data: { page, pageSize: reviewPagination.pageSize },
})
if (res) {
const newList = isRefresh ? res.list : [...this.data.reviewList, ...res.list]
this.setData({
reviewList: newList,
reviewPagination: {
page: res.pagination?.page || page,
pageSize: res.pagination?.pageSize || reviewPagination.pageSize,
total: res.pagination?.total || 0,
totalPages: res.pagination?.totalPages || 0,
},
})
}
} catch (err) {
console.error('获取评价列表失败:', err)
} finally {
this.setData({ reviewLoading: false })
} }
},
// 倒计时变化
handleTimeChange(e: WechatMiniprogram.CustomEvent) {
this.setData({ timeData: e.detail })
},
// 倒计时结束
handleTimeFinish() {
this.setData({ countdownSeconds: 0 })
this.fetchActivityDetail()
},
// 一键报名
async handleRegister() {
const { detail } = this.data
if (!detail) return
// 检查登录状态
const accessToken = app.globalData.accessToken
if (!accessToken) {
wx.showToast({ title: '请先登录', icon: 'error' })
return
}
try {
wx.showLoading({ title: '报名中...' })
const res = await wx.ajax({
url: `/activity/register?id=${this.data.activityId}`,
method: 'POST',
data: {},
})
wx.hideLoading()
if (res) {
wx.showToast({ title: '报名成功', icon: 'success' })
// 更新状态
this.setData({
detail: {
...detail,
isRegistered: true,
regCount: detail.regCount + 1,
},
})
// 跳转到报名成功页面
wx.navigateTo({
url: `/pages/actResult/index?id=${this.data.activityId}`,
})
}
} catch (err: any) {
wx.hideLoading()
const message = err?.message || '报名失败'
wx.showToast({ title: message, icon: 'error' })
}
},
// 签到
async handleCheckin() {
const { detail } = this.data
if (!detail) return
// 检查登录状态
const accessToken = app.globalData.accessToken
if (!accessToken) {
wx.showToast({ title: '请先登录', icon: 'error' })
return
}
try {
wx.showLoading({ title: '签到中...' })
const res = await wx.ajax({
url: `/activity/checkin?id=${this.data.activityId}`,
method: 'POST',
data: {},
})
wx.hideLoading()
if (res) {
// 显示签到成功弹窗
this.setData({
popupShow: true,
popupType: 'checkinSuccess',
popupParams: { checkedAt: res.checkedAt },
})
// 更新状态
this.setData({
detail: {
...detail,
isCheckedIn: true,
checkinCount: detail.checkinCount + 1,
},
})
}
} catch (err: any) {
wx.hideLoading()
const message = err?.message || '签到失败'
wx.showToast({ title: message, icon: 'error' })
}
},
// 分享
async handleShare() {
const { detail } = this.data
if (!detail) return
// 上报分享
try {
await wx.ajax({
url: `/activity/share?id=${this.data.activityId}`,
method: 'POST',
data: { channel: 'friend' },
})
} catch (err) {
console.error('上报分享失败:', err)
}
},
// 打开评论弹窗
handleOpenComment() {
this.setData({ this.setData({
popupShow: false, commentShow: true,
popupType: 'i', commentRating: 5,
commentContent: '',
commentImages: [],
commentAnonymous: false,
})
},
// 评论评分变化
onCommentRatingChange(e: WechatMiniprogram.CustomEvent) {
this.setData({ commentRating: e.detail })
},
// 评论内容变化
onCommentContentChange(e: WechatMiniprogram.TextareaInput) {
this.setData({ commentContent: e.detail.value })
},
// 评论图片上传成功
onCommentImageSuccess(e: WechatMiniprogram.CustomEvent) {
const { file } = e.detail
console.log('上传成功', file)
// 添加上传成功的图片到列表
this.setData({
commentImages: [...this.data.commentImages, file],
}) })
}, },
// 评论图片上传失败
onCommentImageError(e: WechatMiniprogram.CustomEvent) {
const { file, error } = e.detail
console.log('上传失败', file, error)
wx.showToast({ title: '图片上传失败', icon: 'none' })
},
// 删除评论图片
onRemoveCommentImage(e: WechatMiniprogram.TouchEvent) {
const index = e.currentTarget.dataset.index
const commentImages = [...this.data.commentImages]
commentImages.splice(index, 1)
this.setData({ commentImages })
},
// 评论匿名切换
onCommentAnonymousChange(e: WechatMiniprogram.CustomEvent) {
this.setData({ commentAnonymous: e.detail.value })
},
// 提交评论
async handleSubmitComment() {
const { commentRating, commentContent, commentImages, commentAnonymous } = this.data
if (!commentContent.trim()) {
wx.showToast({ title: '请输入评价内容', icon: 'error' })
return
}
try {
wx.showLoading({ title: '提交中...' })
const res = await wx.ajax({
url: `/activity/submit-review?id=${this.data.activityId}`,
method: 'POST',
data: {
activityId: this.data.activityId,
rating: commentRating,
content: commentContent,
images: commentImages.map((img) => img.url),
isAnonymous: commentAnonymous,
},
})
wx.hideLoading()
if (res) {
wx.showToast({ title: '评价成功', icon: 'success' })
this.setData({
commentShow: false,
detail: {
...this.data.detail!,
isReviewed: true,
commentCount: this.data.detail!.commentCount + 1,
},
})
// 刷新评价列表
this.fetchReviewList(true)
}
} catch (err: any) {
wx.hideLoading()
const message = err?.message || '评价失败'
wx.showToast({ title: message, icon: 'error' })
}
},
// 关闭评论弹窗
onCommentClose() { onCommentClose() {
this.setData({ commentShow: false })
},
// 点赞评价
async handleLikeReview(e: WechatMiniprogram.TouchEvent) {
const reviewId = e.currentTarget.dataset.id
const { reviewList } = this.data
// 检查登录状态
const accessToken = app.globalData.accessToken
if (!accessToken) {
wx.showToast({ title: '请先登录', icon: 'error' })
return
}
try {
const res = await wx.ajax({
url: `/activity/toggle-review-like`,
method: 'POST',
data: {
reviewId,
},
})
if (res) {
// 更新评价列表中的点赞状态
const updatedList = reviewList.map((item) => {
if (item.id === reviewId) {
return {
...item,
isLiked: res.isLiked,
likeCount: res.likeCount,
}
}
return item
})
this.setData({ reviewList: updatedList })
}
} catch (err: any) {
const message = err?.message || '操作失败'
wx.showToast({ title: message, icon: 'error' })
}
},
// 弹窗确认
handlePopupOk() {
this.setData({
popupShow: false,
popupType: 'popup1',
})
},
// 弹窗取消
handlePopupCancel() {
this.setData({ this.setData({
commentShow: false, popupShow: false,
popupType: 'popup1',
}) })
}, },
// 分享给朋友
onShareAppMessage() {
const { detail } = this.data
if (!detail) return {}
// 上报分享
this.handleShare()
return {
title: detail.name,
path: `/pages/actDetail/index?id=${this.data.activityId}`,
imageUrl: detail.mainImages[0] || '',
}
},
// 获取活动状态文本
getStatusText(status: string): string {
const statusMap: Record<string, string> = {
draft: '草稿',
pending: '待审核',
approved: '已通过',
registering: '报名中',
running: '进行中',
ended: '已结束',
cancelled: '已取消',
rejected: '已拒绝',
}
return statusMap[status] || status
},
// 获取评分文本
getRatingText(rating: number): string {
if (rating >= 4.5) return '非常满意'
if (rating >= 4) return '满意'
if (rating >= 3) return '一般'
if (rating >= 2) return '不满意'
return '非常不满意'
},
handleBack() {
wx.navigateBack()
},
}) })
export {} export {}

153
src/pages/actDetail/index.wxml

@ -1,123 +1,124 @@
<navbar fixed customStyle="background:{{background}};"> <navbar fixed customStyle="background:{{background}};">
<van-icon class="page-back" name="arrow-left" slot="left" /> <van-icon class="page-back" name="arrow-left" slot="left" bind:tap="handleBack" />
<view class="page-title" slot="title">详情</view> <view class="page-title" slot="title">详情</view>
</navbar> </navbar>
<view <view
class="page" class="page"
style="background: url('/images/bg1.png') no-repeat top center/100% 655rpx;padding-top: {{pageTop}}px;" style="background: url('{{detail.mainImages[0] || '/images/bg1.png'}}') no-repeat top center/100%;padding-top: {{pageTop}}px;"
> >
<view class="info-card"> <view class="info-card">
<view class="title">深职大第十五届校园歌手大赛</view> <view class="title">{{detail.name}}</view>
<view class="stat"> <view class="stat">
<view class="user"> <view class="user">
<view class="avatar-wrap"> <view class="avatar-wrap">
<image class="icon" src="/images/icon18.png"></image> <image
<image class="icon" src="/images/icon19.png"></image> class="icon"
<image class="icon" src="/images/icon20.png"></image> wx:for="{{detail.registrationList.slice(0, 3)}}"
wx:key="userId"
src="{{item.avatarUrl}}"
></image>
</view> </view>
<view class="content">1345人已报名</view> <view class="content">{{detail.regCount}}人已报名</view>
</view> </view>
<view class="pv"> <view class="pv">
<image class="icon" src="/images/icon21.png"></image> <image class="icon" src="/images/icon21.png"></image>
134 {{detail.viewCount}}
</view> </view>
<view class="share"> <view class="share">
<image class="icon" src="/images/icon22.png"></image> <image class="icon" src="/images/icon22.png"></image>
1380 {{detail.shareCount}}
</view> </view>
</view> </view>
<view class="tags"> <view class="tags">
<view class="tag">校园活动</view> <view class="tag" wx:for="{{detail.tags}}" wx:key="*this">{{item.name}}</view>
<view class="tag">学生会</view>
</view> </view>
<view class="row-wrap"> <view class="row-wrap">
<view class="row"> <view class="row" wx:if="{{detail.regType === 1}}">
<image class="icon" src="/images/icon27.png"></image> <image class="icon" src="/images/icon27.png"></image>
<view class="content">报名时间 2026.5.30 - 2026.6.30.</view> <view class="content">报名时间 {{detail.regStartAt}} - {{detail.regEndAt}}</view>
</view> </view>
<view class="row"> <view class="row">
<image class="icon" src="/images/icon28.png"></image> <image class="icon" src="/images/icon28.png"></image>
<view class="content">活动时间 2026.7.1 19:00 - 2026.7.1 21:00</view> <view class="content">活动时间 {{detail.startAt}} - {{detail.endAt}}</view>
</view> </view>
<view class="row"> <view class="row">
<image class="icon" src="/images/icon29.png"></image> <image class="icon" src="/images/icon29.png"></image>
<view class="content">活动地点 留仙洞校区音乐厅</view> <view class="content">活动地点 {{detail.location}}</view>
</view> </view>
<view class="row"> <view class="row" wx:if="{{detail.organizer}}">
<image class="icon" src="/images/icon30.png"></image> <image class="icon" src="/images/icon30.png"></image>
<view class="content">主办方 学生会</view> <view class="content">主办方 {{detail.organizer}}</view>
</view> </view>
</view> </view>
</view> </view>
<view class="process-card"> <!-- 活动议程 -->
<view class="process-card" wx:if="{{detail.agendas.length > 0}}">
<view class="p-title">活动议程</view> <view class="p-title">活动议程</view>
<view class="p-item" wx:for="{{2}}" wx:key="index"> <view class="p-item" wx:for="{{detail.agendas}}" wx:key="id">
<view class="date">6月24日</view> <view class="date">{{item.agendaDate}}</view>
<view class="list"> <view class="list">
<view class="l-item" wx:for="{{3}}" wx:key="index"> <view class="l-item">
<view class="time">19:00</view> <view class="time">{{item.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">开场致辞</view> <view class="title">{{item.title}}</view>
<view class="content">主持人开场,介绍比赛规则及评委</view> <view class="content">{{item.description}}</view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<view class="rich-card"> <!-- 活动介绍 -->
<view class="rich-card" wx:if="{{detail.description}}">
<view class="r-title">活动介绍</view> <view class="r-title">活动介绍</view>
<mp-html class="mp-html" content="{{content}}"></mp-html> <mp-html class="mp-html" content="{{detail.description}}"></mp-html>
</view> </view>
<!-- 精彩评论 -->
<view class="comment-list"> <view class="comment-list">
<view class="c-title">精彩评论 (21条)</view> <view class="c-title">精彩评论 ({{detail.commentCount}}条)</view>
<view class="c-card"> <view class="c-card" wx:for="{{reviewList}}" wx:key="id">
<view class="user"> <view class="user">
<image class="avatar" src="/images/bg1.png"></image> <image class="avatar" src="{{item.isAnonymous ? '/images/avatar-default.png' : item.avatarUrl}}"></image>
<view class="wrap"> <view class="wrap">
<view class="name">李可可</view> <view class="name">{{item.isAnonymous ? '匿名用户' : item.nickname}}</view>
<view class="rate"> <view class="rate">
<van-rate value="{{4.5}}" color="#F7B550" allow-half readonly size="24rpx" void-color="#F7F8FA" /> <van-rate value="{{item.rating}}" color="#F7B550" allow-half readonly size="24rpx" void-color="#F7F8FA" />
<view class="r-status">非常满意</view> <view class="r-status">{{ item.rating }}</view>
</view> </view>
</view> </view>
</view> </view>
<view class="content"> <view class="content">{{item.content}}</view>
本次活动组织有序,流程顺畅,现场氛围良好。活动内容丰富、安排合理,服务贴心到位,整体体验良好。歌手们的实力都很强! <view class="photo-wrap" wx:if="{{item.images.length > 0}}">
</view> <view class="photo-item" wx:for="{{item.images}}" wx:key="*this">
<view class="photo-wrap"> <image class="photo" src="{{item}}"></image>
<view class="photo-item">
<image class="photo" src="/images/bg1.png"></image>
</view> </view>
</view> </view>
<view class="c-footer"> <view class="c-footer">
<view class="date">2026-01-01 12:00:00</view> <view class="date">{{item.createdAt}}</view>
<view class="stat"> <view class="stat">
<view class="s-item"> <view class="s-item" data-id="{{item.id}}" bind:tap="handleLikeReview">
<image class="icon" src="/images/icon31.png"></image> <image class="icon" src="{{item.isLiked ? '/images/icon53.png' : '/images/icon31.png'}}"></image>
<view class="s-content">34</view> <view class="s-content">{{item.likeCount}}</view>
</view>
<view class="s-item">
<image class="icon" src="/images/icon32.png"></image>
<view class="s-content">回复</view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<!-- 底部操作栏 -->
<view class="footer"> <view class="footer">
<van-count-down <van-count-down
use-slot use-slot
time="{{ detail.LeftTime*1000 }}" wx:if="{{countdownSeconds}}"
time="{{countdownSeconds * 1000}}"
bind:change="handleTimeChange" bind:change="handleTimeChange"
bind:finish="handleTimeFinish" bind:finish="handleTimeFinish"
class="count-down-wrap" class="count-down-wrap"
@ -141,14 +142,23 @@
</view> </view>
</van-count-down> </van-count-down>
<view class="options"> <view class="options">
<view class="com">去评论</view> <view wx:if="{{!detail.isReviewed}}" class="com" bind:tap="handleOpenComment">去评论</view>
<view class="btn">我要报名</view> <view class="btn" wx:if="{{!detail.isRegistered}}" bind:tap="handleRegister">我要报名</view>
<view
class="btn disabled"
wx:elif="{{detail.isRegistered && !detail.isCheckedIn && detail.status === 'running'}}"
bind:tap="handleCheckin"
>
签到
</view>
<view class="btn disabled" wx:else>已报名</view>
</view> </view>
</view> </view>
</view> </view>
<!-- 分享按钮 -->
<view class="slidebar-share"> <view class="slidebar-share">
<image class="icon" src="/images/icon42.png"></image> <image class="icon" src="{{detail.isFavorited ? '/images/icon42-active.png' : '/images/icon42.png'}}"></image>
</view> </view>
<!-- 评论控件 --> <!-- 评论控件 -->
@ -157,28 +167,55 @@
<view class="title">评分</view> <view class="title">评分</view>
<view class="rate-wrap"> <view class="rate-wrap">
<view class="rate"> <view class="rate">
<van-rate value="{{4.5}}" color="#F7B550" allow-half size="46rpx" void-color="#F7F8FA" /> <van-rate
<view class="num">4.5</view> value="{{commentRating}}"
color="#F7B550"
allow-half
size="46rpx"
void-color="#F7F8FA"
bind:change="onCommentRatingChange"
/>
<view class="num">{{commentRating}}</view>
</view> </view>
<view class="status">非常满意</view>
</view> </view>
<view class="area-wrap"> <view class="area-wrap">
<textarea class="txa" placeholder-class="txa-place" placeholder="请聊聊本次活动的感受"></textarea> <textarea
class="txa"
placeholder-class="txa-place"
placeholder="请聊聊本次活动的感受"
value="{{commentContent}}"
bindinput="onCommentContentChange"
></textarea>
<view class="upload-list"> <view class="upload-list">
<uploadFile maxNum="{{3}}" useSlot> <view class="preview-list">
<view class="preview-item" wx:for="{{commentImages}}" wx:key="uid" data-index="{{index}}" bind:tap="onRemoveCommentImage">
<image class="p-img" src="{{item.url}}"></image>
<view class="close">
<van-icon name="cross" />
</view>
</view>
</view>
<uploadFile
maxCount="{{9}}"
useSlot="{{true}}"
accept="{{['image']}}"
fileList="{{commentImages}}"
bind:success="onCommentImageSuccess"
bind:error="onCommentImageError"
>
<view class="upload" slot="upload-area"> <view class="upload" slot="upload-area">
<image class="icon" src="/images/icon43.png"></image> <image class="icon" src="/images/icon43.png"></image>
</view> </view>
</uploadFile> </uploadFile>
</view> </view>
</view> </view>
<checkbox class="anonymous" color="#4AB8FD"> <checkbox class="anonymous" color="#4AB8FD" checked="{{commentAnonymous}}" bindchange="onCommentAnonymousChange">
匿名评价 匿名评价
<text class="content">你的头像、昵称将隐藏</text> <text class="content">你的头像、昵称将隐藏</text>
</checkbox> </checkbox>
<view class="c-footer"> <view class="c-footer">
<view class="cancel">取消</view> <view class="cancel" bind:tap="onCommentClose">取消</view>
<view class="submit">发布</view> <view class="submit" bind:tap="handleSubmitComment">发布</view>
</view> </view>
</view> </view>
</van-popup> </van-popup>

85
src/pages/actResult/index.ts

@ -1,8 +1,83 @@
const _app = getApp<IAppOption>(); const app = getApp<IAppOption>()
interface IActivityItem {
id: number
name: string
mainImages: string[]
startAt: string
endAt: string
location: string
status: string
regCount: number
}
Page({ Page({
data: {}, data: {
onLoad() {}, activityId: 0,
}); detail: null as any,
recommendList: [] as IActivityItem[],
},
onLoad(options: { id?: string }) {
const activityId = options.id ? Number(options.id) : 0
this.setData({ activityId })
app.waitLogin({ type: 0 }).then(() => {
this.fetchActivityDetail()
this.fetchRecommendList()
})
},
// 获取活动详情
async fetchActivityDetail() {
try {
const res = await wx.ajax({
url: `/activity/detail?id=${this.data.activityId}`,
method: 'GET',
data: {},
})
if (res) {
this.setData({ detail: res })
}
} catch (err) {
console.error('获取活动详情失败:', err)
}
},
// 获取推荐活动列表
async fetchRecommendList() {
try {
const res = await wx.ajax({
url: '/activity/list',
method: 'GET',
data: {
page: 1,
pageSize: 3,
isRecommended: true,
},
})
if (res && res.list) {
this.setData({ recommendList: res.list })
}
} catch (err) {
console.error('获取推荐活动列表失败:', err)
}
},
// 返回活动页
handleBack() {
wx.switchTab({
url: '/pages/act/index',
})
},
// 查看活动详情
handleDetail(e: WechatMiniprogram.TouchEvent) {
const id = e.currentTarget.dataset.id
wx.navigateTo({
url: `/pages/actDetail/index?id=${id}`,
})
},
})
export {} export {}

26
src/pages/actResult/index.wxml

@ -2,35 +2,33 @@
<view class="container"> <view class="container">
<image class="status-icon" src="/images/icon34.png"></image> <image class="status-icon" src="/images/icon34.png"></image>
<view class="status">报名成功</view> <view class="status">报名成功</view>
<view class="content">活动将于2026年7月1日19:00开始, 请记得准时参加</view> <view class="content">活动将于{{detail.startAt}}开始,请记得准时参加</view>
<view class="code-wrap" style="background: url('/images/bg2.png') no-repeat center/cover"> <view class="code-wrap" style="background: url('/images/bg2.png') no-repeat center/cover">
<image class="code" src="/images/bg1.png"></image> <image class="code" src="{{detail.mainImages[0] || '/images/bg1.png'}}"></image>
</view> </view>
<view class="btn">返回活动页</view> <view class="btn" bind:tap="handleBack">返回活动页</view>
</view> </view>
<view class="act"> <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="{{2}}" wx:key="index" 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">进行中</view> <view class="status">{{item.status === 'registering' ? '报名中' : item.status === 'running' ? '进行中' : '已结束'}}</view>
<image class="p-img" src="/images/bg1.png"></image> <image class="p-img" src="{{item.mainImages[0] || '/images/bg1.png'}}" mode="aspectFill"></image>
<view class="user">128人已报名</view> <view class="user">{{item.regCount}}人已报名</view>
</view> </view>
<view class="wrap"> <view class="wrap">
<view class="title"> <view class="title">{{item.name}}</view>
深职大第十五届校园歌手大赛深职大第十五届校园歌手大深职大第十五届校园歌手大深职大第十五届校园歌手大赛赛赛
</view>
<view class="date"> <view class="date">
<image class="icon" src="/images/icon3.png"></image> <image class="icon" src="/images/icon3.png"></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">
<image class="icon" src="/images/icon15.png"></image> <image class="icon" src="/images/icon15.png"></image>
<view class="content">留仙洞校区音乐厅</view> <view class="content">{{item.location}}</view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>

12
src/pages/login/index.ts

@ -97,27 +97,21 @@ Page({
wx.hideLoading() wx.hideLoading()
this.setData({ loading: false }) this.setData({ loading: false })
const { accessToken, expireIn, needBind, user } = response const { accessToken, needBind, user } = response
// 存储 accessToken // 存储 accessToken
app.globalData.accessToken = accessToken app.globalData.accessToken = accessToken
app.globalData.tokenExpireIn = expireIn app.globalData.needBind = needBind
// 存储用户信息 // 存储用户信息
if (user) { if (user) {
app.globalData.userInfo = user app.globalData.userInfo = user
} }
// 更新 initLoginInfo
app.globalData.initLoginInfo = {
needBind,
user,
}
// 绑定成功,跳转到目标页面 // 绑定成功,跳转到目标页面
wx.showToast({ wx.showToast({
title: '绑定成功', title: '绑定成功',
icon: 'success', icon: 'none',
}) })
setTimeout(() => { setTimeout(() => {

220
src/pages/schedule/index.scss

@ -201,110 +201,130 @@
} }
.format2 { .format2 {
} margin-top: 30rpx;
background: #fff;
border-radius: 24rpx;
box-shadow: 0rpx 15rpx 30rpx 0rpx rgba(74, 172, 219, 0.09);
overflow: hidden;
.format3 { /* 顶部表头(吸顶) */
/* 外层容器 */ .schedule-header {
.schedule-wrap { position: sticky;
border: 1rpx solid #eee; top: 0;
border-radius: 16rpx; z-index: 10;
overflow: hidden;
}
/* 表头横向滚动 */
.header-scroll {
width: 100%;
}
.header-row {
display: flex;
}
/* 左侧时间列统一宽度 */
.time-col {
width: 160rpx;
flex-shrink: 0;
}
.header-time {
display: flex;
align-items: center;
justify-content: center;
border-right: 1rpx solid #eee;
border-bottom: 1rpx solid #eee;
color: #999;
}
/* 每一天的列宽统一 */
.day-col {
width: 120rpx;
flex-shrink: 0;
}
.header-day {
text-align: center;
padding: 16rpx 0;
border-right: 1rpx solid #eee;
border-bottom: 1rpx solid #eee;
}
.header-day .week {
font-size: 26rpx;
color: #666;
}
.header-day .date {
font-size: 30rpx;
margin-top: 4rpx;
}
/* 主体横向+纵向滚动 */
.body-scroll {
height: calc(100vh - 160rpx);
}
.body-row {
display: flex;
}
/* 左侧课时列表 */
.body-time {
border-right: 1rpx solid #eee;
}
.time-item {
height: 140rpx; /* 单节高度,和课程网格行高一致 */
padding: 10rpx;
border-bottom: 1rpx solid #eee;
display: flex; display: flex;
flex-direction: column;
justify-content: center;
align-items: center; align-items: center;
color: #666; border-bottom: 1rpx solid #f0f0f0;
font-size: 24rpx; background-color: #fff; /* 吸顶时需要背景色 */
} .header-time {
.section-name { width: 100rpx;
font-weight: 500; flex-shrink: 0;
} text-align: center;
.time-range { font-size: 22rpx;
font-size: 22rpx; color: #94a3b8;
color: #999; padding: 20rpx 0;
margin-top: 6rpx; border-right: 1rpx solid #f0f0f0;
} }
/* 课程网格核心:CSS Grid */ .header-days {
.grid-container { flex: 1;
display: flex; display: flex;
} .day-header {
.grid-day { flex: 1; /* 自适应宽度 */
display: grid; text-align: center;
grid-template-rows: repeat(9, 140rpx); /* 9节课,每行140rpx */ padding: 16rpx 0;
border-right: 1rpx solid #eee; &.active {
position: relative; background-color: rgba(74, 184, 253, 1);
} .day-week,
/* 课程卡片样式 */ .day-date {
.course-card { color: #fff;
margin: 8rpx; }
border-radius: 8rpx; }
padding: 10rpx; .day-week {
color: #333; font-size: 22rpx;
font-size: 24rpx; color: #94a3b8;
} }
.course-name { .day-date {
font-weight: 500; margin-top: 4rpx;
line-height: 1.4; font-size: 28rpx;
color: #1e293b;
}
}
}
} }
.course-loc {
margin-top: 8rpx; /* 课程主体区域 */
font-size: 22rpx; .schedule-body {
opacity: 0.8; height: calc(100vh - 500rpx);
.body-row {
display: flex;
position: relative;
}
/* 左侧时间列 */
.body-time {
width: 100rpx;
flex-shrink: 0;
border-right: 1rpx solid #f0f0f0;
.time-slot {
height: 140rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-bottom: 1rpx solid #f5f5f5;
.slot-name {
font-size: 24rpx;
color: #1e293b;
font-weight: 500;
}
.slot-range {
margin-top: 8rpx;
font-size: 20rpx;
color: #94a3b8;
white-space: pre-line;
text-align: center;
line-height: 1.4;
}
}
}
/* 右侧课程网格 */
.body-grid {
flex: 1;
display: flex;
position: relative;
.day-column {
flex: 1; /* 自适应宽度,与表头保持一致 */
border-right: 1rpx solid #f5f5f5;
position: relative;
min-height: 1260rpx; /* 9节 * 140rpx */
/* 网格单元格 */
.grid-cell {
height: 140rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.course-block {
position: absolute;
left: 4rpx;
right: 4rpx;
padding: 12rpx;
border-radius: 12rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
.course-title {
font-size: 22rpx;
font-weight: 500;
line-height: 1.4;
word-break: break-all;
}
.course-loc {
margin-top: 20rpx;
font-size: 18rpx;
line-height: 1.3;
}
}
}
}
} }
} }
} }

26
src/pages/schedule/index.ts

@ -2,8 +2,6 @@ const _app = getApp<IAppOption>()
Page({ Page({
data: { data: {
scrollX: 0, // 横向滚动偏移量
isSyncing: false, // 同步锁,防止双向滚动死循环
todayIndex: 1, // 今天对应的索引(周二=1) todayIndex: 1, // 今天对应的索引(周二=1)
// 左侧9节课时间段 // 左侧9节课时间段
sectionList: [ sectionList: [
@ -120,30 +118,6 @@ Page({
], ],
}, },
onLoad() {}, onLoad() {},
// 滑动表头时,同步主体滚动
onHeaderScroll(e) {
if (this.data.isSyncing) return
this.setData({
isSyncing: true,
scrollX: e.detail.scrollLeft,
})
// 延迟解锁同步锁,避免双向触发循环
setTimeout(() => {
this.setData({ isSyncing: false })
}, 80)
},
// 滑动课程主体时,同步表头滚动
onBodyScroll(e) {
if (this.data.isSyncing) return
this.setData({
isSyncing: true,
scrollX: e.detail.scrollLeft,
})
setTimeout(() => {
this.setData({ isSyncing: false })
}, 80)
},
}) })
export {} export {}

47
src/pages/schedule/index.wxml

@ -61,7 +61,7 @@
</view> </view>
<view class="format2" wx:else> <view class="format2" wx:else>
<!-- 顶部日期表头 --> <!-- 顶部日期表头(吸顶) -->
<view class="schedule-header"> <view class="schedule-header">
<view class="header-time">时间</view> <view class="header-time">时间</view>
<view class="header-days"> <view class="header-days">
@ -73,7 +73,7 @@
</view> </view>
<!-- 课程主体区域 --> <!-- 课程主体区域 -->
<scroll-view scroll-x class="schedule-body"> <scroll-view scroll-y enhanced showScrollbar="{{false}}" class="schedule-body">
<view class="body-row"> <view class="body-row">
<!-- 左侧节次列 --> <!-- 左侧节次列 -->
<view class="body-time"> <view class="body-time">
@ -85,6 +85,9 @@
<!-- 右侧课程网格 --> <!-- 右侧课程网格 -->
<view class="body-grid"> <view class="body-grid">
<view wx:for="{{weekList}}" wx:key="date" class="day-column"> <view wx:for="{{weekList}}" wx:key="date" class="day-column">
<!-- 网格单元格(背景) -->
<view wx:for="{{sectionList}}" wx:key="index" class="grid-cell"></view>
<!-- 课程卡片 -->
<view <view
wx:for="{{item.courseList}}" wx:for="{{item.courseList}}"
wx:key="id" wx:key="id"
@ -99,44 +102,4 @@
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
<view class="format3">
<view class="schedule-wrap">
<!-- 顶部表头:绑定滚动事件 -->
<scroll-view scroll-x class="header-scroll" bindscroll="onHeaderScroll" scroll-left="{{scrollX}}">
<view class="header-row">
<view class="time-col header-time">时间</view>
<view wx:for="{{weekList}}" wx:key="date" class="day-col header-day">
<view class="week">{{item.week}}</view>
<view class="date">{{item.date}}</view>
</view>
</view>
</scroll-view>
<!-- 主体内容:绑定滚动事件,同步横向偏移 -->
<scroll-view scroll-x scroll-y class="body-scroll" bindscroll="onBodyScroll" scroll-left="{{scrollX}}">
<view class="body-row">
<view class="time-col body-time">
<view wx:for="{{sectionList}}" wx:key="index" class="time-item">
<view class="section-name">第{{item.index}}节</view>
<view class="time-range">{{item.time}}</view>
</view>
</view>
<view class="grid-container">
<view wx:for="{{weekList}}" wx:key="date" class="day-col grid-day">
<view
wx:for="{{item.courseList}}"
wx:key="id"
class="course-card"
style="background:{{item.bgColor}}; grid-row: {{item.start}} / span {{item.rowNum}}"
>
<view class="course-name">{{item.name}}</view>
<view class="course-loc">{{item.loc}}</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</view> </view>

12
typings/index.d.ts vendored

@ -26,27 +26,19 @@ interface IAppOption {
Timestamp: number Timestamp: number
waitBindDoctorId: string waitBindDoctorId: string
initLoginInfo: Partial<{
isLogin: 0 | 1
isReg: 0 | 1
loginType: 1 | 2
needBind?: boolean
user?: UserInfo
}>
// JWT 令牌相关 // JWT 令牌相关
accessToken?: string accessToken?: string
tokenExpireIn?: number tokenExpireIn?: number
openidSession?: string // 临时凭证(用于绑定) openidSession?: string // 临时凭证(用于绑定)
userInfo?: UserInfo userInfo?: UserInfo | {}
loginRedirectUrl?: string // 登录后返回的页面路径 loginRedirectUrl?: string // 登录后返回的页面路径
[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) => void
waitLogin: (params?: { type?: 0 | 1 | 2 | 'any' }) => Promise<void> waitLogin: (params?: { type?: 0 | 1 }) => Promise<void>
checkLoginType: (type: 0 | 1 | 2 | 'any') => boolean
[propName: string]: any [propName: string]: any
} }

Loading…
Cancel
Save