diff --git a/AGENTS.md b/AGENTS.md index ddea265..41d4b0c 100644 --- a/AGENTS.md +++ b/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. - **`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. +- **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. ## 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. - 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`. +- **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 diff --git a/src/api/request.ts b/src/api/request.ts index 8251f2c..bfc395d 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -25,10 +25,13 @@ export const request = function ( }) } - const app = getApp() + // 获取 accessToken 并构建 Authorization 头 + const accessToken = getApp().globalData.accessToken + const authHeader = accessToken ? { Authorization: `Bearer ${accessToken}` } : {} wx.request({ header: { + ...authHeader, ...header, }, url: gUrl + url, diff --git a/src/app.ts b/src/app.ts index 236f6f4..12f2d3a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -13,7 +13,7 @@ dayjs.extend(relativeTime) App({ globalData: { url: 'https://app.gohighedu.cn', - upFileUrl: 'https://app.gohighedu.cn', + upFileUrl: 'https://app.gohighedu.cn/upload/index', imageUrl: 'https://app.gohighedu.cn/images', Timestamp: new Date().getTime(), @@ -23,6 +23,9 @@ App({ initLoginInfo: {}, userInfo: {}, + + accessToken: '', + needBind: true, }, onLaunch() { this.autoUpdate() @@ -40,40 +43,35 @@ App({ startLogin(callback?: () => void) { wx.login({ success: (res) => { - console.log("DEBUGPRINT[244]: app.ts:42: res=", res) - // // 调用静默登录接口 - // wx.ajax({ - // method: 'POST', - // url: '/auth/silent-login', - // showMsg: false, // 隐藏错误提示 - // data: { - // code: res.code, - // }, - // }) - // .then((response: any) => { - // const { accessToken, user } = response - // - // // 存储 accessToken - // this.globalData.accessToken = accessToken - // - // // 存储用户信息 - // if (user) { - // this.globalData.userInfo = user - // } - // - // // 更新 initLoginInfo - // this.globalData.initLoginInfo = { - // user, - // } - // - // if (callback) { - // callback() - // } - // }) - // .catch((err: any) => { - // // 静默失败,不提示用户 - // console.error('静默登录请求失败:', err) - // }) + // 调用静默登录接口 + wx.ajax({ + method: 'POST', + url: '/auth/silent-login', + showMsg: false, // 隐藏错误提示 + data: { + code: res.code, + }, + }) + .then((response: any) => { + const { accessToken, user, needBind } = response + + // 存储 accessToken + this.globalData.accessToken = accessToken + this.globalData.needBind = needBind + + // 存储用户信息 + if (user) { + this.globalData.userInfo = user + } + + if (callback) { + callback() + } + }) + .catch((err: any) => { + // 静默失败,不提示用户 + console.error('静默登录请求失败:', err) + }) }, fail: (err) => { // 静默失败,不提示用户 @@ -81,52 +79,16 @@ App({ }, }) }, - updateLoginInfo(callback?: () => void) { - wx.ajax({ - 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((resolve, reject) => { + waitLogin({ type }: { type?: 0 | 1 } = { type: 0 }) { + return new Promise((resolve) => { const checkLogin = () => { // type = 0:不需要登录即可访问 if (type === 0) { resolve() return } - - // type = 'any':不检查登录状态 - 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 + if (type === 1 && this.globalData.needBind) { + this.redirectToLogin() } resolve() @@ -138,7 +100,7 @@ App({ /** * 重定向到登录页,并记录当前页面路径 */ - redirectToLogin(_type: 1 | 2) { + redirectToLogin() { // 获取当前页面路径 const pages = getCurrentPages() const currentPage = pages[pages.length - 1] @@ -153,37 +115,6 @@ App({ 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) { const url: Record = { 0: '?r=wtx/user/userinfo', diff --git a/src/components/pagination/index.js b/src/components/pagination/index.js new file mode 100644 index 0000000..856bf91 --- /dev/null +++ b/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 + }, + }, +}) diff --git a/src/components/pagination/index.json b/src/components/pagination/index.json new file mode 100644 index 0000000..24029b0 --- /dev/null +++ b/src/components/pagination/index.json @@ -0,0 +1,6 @@ +{ + "component": true, + "usingComponents": { + "van-divider": "@vant/weapp/divider/index" + } +} diff --git a/src/components/pagination/index.scss b/src/components/pagination/index.scss new file mode 100644 index 0000000..08a9dea --- /dev/null +++ b/src/components/pagination/index.scss @@ -0,0 +1,6 @@ +/* components/pagination/index.wxss */ +.none { + display: block; + margin: 30rpx auto; + width: 80%; +} diff --git a/src/components/pagination/index.wxml b/src/components/pagination/index.wxml new file mode 100644 index 0000000..1cfc920 --- /dev/null +++ b/src/components/pagination/index.wxml @@ -0,0 +1,9 @@ + + + + + + + 加载中... + +没有更多了 diff --git a/src/components/uploadFile/README.md b/src/components/uploadFile/README.md index 5380ee6..a1b4ab4 100644 --- a/src/components/uploadFile/README.md +++ b/src/components/uploadFile/README.md @@ -8,6 +8,10 @@ 上传接口地址固定为 `app.globalData.upFileUrl`,组件内部自动附加 `loginState`,无需外部传入。 +组件仅提供上传按钮,不维护文件列表状态,不显示文件列表,不包含文件预览功能。父页面需要自行处理文件列表的显示和管理。 + +组件通过事件通知父页面文件选择、上传进度、上传成功、上传失败等状态,父页面需要监听这些事件并更新自己的文件列表。 + ## Properties 入参 | 参数 | 说明 | 类型 | 默认值 | 必填 | @@ -16,9 +20,9 @@ | `maxSize` | 单文件最大限制(byte) | `Number` | `10485760`(10MB) | 否 | | `accept` | 允许的文件类型数组,可选值:`'image'` \| `'video'` \| `'file'` | `Array` | `['image']` | 否 | | `extensions` | 自定义文件后缀(仅 accept 含 `'file'` 时生效),如 `['.pdf', '.doc']` | `Array` | `[]` | 否 | -| `readonly` | 只读模式(不显示上传和删除按钮) | `Boolean` | `false` | 否 | +| `readonly` | 只读模式(不显示上传按钮) | `Boolean` | `false` | 否 | | `useSlot` | 是否使用自定义上传区域插槽 | `Boolean` | `false` | 否 | -| `fileList` | 已有文件列表(用于回显) | `Array` | `[]` | 否 | +| `fileList` | 已有文件列表(用于判断是否还能上传) | `Array` | `[]` | 否 | ### fileList 数据结构 @@ -36,10 +40,8 @@ | 事件名 | 说明 | 回调参数 | | --- | --- | --- | -| `bind:select` | 选中本地文件后触发 | `{ files: UploadFile[] }` | | `bind:success` | 单文件上传完成 | `{ file: UploadFile, response: any }` | | `bind:error` | 上传失败 | `{ file: UploadFile, error: Error }` | -| `bind:remove` | 删除文件 | `{ file: UploadFile, fileList: UploadFile[] }` | ### 事件返回的 UploadFile 结构 @@ -67,13 +69,9 @@ | --- | --- | --- | | `--upload-bg` | 上传区域背景色 | `#f7f8fa` | | `--upload-border` | 边框颜色 | `#e5e7eb` | -| `--upload-text` | 主文字颜色 | `#1f2937` | | `--upload-text-secondary` | 次要文字颜色 | `#9ca3af` | -| `--upload-primary` | 主题色 | `#3b82f6` | -| `--upload-error` | 错误色 | `#ef4444` | -| `--upload-mask` | 遮罩颜色 | `rgba(0, 0, 0, 0.5)` | | `--upload-radius` | 圆角大小 | `16rpx` | -| `--upload-size` | 文件项尺寸(仅默认上传框生效,slot 模式自适应) | `160rpx` | +| `--upload-size` | 上传按钮尺寸(仅默认上传框生效,slot 模式自适应) | `160rpx` | ## 使用案例 @@ -94,27 +92,30 @@ maxCount="{{3}}" maxSize="{{5242880}}" accept="{{['image']}}" - bind:select="onUploadSelect" + fileList="{{fileList}}" bind:success="onUploadSuccess" bind:error="onUploadError" - bind:remove="onUploadRemove" /> ``` ```ts // page.ts Page({ - onUploadSelect(e: WechatMiniprogram.CustomEvent) { - console.log('选中文件', e.detail.files) + data: { + fileList: [] as UploadFile[], }, + onUploadSuccess(e: WechatMiniprogram.CustomEvent) { console.log('上传成功', e.detail.file, e.detail.response) + // 添加上传成功的文件到列表 + this.setData({ + fileList: [...this.data.fileList, e.detail.file], + }) }, + onUploadError(e: WechatMiniprogram.CustomEvent) { console.log('上传失败', e.detail.file, e.detail.error) - }, - onUploadRemove(e: WechatMiniprogram.CustomEvent) { - console.log('删除文件', e.detail.file, e.detail.fileList) + wx.showToast({ title: '文件上传失败', icon: 'none' }) }, }) ``` @@ -127,6 +128,7 @@ Page({ useSlot="{{true}}" maxCount="{{5}}" accept="{{['image', 'video']}}" + fileList="{{fileList}}" bind:success="onUploadSuccess" > diff --git a/src/components/uploadFile/index.scss b/src/components/uploadFile/index.scss index 1efcfa2..81da59f 100644 --- a/src/components/uploadFile/index.scss +++ b/src/components/uploadFile/index.scss @@ -7,218 +7,14 @@ page { --upload-bg: #f7f8fa; --upload-border: #e5e7eb; - --upload-text: #1f2937; --upload-text-secondary: #9ca3af; - --upload-primary: #3b82f6; - --upload-error: #ef4444; - --upload-mask: rgba(0, 0, 0, 0.5); --upload-radius: 16rpx; --upload-size: 160rpx; - --upload-preview-height: 160rpx; // 预览项高度(独立控制) } .upload { 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 { width: var(--upload-size); @@ -226,7 +22,7 @@ page { &--slot { width: 100%; // slot 模式下宽度撑满父容器 - height: auto; // 高度自适应 slot 内容 + height: auto; // slot 模式下高度自适应内容 } .upload-trigger-default { diff --git a/src/components/uploadFile/index.ts b/src/components/uploadFile/index.ts index dbad1d7..301b53a 100644 --- a/src/components/uploadFile/index.ts +++ b/src/components/uploadFile/index.ts @@ -1,6 +1,7 @@ /** * Upload 上传组件 - * 支持图片/视频/文件上传,多文件选择,实时进度条,本地预览,删除附件,失败重试 + * 仅提供上传按钮,不维护文件列表状态 + * 父页面需要自行处理文件列表的显示和管理 */ /** 单个文件对象 */ @@ -57,7 +58,7 @@ Component({ type: Array, value: [], }, - /** 只读模式(不显示上传和删除按钮) */ + /** 只读模式(不显示上传按钮) */ readonly: { type: Boolean, value: false, @@ -67,45 +68,24 @@ Component({ type: Boolean, value: false, }, - /** 已有文件列表(用于回显) */ + /** 已有文件列表(用于判断是否还能上传) */ fileList: { type: Array, 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: { /** * 选择文件 */ onChooseFile() { - if (this.data.readonly) return - const { _fileList, maxCount } = this.data - const remaining = maxCount - _fileList.length + if (this.properties.readonly) return + + const fileList = this.properties.fileList as any[] + const maxCount = this.properties.maxCount + const remaining = maxCount - fileList.length + if (remaining <= 0) { wx.showToast({ title: `最多上传${maxCount}个文件`, icon: 'none' }) return @@ -217,13 +197,6 @@ Component({ if (validFiles.length === 0) return - // 触发 onSelect 事件 - this.triggerEvent('select', { files: validFiles }) - - // 更新文件列表 - const newList = [...this.data._fileList, ...validFiles] - this.setData({ _fileList: newList }) - // 逐个上传 validFiles.forEach((file) => { this.uploadFile(file) @@ -236,128 +209,50 @@ Component({ uploadFile(file: UploadFile) { const app = getApp() 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({ url: action, filePath: file.url, name: 'file', - header: { loginState }, - formData: { loginState }, + header: { + Authorization: accessToken ? `Bearer ${accessToken}` : '', + }, + formData: { type: fileType }, success: (res) => { - // 尝试解析返回数据 let data: any try { data = JSON.parse(res.data) } catch { data = res.data } - this.updateFile(file.uid, { - status: 'success', - progress: 100, - url: data.url || data.data?.url || file.url, - }) - this.triggerEvent('success', { file: this.getFile(file.uid), response: data }) + if (data.code === 0 && data.data?.url) { + // 上传成功,更新文件信息 + const successFile = { + ...file, + status: 'success', + 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) => { - 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) { - 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 }, }, }) diff --git a/src/components/uploadFile/index.wxml b/src/components/uploadFile/index.wxml index f06131f..19764b5 100644 --- a/src/components/uploadFile/index.wxml +++ b/src/components/uploadFile/index.wxml @@ -1,92 +1,20 @@ - - - - - - - - - - - - - - - - - - {{item.name}} - - {{item.name}} - - - - - - - - {{item.progress}}% - - - - - 上传失败 - - 重试 - - - - - - - - - - - - - - - - - + + + + + + + diff --git a/src/images/none.png b/src/images/none.png new file mode 100644 index 0000000..e0c650f Binary files /dev/null and b/src/images/none.png differ diff --git a/src/pages/act/index.json b/src/pages/act/index.json index e33ae3f..5e44518 100644 --- a/src/pages/act/index.json +++ b/src/pages/act/index.json @@ -2,6 +2,7 @@ "navigationStyle": "default", "navigationBarTitleText": "活动", "usingComponents": { - "van-popup": "@vant/weapp/popup/index" + "van-popup": "@vant/weapp/popup/index", + "pagination": "/components/pagination/index" } } diff --git a/src/pages/act/index.scss b/src/pages/act/index.scss index 770d218..5979e9c 100644 --- a/src/pages/act/index.scss +++ b/src/pages/act/index.scss @@ -7,7 +7,7 @@ page { padding: 30rpx 30rpx 0; background-color: #fff; .search { - margin: 30rpx; + margin: 30rpx 0; padding: 12rpx 24rpx; display: flex; align-items: center; @@ -27,6 +27,7 @@ page { .options { display: flex; .tabs { + margin: 0 -16rpx; flex: 1; overflow-x: auto; display: flex; @@ -38,7 +39,7 @@ page { } .tab { flex-shrink: 0; - padding: 10rpx 18rpx 16rpx; + padding: 10rpx 32rpx 16rpx; font-size: 28rpx; color: rgba(71, 85, 105, 1); &.active { @@ -128,12 +129,13 @@ page { } } .banner { - margin: 30rpx 30rpx 0; + margin: 0 30rpx 0; height: 230rpx; .b-img { display: block; width: 100%; height: 100%; + border-radius: 24rpx; } } .list { @@ -207,9 +209,10 @@ page { font-size: 28rpx; color: rgba(100, 116, 139, 1); display: flex; - align-items: center; gap: 8rpx; .icon { + margin-top: 8rpx; + flex-shrink: 0; width: 22rpx; height: 22rpx; } @@ -271,6 +274,12 @@ page { padding: 15rpx 30rpx; background: #f7f8fa; 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 { diff --git a/src/pages/act/index.ts b/src/pages/act/index.ts index d1531fc..49c4720 100644 --- a/src/pages/act/index.ts +++ b/src/pages/act/index.ts @@ -1,52 +1,489 @@ -const _app = getApp() +const app = getApp() + +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({ 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: [ { + id: 0, name: '全部分类', - icon: '5', - iconActive: '6', + icon: '/images/icon5.png', + iconActive: '/images/icon6.png', + isSelected: true, }, - { - name: '学术科技', - icon: '7', - iconActive: '8', + ] as Array<{ id: number; name: string; icon: string; iconActive: string; isSelected: boolean }>, + // 当前选中的分类 ID 数组(支持多选) + selectedCategoryIds: [] as number[], + // 活动列表 + 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 = { + 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: '文体艺术', - icon: '9', - iconActive: '10', + }) + this.fetchActivityList(true) + }, + + // 切换等级 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: '志愿公益', - icon: '11', - iconActive: '12', + }) + this.fetchActivityList(true) + }, + + // 下拉刷新 + onPullDownRefresh() { + this.setData({ + activityList: [], + pagination: { + page: 1, + pageSize: 20, + pages: 0, + count: 0, }, - { - name: '创新创业', - icon: '13', - iconActive: '14', + }) + this.fetchActivityList(true).then(() => { + wx.stopPullDownRefresh() + }) + }, + + // 上拉加载更多 + 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() { this.setData({ 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() { wx.navigateTo({ url: '/pages/actAdd/index', }) }, - handleDetail() { + + // 查看活动详情 + handleDetail(e: WechatMiniprogram.TouchEvent) { + const id = e.currentTarget.dataset.id wx.navigateTo({ - url: '/pages/actDetail/index', + url: `/pages/actDetail/index?id=${id}`, }) }, + + // 获取活动状态文本 + getStatusText(status: string): string { + const statusMap: Record = { + registering: '报名中', + running: '进行中', + ended: '已结束', + } + return statusMap[status] || status + }, }) export {} diff --git a/src/pages/act/index.wxml b/src/pages/act/index.wxml index 0ee4220..039fc3b 100644 --- a/src/pages/act/index.wxml +++ b/src/pages/act/index.wxml @@ -6,89 +6,122 @@ - 全部 + + {{item.name}} + - + 筛选 + + - - - - 全部分类 + + + + {{item.name}} + + + + - + - 进行中 - - 128人已报名 + {{item.activityStatusName}} + + {{item.regCount}}人已报名 - - 深职大第十五届校园歌手大赛深职大第十五届校园歌手大深职大第十五届校园歌手大深职大第十五届校园歌手大赛赛赛 - + {{item.name}} - 2026.04.01-2026.05.30 + {{item.startAt}} - {{item.endAt}} - 留仙洞校区音乐厅 + {{item.location}} + + + + 活动申请 + 活动开始时间 - 全部时间 - 今天 - 本周 - 本月 - 自定义时间 + + {{item.name}} + - + 时间范围 - + - 2026-05-28 + {{customStartTime || '请选择'}} - - + - 2026-05-28 + {{customEndTime || '请选择'}} - 取消 - 确定 + 取消 + 确定 diff --git a/src/pages/actAdd/index.json b/src/pages/actAdd/index.json index c72cee9..ef5b8d3 100644 --- a/src/pages/actAdd/index.json +++ b/src/pages/actAdd/index.json @@ -2,6 +2,7 @@ "navigationBarTitleText": "创建活动", "navigationStyle": "default", "usingComponents": { - "upload-file": "/components/uploadFile/index" + "upload-file": "/components/uploadFile/index", + "van-icon": "@vant/weapp/icon/index" } } diff --git a/src/pages/actAdd/index.scss b/src/pages/actAdd/index.scss index c81123c..da71571 100644 --- a/src/pages/actAdd/index.scss +++ b/src/pages/actAdd/index.scss @@ -15,6 +15,13 @@ page { background: #f5f7fa; } +/* ========== 自定义预览样式 ========== */ +.upload-preview-image { + width: 100%; + height: 100%; + display: block; +} + /* ========== 步骤条 ========== */ .step-bar { 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 { display: grid; diff --git a/src/pages/actAdd/index.ts b/src/pages/actAdd/index.ts index 08e3dad..a37997a 100644 --- a/src/pages/actAdd/index.ts +++ b/src/pages/actAdd/index.ts @@ -1,10 +1,36 @@ const app = getApp() -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 - time: string + name: string + sort: number + activityCount: number +} + +interface AgendaItem { + agendaDate: string + agendaTime: string title: string - desc: string + description: string + sort: number } Page({ @@ -17,25 +43,43 @@ Page({ ], // 步骤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: '', + type: 1, + typeOptions: [ + { id: 1, name: '讲座' }, + { id: 2, name: '比赛' }, + { id: 3, name: '社团' }, + { id: 4, name: '志愿' }, + { id: 5, name: '文体' }, + { id: 6, name: '其他' }, + ], + typeOther: '', + summary: '', startTime: '', endTime: '', detail: '', - level: '', - levels: ['校级', '院级', '系级', '班级', '其他'], - category: '', - categories: ['讲座', '比赛', '社团活动', '志愿服务', '文体活动', '学术交流', '其他'], + detailImages: [] as string[], location: '', organizer: '', contactName: '', contactPhone: '', - // 步骤2 活动议程 - agendas: [{ id: 1, time: '', title: '', desc: '' }] as AgendaItem[], - nextAgendaId: 2, + // 活动等级 + levelList: [] as ILevelItem[], + levelId: 0, + + // 活动分类 + categoryList: [] as ICategoryItem[], + selectedCategoryIds: [] as number[], + categoryTags: [] as Array<{ id: number; name: string; isSelected: boolean }>, // 用于显示选中状态 + + // 活动标签 + tagList: [] as ITagItem[], + selectedTagIds: [] as number[], + tagTags: [] as Array<{ id: number; name: string; isSelected: boolean }>, // 用于显示选中状态 - // 步骤3 报名签到设置 + // 步骤2 报名签到设置 needRegister: true, registerStartTime: '', registerEndTime: '', @@ -46,120 +90,446 @@ Page({ checkinWay: 'dynamic', checkinStartTime: '', checkinEndTime: '', + + // 步骤3 活动议程 + agendas: [{ agendaDate: '', agendaTime: '', title: '', description: '', sort: 0 }] as AgendaItem[], + nextAgendaId: 2, + + // 提交状态 + submitting: false, }, - onLoad() { - app.waitLogin() + async onLoad() { + 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 = {}) { + const keys = [ + 'currentStep', + 'coverImageList', + 'title', + 'type', + 'typeOther', + 'summary', + 'startTime', + 'endTime', + 'detail', + 'detailImages', + 'location', + 'organizer', + 'contactName', + 'contactPhone', + 'levelId', + 'selectedCategoryIds', + 'selectedTagIds', + 'needRegister', + 'registerStartTime', + 'registerEndTime', + 'registerLimit', + 'registerLimitCount', + 'registerCondition', + 'checkinWay', + 'checkinStartTime', + 'checkinEndTime', + 'agendas', + ] + + const draft: Record = {} + keys.forEach((k) => { + if (Object.prototype.hasOwnProperty.call(partial, k)) { + draft[k] = partial[k] + } else { + draft[k] = (this.data as any)[k] + } + }) + return draft + }, + + saveDraft(partial: Record = {}) { + try { + const draft = this.buildDraft(partial) + wx.setStorageSync(DRAFT_KEY, draft) + } catch (err) { + console.error('保存草稿失败:', err) + } + }, + + clearDraft() { + try { + wx.removeStorageSync(DRAFT_KEY) + } catch { + /* ignore */ + } + }, + + restoreDraft(draft: Record) { + if (!draft) return + const safeDraft = { ...draft } + safeDraft.coverImageList = safeDraft.coverImageList || [] + safeDraft.detailImages = safeDraft.detailImages || [] + safeDraft.agendas = safeDraft.agendas || [] + safeDraft.selectedCategoryIds = safeDraft.selectedCategoryIds || [] + safeDraft.selectedTagIds = safeDraft.selectedTagIds || [] + + this.setData(safeDraft) + + // 重建标签选中状态显示 + const categoryTags = (this.data.categoryList || []).map((item: ICategoryItem) => ({ + id: item.id, + name: item.name, + isSelected: safeDraft.selectedCategoryIds.includes(item.id), + })) + const tagTags = (this.data.tagList || []).map((item: ITagItem) => ({ + id: item.id, + name: item.name, + isSelected: safeDraft.selectedTagIds.includes(item.id), + })) + this.setData({ categoryTags, tagTags }) + }, + + setAndSave(patch: Record) { + this.setData(patch) + this.saveDraft(patch) + }, + + // 获取活动等级列表 + async fetchLevelList() { + try { + const res = await wx.ajax({ + url: '/activity-level/list', + method: 'GET', + data: {}, + }) + if (res && res.list) { + this.setData({ levelList: res.list }) + } + } catch (err) { + console.error('获取活动等级列表失败:', err) + } + }, + + // 获取活动分类列表 + async fetchCategoryList() { + try { + const res = await wx.ajax({ + url: '/activity-category/list', + method: 'GET', + data: {}, + }) + if (res && res.list) { + const { selectedCategoryIds } = this.data + const categoryTags = res.list.map((item: ICategoryItem) => ({ + id: item.id, + name: item.name, + isSelected: selectedCategoryIds.includes(item.id), + })) + this.setData({ + categoryList: res.list, + categoryTags, + }) + } + } catch (err) { + console.error('获取活动分类列表失败:', err) + } + }, + + // 获取活动标签列表 + async fetchTagList() { + try { + const res = await wx.ajax({ + url: '/activity-tag/list', + method: 'GET', + data: {}, + }) + if (res && res.list) { + const { selectedTagIds } = this.data + const tagTags = res.list.map((item: ITagItem) => ({ + id: item.id, + name: item.name, + isSelected: selectedTagIds.includes(item.id), + })) + this.setData({ + tagList: res.list, + tagTags, + }) + } + } catch (err) { + console.error('获取活动标签列表失败:', err) + } }, // ========== 步骤切换 ========== goStep(step: number) { if (step < 1 || step > 4) return - this.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() { + // 先验证当前步骤 + if (!this.validateCurrentStep()) { + return + } + const next = this.data.currentStep + 1 - if (next <= 4) this.setData({ currentStep: next }) + if (next <= 4) this.setAndSave({ currentStep: next }) }, onPrevStep() { 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 - 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) { - this.setData({ coverImageList: [] }) + onDetailImageSuccess(e: WechatMiniprogram.CustomEvent) { + const { urls } = e.detail + this.setAndSave({ detailImages: urls }) }, // ========== 输入绑定 ========== onInputChange(e: WechatMiniprogram.Input) { const { field } = e.currentTarget.dataset - this.setData({ [field]: e.detail.value }) + this.setAndSave({ [field]: e.detail.value }) }, onTextareaChange(e: WechatMiniprogram.TextareaInput) { const { field } = e.currentTarget.dataset - this.setData({ [field]: e.detail.value }) + this.setAndSave({ [field]: e.detail.value }) }, // ========== 时间选择 ========== onPickTime(e: WechatMiniprogram.PickerChange) { 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 - 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) { - const { value } = e.currentTarget.dataset - this.setData({ category: value }) + const { id } = e.currentTarget.dataset + let { selectedCategoryIds, categoryTags } = this.data + + const index = selectedCategoryIds.indexOf(id) + if (index > -1) { + selectedCategoryIds = selectedCategoryIds.filter((item) => item !== id) + } else { + selectedCategoryIds = [...selectedCategoryIds, id] + } + + categoryTags = categoryTags.map((item) => ({ + ...item, + isSelected: selectedCategoryIds.includes(item.id), + })) + + this.setAndSave({ selectedCategoryIds, categoryTags }) + }, + + // ========== 活动标签选择(多选) ========== + onSelectTag(e: WechatMiniprogram.TouchEvent) { + const { id } = e.currentTarget.dataset + let { selectedTagIds, tagTags } = this.data + + const index = selectedTagIds.indexOf(id) + if (index > -1) { + selectedTagIds = selectedTagIds.filter((item) => item !== id) + } else { + selectedTagIds = [...selectedTagIds, id] + } + + tagTags = tagTags.map((item) => ({ + ...item, + isSelected: selectedTagIds.includes(item.id), + })) + + this.setAndSave({ selectedTagIds, tagTags }) }, // ========== 议程管理 ========== onAddAgenda() { const agendas = this.data.agendas agendas.push({ - id: this.data.nextAgendaId, - time: '', + agendaDate: '', + agendaTime: '', title: '', - desc: '', - }) - this.setData({ - agendas, - nextAgendaId: this.data.nextAgendaId + 1, + description: '', + sort: agendas.length, }) + this.setAndSave({ agendas }) }, onRemoveAgenda(e: WechatMiniprogram.TouchEvent) { const { index } = e.currentTarget.dataset const agendas = this.data.agendas.filter((_, i) => i !== index) - this.setData({ agendas }) + this.setAndSave({ agendas }) }, onAgendaInput(e: WechatMiniprogram.Input | WechatMiniprogram.TextareaInput) { const { index, field } = e.currentTarget.dataset const agendas = this.data.agendas agendas[index][field] = e.detail.value - this.setData({ agendas }) + this.setAndSave({ agendas }) }, onAgendaTime(e: WechatMiniprogram.PickerChange) { - const { index } = e.currentTarget.dataset + const { index, field } = e.currentTarget.dataset const agendas = this.data.agendas - agendas[index].time = e.detail.value as string - this.setData({ agendas }) + agendas[index][field] = e.detail.value as string + this.setAndSave({ agendas }) }, // ========== 报名签到设置 ========== onToggleRegister(e: WechatMiniprogram.TouchEvent) { const { value } = e.currentTarget.dataset - this.setData({ needRegister: value === 'yes' }) + this.setAndSave({ needRegister: value === 'yes' }) }, onToggleRegisterLimit(e: WechatMiniprogram.TouchEvent) { const { value } = e.currentTarget.dataset - this.setData({ registerLimit: value }) + this.setAndSave({ registerLimit: value }) }, onSelectCheckinWay(e: WechatMiniprogram.TouchEvent) { const { value } = e.currentTarget.dataset - this.setData({ checkinWay: value }) + this.setAndSave({ checkinWay: value }) }, // ========== 底部操作 ========== onSaveDraft() { - wx.showToast({ title: '已保存草稿', icon: 'success' }) + this.submitActivity(1) // activityStatus = 1 (草稿) }, onSubmit() { @@ -168,13 +538,169 @@ Page({ content: '提交后将进入审核流程,是否继续?', success: (res) => { 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 = { + dynamic: 1, + fixed: 2, + none: 3, + } + + const params: Record = { + 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() { + this.clearDraft() wx.switchTab({ url: '/pages/index/index' }) }, }) diff --git a/src/pages/actAdd/index.wxml b/src/pages/actAdd/index.wxml index be2ba70..f6ec8be 100644 --- a/src/pages/actAdd/index.wxml +++ b/src/pages/actAdd/index.wxml @@ -26,19 +26,18 @@ 活动头图 * - + 点击上传 + + + + + + @@ -65,13 +64,13 @@ 活动时间 * - + {{startTime || '请选择开始时间'}} - + {{endTime || '请选择结束时间'}} @@ -105,13 +104,13 @@ 活动等级 - {{item}} + {{item.name}} @@ -120,19 +119,22 @@ 活动分类 - {{item}} + {{item.name}} - 活动地点 + + 活动地点 + * + 报名时间 * - + {{registerStartTime || '请选择开始时间'}} @@ -245,7 +247,7 @@ - + {{registerEndTime || '请选择结束时间'}} @@ -361,7 +363,7 @@ 签到时间 * - + {{checkinStartTime || '请选择签到开始时间'}} @@ -369,7 +371,7 @@ - + {{checkinEndTime || '请选择签到结束时间'}} @@ -424,9 +426,17 @@ 议程时间 * - + - {{item.time || '请选择时间'}} + + {{item.agendaTime || '请选择时间'}} + @@ -477,15 +487,4 @@ - - - - - - - 提交成功 - 活动已提交审核,请耐心等待 - 返回首页 - - diff --git a/src/pages/actAddResult/index.ts b/src/pages/actAddResult/index.ts index 067b6bb..2950c8e 100644 --- a/src/pages/actAddResult/index.ts +++ b/src/pages/actAddResult/index.ts @@ -1,8 +1,52 @@ -const _app = getApp(); +const app = getApp() Page({ - data: {}, - onLoad() {}, -}); + data: { + 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 {} diff --git a/src/pages/actAddResult/index.wxml b/src/pages/actAddResult/index.wxml index ef481c0..2b5a240 100644 --- a/src/pages/actAddResult/index.wxml +++ b/src/pages/actAddResult/index.wxml @@ -2,12 +2,12 @@ 申请提交成功 我们将会在 7 个工作日内完成审核,结果将通过公 众号发送给你,请注意查收~ - 继续发布 + 继续发布 返回活动页 关注xxx公众号 - + 长按识别二维码 diff --git a/src/pages/actDetail/index.scss b/src/pages/actDetail/index.scss index b0023c8..7a6e550 100644 --- a/src/pages/actDetail/index.scss +++ b/src/pages/actDetail/index.scss @@ -13,7 +13,7 @@ page { .page { min-height: 100vh; box-sizing: border-box; - padding-bottom: 280rpx; + padding-bottom: 320rpx; .info-card { margin: 349rpx 30rpx 0; padding: 48rpx 30rpx 40rpx; @@ -83,9 +83,9 @@ page { .row { margin-top: 34rpx; display: flex; - align-items: center; gap: 12rpx; .icon { + margin-top: 4rpx; flex-shrink: 0; width: 36rpx; height: 36rpx; @@ -418,6 +418,35 @@ page { } .upload-list { 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 { width: 108rpx; height: 108rpx; diff --git a/src/pages/actDetail/index.ts b/src/pages/actDetail/index.ts index 6f0d4c8..f529a33 100644 --- a/src/pages/actDetail/index.ts +++ b/src/pages/actDetail/index.ts @@ -1,33 +1,510 @@ -const _app = getApp() +const app = getApp() + +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({ data: { + activityId: 0, + detail: null as IActivityDetail | null, + loading: true, + + // 倒计时 + countdownSeconds: 0, + timeData: { days: 0, hours: 0, minutes: 0, seconds: 0 }, + + // 弹窗 popupShow: false, - popupType: 'popup1', // 签到成功弹窗 + popupType: 'popup1', popupParams: {} as any, + // 评论 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() { - const { popupType } = this.data - if (popupType === 'argument') { + + onLoad(options: { id?: string }) { + const activityId = options.id ? Number(options.id) : 0 + 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({ - popupShow: false, - popupType: 'i', + commentShow: true, + 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() { + 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({ - 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 = { + 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 {} diff --git a/src/pages/actDetail/index.wxml b/src/pages/actDetail/index.wxml index df3161b..cd98e53 100644 --- a/src/pages/actDetail/index.wxml +++ b/src/pages/actDetail/index.wxml @@ -1,123 +1,124 @@ - + 详情 - 深职大第十五届校园歌手大赛 + {{detail.name}} - - - + - 1345人已报名 + {{detail.regCount}}人已报名 - 134次 + {{detail.viewCount}}次 - 校园活动 - 学生会 + {{item.name}} - + - 报名时间 2026.5.30 - 2026.6.30. + 报名时间 {{detail.regStartAt}} - {{detail.regEndAt}} - 活动时间 2026.7.1 19:00 - 2026.7.1 21:00 + 活动时间 {{detail.startAt}} - {{detail.endAt}} - 活动地点 留仙洞校区音乐厅 + 活动地点 {{detail.location}} - + - 主办方 学生会 + 主办方 {{detail.organizer}} - + + 活动议程 - - 6月24日 + + {{item.agendaDate}} - - 19:00 + + {{item.agendaTime}} - 开场致辞 - 主持人开场,介绍比赛规则及评委 + {{item.title}} + {{item.description}} - + + 活动介绍 - + + - 精彩评论 (21条) - + 精彩评论 ({{detail.commentCount}}条) + - + - 李可可 + {{item.isAnonymous ? '匿名用户' : item.nickname}} - - 非常满意 + + {{ item.rating }} - - 本次活动组织有序,流程顺畅,现场氛围良好。活动内容丰富、安排合理,服务贴心到位,整体体验良好。歌手们的实力都很强! - - - - + {{item.content}} + + + - 2026-01-01 12:00:00 + {{item.createdAt}} - - - 34 - - - - 回复 + + + {{item.likeCount}} + - 去评论 - 我要报名 + 去评论 + 我要报名 + + 签到 + + 已报名 + - + @@ -157,28 +167,55 @@ 评分 - - 4.5 + + {{commentRating}} - 非常满意 - + - + + + + + + + + + - + 匿名评价 你的头像、昵称将隐藏 - 取消 - 发布 + 取消 + 发布 diff --git a/src/pages/actResult/index.ts b/src/pages/actResult/index.ts index 067b6bb..541290e 100644 --- a/src/pages/actResult/index.ts +++ b/src/pages/actResult/index.ts @@ -1,8 +1,83 @@ -const _app = getApp(); +const app = getApp() + +interface IActivityItem { + id: number + name: string + mainImages: string[] + startAt: string + endAt: string + location: string + status: string + regCount: number +} Page({ - data: {}, - onLoad() {}, -}); + data: { + 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 {} \ No newline at end of file diff --git a/src/pages/actResult/index.wxml b/src/pages/actResult/index.wxml index 61761fc..d09d5b1 100644 --- a/src/pages/actResult/index.wxml +++ b/src/pages/actResult/index.wxml @@ -2,35 +2,33 @@ 报名成功 - 活动将于2026年7月1日19:00开始, 请记得准时参加 + 活动将于{{detail.startAt}}开始,请记得准时参加 - + - 返回活动页 + 返回活动页 - + 你可能感兴趣的活动 - + - 进行中 - - 128人已报名 + {{item.status === 'registering' ? '报名中' : item.status === 'running' ? '进行中' : '已结束'}} + + {{item.regCount}}人已报名 - - 深职大第十五届校园歌手大赛深职大第十五届校园歌手大深职大第十五届校园歌手大深职大第十五届校园歌手大赛赛赛 - + {{item.name}} - 2026.04.01-2026.05.30 + {{item.startAt}} - {{item.endAt}} - 留仙洞校区音乐厅 + {{item.location}} - + \ No newline at end of file diff --git a/src/pages/login/index.ts b/src/pages/login/index.ts index fbf1963..e523f89 100644 --- a/src/pages/login/index.ts +++ b/src/pages/login/index.ts @@ -97,27 +97,21 @@ Page({ wx.hideLoading() this.setData({ loading: false }) - const { accessToken, expireIn, needBind, user } = response + const { accessToken, needBind, user } = response // 存储 accessToken app.globalData.accessToken = accessToken - app.globalData.tokenExpireIn = expireIn + app.globalData.needBind = needBind // 存储用户信息 if (user) { app.globalData.userInfo = user } - // 更新 initLoginInfo - app.globalData.initLoginInfo = { - needBind, - user, - } - // 绑定成功,跳转到目标页面 wx.showToast({ title: '绑定成功', - icon: 'success', + icon: 'none', }) setTimeout(() => { diff --git a/src/pages/schedule/index.scss b/src/pages/schedule/index.scss index a34ae29..b1b5b3a 100644 --- a/src/pages/schedule/index.scss +++ b/src/pages/schedule/index.scss @@ -201,110 +201,130 @@ } .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-wrap { - border: 1rpx solid #eee; - border-radius: 16rpx; - 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; + /* 顶部表头(吸顶) */ + .schedule-header { + position: sticky; + top: 0; + z-index: 10; display: flex; - flex-direction: column; - justify-content: center; align-items: center; - color: #666; - font-size: 24rpx; - } - .section-name { - font-weight: 500; - } - .time-range { - font-size: 22rpx; - color: #999; - margin-top: 6rpx; - } - /* 课程网格核心:CSS Grid */ - .grid-container { - display: flex; - } - .grid-day { - display: grid; - grid-template-rows: repeat(9, 140rpx); /* 9节课,每行140rpx */ - border-right: 1rpx solid #eee; - position: relative; - } - /* 课程卡片样式 */ - .course-card { - margin: 8rpx; - border-radius: 8rpx; - padding: 10rpx; - color: #333; - font-size: 24rpx; - } - .course-name { - font-weight: 500; - line-height: 1.4; + border-bottom: 1rpx solid #f0f0f0; + background-color: #fff; /* 吸顶时需要背景色 */ + .header-time { + width: 100rpx; + flex-shrink: 0; + text-align: center; + font-size: 22rpx; + color: #94a3b8; + padding: 20rpx 0; + border-right: 1rpx solid #f0f0f0; + } + .header-days { + flex: 1; + display: flex; + .day-header { + flex: 1; /* 自适应宽度 */ + text-align: center; + padding: 16rpx 0; + &.active { + background-color: rgba(74, 184, 253, 1); + .day-week, + .day-date { + color: #fff; + } + } + .day-week { + font-size: 22rpx; + color: #94a3b8; + } + .day-date { + margin-top: 4rpx; + font-size: 28rpx; + color: #1e293b; + } + } + } } - .course-loc { - margin-top: 8rpx; - font-size: 22rpx; - opacity: 0.8; + + /* 课程主体区域 */ + .schedule-body { + 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; + } + } + } + } } } } diff --git a/src/pages/schedule/index.ts b/src/pages/schedule/index.ts index 89d1a56..a01b057 100644 --- a/src/pages/schedule/index.ts +++ b/src/pages/schedule/index.ts @@ -2,8 +2,6 @@ const _app = getApp() Page({ data: { - scrollX: 0, // 横向滚动偏移量 - isSyncing: false, // 同步锁,防止双向滚动死循环 todayIndex: 1, // 今天对应的索引(周二=1) // 左侧9节课时间段 sectionList: [ @@ -120,30 +118,6 @@ Page({ ], }, 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 {} diff --git a/src/pages/schedule/index.wxml b/src/pages/schedule/index.wxml index a418e39..7fb8351 100644 --- a/src/pages/schedule/index.wxml +++ b/src/pages/schedule/index.wxml @@ -61,7 +61,7 @@ - + 时间 @@ -73,7 +73,7 @@ - + @@ -85,6 +85,9 @@ + + + - - - - - - - 时间 - - {{item.week}} - {{item.date}} - - - - - - - - - - 第{{item.index}}节 - {{item.time}} - - - - - - {{item.name}} - {{item.loc}} - - - - - - - diff --git a/typings/index.d.ts b/typings/index.d.ts index ee96425..62e9382 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -26,27 +26,19 @@ interface IAppOption { Timestamp: number waitBindDoctorId: string - initLoginInfo: Partial<{ - isLogin: 0 | 1 - isReg: 0 | 1 - loginType: 1 | 2 - needBind?: boolean - user?: UserInfo - }> // JWT 令牌相关 accessToken?: string tokenExpireIn?: number openidSession?: string // 临时凭证(用于绑定) - userInfo?: UserInfo + userInfo?: UserInfo | {} loginRedirectUrl?: string // 登录后返回的页面路径 [propName: string]: any } getUserInfo: (type?: 0 | 1 | 2) => Promise startLogin: (callback?: () => void) => void - waitLogin: (params?: { type?: 0 | 1 | 2 | 'any' }) => Promise - checkLoginType: (type: 0 | 1 | 2 | 'any') => boolean + waitLogin: (params?: { type?: 0 | 1 }) => Promise [propName: string]: any }