| @ -0,0 +1 @@ | |||
| unpackage/ | |||
| @ -0,0 +1,23 @@ | |||
| <script> | |||
| export default { | |||
| onLaunch() { | |||
| // 请求初始化一切配置 | |||
| console.log('App OnLaunch') | |||
| }, | |||
| async onShow() { | |||
| console.log('App Show') | |||
| await this.$store.dispatch('initData') | |||
| // console.log('配置数据初始化完成') | |||
| }, | |||
| onHide: function() { | |||
| console.log('App Hide') | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss"> | |||
| // @import '@/uni_modules/uv-ui-tools/index.scss'; | |||
| </style> | |||
| @ -0,0 +1,56 @@ | |||
| // 这里书写防抖,节流 | |||
| import request from "@/api/request"; | |||
| // 全局管理的存储状态 | |||
| const requestControlMap = new Map() | |||
| const MAX_MAP_SIZE = 1000 // 防止内存轻易泄露 | |||
| // 请求标识生成器(更稳健的版本) | |||
| const generateApiKey = (config) => { | |||
| const { method, url, header, debounce, throttle } = config | |||
| return `DEBOUNCE_AND_THROTTLE:${method}:${url}:${JSON.stringify(header)}:${debounce}:${throttle}`; | |||
| } | |||
| export default function http (config) { | |||
| const apiKey = generateApiKey(config) | |||
| // 空间保护 | |||
| if (requestControlMap.size > MAX_MAP_SIZE) { | |||
| requestControlMap.clear() // 清空缓存 | |||
| // 类型保护 | |||
| }else if (config.debounce > 0 && config.throttle > 0) { | |||
| throw new Error('请勿同时使用防抖和节流!') | |||
| } | |||
| // 如果有防抖的需求 | |||
| if (config.debounce > 0 ){ | |||
| clearTimeout(requestControlMap.get(apiKey)?.timer) | |||
| return new Promise((resolve, reject) => { | |||
| requestControlMap.set(apiKey, { | |||
| timer: setTimeout(() => { | |||
| // 防抖时间到了,清除缓存并发起请求 | |||
| requestControlMap.delete(apiKey) | |||
| request(config).then(resolve).catch(reject) | |||
| }, config.debounce), | |||
| timeStamp: Date.now() | |||
| }) | |||
| }) | |||
| } | |||
| // 如果需要节流 | |||
| if (config.throttle > 0){ | |||
| const record = requestControlMap.get(apiKey) | |||
| if (record && Date.now() - record.lastTime < config.throttle) { | |||
| // 节流时间未到,不发起请求 | |||
| return Promise.reject(new Error('请求过于频繁')) | |||
| } | |||
| requestControlMap.set(apiKey, { | |||
| lastTime: Date.now(), | |||
| timeStamp: Date.now() | |||
| }) | |||
| } | |||
| // 正常发起请求 | |||
| return request(config) | |||
| } | |||
| @ -0,0 +1,19 @@ | |||
| import user from '@/api/modules/user' | |||
| import shop from '@/api/modules/shop' | |||
| import score from '@/api/modules/score' | |||
| import config from '@/api/modules/config' | |||
| import home from '@/api/modules/home' | |||
| import activity from '@/api/modules/activity' | |||
| import login from '@/api/modules/login' | |||
| import community from '@/api/modules/community' | |||
| export { | |||
| user, | |||
| shop, | |||
| score, | |||
| config, | |||
| home, | |||
| activity, | |||
| login, | |||
| community | |||
| } | |||
| @ -0,0 +1,73 @@ | |||
| // import request from '@/api/request' | |||
| import http from '@/api/http' | |||
| export default { | |||
| // 活动- 活动报名 | |||
| async applyActivity(data) { | |||
| return http({ | |||
| url: '/activity/applyActivity', | |||
| method: 'POST', | |||
| data | |||
| }) | |||
| }, | |||
| // 活动- 收藏活动 | |||
| async collectionActivity(data) { | |||
| return http({ | |||
| url: '/activity/collectionActivity', | |||
| method: 'POST', | |||
| data | |||
| }) | |||
| }, | |||
| // 活动- 获取活动详情 | |||
| async queryActivityById(data) { | |||
| return http({ | |||
| url: '/activity/queryActivityById', | |||
| method: 'GET', | |||
| data, | |||
| noToken: true | |||
| }) | |||
| }, | |||
| // 活动- 获取我收藏的活动列表 | |||
| async queryActivityCollectionList(data) { | |||
| return http({ | |||
| url: '/activity/queryActivityCollectionList', | |||
| method: 'POST', | |||
| data | |||
| }) | |||
| }, | |||
| // 活动- 获取活动列表 | |||
| async queryActivityList(data) { | |||
| return http({ | |||
| url: '/activity/queryActivityList', | |||
| method: 'GET', | |||
| data, | |||
| noToken: true, | |||
| debounce: 200, | |||
| // showLoading: true | |||
| }) | |||
| }, | |||
| // 我的报名- 获取我报名的活动列表 | |||
| async queryApplyList(data) { | |||
| return http({ | |||
| url: '/activity/queryApplyList', | |||
| method: 'POST', | |||
| data, | |||
| // showLoading: true | |||
| }) | |||
| }, | |||
| // 我的报名- 活动签到 | |||
| async signActivity(data) { | |||
| return http({ | |||
| url: '/activity/signActivity', | |||
| method: 'POST', | |||
| data, | |||
| // showToast: false | |||
| }) | |||
| }, | |||
| } | |||
| @ -0,0 +1,26 @@ | |||
| // import request from "@/api/request"; | |||
| import http from "@/api/http"; | |||
| export default { | |||
| // 社区- 获取帖子列表 | |||
| async queryPostList(data) { | |||
| return http({ | |||
| url: '/comment/queryPostList', | |||
| method: 'GET', | |||
| data, | |||
| noToken: true, | |||
| debounce: 200, | |||
| }) | |||
| }, | |||
| // 社区- 上传帖子 | |||
| async addPost(data) { | |||
| return http({ | |||
| url: '/comment/addPost', | |||
| method: 'POST', | |||
| data, | |||
| showLoading: true, | |||
| // noToken: true | |||
| }) | |||
| } | |||
| } | |||
| @ -0,0 +1,47 @@ | |||
| // import request from "@/api/request"; | |||
| import http from "@/api/http"; | |||
| export default { | |||
| async queryCareerList() { | |||
| return http({ | |||
| url: '/config/queryCareerList', | |||
| method: 'GET', | |||
| noToken: true | |||
| }) | |||
| }, | |||
| async queryConfigList() { | |||
| return http({ | |||
| url: '/config/queryConfigList', | |||
| method: 'GET', | |||
| noToken: true | |||
| }) | |||
| }, | |||
| async queryQualificationList() { | |||
| return http({ | |||
| url: '/config/queryQualificationList', | |||
| method: 'GET', | |||
| noToken: true | |||
| }) | |||
| }, | |||
| // 系统配置- 查询活动分类列表 | |||
| async queryCategoryActivityList() { | |||
| return http({ | |||
| url: '/config/queryCategoryActivityList', | |||
| method: 'GET', | |||
| noToken: true | |||
| }) | |||
| }, | |||
| // 系统配置- 查询商品分类列表 | |||
| async queryCategoryGoodsList() { | |||
| return http({ | |||
| url: '/config/queryCategoryGoodsList', | |||
| method: 'GET', | |||
| noToken: true | |||
| }) | |||
| }, | |||
| } | |||
| @ -0,0 +1,53 @@ | |||
| // import request from "@/api/request"; | |||
| import http from "@/api/http"; | |||
| export default { | |||
| // 首页- 申请成为志愿者 | |||
| async applyVolunteer(data) { | |||
| return http({ | |||
| url: '/index/applyVolunteer', | |||
| method: 'POST', | |||
| data | |||
| }) | |||
| }, | |||
| // 首页- 查看志愿者信息复制接口复制文档复制地址 | |||
| // POST | |||
| // / community - admin / community / index /queryVolunteer | |||
| async queryVolunteer() { | |||
| return http({ | |||
| url: '/index/queryVolunteer', | |||
| method: 'POST' | |||
| }) | |||
| }, | |||
| // 首页- 获取banner图列表 | |||
| async queryBannerList(data) { | |||
| return http({ | |||
| url: '/index/queryBannerList', | |||
| method: 'GET', | |||
| noToken: true, | |||
| data | |||
| }) | |||
| }, | |||
| // 首页- 获取公告详情 | |||
| async queryNoticeById(data) { | |||
| return http({ | |||
| url: '/index/queryNoticeById', | |||
| method: 'GET', | |||
| data, | |||
| noToken: true | |||
| }) | |||
| }, | |||
| // 首页- 获取公告列表 | |||
| async queryNoticeList(data) { | |||
| return http({ | |||
| url: '/index/queryNoticeList', | |||
| method: 'GET', | |||
| data, | |||
| noToken: true | |||
| }) | |||
| }, | |||
| } | |||
| @ -0,0 +1,25 @@ | |||
| // import request from "@/api/request"; | |||
| import http from "@/api/http"; | |||
| export default { | |||
| // 程序-绑定手机号码 | |||
| async bindPhone(data) { | |||
| return http({ | |||
| url: '/login/bindPhone', | |||
| method: 'GET', | |||
| data | |||
| }) | |||
| }, | |||
| async login(data) { | |||
| return http({ | |||
| url: '/login/login', | |||
| method: 'GET', | |||
| data, | |||
| // header: { | |||
| // 'Content-Type': 'application/x-www-form-urlencoded' | |||
| // }, | |||
| showLoading: true, | |||
| noToken: true | |||
| }) | |||
| } | |||
| } | |||
| @ -0,0 +1,25 @@ | |||
| // import request from "@/api/request"; | |||
| import http from "@/api/http"; | |||
| export default { | |||
| // 可用积分- 获取积分明细列表 | |||
| async queryScoreList(data) { | |||
| return http({ | |||
| url: '/score/queryScoreList', | |||
| method: 'POST', | |||
| data, | |||
| debounce: 300 | |||
| }) | |||
| }, | |||
| // 首页- 积分排行榜复制接口复制文档复制地址 | |||
| // POST | |||
| // / community - admin / community / score / queryScoreRank | |||
| async queryScoreRank(data) { | |||
| return http({ | |||
| url: '/score/queryScoreRank', | |||
| method: 'POST', | |||
| data, | |||
| noToken: true | |||
| }) | |||
| } | |||
| } | |||
| @ -0,0 +1,55 @@ | |||
| // import request from "@/api/request"; | |||
| import http from "@/api/http"; | |||
| export default { | |||
| // 首页-兑换商品 | |||
| async buyGoods(data) { | |||
| return http({ | |||
| url: '/goods/buyGoods', | |||
| method: 'POST', | |||
| data, | |||
| showLoading: true | |||
| }) | |||
| }, | |||
| // 首页- 收藏商品 | |||
| async collectionGoods(data) { | |||
| return http({ | |||
| url: '/goods/collectionGoods', | |||
| method: 'POST', | |||
| data, | |||
| showLoading: true | |||
| }) | |||
| }, | |||
| // 首页- 获取商品详情 | |||
| async queryGoodsById(data) { | |||
| return http({ | |||
| url: '/goods/queryGoodsById', | |||
| method: 'GET', | |||
| data, | |||
| noToken: true | |||
| }) | |||
| }, | |||
| // 商品收藏- 获取我收藏的商品列表 | |||
| async queryGoodsCollectionList(data) { | |||
| return http({ | |||
| url: '/goods/queryGoodsCollectionList', | |||
| method: 'POST', | |||
| data | |||
| }) | |||
| }, | |||
| // 首页- 获取商品列表 | |||
| async queryGoodsList(data) { | |||
| return http({ | |||
| url: '/goods/queryGoodsList', | |||
| method: 'GET', | |||
| data, | |||
| noToken: true, | |||
| debounce: 200, | |||
| // showLoading: true | |||
| }) | |||
| } | |||
| } | |||
| @ -0,0 +1,49 @@ | |||
| // import request from "@/api/request"; | |||
| import http from "@/api/http"; | |||
| export default { | |||
| // 兑换记录- 确认取货 | |||
| async finishOrder(data) { | |||
| return http ({ | |||
| url: '/order/finishOrder', | |||
| method: 'POST', | |||
| data | |||
| }) | |||
| }, | |||
| // 兑换记录- 查看订单详情 | |||
| async queryOrderById(data) { | |||
| return http({ | |||
| url: '/order/queryOrderById', | |||
| method: 'GET', | |||
| data | |||
| }) | |||
| }, | |||
| // 兑换记录- 查看订单列表 | |||
| async queryOrderList(data) { | |||
| return http({ | |||
| url: '/order/queryOrderList', | |||
| method: 'GET', | |||
| data, | |||
| // showLoading: true | |||
| }) | |||
| }, | |||
| // 我的资料- 获取个人信息 | |||
| async queryUser() { | |||
| return http({ | |||
| url: '/userInfo/queryUser', | |||
| method: 'GET' | |||
| }) | |||
| }, | |||
| // 我的资料- 修改个人信息 | |||
| async updateUser(data) { | |||
| return http({ | |||
| url: '/userInfo/updateUser', | |||
| method: 'POST', | |||
| data | |||
| }) | |||
| }, | |||
| } | |||
| @ -0,0 +1,115 @@ | |||
| import config from "@/config"; | |||
| export default function request ( { | |||
| url = '', | |||
| method = 'GET', | |||
| data = {}, | |||
| showLoading = false, | |||
| header = {} , | |||
| noToken = false, // 不需要token的接口 | |||
| showToast = true // 默认显示失败的提示 | |||
| } ) { | |||
| if (showLoading) uni.showLoading({title: '加载中'}) | |||
| if(!noToken) { | |||
| const token = uni.getStorageSync('token') | |||
| if (token) { | |||
| header['X-Access-Token'] = token | |||
| }else { | |||
| uni.showToast({ | |||
| title: '请先登录', | |||
| icon: 'none' | |||
| }) | |||
| uni.reLaunch({ url: '/subPages/login/login' }) | |||
| return | |||
| } | |||
| } | |||
| return new Promise((resolve, reject) => { | |||
| uni.request({ | |||
| url: config.baseURL + url, | |||
| method, | |||
| data, | |||
| header: { | |||
| 'Content-Type': 'application/x-www-form-urlencoded', | |||
| ...header | |||
| }, | |||
| success: (res) => { | |||
| console.log(`Success ${method} ${url}`, res); | |||
| // 优先处理业务逻辑响应 | |||
| if (res.statusCode === 200 && res.data) { | |||
| // 业务成功 | |||
| if (res.data.code === 200 ) { | |||
| resolve(res.data) | |||
| return | |||
| } | |||
| // 业务失败但有具体错误信息 | |||
| const errorMsg = res.data.message || '请求失败' | |||
| if (showToast) { | |||
| uni.showToast({ | |||
| title: errorMsg, | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| reject({ | |||
| code: res.data.code, | |||
| message: errorMsg, | |||
| data: res.data | |||
| }) | |||
| return | |||
| } | |||
| // 处理HTTP状态码错误(无有效响应体的情况) | |||
| const error = { | |||
| code: res.statusCode, | |||
| message: '网络请求错误' | |||
| } | |||
| switch (res.statusCode) { | |||
| case 401: | |||
| case 403: | |||
| uni.removeStorageSync('token') | |||
| uni.reLaunch({ url: '/subPages/login/login' }) | |||
| error.message = '登录已过期,请重新登录' | |||
| break; | |||
| case 404: | |||
| error.message = '资源不存在' | |||
| break; | |||
| case 500: | |||
| error.message = '服务器错误' | |||
| break; | |||
| } | |||
| if (showToast) { | |||
| uni.showToast({ | |||
| title: error.message, | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| reject(error) | |||
| }, | |||
| fail: (err) => { | |||
| console.log(`Fail ${method} ${url}`, err); | |||
| const errorMsg = err.errMsg || '请求失败' | |||
| if (showToast) { | |||
| uni.showToast({ | |||
| title: errorMsg, | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| reject({ | |||
| code: -1, | |||
| message: errorMsg, | |||
| data: err | |||
| }) | |||
| }, | |||
| complete: () => { | |||
| if (showLoading) { | |||
| uni.hideLoading() | |||
| } | |||
| } | |||
| }) | |||
| }) | |||
| } | |||
| @ -0,0 +1,77 @@ | |||
| // 环境配置相关 | |||
| /** | |||
| * 环境配置 | |||
| * env 环境变量字段 | |||
| * netConfig 网络配置 | |||
| * aliOSSConfig 阿里云配置 | |||
| * debounceConfig 防抖相关配置 | |||
| */ | |||
| const env = 'development' | |||
| // 全局配置 | |||
| const config = { | |||
| // 网络全局配置 | |||
| netConfig: { | |||
| development: { | |||
| baseURL: 'http://augcl.natapp1.cc/community-admin/community', | |||
| }, | |||
| testing: { | |||
| baseURL: 'https://mulinyouni.augcl.com/community-admin/community', | |||
| }, | |||
| production: { | |||
| baseURL: 'https://www.mulinyouni.com/community-admin/community', | |||
| } | |||
| }, | |||
| // 阿里云配置 | |||
| aliOSSConfig :{ | |||
| development: { | |||
| aliOSS_accessKey: 'LTAI5tQSs47izVy8DLVdwUU9', | |||
| aliOSS_secretKey: 'qHI7C3PaXYZySr84HTToviC71AYlFq', | |||
| aliOSS_bucketName: 'hanhaiimage', | |||
| endpoint: 'oss-cn-shenzhen.aliyuncs.com', | |||
| staticDomain: 'https://image.hhlm1688.com/' | |||
| }, | |||
| testing: { | |||
| aliOSS_accessKey: 'LTAI5tQSs47izVy8DLVdwUU9', | |||
| aliOSS_secretKey: 'qHI7C3PaXYZySr84HTToviC71AYlFq', | |||
| aliOSS_bucketName: 'hanhaiimage', | |||
| endpoint: 'oss-cn-shenzhen.aliyuncs.com', | |||
| staticDomain: 'https://image.hhlm1688.com/' | |||
| }, | |||
| production: { | |||
| aliOSS_accessKey: 'LTAI5tRqoxbC9BKrWJduKDVT', | |||
| aliOSS_secretKey: 's5ANiOq4kYpzuMLQhqPMYL4IybMR7L', | |||
| aliOSS_bucketName: 'mulinyouni', | |||
| endpoint: 'oss-cn-beijing.aliyuncs.com', | |||
| staticDomain: 'https://image.mulinyouni.com/' | |||
| }, | |||
| }, | |||
| // 防抖相关配置 | |||
| debounceConfig : { | |||
| DEFAULT_DEBOUNCE_TIME: 0, | |||
| DEFAULT_THROTTLE_TIME: 0, | |||
| MAX_MAP_SIZE: 1000, | |||
| } | |||
| } | |||
| // 全自动导入并生成平坦化结构 | |||
| const finalConfig = Object.keys(config).reduce((finallyConfig, key) => { | |||
| let tempConfig = {} | |||
| if (key === 'netConfig' || key === 'aliOSSConfig') { | |||
| tempConfig = config[key][env] | |||
| }else { | |||
| tempConfig = config[key] | |||
| } | |||
| return { | |||
| ...finallyConfig, | |||
| ...tempConfig, | |||
| } | |||
| }, {}) | |||
| export default finalConfig | |||
| @ -0,0 +1,20 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8" /> | |||
| <script> | |||
| var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') || | |||
| CSS.supports('top: constant(a)')) | |||
| document.write( | |||
| '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + | |||
| (coverSupport ? ', viewport-fit=cover' : '') + '" />') | |||
| </script> | |||
| <title></title> | |||
| <!--preload-links--> | |||
| <!--app-context--> | |||
| </head> | |||
| <body> | |||
| <div id="app"><!--app-html--></div> | |||
| <script type="module" src="/main.js"></script> | |||
| </body> | |||
| </html> | |||
| @ -0,0 +1,44 @@ | |||
| import App from './App' | |||
| // #ifndef VUE3 | |||
| import GlobalPopup from '@/pages/components/GlobalPopup.vue' | |||
| import Vue from 'vue' | |||
| import './uni.promisify.adaptor' | |||
| import * as api from '@/api' | |||
| import utils from '@/utils' | |||
| import config from '@/config' | |||
| import MixinConfig from '@/mixins/config' | |||
| import store from '@/stores' | |||
| Vue.config.productionTip = false | |||
| // 全局混入获取配置相关信息的方法 | |||
| Vue.mixin(MixinConfig) | |||
| // 全局注册弹窗组件 | |||
| Vue.component('GlobalPopup', GlobalPopup) | |||
| // 将api挂载到Vue的原型 | |||
| Vue.prototype.$api = api | |||
| Vue.prototype.$utils = utils | |||
| Vue.prototype.$config = config // 这里是静态config | |||
| App.mpType = 'app' | |||
| const app = new Vue({ | |||
| ...App, | |||
| store | |||
| }) | |||
| app.$mount() | |||
| // #endif | |||
| // #ifdef VUE3 | |||
| import { createSSRApp } from 'vue' | |||
| export function createApp() { | |||
| const app = createSSRApp(App) | |||
| return { | |||
| app | |||
| } | |||
| } | |||
| // #endif | |||
| @ -0,0 +1,78 @@ | |||
| { | |||
| "name" : "木邻有你", | |||
| "appid" : "__UNI__DDC9EFD", | |||
| "description" : "", | |||
| "versionName" : "1.0.0", | |||
| "versionCode" : "100", | |||
| "transformPx" : false, | |||
| /* 5+App特有相关 */ | |||
| "app-plus" : { | |||
| "usingComponents" : true, | |||
| "nvueStyleCompiler" : "uni-app", | |||
| "compilerVersion" : 3, | |||
| "splashscreen" : { | |||
| "alwaysShowBeforeRender" : true, | |||
| "waiting" : true, | |||
| "autoclose" : true, | |||
| "delay" : 0 | |||
| }, | |||
| /* 模块配置 */ | |||
| "modules" : {}, | |||
| /* 应用发布信息 */ | |||
| "distribute" : { | |||
| /* android打包配置 */ | |||
| "android" : { | |||
| "permissions" : [ | |||
| "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>", | |||
| "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>", | |||
| "<uses-permission android:name=\"android.permission.VIBRATE\"/>", | |||
| "<uses-permission android:name=\"android.permission.READ_LOGS\"/>", | |||
| "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>", | |||
| "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>", | |||
| "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>", | |||
| "<uses-permission android:name=\"android.permission.CAMERA\"/>", | |||
| "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>", | |||
| "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>", | |||
| "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>", | |||
| "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>", | |||
| "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>", | |||
| "<uses-feature android:name=\"android.hardware.camera\"/>", | |||
| "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>" | |||
| ] | |||
| }, | |||
| /* ios打包配置 */ | |||
| "ios" : {}, | |||
| /* SDK配置 */ | |||
| "sdkConfigs" : {} | |||
| } | |||
| }, | |||
| /* 快应用特有相关 */ | |||
| "quickapp" : {}, | |||
| /* 小程序特有相关 */ | |||
| "mp-weixin" : { | |||
| "appid" : "wxb6f11363a55f9535", | |||
| "setting" : { | |||
| "urlCheck" : false | |||
| }, | |||
| "requiredPrivateInfos" : [ "getLocation", "chooseLocation" ], | |||
| "permission" : { | |||
| "scope.userLocation" : { | |||
| "desc" : "你的位置信息将用于定位" | |||
| } | |||
| }, | |||
| "usingComponents" : true | |||
| }, | |||
| "mp-alipay" : { | |||
| "usingComponents" : true | |||
| }, | |||
| "mp-baidu" : { | |||
| "usingComponents" : true | |||
| }, | |||
| "mp-toutiao" : { | |||
| "usingComponents" : true | |||
| }, | |||
| "uniStatistics" : { | |||
| "enable" : false | |||
| }, | |||
| "vueVersion" : "2" | |||
| } | |||
| @ -0,0 +1,49 @@ | |||
| export default { | |||
| data() { | |||
| return { | |||
| } | |||
| }, | |||
| methods: { | |||
| // 自定义分享内容 | |||
| mixinCustomShare() { | |||
| return { | |||
| } | |||
| } | |||
| }, | |||
| computed: { | |||
| // 获取全局配置的文本 | |||
| configParamText() { | |||
| return key => this.$store.state.configList[key]?.paramText || '默认文本' | |||
| }, | |||
| // 获取全局配置的图片 | |||
| configParamImage() { | |||
| return key => this.$store.state.configList[key]?.paramImage || '/static/默认图片.png' | |||
| }, | |||
| // 获取全局配置的富文本 | |||
| configParamTextarea() { | |||
| return key => this.$store.state.configList[key]?.paramTextarea || '默认富文本' | |||
| }, | |||
| // 默认的全局分享参数 | |||
| GShare() { | |||
| return { | |||
| title: this.configParamText('config_app_name'), | |||
| desc: this.configParamText('share_desc'), | |||
| imageUrl: this.configParamImage('config_logo'), | |||
| path: '/pages/index/index' | |||
| } | |||
| } | |||
| }, | |||
| onShareAppMessage() { | |||
| return { | |||
| ...this.GShare, | |||
| ...this.mixinCustomShare() | |||
| } | |||
| }, | |||
| onShareTimeline() { | |||
| return { | |||
| ...this.GShare, | |||
| ...this.mixinCustomShare() | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,133 @@ | |||
| // 简化版列表的混入 | |||
| export default { | |||
| data() { | |||
| return { | |||
| list: [], | |||
| pageNo : 1, | |||
| pageSize : 10, | |||
| mixinListApi: '', | |||
| isLoading: false, | |||
| hasMore: true, | |||
| // 额外返回出去的数据 | |||
| extraData: null, | |||
| // 每次更新数据后执行的函数 可以进行数据处理 | |||
| afterUpdateDataFn: function() {}, | |||
| // 每次更新数据前执行的函数, | |||
| beforeUpdateDataFn: function() {}, | |||
| // 混入配置 | |||
| mixinListConfig: { | |||
| // 数据返回的直接路径 | |||
| responsePath: 'result.records', | |||
| // 列表是否需要下拉刷新 | |||
| isPullDownRefresh: true, | |||
| // 列表是否需要上拉加载 | |||
| isReachBottomLoad: true, | |||
| // 额外返回出去的数据的路径 | |||
| extraDataPath: '' | |||
| } | |||
| } | |||
| }, | |||
| computed: { | |||
| // 自定义onShow前会执行的函数 | |||
| mixinFnBeforePageShow() { | |||
| return function() {} | |||
| } | |||
| }, | |||
| methods: { | |||
| // 获取文件的自定义传参 -- 可以在页面中重写 | |||
| mixinSetParams() { | |||
| return {} | |||
| }, | |||
| // 解析分路径获取嵌套值 | |||
| resolvePath(obj, path) { | |||
| if (path){ | |||
| return path.split('.').reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : null), obj) | |||
| }else { | |||
| return obj | |||
| } | |||
| }, | |||
| // 初始化分页 | |||
| initPage(){ | |||
| this.pageNo = 1, | |||
| this.hasMore = true | |||
| }, | |||
| // 获取列表 | |||
| async getList(isRefresh = false) { | |||
| if (!this.hasMore) { | |||
| return | |||
| } | |||
| this.isLoading = true | |||
| const apiMethod = this.resolvePath(this.$api, this.mixinListApi) | |||
| if (typeof apiMethod !== 'function') { | |||
| console.log('mixinApi不存在', this.mixinListApi); | |||
| return | |||
| } | |||
| // 每次更新数据前执行的函数 | |||
| if (this.beforeUpdateDataFn) { | |||
| this.beforeUpdateDataFn(this.list) | |||
| } | |||
| const res = await apiMethod({ | |||
| pageNo: this.pageNo, | |||
| pageSize: this.pageSize, | |||
| ...this.mixinSetParams() | |||
| }) | |||
| const resData = this.resolvePath(res, this.mixinListConfig.responsePath) || [] | |||
| if (res.code === 200) { | |||
| // 如果没有值了 | |||
| if (!resData.length) { | |||
| this.hasMore = false | |||
| uni.showToast({ | |||
| title: '暂无更多数据', | |||
| icon: 'none' | |||
| }) | |||
| }else { | |||
| this.pageNo++ | |||
| } | |||
| if (isRefresh ) { | |||
| // 如果是刷新,直接覆盖 | |||
| this.list = resData | |||
| } else { | |||
| this.list = [...this.list, ...resData] | |||
| } | |||
| // 如果有额外数据的路径,刷新后,需要将额外数据也刷新 | |||
| if (this.mixinListConfig.extraDataPath !== '') { | |||
| this.extraData = this.resolvePath(res, this.mixinListConfig.extraDataPath) | |||
| } | |||
| } | |||
| // 每次更新数据后执行的函数 | |||
| if (this.afterUpdateDataFn) { | |||
| this.afterUpdateDataFn(this.list) | |||
| } | |||
| // 如果有在加载中 | |||
| if (this.isLoading) { | |||
| this.isLoading = false | |||
| } | |||
| // 有过有在下拉加载 | |||
| uni.stopPullDownRefresh() | |||
| }, | |||
| }, | |||
| async onShow() { | |||
| // if (!this.list.length) { | |||
| if (this.mixinFnBeforePageShow) this.mixinFnBeforePageShow() | |||
| this.initPage() | |||
| await this.getList(true) | |||
| // } | |||
| }, | |||
| async onPullDownRefresh() { | |||
| // 在下拉还没结束前 不做任何操作 | |||
| if (this.isLoading) { | |||
| return | |||
| } | |||
| this.initPage() | |||
| await this.getList(true) | |||
| }, | |||
| async onReachBottom() { | |||
| if (this.isLoading) { | |||
| return | |||
| } | |||
| await this.getList() | |||
| } | |||
| } | |||
| @ -0,0 +1,242 @@ | |||
| { | |||
| "pages": [ | |||
| { | |||
| "path": "pages/index/index", | |||
| "style": { | |||
| "navigationStyle": "custom", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "pages/index/shop", | |||
| "style": { | |||
| "navigationStyle": "custom", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "pages/index/activity", | |||
| "style": { | |||
| "navigationStyle": "custom", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "pages/index/community", | |||
| "style": { | |||
| "navigationBarTitleText": "社区", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "pages/index/my", | |||
| "style": { | |||
| "navigationStyle": "custom", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| } | |||
| ], | |||
| "subPackages":[ | |||
| { | |||
| "root": "subPages", | |||
| "pages":[ | |||
| { | |||
| "path": "index/announcement", | |||
| "style": { | |||
| "navigationBarTitleText": "公告", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "login/login", | |||
| "style": { | |||
| "navigationStyle": "custom" | |||
| } | |||
| }, | |||
| { | |||
| "path": "login/userInfo", | |||
| "style": { | |||
| "navigationBarTitleText": "用户信息", | |||
| "navigationBarBackButtonHidden": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "index/announcementDetail", | |||
| "style": { | |||
| "navigationBarTitleText": "公告详情" | |||
| } | |||
| }, | |||
| { | |||
| "path": "index/ranking", | |||
| "style": { | |||
| "navigationStyle": "custom", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "index/volunteerApply", | |||
| "style": { | |||
| "navigationBarTitleText": "申请志愿者" | |||
| } | |||
| }, | |||
| { | |||
| "path": "index/organizationIntroduction", | |||
| "style": { | |||
| "navigationBarTitleText": "组织介绍" | |||
| } | |||
| }, | |||
| { | |||
| "path": "index/activityCalendar", | |||
| "style": { | |||
| "navigationBarTitleText": "活动日历", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "index/activityDetail", | |||
| "style": { | |||
| "navigationBarTitleText": "活动详情", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "shop/goodsDetail", | |||
| "style": { | |||
| "navigationBarTitleText": "商品详情" | |||
| } | |||
| }, | |||
| { | |||
| "path": "shop/pointsDetail", | |||
| "style": { | |||
| "navigationBarTitleText": "积分详情", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "community/publishPost", | |||
| "style": { | |||
| "navigationBarTitleText": "发布动态" | |||
| } | |||
| }, | |||
| { | |||
| "path": "my/activityFavorites", | |||
| "style": { | |||
| "navigationBarTitleText": "活动收藏", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "my/myProfile", | |||
| "style": { | |||
| "navigationBarTitleText": "我的资料" | |||
| } | |||
| }, | |||
| { | |||
| "path": "my/myRegistrations", | |||
| "style": { | |||
| "navigationBarTitleText": "我的报名", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "my/myActivityDetail", | |||
| "style": { | |||
| "navigationBarTitleText": "活动详情" | |||
| } | |||
| }, | |||
| { | |||
| "path": "my/exchangeRecord", | |||
| "style": { | |||
| "navigationBarTitleText": "兑换记录", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "my/exchangeDetail", | |||
| "style": { | |||
| "navigationBarTitleText": "商品详情", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "my/productFavorites", | |||
| "style": { | |||
| "navigationBarTitleText": "商品收藏", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "my/activityCheckin", | |||
| "style": { | |||
| "navigationBarTitleText": "活动签到", | |||
| "enablePullDownRefresh": true | |||
| } | |||
| }, | |||
| { | |||
| "path": "my/checkinCode", | |||
| "style": { | |||
| "navigationBarTitleText": "签到码" | |||
| } | |||
| }, | |||
| { | |||
| "path": "my/signupSuccess", | |||
| "style": { | |||
| "navigationBarTitleText": "报名成功!" | |||
| } | |||
| } | |||
| ] | |||
| } | |||
| ], | |||
| "preloadRule": { | |||
| "pages/index/index": { | |||
| "network": "all", | |||
| "packages": ["subPages"] | |||
| } | |||
| }, | |||
| "globalStyle": { | |||
| "navigationBarTextStyle": "white", | |||
| "navigationBarTitleText": "uni-app", | |||
| "navigationBarBackgroundColor": "#1488DB", | |||
| "backgroundColor": "#218CDD" | |||
| // "enablePullDownRefresh": true | |||
| }, | |||
| "tabBar": { | |||
| "color": "#999999", | |||
| "selectedColor": "#2E66F4", | |||
| "borderStyle": "white", | |||
| "backgroundColor": "#ffffff", | |||
| "list": [ | |||
| { | |||
| "pagePath": "pages/index/index", | |||
| "text": "主页", | |||
| "iconPath": "static/主页.png", | |||
| "selectedIconPath": "static/主页_点击.png" | |||
| }, | |||
| { | |||
| "pagePath": "pages/index/shop", | |||
| "text": "商城", | |||
| "iconPath": "static/商城.png", | |||
| "selectedIconPath": "static/商城_点击.png" | |||
| }, | |||
| { | |||
| "pagePath": "pages/index/activity", | |||
| "text": "活动", | |||
| "iconPath": "static/活动.png", | |||
| "selectedIconPath": "static/活动_点击.png" | |||
| }, | |||
| { | |||
| "pagePath": "pages/index/community", | |||
| "text": "社区", | |||
| "iconPath": "static/社区.png", | |||
| "selectedIconPath": "static/社区_点击.png" | |||
| }, | |||
| { | |||
| "pagePath": "pages/index/my", | |||
| "text": "我的", | |||
| "iconPath": "static/我的.png", | |||
| "selectedIconPath": "static/我的_点击.png" | |||
| } | |||
| ] | |||
| } | |||
| } | |||
| @ -0,0 +1,198 @@ | |||
| <template> | |||
| <!-- 如何让他点击遮罩层的时候关闭弹窗 --> | |||
| <view v-if="visible" class="popup-overlay" @click="close"> | |||
| <view class="popup-content" @click.stop :animation="animationData"> | |||
| <image class="popup-bg" :src="bgImage" mode="aspectFit"></image> | |||
| <image class="popup-title" :src="titleImage" mode="aspectFit"></image> | |||
| <!-- <view class="popup-header"> --> | |||
| <text class="popup-text">{{ content }}</text> | |||
| <text class="popup-subtext">{{ subContent }}</text> | |||
| <!-- </view> --> | |||
| <button v-if="popupType === 'success'" class="popup-btn success" @click="close">我知道了</button> | |||
| <button v-else class="popup-btn fail" @click="close">好的</button> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'GlobalPopup', | |||
| data() { | |||
| return { | |||
| visible: false, | |||
| content: '', | |||
| subContent: '', | |||
| titleType: '', | |||
| popupType: '', | |||
| animationData: {}, | |||
| closefn: () => {} | |||
| } | |||
| }, | |||
| computed: { | |||
| bgImage() { | |||
| return `${this.popupType === 'success' ? '/static/成功弹窗.png' : '/static/失败弹窗.png'}` | |||
| }, | |||
| titleImage() { | |||
| switch (this.titleType) { | |||
| case 'exchange': | |||
| if (this.popupType === 'success') { | |||
| return '/static/兑换成功.png' | |||
| }else{ | |||
| return '/static/兑换失败.png' | |||
| } | |||
| case 'signup': | |||
| return '/static/报名成功.png' | |||
| case 'submit': | |||
| return '/static/提交成功.png' | |||
| default: | |||
| return '' | |||
| } | |||
| } | |||
| }, | |||
| methods: { | |||
| close() { | |||
| this.createHideAnimation() | |||
| this.closefn() | |||
| }, | |||
| // 主内容 副内容 标题类型 弹窗类型 | |||
| open({ | |||
| content = '默认内容', | |||
| subContent = '默认子内容', | |||
| titleType = 'exchange', // 报名 兑换 提交 | |||
| popupType = 'success', // 成功 失败 | |||
| closefn = () => {} | |||
| }) { | |||
| this.content = content | |||
| this.subContent = subContent | |||
| this.titleType = titleType | |||
| this.popupType = popupType | |||
| // 先设置初始动画状态 | |||
| this.setInitialAnimation() | |||
| // 显示弹窗 | |||
| this.visible = true | |||
| // 延迟执行显示动画 | |||
| this.$nextTick(() => { | |||
| setTimeout(() => { | |||
| this.createShowAnimation() | |||
| }, 50) | |||
| }) | |||
| this.closefn = closefn | |||
| }, | |||
| // 设置初始动画状态 | |||
| setInitialAnimation() { | |||
| const animation = uni.createAnimation({ | |||
| transformOrigin: "50% 50%", | |||
| duration: 0, | |||
| timingFunction: "ease-out", | |||
| delay: 0 | |||
| }) | |||
| animation.translateX('-50%').translateY('-50%').scale(0).opacity(0).step() | |||
| this.animationData = animation.export() | |||
| }, | |||
| // 创建弹窗显示动画 | |||
| createShowAnimation() { | |||
| const animation = uni.createAnimation({ | |||
| transformOrigin: "50% 50%", | |||
| duration: 200, | |||
| timingFunction: "ease-out", | |||
| delay: 0 | |||
| }) | |||
| // 动画到最终状态:保持居中,缩放为1,透明度为1 | |||
| animation.translateX('-50%').translateY('-50%').scale(1).opacity(1).step() | |||
| this.animationData = animation.export() | |||
| }, | |||
| // 创建弹窗隐藏动画 | |||
| createHideAnimation() { | |||
| const animation = uni.createAnimation({ | |||
| transformOrigin: "50% 50%", | |||
| duration: 200, | |||
| timingFunction: "ease-in", | |||
| delay: 0 | |||
| }) | |||
| animation.translateX('-50%').translateY('-50%').scale(0).opacity(0).step() | |||
| this.animationData = animation.export() | |||
| // 动画结束后隐藏弹窗 | |||
| setTimeout(() => { | |||
| this.visible = false | |||
| }, 200) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .popup-overlay { | |||
| position: fixed; | |||
| inset: 0; | |||
| width: 100%; | |||
| height: 100%; | |||
| background: #00000050; | |||
| z-index: 999; | |||
| .popup-content { | |||
| position: relative; | |||
| top: 50%; | |||
| left: 50%; | |||
| width: 632rpx; | |||
| padding: 0; | |||
| height: 830rpx; | |||
| // background: red; | |||
| // border-radius: 20rpx; | |||
| // background-size: 100% 100%; | |||
| .popup-bg{ | |||
| position: absolute; | |||
| inset: 0; | |||
| z-index: -1; | |||
| width: 632rpx; | |||
| height: 830rpx; | |||
| } | |||
| .popup-title{ | |||
| position: absolute; | |||
| top: 44rpx; | |||
| left: 88rpx; | |||
| height: 100rpx; | |||
| width: 254rpx; | |||
| } | |||
| .popup-btn{ | |||
| position: absolute; | |||
| bottom: 30rpx; | |||
| left: 50%; | |||
| transform: translateX(-50%); | |||
| width: 432rpx; | |||
| height: 94rpx; | |||
| border-radius: 20.5px; | |||
| // width: 28px; | |||
| font-size: 14px; | |||
| line-height: 94rpx; | |||
| color: #ffffff; | |||
| } | |||
| .success{ | |||
| background: #1488db; | |||
| } | |||
| .fail{ | |||
| background: #e54b4b; | |||
| } | |||
| .popup-text{ | |||
| font-size: 16px; | |||
| position: absolute; | |||
| top: 480rpx; | |||
| left: 50%; | |||
| transform: translateX(-50%); | |||
| font-weight: 700; | |||
| color: #000000; | |||
| white-space: nowrap; | |||
| // text-align: center; | |||
| } | |||
| .popup-subtext{ | |||
| position: absolute; | |||
| bottom: 252rpx; | |||
| left: 50%; | |||
| transform: translateX(-50%); | |||
| font-size: 14px; | |||
| color: #999999; | |||
| white-space: nowrap; | |||
| // text-align: center; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,23 @@ | |||
| <template> | |||
| <view :style="{width: '100%'}"> | |||
| <view class="nav-container" > | |||
| <!-- 我是占位符 --> | |||
| </view> | |||
| <view class="placeholder"> | |||
| </view > | |||
| </view> | |||
| </template> | |||
| <style lang="scss" scoped> | |||
| .nav-container { | |||
| position: absolute; | |||
| // z-index: -1; | |||
| width: 100%; | |||
| height: 270rpx; | |||
| background: linear-gradient(180deg,#1488db, #98b5f1); | |||
| } | |||
| .placeholder{ | |||
| width: 100%; | |||
| height: 160rpx; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,226 @@ | |||
| <template> | |||
| <!-- 自定义搜索框组件 --> | |||
| <view class="search-container" > | |||
| <view class="search-input" :style="{ backgroundColor: bgColor }"> | |||
| <!-- 搜索图标 --> | |||
| <view v-if="searchIconAlign === 'left' && showIcon" class="search-icon left" @click="clickIcon"> | |||
| <text class="iconfont">🔍</text> | |||
| </view> | |||
| <view v-if="searchIconAlign === 'center' && showIcon" class="search-icon center" @click="clickIcon"> | |||
| <text class="iconfont">🔍</text> | |||
| </view> | |||
| <input | |||
| :value="value" | |||
| :placeholder="placeholder" | |||
| type="text" | |||
| :disabled="disabled" | |||
| :maxlength="maxLength" | |||
| @input="input" | |||
| @confirm="search" | |||
| @keyup.enter="search" | |||
| class="input-field" | |||
| :style="{ | |||
| textAlign: textAlign, | |||
| borderRadius: borderRadius, | |||
| height: height, | |||
| width: width }" | |||
| /> | |||
| <!-- 清除按钮 --> | |||
| <view v-if="value && !disabled" class="clear-icon" @click="clear"> | |||
| <text class="iconfont">✕</text> | |||
| </view> | |||
| <!-- 搜索图标在右侧 --> | |||
| <view v-if="searchIconAlign === 'right' && showIcon" class="search-icon right" @click="clickIcon"> | |||
| <text class="iconfont">🔍</text> | |||
| </view> | |||
| </view> | |||
| <!-- 取消按钮 --> | |||
| <view v-if="showCancel" class="cancel-btn" @click="cancel"> | |||
| <text>取消</text> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default{ | |||
| name:'Search', | |||
| props:{ | |||
| placeholder:{ | |||
| type:String, | |||
| default:'搜索' | |||
| }, | |||
| // 是否展示搜索图标 | |||
| showIcon:{ | |||
| type:Boolean, | |||
| default:true | |||
| }, | |||
| // 是否展示右侧的取消按钮 | |||
| showCancel:{ | |||
| type:Boolean, | |||
| default:true | |||
| }, | |||
| // 搜索图标对齐位置 | |||
| searchIconAlign:{ | |||
| type:String, | |||
| default:'left' | |||
| }, | |||
| // 搜索框内容对齐方式 | |||
| textAlign:{ | |||
| type:String, | |||
| default:'left' | |||
| }, | |||
| // v-model传入的内容怎么用 | |||
| value:{ | |||
| type:String, | |||
| default:'' | |||
| }, | |||
| // 搜索框内容改变时触发的事件 | |||
| height:{ | |||
| type:String, | |||
| default:'60rpx' | |||
| }, | |||
| width:{ | |||
| type:String, | |||
| default:'100%' | |||
| }, | |||
| bgColor:{ | |||
| type:String, | |||
| default:'#f3f7f8' | |||
| }, | |||
| disabled:{ | |||
| type:Boolean, | |||
| default:false | |||
| }, | |||
| maxLength:{ | |||
| type:Number, | |||
| default:100 | |||
| }, | |||
| borderRadius:{ | |||
| type:String, | |||
| default:'30rpx' | |||
| }, | |||
| }, | |||
| methods:{ | |||
| clickIcon(){ | |||
| console.log('clickIcon'); | |||
| this.search() | |||
| }, | |||
| clear(){ | |||
| // console.log('clear'); | |||
| this.$emit('clear', '') | |||
| this.$emit('input', '') | |||
| }, | |||
| search(){ | |||
| console.log('search', this.value); | |||
| this.$emit('search', this.value) | |||
| }, | |||
| cancel(){ | |||
| // console.log('cancel'); | |||
| this.$emit('cancel') | |||
| this.$emit('input', '') | |||
| }, | |||
| input(e){ | |||
| const value = e.detail.value | |||
| console.log('input', value) | |||
| this.$emit('input', value) | |||
| // this.onChange(value) | |||
| } | |||
| }, | |||
| watch:{ | |||
| value(newVal){ | |||
| console.log(newVal) | |||
| this.$emit('change', newVal) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .search-container { | |||
| display: flex; | |||
| align-items: center; | |||
| padding: 20rpx; | |||
| // background-color: #fff; | |||
| .search-input { | |||
| flex: 1; | |||
| display: flex; | |||
| align-items: center; | |||
| // height: 60rpx; | |||
| border-radius: 30rpx; | |||
| padding: 0 20rpx; | |||
| position: relative; | |||
| .search-icon { | |||
| // flex: 0.1; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| width: 40rpx; | |||
| height: 40rpx; | |||
| &.left { | |||
| margin-right: 10rpx; | |||
| } | |||
| &.right { | |||
| margin-left: 10rpx; | |||
| } | |||
| .iconfont { | |||
| font-size: 28rpx; | |||
| color: #999; | |||
| } | |||
| } | |||
| .input-field { | |||
| flex: 1; | |||
| padding-left: 4rpx; | |||
| border: none; | |||
| outline: none; | |||
| background: transparent; | |||
| font-size: 28rpx; | |||
| color: #333; | |||
| // background-color: #f1f6ff; | |||
| &::placeholder { | |||
| color: #999; | |||
| } | |||
| } | |||
| .clear-icon { | |||
| display: flex; | |||
| // align-items: center; | |||
| line-height: 40rpx; | |||
| justify-content: center; | |||
| width: 40rpx; | |||
| height: 40rpx; | |||
| margin-left: 10rpx; | |||
| border-radius: 50%; | |||
| background: #f0f0f0; | |||
| .iconfont { | |||
| font-size: 20rpx; | |||
| color: #b2b2b2; | |||
| } | |||
| } | |||
| } | |||
| .cancel-btn { | |||
| margin-left: 20rpx; | |||
| padding: 0 20rpx; | |||
| text { | |||
| font-size: 28rpx; | |||
| color: #007aff; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,236 @@ | |||
| <template> | |||
| <view class="recommended-activities"> | |||
| <view class="activities-header"> | |||
| <view class="header-left"> | |||
| <image class="header-icon" src="/static/推荐活动.png" mode="aspectFit"></image> | |||
| <!-- <text class="header-title">推荐活动</text> --> | |||
| </view> | |||
| <view class="more" @click="goToMoreActivities"> | |||
| <text class="more-text">更多</text> | |||
| <uv-icon name="arrow-right" color="#999" size="12"></uv-icon> | |||
| </view> | |||
| </view> | |||
| <view class="activity-list"> | |||
| <view class="activity-item" v-for="(item, index) in activityList" :key="index" @click="viewActivityDetail(item)"> | |||
| <image class="activity-image" :src="item.image" mode="aspectFill"></image> | |||
| <view class="activity-info"> | |||
| <view class="title-row"> | |||
| <view class="activity-badge"> | |||
| <text class="badge-text">{{item.score}}分</text> | |||
| </view> | |||
| <text class="activity-title">{{item.title}}</text> | |||
| </view> | |||
| <view class="activity-location"> | |||
| <uv-icon name="map-fill" size="14" color="#999"></uv-icon> | |||
| <text class="location-text">{{item.address}}</text> | |||
| </view> | |||
| <view class="activity-time"> | |||
| <uv-icon name="calendar" size="14" color="#999"></uv-icon> | |||
| <text class="time-text">{{item.activityTime}}</text> | |||
| </view> | |||
| <view class="activity-participants"> | |||
| <uv-icon name="account-fill" size="14" color="#999"></uv-icon> | |||
| <text class="participants-text">{{item.numActivity}}人已报名</text> | |||
| </view> | |||
| </view> | |||
| <view class="activity-action"> | |||
| <uv-button type="primary" size="mini" :text="item.isApply ? '已报名' : (item.numActivity >= item.numLimit ? '已满人' : '报名中')" @click.stop="signUpActivity(item)"></uv-button> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'RecommendedActivities', | |||
| data() { | |||
| return { | |||
| activityList: [ | |||
| { | |||
| id: 1, | |||
| title: '关爱自闭儿童活动', | |||
| image: '/static/bannerImage.png', | |||
| location: '七步沙社区文化中心', | |||
| time: '2025-06-12 14:30', | |||
| participants: 12 | |||
| }, | |||
| { | |||
| id: 1, | |||
| title: '关爱自闭儿童活动', | |||
| image: '/static/bannerImage.png', | |||
| location: '七步沙社区文化中心', | |||
| time: '2025-06-12 14:30', | |||
| participants: 12 | |||
| }, | |||
| { | |||
| id: 1, | |||
| title: '关爱自闭儿童活动', | |||
| image: '/static/bannerImage.png', | |||
| location: '七步沙社区文化中心', | |||
| time: '2025-06-12 14:30', | |||
| participants: 12 | |||
| }, | |||
| { | |||
| id: 1, | |||
| title: '关爱自闭儿童活动', | |||
| image: '/static/bannerImage.png', | |||
| location: '七步沙社区文化中心', | |||
| time: '2025-06-12 14:30', | |||
| participants: 12 | |||
| }, | |||
| ] | |||
| } | |||
| }, | |||
| methods: { | |||
| goToMoreActivities() { | |||
| // 跳转到更多活动页面 | |||
| uni.switchTab({ | |||
| url: '/pages/index/activity' | |||
| }) | |||
| }, | |||
| viewActivityDetail(activity) { | |||
| // 查看活动详情 | |||
| uni.navigateTo({ | |||
| url: `/subPages/index/activityDetail?id=${activity.id}` | |||
| }) | |||
| }, | |||
| signUpActivity(activity) { | |||
| // 报名活动 | |||
| uni.navigateTo({ | |||
| url: `/subPages/index/activityDetail?id=${activity.id}` | |||
| }) | |||
| }, | |||
| async getActivityList() { | |||
| const res = await this.$api.activity.queryActivityList({ | |||
| pageNo: 1, | |||
| pageSize: 5, | |||
| isHot: 1 | |||
| }) | |||
| this.activityList = res.result.records | |||
| }, | |||
| }, | |||
| // created() { | |||
| // this.getActivityList() | |||
| // } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .recommended-activities { | |||
| margin: 20rpx; | |||
| // background-color: #fff; | |||
| border-radius: 12rpx; | |||
| // padding: 20rpx; | |||
| .activities-header { | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| margin-bottom: 10rpx; | |||
| padding-left: 20rpx; | |||
| .header-left { | |||
| display: flex; | |||
| align-items: center; | |||
| .header-icon { | |||
| width: 158rpx; | |||
| height: 50rpx; | |||
| margin-right: 10rpx; | |||
| } | |||
| .header-title { | |||
| font-size: 30rpx; | |||
| font-weight: bold; | |||
| color: $uni-text-color; | |||
| } | |||
| } | |||
| .more { | |||
| display: flex; | |||
| align-items: center; | |||
| .more-text { | |||
| font-size: 24rpx; | |||
| color: $uni-text-color-grey; | |||
| margin-right: 4rpx; | |||
| } | |||
| } | |||
| } | |||
| .activity-list { | |||
| .activity-item { | |||
| display: flex; | |||
| margin-bottom: 30rpx; | |||
| background: #fff; | |||
| border-radius: 12rpx; | |||
| padding: 20rpx; | |||
| .activity-image { | |||
| width: 180rpx; | |||
| height: 180rpx; | |||
| border-radius: 8rpx; | |||
| margin-right: 20rpx; | |||
| } | |||
| .activity-info { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| .title-row { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 10rpx; | |||
| .activity-badge { | |||
| width: 31px; | |||
| height: 20px; | |||
| background: #218cdd; | |||
| border-radius: 3.5px; | |||
| margin-right: 7rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| .badge-text { | |||
| font-size: 18rpx; | |||
| color: #fff; | |||
| // font-weight: bold; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .activity-title { | |||
| font-size: 28rpx; | |||
| font-weight: bold; | |||
| color: $uni-text-color; | |||
| } | |||
| .activity-location, .activity-time, .activity-participants { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 6rpx; | |||
| .location-text, .time-text, .participants-text { | |||
| font-size: 24rpx; | |||
| color: $uni-text-color-grey; | |||
| margin-left: 6rpx; | |||
| } | |||
| } | |||
| } | |||
| .activity-action { | |||
| display: flex; | |||
| align-items: flex-end; | |||
| padding-bottom: 10rpx; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,143 @@ | |||
| <template> | |||
| <view class="volunteer-features"> | |||
| <view class="features-container"> | |||
| <!-- 左侧成为志愿者 --> | |||
| <view class="feature-block left-feature" @click="navigateTo('/subPages/index/volunteerApply')"> | |||
| <image class="feature-bg" src="/static/成为志愿者.png" mode="aspectFill"></image> | |||
| <view class="feature-content"> | |||
| <view class="feature-info"> | |||
| <text class="feature-title">成为志愿者</text> | |||
| <text class="feature-desc">加入我们的行列</text> | |||
| <image class="arrow-icon" src="/static/志愿者箭头.png" mode="aspectFit"></image> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 右侧两个功能 --> | |||
| <view class="right-features"> | |||
| <!-- 组织介绍 --> | |||
| <view class="feature-block right-feature" @click="navigateTo('/subPages/index/organizationIntroduction')"> | |||
| <image class="feature-bg" src="/static/组织介绍.png" mode="aspectFill"></image> | |||
| <view class="feature-content"> | |||
| <view class="feature-info"> | |||
| <text class="feature-title">组织介绍</text> | |||
| <text class="feature-desc">了解我们的组织</text> | |||
| <image class="arrow-icon" src="/static/组织箭头.png" mode="aspectFit"></image> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 活动日历 --> | |||
| <view class="feature-block right-feature" @click="navigateTo('/subPages/index/activityCalendar')"> | |||
| <image class="feature-bg" src="/static/活动日历.png" mode="aspectFill"></image> | |||
| <view class="feature-content"> | |||
| <view class="feature-info"> | |||
| <text class="feature-title">活动日历</text> | |||
| <text class="feature-desc">查看近期活动安排</text> | |||
| <image class="arrow-icon" src="/static/活动箭头.png" mode="aspectFit"></image> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'VolunteerFeatures', | |||
| data() { | |||
| return {} | |||
| }, | |||
| methods: { | |||
| navigateTo(url) { | |||
| uni.navigateTo({ | |||
| url | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .volunteer-features { | |||
| margin: 20rpx; | |||
| .features-container { | |||
| display: flex; | |||
| gap: 40rpx; | |||
| height: 400rpx; | |||
| .feature-block { | |||
| position: relative; | |||
| border-radius: 20rpx; | |||
| overflow: hidden; | |||
| } | |||
| .left-feature { | |||
| flex: 1; | |||
| height: 100%; | |||
| } | |||
| .feature-bg { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 100%; | |||
| object-fit: cover; | |||
| z-index: 1; | |||
| } | |||
| .right-features { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 20rpx; | |||
| .right-feature { | |||
| flex: 1; | |||
| } | |||
| } | |||
| .feature-content { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| padding: 30rpx; | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: flex-end; | |||
| // background: linear-gradient(to top, rgba(0,0,0,0.6), rgba(0,0,0,0)); | |||
| z-index: 2; | |||
| .feature-info { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 10rpx; | |||
| .feature-title { | |||
| font-size: 32rpx; | |||
| // font-weight: bold; | |||
| color: #000000; | |||
| margin-bottom: 5rpx; | |||
| } | |||
| .feature-desc { | |||
| font-size: 20rpx; | |||
| color: #999999; | |||
| } | |||
| .arrow-icon { | |||
| width: 54rpx; | |||
| height: 29rpx; | |||
| margin-left: 20rpx; | |||
| // position: absolute; | |||
| // right: 15rpx; | |||
| // bottom: 15rpx; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,192 @@ | |||
| <template> | |||
| <view class="volunteer-header"> | |||
| <view class="swiper-container" > | |||
| <uv-swiper :list="bannerList" indicator indicatorMode="dot" height="270rpx" circular></uv-swiper> | |||
| <!-- <view class="header-title"> | |||
| <text class="title-text">国际志愿者日</text> | |||
| <text class="date-text">12/05</text> | |||
| </view> --> | |||
| <!-- <image class="dove-icon" src="/static/路径 6665@2x.png" mode="aspectFit"></image> --> | |||
| </view> | |||
| <view class="notice-bar" @click="goToAnnouncement"> | |||
| <image class="horn-icon" src="/static/首页_小喇叭.png" mode="aspectFit"></image> | |||
| <view class="notice-scroll-container"> | |||
| <view class="notice-scroll" :animation="animationData"> | |||
| <text class="notice-text" v-for="(notice, index) in noticeList" :key="index"> | |||
| {{ notice.title }} | |||
| </text> | |||
| </view> | |||
| </view> | |||
| <uv-icon name="arrow-right" color="#999" size="14"></uv-icon> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'VolunteerHeader', | |||
| data() { | |||
| return { | |||
| bannerList: [ | |||
| '/static/bannerImage.png', | |||
| '/static/bannerImage.png', | |||
| '/static/bannerImage.png', | |||
| ], | |||
| noticeList: [ | |||
| { id: 1, title: '【重要通知】志愿者服务活动报名开始啦,欢迎大家踊跃参与!' }, | |||
| { id: 2, title: '【活动预告】本周六将举办环保志愿活动,期待您的参与' }, | |||
| { id: 3, title: '【表彰通知】优秀志愿者表彰大会将于下周举行' }, | |||
| { id: 4, title: '【温馨提醒】请各位志愿者及时更新个人信息' } | |||
| ], | |||
| animationData: {}, | |||
| currentIndex: 0, | |||
| scrollTimer: null | |||
| } | |||
| }, | |||
| methods: { | |||
| goToAnnouncement() { | |||
| // 跳转到公告页面 | |||
| uni.navigateTo({ | |||
| url: '/subPages/index/announcement' | |||
| }) | |||
| }, | |||
| async queryBannerList() { | |||
| const res = await this.$api.home.queryBannerList({ | |||
| type: 0 | |||
| }) | |||
| this.bannerList = res.result.records.map(item => item.image) | |||
| }, | |||
| // 获取公告列表 | |||
| async queryNoticeList() { | |||
| const res = await this.$api.home.queryNoticeList({ | |||
| pageNo: 1, | |||
| pageSize: 4, | |||
| }) | |||
| this.noticeList = res.result.records | |||
| }, | |||
| // 开始滚动动画 | |||
| startScroll() { | |||
| this.scrollTimer = setInterval(() => { | |||
| this.currentIndex = (this.currentIndex + 1) % this.noticeList.length | |||
| const animation = uni.createAnimation({ | |||
| duration: 500, | |||
| timingFunction: 'ease-in-out' | |||
| }) | |||
| animation.translateY(-this.currentIndex * 30).step() | |||
| this.animationData = animation.export() | |||
| }, 2000) | |||
| }, | |||
| // 停止滚动动画 | |||
| stopScroll() { | |||
| if (this.scrollTimer) { | |||
| clearInterval(this.scrollTimer) | |||
| this.scrollTimer = null | |||
| } | |||
| } | |||
| }, | |||
| async mounted() { | |||
| console.log('出发喽'); | |||
| await this.queryBannerList() | |||
| await this.queryNoticeList() | |||
| // 启动公告滚动 | |||
| this.startScroll() | |||
| }, | |||
| beforeDestroy() { | |||
| // 清理定时器 | |||
| this.stopScroll() | |||
| }, | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .volunteer-header { | |||
| width: 100%; | |||
| .swiper-container { | |||
| position: relative; | |||
| margin: 20rpx; | |||
| border-radius: 20rpx; | |||
| overflow: hidden; | |||
| .header-title { | |||
| position: absolute; | |||
| bottom: 20rpx; | |||
| left: 20rpx; | |||
| z-index: 10; | |||
| display: flex; | |||
| flex-direction: column; | |||
| background-color: rgba(0,0,0,0.4); | |||
| padding: 10rpx 20rpx; | |||
| border-radius: 10rpx; | |||
| .title-text { | |||
| font-size: 36rpx; | |||
| font-weight: bold; | |||
| color: #fff; | |||
| } | |||
| .date-text { | |||
| font-size: 28rpx; | |||
| color: #2c5e2e; | |||
| margin-top: 6rpx; | |||
| } | |||
| } | |||
| .dove-icon { | |||
| position: absolute; | |||
| right: 20rpx; | |||
| bottom: 20rpx; | |||
| z-index: 10; | |||
| width: 70rpx; | |||
| height: 70rpx; | |||
| background-color: #fff; | |||
| border-radius: 50%; | |||
| padding: 10rpx; | |||
| } | |||
| } | |||
| .notice-bar { | |||
| display: flex; | |||
| align-items: center; | |||
| background-color: #fff; | |||
| padding: 10rpx 20rpx; | |||
| margin: 0 20rpx 20rpx; | |||
| border-radius: 12rpx; | |||
| box-shadow: 2rpx 3rpx 2rpx 2rpx #3a94e1; ; | |||
| // border-bottom: 1rpx solid #3A94E1; | |||
| .horn-icon { | |||
| width: 40rpx; | |||
| height: 40rpx; | |||
| margin-right: 10rpx; | |||
| } | |||
| .notice-scroll-container { | |||
| flex: 1; | |||
| height: 60rpx; | |||
| overflow: hidden; | |||
| position: relative; | |||
| } | |||
| .notice-scroll { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| width: 100%; | |||
| } | |||
| .notice-text { | |||
| display: block; | |||
| height: 60rpx; | |||
| line-height: 60rpx; | |||
| font-size: 28rpx; | |||
| color: #000; | |||
| font-weight: 700; | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,224 @@ | |||
| <template> | |||
| <view class="volunteer-ranking"> | |||
| <view class="ranking-header"> | |||
| <image class="ranking-title-img" src="/static/积分排行榜.png" mode="aspectFit"></image> | |||
| <view class="more" @click="goToRankingList"> | |||
| <text class="more-text">更多</text> | |||
| <uv-icon name="arrow-right" color="#999" size="12"></uv-icon> | |||
| </view> | |||
| </view> | |||
| <view class="ranking-scroll-container"> | |||
| <scroll-view class="ranking-list" scroll-x show-scrollbar="false" enhanced="true" enable-flex="true" scroll-with-animation="true" @scroll="onScrollChange"> | |||
| <view class="ranking-content"> | |||
| <view class="ranking-item" v-for="(item, index) in rankingList" :key="index" @click="viewVolunteerDetail"> | |||
| <view class="avatar-container"> | |||
| <view class="avatar-with-border"> | |||
| <image :src="item.headImage || '/static/默认头像.png'" class="avatar-image" mode="aspectFill"></image> | |||
| </view> | |||
| </view> | |||
| <view class="points-container"> | |||
| <image class="points-icon" src="/static/积分图标.png" mode="aspectFit"></image> | |||
| <text class="volunteer-points">{{item.score}}</text> | |||
| </view> | |||
| <text class="volunteer-name">{{item.nickName}}</text> | |||
| </view> | |||
| </view> | |||
| </scroll-view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'VolunteerRanking', | |||
| data() { | |||
| return { | |||
| rankingList: [ | |||
| ], | |||
| currentScrollIndex: 0 | |||
| } | |||
| }, | |||
| methods: { | |||
| goToRankingList() { | |||
| // 跳转到排行榜详情页 | |||
| uni.navigateTo({ | |||
| url: '/subPages/index/ranking' | |||
| }) | |||
| }, | |||
| viewVolunteerDetail() { | |||
| uni.navigateTo({ | |||
| url: '/subPages/index/ranking' | |||
| }) | |||
| }, | |||
| onScrollChange(e) { | |||
| // 根据滚动位置更新指示器 | |||
| const scrollLeft = e.detail.scrollLeft; | |||
| const scrollWidth = e.detail.scrollWidth; | |||
| const clientWidth = e.detail.scrollWidth / this.rankingList.length * 3; | |||
| // 计算当前滚动索引(每3个为一组) | |||
| if (scrollLeft < clientWidth / 3) { | |||
| this.currentScrollIndex = 0; | |||
| } else if (scrollLeft < clientWidth * 2 / 3) { | |||
| this.currentScrollIndex = 1; | |||
| } else { | |||
| this.currentScrollIndex = 2; | |||
| } | |||
| }, | |||
| // 获取志愿者积分排名 | |||
| async getVolunteerRanking() { | |||
| const res = await this.$api.score.queryScoreRank({ | |||
| pageNo: 1, | |||
| pageSize: 10 | |||
| }) | |||
| if (res.code === 200) { | |||
| this.rankingList = res.result.scoreList.records | |||
| } | |||
| } | |||
| }, | |||
| // async mounted() { | |||
| // await this.getVolunteerRanking() | |||
| // } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .volunteer-ranking { | |||
| background-color: #fff; | |||
| margin: 20rpx; | |||
| border-radius: 10rpx; | |||
| padding: 20rpx; | |||
| .ranking-header { | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| margin-bottom: 20rpx; | |||
| .ranking-title-img { | |||
| height: 60rpx; | |||
| width: 200rpx; | |||
| } | |||
| .more { | |||
| display: flex; | |||
| align-items: center; | |||
| .more-text { | |||
| font-size: 24rpx; | |||
| color: $uni-text-color-grey; | |||
| margin-right: 4rpx; | |||
| } | |||
| } | |||
| } | |||
| .ranking-scroll-container { | |||
| position: relative; | |||
| width: 100%; | |||
| } | |||
| .ranking-list { | |||
| white-space: nowrap; | |||
| padding: 15rpx 0; | |||
| width: 100%; | |||
| overflow-x: auto; | |||
| -webkit-overflow-scrolling: touch; | |||
| .ranking-content { | |||
| display: flex; | |||
| padding: 0 20rpx; | |||
| min-width: max-content; | |||
| } | |||
| .ranking-item { | |||
| display: inline-flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| margin-right: 40rpx; | |||
| flex-shrink: 0; | |||
| min-width: 100rpx; | |||
| transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); | |||
| &:hover, &:active { | |||
| transform: scale(1.08); | |||
| } | |||
| &:last-child { | |||
| margin-right: 20rpx; | |||
| } | |||
| .avatar-container { | |||
| position: relative; | |||
| width: 110rpx; | |||
| height: 110rpx; | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| .avatar-with-border { | |||
| width: 110rpx; | |||
| height: 110rpx; | |||
| border: 3rpx solid #1f8bdc; | |||
| border-radius: 50%; | |||
| // box-shadow: 0 4rpx 12rpx rgba(33, 140, 221, 0.2); | |||
| transition: all 0.3s ease; | |||
| overflow: hidden; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| .avatar-image { | |||
| width: 100%; | |||
| height: 100%; | |||
| border-radius: 50%; | |||
| } | |||
| } | |||
| } | |||
| .points-container { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| margin-top: -10rpx; | |||
| background-color: #1f8bdc; | |||
| border-radius: 7rpx; | |||
| // padding: 4rpx 12rpx; | |||
| width: 80rpx; | |||
| height: 25rpx; | |||
| // box-shadow: 0 2rpx 8rpx rgba(33, 140, 221, 0.3); | |||
| z-index: 2; | |||
| .points-icon { | |||
| width: 20rpx; | |||
| height: 20rpx; | |||
| margin-right: 4rpx; | |||
| filter: brightness(0) invert(1); | |||
| } | |||
| .volunteer-points { | |||
| font-size: 18rpx; | |||
| color: #fff; | |||
| // font-weight: bold; | |||
| margin: 0; | |||
| } | |||
| } | |||
| .volunteer-name { | |||
| font-size: 24rpx; | |||
| color: $uni-text-color; | |||
| margin-top: 10rpx; | |||
| max-width: 100rpx; | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| font-weight: 500; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,158 @@ | |||
| <template> | |||
| <view class="demo-container"> | |||
| <view class="demo-title">搜索组件演示</view> | |||
| <!-- 基础搜索框 --> | |||
| <view class="demo-section"> | |||
| <view class="section-title">基础搜索框</view> | |||
| <Search | |||
| v-model="searchValue1" | |||
| placeholder="请输入搜索内容" | |||
| @search="onSearch" | |||
| @clear="onClear" | |||
| @clickIcon="onClickIcon" | |||
| /> | |||
| <view class="result">搜索内容:{{ searchValue1 }}</view> | |||
| </view> | |||
| <!-- 右侧搜索图标 --> | |||
| <view class="demo-section"> | |||
| <view class="section-title">右侧搜索图标</view> | |||
| <Search | |||
| v-model="searchValue2" | |||
| placeholder="搜索图标在右侧" | |||
| searchIconAlign="right" | |||
| @search="onSearch" | |||
| /> | |||
| </view> | |||
| <!-- 居中对齐 --> | |||
| <view class="demo-section"> | |||
| <view class="section-title">居中对齐</view> | |||
| <Search | |||
| v-model="searchValue3" | |||
| placeholder="居中对齐的搜索框" | |||
| textAlign="center" | |||
| :showIcon="false" | |||
| @search="onSearch" | |||
| /> | |||
| </view> | |||
| <!-- 自定义样式 --> | |||
| <!-- <view class="demo-section"> --> | |||
| <view class="section-title">自定义样式</view> | |||
| <Search | |||
| v-model="searchValue4" | |||
| placeholder="自定义背景色和高度" | |||
| :placeholderClass="{ | |||
| color:'red', | |||
| fontSize:'24rpx' | |||
| }" | |||
| bgColor="#e8f4fd" | |||
| height="100rpx" | |||
| :showCancel="false" | |||
| @search="onSearch" | |||
| /> | |||
| <!-- </view> --> | |||
| <!-- 禁用状态 --> | |||
| <view class="demo-section"> | |||
| <view class="section-title">禁用状态</view> | |||
| <Search | |||
| v-model="searchValue5" | |||
| placeholder="禁用状态的搜索框" | |||
| :disabled="true" | |||
| /> | |||
| </view> | |||
| <!-- 不显示取消按钮 --> | |||
| <view class="demo-section"> | |||
| <view class="section-title">不显示取消按钮</view> | |||
| <Search | |||
| v-model="searchValue6" | |||
| placeholder="没有取消按钮" | |||
| :showCancel="false" | |||
| @search="onSearch" | |||
| /> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import Search from '@/pages/components/Search.vue' | |||
| export default { | |||
| components: { | |||
| Search | |||
| }, | |||
| data() { | |||
| return { | |||
| searchValue1: '', | |||
| searchValue2: '', | |||
| searchValue3: '', | |||
| searchValue4: '', | |||
| searchValue5: '禁用状态示例', | |||
| searchValue6: '' | |||
| } | |||
| }, | |||
| methods: { | |||
| onSearch(value) { | |||
| uni.showToast({ | |||
| title: `搜索:${value}`, | |||
| icon: 'none' | |||
| }) | |||
| }, | |||
| onClear() { | |||
| uni.showToast({ | |||
| title: '清除搜索内容', | |||
| icon: 'none' | |||
| }) | |||
| }, | |||
| onClickIcon() { | |||
| uni.showToast({ | |||
| title: '点击了搜索图标', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .demo-container { | |||
| padding: 40rpx; | |||
| background-color: #f5f5f5; | |||
| min-height: 100vh; | |||
| } | |||
| .demo-title { | |||
| font-size: 36rpx; | |||
| font-weight: bold; | |||
| text-align: center; | |||
| margin-bottom: 40rpx; | |||
| color: #333; | |||
| } | |||
| .demo-section { | |||
| margin-bottom: 40rpx; | |||
| background-color: #fff; | |||
| border-radius: 20rpx; | |||
| overflow: hidden; | |||
| } | |||
| .section-title { | |||
| padding: 30rpx; | |||
| font-size: 28rpx; | |||
| font-weight: bold; | |||
| color: #333; | |||
| background-color: #f8f9fa; | |||
| border-bottom: 1px solid #eee; | |||
| } | |||
| .result { | |||
| padding: 20rpx 30rpx; | |||
| font-size: 24rpx; | |||
| color: #666; | |||
| background-color: #f8f9fa; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,146 @@ | |||
| <template> | |||
| <view class="points-card"> | |||
| <!-- 可用积分背景 --> | |||
| <view class="points-background"> | |||
| <image src="/static/可用积分背景图.png" class="bg-image" mode="aspectFill"></image> | |||
| <!-- 积分内容 --> | |||
| <view class="points-content"> | |||
| <view class="points-text"> | |||
| <text class="points-label">可用积分</text> | |||
| <text class="points-value">{{ points }}</text> | |||
| </view> | |||
| </view> | |||
| <!-- 积分明细按钮 --> | |||
| <view class="points-detail" @click="showPointsDetail"> | |||
| <image src="/static/商城_积分明细框.png" class="detail-bg" mode="aspectFit"></image> | |||
| <text class="detail-text">积分明细</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'PointsCard', | |||
| props: { | |||
| points: { | |||
| type: [String, Number], | |||
| default: 1385 | |||
| } | |||
| }, | |||
| methods: { | |||
| showPointsDetail() { | |||
| // 跳转到积分明细页面 | |||
| uni.navigateTo({ | |||
| url: '/subPages/shop/pointsDetail' | |||
| }) | |||
| }, | |||
| // handleClick() { | |||
| // // 跳转到积分明细页面 | |||
| // uni.navigateTo({ | |||
| // url: '/pages/components/searchDemo' | |||
| // }) | |||
| // } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .points-card { | |||
| width: 96%; | |||
| margin: 0 auto; | |||
| // margin-top: -80rpx; | |||
| position: relative; | |||
| z-index: 10; | |||
| } | |||
| .points-background { | |||
| position: relative; | |||
| width: 100%; | |||
| height: 290rpx; | |||
| border-radius: 20rpx; | |||
| overflow: hidden; | |||
| .bg-image { | |||
| width: 100%; | |||
| height: 100%; | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| } | |||
| .points-content { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 100%; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| padding: 0 40rpx; | |||
| .points-text { | |||
| // background: red; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| // justify-content: center; | |||
| gap: 10rpx; | |||
| .points-label { | |||
| font-size: 30rpx; | |||
| color: #ffffff; | |||
| margin-bottom: 15rpx; | |||
| opacity: 0.9; | |||
| } | |||
| .points-value { | |||
| font-size: 58rpx; | |||
| color: #ffffff; | |||
| font-weight: bold; | |||
| line-height: 1; | |||
| } | |||
| } | |||
| .points-icon { | |||
| width: 120rpx; | |||
| height: 120rpx; | |||
| .icon-image { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| } | |||
| } | |||
| .points-detail { | |||
| position: absolute; | |||
| bottom: 10rpx; | |||
| left: 30rpx; | |||
| width: 160rpx; | |||
| height: 60rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| .detail-bg { | |||
| position: absolute; | |||
| width: 100%; | |||
| height: 100%; | |||
| top: 0; | |||
| left: 0; | |||
| } | |||
| .detail-text { | |||
| font-size: 24rpx; | |||
| color: $uni-color-primary; | |||
| position: relative; | |||
| z-index: 1; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,423 @@ | |||
| <template> | |||
| <view class="shop-content"> | |||
| <!-- 搜索框 --> | |||
| <view class="search-container"> | |||
| <uv-search | |||
| v-model="title" | |||
| placeholder="搜索商品名" | |||
| :show-action="false" | |||
| bg-color="#f3f7f8" | |||
| inputAlign="left" | |||
| height="40" | |||
| margin="10rpx" | |||
| @search="onSearch" | |||
| @clickIcon="onSearch" | |||
| @clear="onSearch" | |||
| ></uv-search> | |||
| </view> | |||
| <!-- <Search | |||
| v-model="title" | |||
| placeholder="🔍搜索商品名" | |||
| :show-cancel="false" | |||
| :show-icon="false" | |||
| text-align="center" | |||
| height="80rpx" | |||
| width="90%" | |||
| bg-color="#f3f7f8" | |||
| @search="onSearch" | |||
| @clickIcon="onSearch" | |||
| @clear="onSearch" | |||
| style="margin: 4rpx 0rpx;" | |||
| /> --> | |||
| <!-- Tab栏 --> | |||
| <view class="tab-container"> | |||
| <scroll-view scroll-x="true" class="tab-scroll"> | |||
| <view class="tab-list"> | |||
| <!-- 固定的前三个Tab --> | |||
| <view | |||
| class="tab-item" | |||
| :class="{ active: currentTab === 0 }" | |||
| @click="onTabClick(0, '全部')" | |||
| > | |||
| <text class="tab-text">全部</text> | |||
| </view> | |||
| <view | |||
| class="tab-item sort-tab" | |||
| :class="{ active: currentTab === 1 }" | |||
| @click="onTabClick(1, '兑换积分')" | |||
| > | |||
| <text class="tab-text">兑换积分</text> | |||
| <view class="sort-arrows"> | |||
| <view class="arrow up" :class="{ active: sortType === 'points_asc' }">▲</view> | |||
| <view class="arrow down" :class="{ active: sortType === 'points_desc' }">▼</view> | |||
| </view> | |||
| </view> | |||
| <view | |||
| class="tab-item sort-tab" | |||
| :class="{ active: currentTab === 2 }" | |||
| @click="onTabClick(2, '兑换量')" | |||
| > | |||
| <text class="tab-text">兑换量</text> | |||
| <view class="sort-arrows"> | |||
| <view class="arrow up" :class="{ active: sortType === 'exchange_asc' }">▲</view> | |||
| <view class="arrow down" :class="{ active: sortType === 'exchange_desc' }">▼</view> | |||
| </view> | |||
| </view> | |||
| <!-- 从store获取的商品分类Tab --> | |||
| <view | |||
| v-for="(category, index) in categoryGoodsList" | |||
| :key="category.id" | |||
| class="tab-item" | |||
| :class="{ active: currentTab === index + 3 }" | |||
| @click="onTabClick(index + 3, category.title, category.id)" | |||
| > | |||
| <text class="tab-text">{{ category.title }}</text> | |||
| </view> | |||
| </view> | |||
| </scroll-view> | |||
| </view> | |||
| <!-- 商品列表 --> | |||
| <view class="goods-container"> | |||
| <view class="goods-grid" v-if="goodsList.length > 0"> | |||
| <view | |||
| class="goods-item click-animation" | |||
| v-for="(item, index) in goodsList" | |||
| :key="index" | |||
| @click="onGoodsClick(item)" | |||
| > | |||
| <view class="goods-image"> | |||
| <image :src="item.image" mode="aspectFit" class="image"></image> | |||
| </view> | |||
| <view class="goods-info"> | |||
| <text class="goods-name">{{ item.title }}</text> | |||
| <view class="goods-bottom"> | |||
| <view class="points-info"> | |||
| <image src="/static/积分图标.png" class="points-icon" mode="aspectFit"></image> | |||
| <text class="points-text">{{ item.price }}积分</text> | |||
| </view> | |||
| <uv-button | |||
| type="primary" | |||
| size="mini" | |||
| text="立即兑换" | |||
| :custom-style="buttonStyle" | |||
| ></uv-button> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <uv-empty | |||
| v-else | |||
| icon="/static/暂无搜索结果.png" | |||
| text="暂无商品数据" | |||
| ></uv-empty> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import Search from '@/pages/components/Search.vue' | |||
| export default { | |||
| name: 'ShopContent', | |||
| data() { | |||
| return { | |||
| // searchValue: '', | |||
| currentTab: 0, | |||
| pageNo: 1, | |||
| pageSize: 10, | |||
| title: '', | |||
| hasMore: true, | |||
| sortType: '', // 排序类型:points_asc, points_desc, exchange_asc, exchange_desc | |||
| goodsList: [], | |||
| buttonStyle: { | |||
| width: '128rpx', | |||
| height: '44rpx', | |||
| borderRadius: '28rpx', | |||
| fontSize: '22rpx' | |||
| }, | |||
| // 额外的传参 | |||
| extraParams : {} | |||
| } | |||
| }, | |||
| components: { | |||
| Search | |||
| }, | |||
| computed: { | |||
| // 从store获取商品分类列表 | |||
| categoryGoodsList() { | |||
| return this.$store.state.categoryGoodsList || [] | |||
| } | |||
| }, | |||
| methods: { | |||
| async onSearch(value) { | |||
| if (value !== null || undefined) this.title = value | |||
| this.initData() | |||
| await this.getGoodsList({isRefresh : true}) | |||
| }, | |||
| async onTabClick(index, tabName, categoryId = null) { | |||
| this.currentTab = index | |||
| this.extraParams = {} // 不带任何额外参数 | |||
| if (index === 0) { | |||
| // 全部Tab | |||
| console.log('点击了全部Tab') | |||
| } else if (index === 1) { | |||
| // 兑换积分Tab - 处理排序 | |||
| if (this.sortType === 'points_asc') { | |||
| this.sortType = 'points_desc' // 积分降序 | |||
| this.extraParams['price'] = 0 | |||
| } else { | |||
| this.sortType = 'points_asc' // 积分升序 | |||
| this.extraParams['price'] = 1 | |||
| } | |||
| console.log('点击了兑换积分Tab,排序类型:', this.sortType) | |||
| } else if (index === 2) { | |||
| // 兑换量Tab - 处理排序 | |||
| if (this.sortType === 'exchange_asc') { | |||
| this.sortType = 'exchange_desc' // 兑换量降序 | |||
| this.extraParams['sales'] = 0 | |||
| } else { | |||
| this.sortType = 'exchange_asc' // 兑换量升序 | |||
| this.extraParams['sales'] = 1 | |||
| } | |||
| console.log('点击了兑换量Tab,排序类型:', this.sortType) | |||
| } else { | |||
| // 商品分类Tab | |||
| console.log('点击了商品分类Tab:', tabName, '分类ID:', categoryId) | |||
| this.extraParams['categoryId'] = categoryId | |||
| } | |||
| this.initData() | |||
| await this.getGoodsList({isRefresh : true}) | |||
| }, | |||
| onGoodsClick(item) { | |||
| // 跳转到商品详情页 | |||
| uni.navigateTo({ | |||
| url: `/subPages/shop/goodsDetail?id=${item.id}` | |||
| }) | |||
| }, | |||
| // 如何默认isRefresh为false | |||
| async getGoodsList({isRefresh = false} = {}) { | |||
| if (!this.hasMore) return | |||
| if (this.title === undefined) this.title = '' | |||
| const res = await this.$api.shop.queryGoodsList({ | |||
| pageNo: this.pageNo, | |||
| pageSize: this.pageSize, | |||
| title: this.title, | |||
| ...this.extraParams | |||
| }) | |||
| if (res.result.records.length) { | |||
| if (isRefresh) { | |||
| this.goodsList = res.result.records | |||
| } else { | |||
| this.goodsList.push(...res.result.records) | |||
| } | |||
| this.pageNo++ | |||
| }else { | |||
| uni.showToast({ | |||
| title: '暂无商品', | |||
| icon: 'none' | |||
| }) | |||
| if (isRefresh) { | |||
| this.goodsList = [] | |||
| } | |||
| this.hasMore = false | |||
| } | |||
| }, | |||
| // 初始化请求参数 | |||
| initData() { | |||
| this.pageNo = 1 | |||
| // this.goodsList = [] | |||
| this.hasMore = true | |||
| } | |||
| }, | |||
| async mounted() { | |||
| // 确保store中的商品分类数据已加载 | |||
| if (this.categoryGoodsList.length === 0) { | |||
| await this.$store.dispatch('getCategoryGoodsList') | |||
| } | |||
| // 初始化商品列表 | |||
| // this.getGoodsList() | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .shop-content { | |||
| background: #f8f8f8; | |||
| min-height: calc(100vh - 400rpx); | |||
| } | |||
| .search-container { | |||
| position: sticky; | |||
| z-index: 999; | |||
| top: 10rpx; | |||
| padding: 15rpx 20rpx; | |||
| background: #ffffff; | |||
| } | |||
| .tab-container { | |||
| position: sticky; | |||
| z-index: 999; | |||
| top: 90rpx; | |||
| background: #ffffff; | |||
| border-bottom: 1rpx solid #f0f0f0; | |||
| padding-bottom: 20rpx; | |||
| .tab-scroll { | |||
| white-space: nowrap; | |||
| .tab-list { | |||
| display: flex; | |||
| padding: 0 30rpx; | |||
| .tab-item { | |||
| flex-shrink: 0; | |||
| display: flex; | |||
| align-items: center; | |||
| padding: 24rpx 32rpx; | |||
| margin-right: 16rpx; | |||
| border-radius: 32rpx; | |||
| background: #f8f9fa; | |||
| transition: all 0.3s ease; | |||
| .tab-text { | |||
| font-size: 28rpx; | |||
| color: #666666; | |||
| font-weight: 500; | |||
| } | |||
| &.active { | |||
| background: #218CDD; | |||
| .tab-text { | |||
| color: #ffffff; | |||
| } | |||
| .sort-arrows .arrow.active { | |||
| color: #ffffff; | |||
| } | |||
| } | |||
| &.sort-tab { | |||
| .sort-arrows { | |||
| margin-left: 8rpx; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| .arrow { | |||
| font-size: 16rpx; | |||
| color: #cccccc; | |||
| line-height: 1; | |||
| transition: color 0.3s ease; | |||
| &.up { | |||
| margin-bottom: 2rpx; | |||
| } | |||
| &.active { | |||
| color: rgb(64, 64, 64); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .goods-container { | |||
| padding: 20rpx 30rpx; | |||
| background: #f8f8f8; | |||
| } | |||
| .goods-grid { | |||
| display: grid; | |||
| grid-template-columns: 1fr 1fr; | |||
| gap: 20rpx; | |||
| } | |||
| .goods-item { | |||
| display: flex; | |||
| flex-direction: column; | |||
| background: #ffffff; | |||
| border-radius: 12rpx; | |||
| padding: 20rpx; | |||
| box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); | |||
| border: 1rpx solid #f5f5f5; | |||
| .goods-image { | |||
| width: 100%; | |||
| height: 230rpx; | |||
| border-radius: 8rpx; | |||
| overflow: hidden; | |||
| margin-bottom: 16rpx; | |||
| border: 2rpx dashed #e0e0e0; | |||
| .image { | |||
| width: 100%; | |||
| height: 100%; | |||
| object-fit: cover; | |||
| } | |||
| } | |||
| .goods-info { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| .goods-name { | |||
| font-size: 28rpx; | |||
| color: #333333; | |||
| line-height: 1.4; | |||
| margin-bottom: 16rpx; | |||
| font-weight: 500; | |||
| display: -webkit-box; | |||
| -webkit-box-orient: vertical; | |||
| -webkit-line-clamp: 2; | |||
| overflow: hidden; | |||
| min-height: 72rpx; | |||
| } | |||
| .goods-bottom { | |||
| display: flex; | |||
| // flex-direction: column; | |||
| gap: 22rpx; | |||
| margin-top: auto; | |||
| .points-info { | |||
| display: flex; | |||
| align-items: center; | |||
| .points-icon { | |||
| width: 24rpx; | |||
| height: 24rpx; | |||
| margin-right: 6rpx; | |||
| } | |||
| .points-text { | |||
| font-size: 28rpx; | |||
| color: #218CDD; | |||
| font-weight: 700; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,403 @@ | |||
| <template> | |||
| <view class="activity-page"> | |||
| <!-- 搜索栏和一级Tab合并容器 --> | |||
| <view class="search-section"> | |||
| <view class="search-bar"> | |||
| <uv-search placeholder="请输入搜索内容" v-model="params.title" @search="handleSearch" @clear="handleSearch" @clickIcon="handleSearch" :showAction="false" ></uv-search> | |||
| </view> | |||
| <!-- 一级Tab:当前活动/往期活动 移到搜索容器内 --> | |||
| <view class="primary-tabs"> | |||
| <view | |||
| class="primary-tab-item" | |||
| :class="{ active: primaryActiveTab === 'current' }" | |||
| @click="switchPrimaryTab('current')" | |||
| > | |||
| 当前活动 | |||
| </view> | |||
| <view | |||
| class="primary-tab-item" | |||
| :class="{ active: primaryActiveTab === 'past' }" | |||
| @click="switchPrimaryTab('past')" | |||
| > | |||
| 往期活动 | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 二级Tab:自定义Tab组件 --> | |||
| <view class="secondary-tabs"> | |||
| <scroll-view scroll-x="true" class="tab-scroll"> | |||
| <view class="tab-list"> | |||
| <!-- 全部Tab --> | |||
| <view | |||
| class="tab-item" | |||
| :class="{ active: secondaryActiveIndex === 0 }" | |||
| @click="switchSecondaryTab(0, '全部')" | |||
| > | |||
| <text class="tab-text">全部</text> | |||
| </view> | |||
| <!-- 从store获取的活动分类Tab --> | |||
| <view | |||
| v-for="(category, index) in categoryActivityList" | |||
| :key="category.id" | |||
| class="tab-item" | |||
| :class="{ active: secondaryActiveIndex === index + 1 }" | |||
| @click="switchSecondaryTab(index + 1, category.title, category.id)" | |||
| > | |||
| <text class="tab-text">{{ category.title }}</text> | |||
| </view> | |||
| <!-- 动画下划线 --> | |||
| <!-- <view class="tab-line" :style="lineStyle"></view> --> | |||
| </view> | |||
| </scroll-view> | |||
| </view> | |||
| <!-- 活动列表 --> | |||
| <view class="activity-list"> | |||
| <view | |||
| class="activity-item" | |||
| v-for="(item, index) in list" | |||
| :key="index" | |||
| @click="goToActivityDetail(item)" | |||
| > | |||
| <!-- 活动图片 --> | |||
| <view class="activity-image"> | |||
| <image :src="item.image" mode="aspectFill" class="image"></image> | |||
| </view> | |||
| <!-- 活动信息 --> | |||
| <view class="activity-info"> | |||
| <view class="title-row"> | |||
| <!-- 活动标签 --> | |||
| <view class="activity-tag" :style="{ backgroundColor: item.tagColor }"> | |||
| {{ item.score }}分 | |||
| </view> | |||
| <view class="activity-title">{{ item.title }}</view> | |||
| </view> | |||
| <view class="activity-location"> | |||
| <uv-icon name="map-fill" size="14" color="#999"></uv-icon> | |||
| <text class="location-text">{{ item.address }}</text> | |||
| </view> | |||
| <view class="activity-time"> | |||
| <uv-icon name="calendar" size="14" color="#999"></uv-icon> | |||
| <text class="time-text">{{ item.createTime }}</text> | |||
| </view> | |||
| <view class="activity-participants"> | |||
| <uv-icon name="account-fill" size="14" color="#999"></uv-icon> | |||
| <text class="participants-text" >报名人数:{{ item.numActivity }}/{{ item.numLimit }}</text> | |||
| </view> | |||
| </view> | |||
| <!-- 报名按钮 --> | |||
| <view class="activity-action"> | |||
| <uv-button v-if="item.status === '1'" type="primary" size="mini" shape="circle" text="已结束" disabled @click.stop="signUpActivity(item)"></uv-button> | |||
| <uv-button v-else-if="item.isApply === 1" type="primary" size="mini" shape="circle" text="已报名" disabled @click.stop="signUpActivity(item)"></uv-button> | |||
| <uv-button v-else type="primary" size="mini" shape="circle" :text="item.numActivity >= item.numLimit ? '已结束' : '报名中'" :disabled="item.numActivity >= item.numLimit" @click.stop="signUpActivity(item)"></uv-button> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 空状态 --> | |||
| <view class="empty-state" v-if="list.length === 0"> | |||
| <uv-empty icon="/static/暂无搜索结果.png" text="暂无活动数据"></uv-empty> | |||
| <!-- <image src="/static/暂无搜索结果.png" style="width: 460rpx; height: 460rpx; margin: 0 auto;"></image> --> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import MixinList from '@/mixins/list' | |||
| export default { | |||
| mixins: [MixinList], | |||
| data() { | |||
| return { | |||
| primaryActiveTab: 'current', // current: 当前活动, past: 往期活动 | |||
| mixinListApi: 'activity.queryActivityList', | |||
| params: { | |||
| title: '', // 搜索关键字 | |||
| status: 0 // 活动的状态 0是当前 1是往期 | |||
| }, | |||
| secondaryActiveIndex: 0, | |||
| // 模拟活动数据 | |||
| list: [] | |||
| } | |||
| }, | |||
| computed: { | |||
| // 从store获取活动分类列表 | |||
| categoryActivityList() { | |||
| return this.$store.state.categoryActivityList || [] | |||
| }, | |||
| }, | |||
| methods: { | |||
| mixinSetParams(){ | |||
| return { | |||
| ...this.params | |||
| } | |||
| }, | |||
| handleSearch(value) { | |||
| if (value) { | |||
| this.params['title'] = value | |||
| } | |||
| this.initPage() | |||
| this.getList(true) | |||
| }, | |||
| // 切换一级tab | |||
| switchPrimaryTab(tab) { | |||
| this.primaryActiveTab = tab; | |||
| this.initPage() | |||
| delete this.params['categoryId'] | |||
| // 标签回到全部 | |||
| this.secondaryActiveIndex = 0 | |||
| this.params['status'] = tab === 'current' ? 0 : 1 | |||
| this.getList(true) | |||
| }, | |||
| // 切换二级tab | |||
| async switchSecondaryTab(index, tabName, categoryId = null) { | |||
| this.initPage() | |||
| this.secondaryActiveIndex = index | |||
| delete this.params['categoryId'] | |||
| if (index === 0) { | |||
| // 全部Tab | |||
| // console.log('点击了全部Tab') | |||
| } else { | |||
| // 活动分类Tab | |||
| this.params['categoryId'] = categoryId | |||
| } | |||
| this.getList(true) | |||
| }, | |||
| // 跳转到活动详情 | |||
| goToActivityDetail(activity) { | |||
| uni.navigateTo({ | |||
| url: `/subPages/index/activityDetail?id=${activity.id}` | |||
| }); | |||
| }, | |||
| // 报名活动 | |||
| signUpActivity(item) { | |||
| uni.navigateTo({ | |||
| url: `/subPages/index/activityDetail?id=${item.id}` | |||
| }); | |||
| }, | |||
| }, | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .activity-page { | |||
| background-color: #f5f5f5; | |||
| min-height: 100vh; | |||
| } | |||
| // 搜索栏样式 - 修改为包含一级Tab | |||
| .search-section { | |||
| height: 350rpx; | |||
| background: linear-gradient(180deg,#1488db, #98b5f1); | |||
| padding-top: 180rpx; /* 使用padding-top避免margin塌陷 */ | |||
| box-sizing: border-box; /* 确保padding包含在高度内 */ | |||
| } | |||
| .search-bar { | |||
| // background-color: white; | |||
| // border-radius: 50rpx; | |||
| padding: 5rpx 40rpx; | |||
| // display: flex; | |||
| // align-items: center; | |||
| // gap: 20rpx; | |||
| // width: 85%; | |||
| // margin: 0 auto ; /* 移除margin-top,只保留左右居中和下边距 */ | |||
| } | |||
| .search-input { | |||
| flex: 1; | |||
| font-size: 28rpx; | |||
| color: #333; | |||
| &::placeholder { | |||
| color: #999; | |||
| } | |||
| } | |||
| // 一级Tab样式 - 调整为在蓝色背景内 | |||
| .primary-tabs { | |||
| display: flex; | |||
| padding: 0 20rpx; | |||
| margin-bottom: 20rpx; | |||
| } | |||
| .primary-tab-item { | |||
| flex: 1; | |||
| text-align: center; | |||
| padding: 20rpx 0; | |||
| font-size: 32rpx; | |||
| color: #000000; /* 白色半透明 */ | |||
| position: relative; | |||
| transition: color 0.3s ease; | |||
| &.active { | |||
| color: white; /* 激活状态为纯白色 */ | |||
| font-weight: 600; | |||
| &::after { | |||
| content: ''; | |||
| position: absolute; | |||
| bottom: 0; | |||
| left: 50%; | |||
| transform: translateX(-50%); | |||
| // transition: transform 0.3s ease; | |||
| width: 100rpx; | |||
| height: 6rpx; | |||
| background-color: white; /* 下划线改为白色 */ | |||
| border-radius: 3rpx; | |||
| } | |||
| } | |||
| } | |||
| // 二级Tab样式 - 自定义实现 | |||
| .secondary-tabs { | |||
| background-color: white; | |||
| border-bottom: 1px solid #f0f0f0; | |||
| position: relative; | |||
| .tab-scroll { | |||
| white-space: nowrap; | |||
| .tab-list { | |||
| display: flex; | |||
| // position: relative; | |||
| justify-content: space-evenly; | |||
| .tab-item { | |||
| flex-shrink: 0; | |||
| padding: 24rpx 32rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| transition: all 0.3s ease; | |||
| .tab-text { | |||
| font-size: 28rpx; | |||
| color: #666666; | |||
| font-weight: 500; | |||
| transition: color 0.5s ease; | |||
| } | |||
| &.active { | |||
| .tab-text { | |||
| color: #007AFF; | |||
| font-weight: 600; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .tab-line { | |||
| position: absolute; | |||
| bottom: 10; | |||
| height: 6rpx; | |||
| background-color: #007AFF; | |||
| border-radius: 3rpx; | |||
| transition: transform 0.3s ease; | |||
| } | |||
| } | |||
| } | |||
| // 活动列表样式 | |||
| .activity-list { | |||
| padding: 20rpx; | |||
| } | |||
| .activity-item { | |||
| background-color: white; | |||
| border-radius: 12rpx; | |||
| margin-bottom: 30rpx; | |||
| padding: 20rpx; | |||
| display: flex; | |||
| box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); | |||
| } | |||
| .activity-image { | |||
| width: 180rpx; | |||
| height: 180rpx; | |||
| border-radius: 8rpx; | |||
| overflow: hidden; | |||
| flex-shrink: 0; | |||
| margin-right: 20rpx; | |||
| } | |||
| .image { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| .activity-info { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| } | |||
| .title-row { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 10rpx; | |||
| } | |||
| .activity-tag { | |||
| width: 31px; | |||
| height: 20px; | |||
| background: #218cdd; | |||
| border-radius: 3.5px; | |||
| margin-right: 7rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| font-size: 18rpx; | |||
| color: white; | |||
| font-weight: 600; | |||
| } | |||
| .activity-title { | |||
| font-size: 28rpx; | |||
| font-weight: bold; | |||
| color: #333; | |||
| line-height: 1.4; | |||
| } | |||
| .activity-location, | |||
| .activity-time, | |||
| .activity-participants { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 6rpx; | |||
| .location-text, | |||
| .time-text, | |||
| .participants-text { | |||
| font-size: 24rpx; | |||
| color: #666; | |||
| margin-left: 6rpx; | |||
| } | |||
| } | |||
| .activity-action { | |||
| display: flex; | |||
| align-items: flex-end; | |||
| padding-bottom: 10rpx; | |||
| } | |||
| // 空状态样式 | |||
| .empty-state { | |||
| padding: 100rpx 40rpx; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,425 @@ | |||
| <template> | |||
| <view class="community-page"> | |||
| <!-- 顶部图片 --> | |||
| <view class="banner-section"> | |||
| <!-- <image class="banner-image" :src="currentTab === 'current' ? '/static/社区_背景.png' : '/static/社区_背景2.png'" mode="aspectFit"></image> --> | |||
| <uv-swiper :list="bannerList" indicator indicatorMode="line" height="375rpx" ></uv-swiper> | |||
| </view> | |||
| <!-- Tab切换区域 --> | |||
| <view class="tab-section"> | |||
| <view class="tab-container"> | |||
| <view | |||
| class="tab-item" | |||
| :class="{ active: currentTab === 'current' }" | |||
| @click="switchTab('current')" | |||
| > | |||
| <text class="tab-text">木邻说</text> | |||
| <view class="tab-line" v-if="currentTab === 'current'"></view> | |||
| </view> | |||
| <view | |||
| class="tab-item" | |||
| :class="{ active: currentTab === 'past' }" | |||
| @click="switchTab('past')" | |||
| > | |||
| <text class="tab-text">木邻见</text> | |||
| <view class="tab-line" v-if="currentTab === 'past'"></view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 动态列表 --> | |||
| <view class="post-list"> | |||
| <view | |||
| class="post-item" | |||
| v-for="(item, index) in list" | |||
| :key="index" | |||
| > | |||
| <!-- 用户信息 --> | |||
| <view class="user-info"> | |||
| <image class="user-avatar" :src="item.member.headImage" mode="aspectFill"></image> | |||
| <view class="user-details"> | |||
| <text class="username">{{ item.member.nickName }}</text> | |||
| <text class="post-time">发布时间:{{ item.createTime }}</text> | |||
| </view> | |||
| </view> | |||
| <!-- 动态内容 --> | |||
| <view class="post-content"> | |||
| <text class="post-text">{{ item.content }}</text> | |||
| <!-- 图片列表 --> | |||
| <view class="image-grid" v-if="item.image && item.image.length > 0"> | |||
| <image | |||
| class="post-image" | |||
| v-for="(img, imgIndex) in item.image.split(',')" | |||
| :key="imgIndex" | |||
| :src="img" | |||
| mode="aspectFill" | |||
| ></image> | |||
| </view> | |||
| </view> | |||
| <!-- 回复列表 --> | |||
| <view class="comment-list" v-if="item.communityCommentList && item.communityCommentList.length > 0"> | |||
| <view class="comment-header"> | |||
| <text class="comment-title">回复 ({{ item.communityCommentList.length }})</text> | |||
| </view> | |||
| <view | |||
| class="comment-item" | |||
| v-for="(comment, commentIndex) in item.communityCommentList" | |||
| :key="commentIndex" | |||
| > | |||
| <view class="comment-user-info"> | |||
| <text class="comment-username">{{ comment.createBy }}</text> | |||
| <text class="comment-time">{{ comment.createTime }}</text> | |||
| </view> | |||
| <text class="comment-content">{{ comment.content }}</text> | |||
| <img :src="comment.image" class="comment-image" mode="aspectFill" alt="" v-if="comment.image"> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 随手拍/我要留言按钮 --> | |||
| <view class="action-btn" :class="currentTab === 'current' ? 'current-btn' : 'photo'" @click="openAction"> | |||
| <uv-icon name="edit-pen-fill" size="20" color="white"></uv-icon> | |||
| <text class="action-text">{{ actionButtonText }}</text> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import ListMixin from '@/mixins/list.js' | |||
| export default { | |||
| mixins: [ListMixin], | |||
| name: 'CommunityPage', | |||
| data() { | |||
| return { | |||
| currentTab: 'current', // current: 木邻说, past: 木邻见 | |||
| mixinListApi: 'community.queryPostList', | |||
| bannerList: [] | |||
| } | |||
| }, | |||
| computed: { | |||
| actionButtonText() { | |||
| return this.currentTab === 'current' ? '我要留言' : '随手拍' | |||
| } | |||
| }, | |||
| methods: { | |||
| mixinSetParams(){ | |||
| if (uni.getStorageSync('token')){ | |||
| return { | |||
| token: uni.getStorageSync('token'), | |||
| type: this.currentTab === 'current' ? 0 : 1 | |||
| } | |||
| } | |||
| return { | |||
| type: this.currentTab === 'current' ? 0 : 1 | |||
| } | |||
| }, | |||
| switchTab(tab) { | |||
| this.currentTab = tab | |||
| this.initPage() | |||
| this.getList(true) | |||
| this.getBannerList() | |||
| }, | |||
| openAction() { | |||
| if (this.currentTab === 'current') { | |||
| // 我要留言功能 | |||
| this.goToComment() | |||
| } else { | |||
| this.takePhoto() | |||
| } | |||
| }, | |||
| takePhoto() { | |||
| uni.navigateTo({ | |||
| url: '/subPages/community/publishPost?page=photo' | |||
| }) | |||
| }, | |||
| goToComment() { | |||
| uni.navigateTo({ | |||
| url: '/subPages/community/publishPost' | |||
| }) | |||
| }, | |||
| // 获取帖子数据 | |||
| async getPostList() { | |||
| const res = await this.$api.community.queryPostList({ | |||
| pageNo: this.pageNo, | |||
| pageSize: this.pageSize, | |||
| type: this.currentTab === 'current' ? 0 : 1 | |||
| }) | |||
| if (res.result.records.length) { | |||
| this.postList.push(...res.result.records) | |||
| this.pageNo++ | |||
| }else { | |||
| uni.showToast({ | |||
| title: '暂无数据', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| }, | |||
| // 获取顶部轮播图 | |||
| async getBannerList() { | |||
| const res = await this.$api.home.queryBannerList({ | |||
| type: this.currentTab === 'current' ? 1 : 2 | |||
| }) | |||
| // console.log('返回的结果', res); | |||
| if (res.result.records.length) { | |||
| this.bannerList = res.result.records.map(item => item.image) | |||
| } | |||
| } | |||
| }, | |||
| onShow() { | |||
| this.getBannerList() | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .community-page { | |||
| min-height: 100vh; | |||
| background-color: #f8f9fa; | |||
| position: relative; | |||
| padding-bottom: 120rpx; | |||
| } | |||
| // 横幅样式 | |||
| .banner-section { | |||
| height: 375rpx; | |||
| overflow: hidden; | |||
| } | |||
| // .banner-image { | |||
| // width: 100%; | |||
| // height: 100%; | |||
| // } | |||
| // Tab切换区域 | |||
| .tab-section { | |||
| background: white; | |||
| padding: 0 40rpx; | |||
| border-bottom: 1rpx solid #f0f0f0; | |||
| box-shadow: 0px 1.5px 3px 0px rgba(0,0,0,0.16); | |||
| } | |||
| .tab-container { | |||
| display: flex; | |||
| // gap: 60rpx; | |||
| justify-content: space-evenly; | |||
| } | |||
| .tab-item { | |||
| position: relative; | |||
| padding: 30rpx 0; | |||
| .tab-text { | |||
| font-size: 32rpx; | |||
| color: #666; | |||
| font-weight: 500; | |||
| transition: color 0.3s ease; | |||
| } | |||
| &.active { | |||
| .tab-text { | |||
| color: #007AFF; | |||
| font-weight: bold; | |||
| } | |||
| } | |||
| } | |||
| .tab-line { | |||
| position: absolute; | |||
| bottom: 0; | |||
| left: 50%; | |||
| transform: translateX(-50%); | |||
| width: 40rpx; | |||
| height: 6rpx; | |||
| background: #007AFF; | |||
| border-radius: 3rpx; | |||
| animation: slideIn 0.3s ease; | |||
| } | |||
| @keyframes slideIn { | |||
| from { | |||
| width: 0; | |||
| } | |||
| to { | |||
| width: 40rpx; | |||
| } | |||
| } | |||
| // 动态列表样式 | |||
| .post-list { | |||
| // padding: 20rpx; | |||
| } | |||
| .post-item { | |||
| background-color: white; | |||
| border-radius: 16rpx; | |||
| // margin-bottom: 24rpx; | |||
| padding: 32rpx; | |||
| box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06); | |||
| border: 1rpx solid #f5f5f5; | |||
| transition: transform 0.2s ease, box-shadow 0.2s ease; | |||
| &:active { | |||
| transform: scale(0.98); | |||
| box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); | |||
| } | |||
| } | |||
| .user-info { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 24rpx; | |||
| } | |||
| .user-avatar { | |||
| width: 88rpx; | |||
| height: 88rpx; | |||
| border-radius: 50%; | |||
| margin-right: 24rpx; | |||
| border: 2rpx solid #f0f0f0; | |||
| } | |||
| .user-details { | |||
| flex: 1; | |||
| } | |||
| .username { | |||
| font-size: 30rpx; | |||
| font-weight: bold; | |||
| color: #333; | |||
| display: block; | |||
| margin-bottom: 8rpx; | |||
| } | |||
| .post-time { | |||
| font-size: 24rpx; | |||
| color: #999; | |||
| } | |||
| .post-content { | |||
| .post-text { | |||
| font-size: 30rpx; | |||
| color: #333; | |||
| line-height: 1.6; | |||
| display: block; | |||
| margin-bottom: 24rpx; | |||
| } | |||
| } | |||
| .image-grid { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| gap: 12rpx; | |||
| } | |||
| .post-image { | |||
| width: 227rpx; | |||
| height: 303rpx; | |||
| border-radius: 12rpx; | |||
| border: 1rpx solid #f0f0f0; | |||
| } | |||
| // 回复列表样式 | |||
| .comment-list { | |||
| margin-top: 24rpx; | |||
| padding-top: 24rpx; | |||
| border-top: 1rpx solid #f3f7f8; | |||
| } | |||
| .comment-header { | |||
| margin-bottom: 20rpx; | |||
| } | |||
| .comment-title { | |||
| font-size: 28rpx; | |||
| color: #666; | |||
| font-weight: 500; | |||
| } | |||
| .comment-item { | |||
| background-color: #f8f9fa; | |||
| border-radius: 12rpx; | |||
| padding: 20rpx; | |||
| margin-bottom: 16rpx; | |||
| &:last-child { | |||
| margin-bottom: 0; | |||
| } | |||
| } | |||
| .comment-user-info { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| margin-bottom: 12rpx; | |||
| } | |||
| .comment-username { | |||
| font-size: 26rpx; | |||
| color: #007AFF; | |||
| font-weight: 500; | |||
| } | |||
| .comment-time { | |||
| font-size: 22rpx; | |||
| color: #999; | |||
| } | |||
| .comment-content { | |||
| font-size: 28rpx; | |||
| color: #333; | |||
| line-height: 1.5; | |||
| display: block; | |||
| } | |||
| .comment-image { | |||
| width: 95px; | |||
| height: 95px; | |||
| background: rgba(0,0,0,0.00); | |||
| border-radius: 4.5px; | |||
| } | |||
| // 随手拍/我要留言按钮样式 | |||
| .action-btn { | |||
| position: fixed; | |||
| bottom: 120rpx; | |||
| right: 30rpx; | |||
| width: 120rpx; | |||
| height: 120rpx; | |||
| background: linear-gradient(135deg, #007AFF 0%, #0056CC 100%); | |||
| border-radius: 50%; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| justify-content: center; | |||
| box-shadow: 0 8rpx 24rpx rgba(0, 122, 255, 0.4); | |||
| z-index: 100; | |||
| transition: transform 0.2s ease, box-shadow 0.2s ease; | |||
| &:active { | |||
| transform: scale(0.95); | |||
| box-shadow: 0 4rpx 16rpx rgba(0, 122, 255, 0.6); | |||
| } | |||
| &.photo { | |||
| background: linear-gradient(135deg, #FF6666 0%, #CC3333 100%); | |||
| } | |||
| } | |||
| .action-text { | |||
| font-size: 20rpx; | |||
| color: white; | |||
| margin-top: 8rpx; | |||
| font-weight: bold; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,67 @@ | |||
| <template> | |||
| <view class="volunteer-day-container"> | |||
| <home-page-nav></home-page-nav> | |||
| <!-- 顶部通知栏 --> | |||
| <volunteer-header></volunteer-header> | |||
| <!-- 志愿者排行榜 --> | |||
| <volunteer-ranking ref="rankRef"></volunteer-ranking> | |||
| <!-- 功能导航 --> | |||
| <volunteer-features></volunteer-features> | |||
| <!-- 推荐活动 --> | |||
| <recommended-activities ref="recommendedActivities"></recommended-activities> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import VolunteerHeader from '@/pages/components/index/VolunteerHeader.vue'; | |||
| import VolunteerRanking from '@/pages/components/index/VolunteerRanking.vue'; | |||
| import VolunteerFeatures from '@/pages/components/index/VolunteerFeatures.vue'; | |||
| import RecommendedActivities from '@/pages/components/index/RecommendedActivities.vue'; | |||
| import HomePageNav from '@/pages/components/HomePageNav.vue'; | |||
| export default { | |||
| components: { | |||
| VolunteerHeader, | |||
| VolunteerRanking, | |||
| VolunteerFeatures, | |||
| RecommendedActivities, | |||
| HomePageNav | |||
| }, | |||
| data() { | |||
| return {} | |||
| }, | |||
| methods: { | |||
| getPageData() { | |||
| this.$refs.rankRef.getVolunteerRanking(); | |||
| } | |||
| }, | |||
| onShow() { | |||
| // 页面加载时获取数据 | |||
| this.getPageData(); | |||
| // 刷新推荐活动 | |||
| this.$refs.recommendedActivities.getActivityList() | |||
| }, | |||
| async onPullDownRefresh() { | |||
| // 下拉刷新时获取数据 | |||
| await this.getPageData(); | |||
| await this.$refs.recommendedActivities.getActivityList() | |||
| // 刷新完成后调用 | |||
| uni.stopPullDownRefresh(); | |||
| }, | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .volunteer-day-container { | |||
| min-height: 100vh; | |||
| background-color: #f5f5f5; | |||
| padding-bottom: 30rpx; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,375 @@ | |||
| <template> | |||
| <view class="my-page"> | |||
| <!-- 顶部渐变背景区域 --> | |||
| <view class="header-section"> | |||
| <!-- 右上角图标 --> | |||
| <!-- <view class="header-icons"> | |||
| <uv-icon name="more-dot-fill" size="20" color="white"></uv-icon> | |||
| <uv-icon name="setting" size="20" color="white"></uv-icon> | |||
| </view> --> | |||
| <!-- 用户信息区域 --> | |||
| <view class="user-info" @click="navigateTo('profile')"> | |||
| <view class="avatar-container"> | |||
| <image class="avatar" :src="userInfo.headImage || '/static/默认头像.png'" mode="aspectFill"></image> | |||
| </view> | |||
| <text class="username">{{ userInfo.nickName }}</text> | |||
| </view> | |||
| </view> | |||
| <!-- 积分信息区域 --> | |||
| <view class="points-section"> | |||
| <view class="points-item yellow" @click="navigateTo('favoritesActivity')"> | |||
| <view class="points-content"> | |||
| <text class="points-number">{{ userInfo.collectionNum }}</text> | |||
| <text class="points-label yellow">我的收藏</text> | |||
| </view> | |||
| <view class="points-icon"> | |||
| <!-- <uv-icon name="star-fill" size="28" color="#FFD700"></uv-icon> --> | |||
| <image class="points-icon-img" src="/static/我的_活动收藏.png" mode="aspectFit"></image> | |||
| </view> | |||
| </view> | |||
| <view class="points-item blue" @click="navigateTo('points')"> | |||
| <view class="points-content"> | |||
| <text class="points-number">{{ userInfo.score }}</text> | |||
| <text class="points-label blue">可用积分</text> | |||
| </view> | |||
| <view class="points-icon"> | |||
| <image class="points-icon-img" src="/static/我的_积分.png" mode="aspectFit"></image> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 常用功能区域 --> | |||
| <view class="functions-container"> | |||
| <text class="section-title">常用功能</text> | |||
| <view class="functions-grid"> | |||
| <!-- 第一行 --> | |||
| <view class="function-item" @click="navigateTo('profile')"> | |||
| <image class="function-icon" src="/static/我的_我的资料.png" mode="aspectFit"></image> | |||
| <text class="function-text">我的资料</text> | |||
| </view> | |||
| <view class="function-item" @click="navigateTo('reports')"> | |||
| <image class="function-icon" src="/static/我的_我的报名.png" mode="aspectFit"></image> | |||
| <text class="function-text">我的报名</text> | |||
| </view> | |||
| <view class="function-item" @click="navigateTo('records')"> | |||
| <image class="function-icon" src="/static/我的_兑换记录.png" mode="aspectFit"></image> | |||
| <text class="function-text">兑换记录</text> | |||
| </view> | |||
| <view class="function-item" @click="navigateTo('favorites')"> | |||
| <image class="function-icon" src="/static/我的_商品收藏.png" mode="aspectFit"></image> | |||
| <text class="function-text">商品收藏</text> | |||
| </view> | |||
| <!-- 第二行 --> | |||
| <view class="function-item" @click="logout"> | |||
| <image class="function-icon" src="/static/我的_退出登录.png" mode="aspectFit"></image> | |||
| <text class="function-text">退出登录</text> | |||
| </view> | |||
| <view class="function-item" @click="navigateTo('about')"> | |||
| <image class="function-icon" src="/static/我的_关于我们.png" mode="aspectFit"></image> | |||
| <text class="function-text">关于我们</text> | |||
| </view> | |||
| <view class="function-item" @click="navigateTo('checkin')" v-if="userInfo.role === 1"> | |||
| <view class="function-icon-wrapper"> | |||
| <uv-icon name="file-text-fill" size="32" color="#218cdd"></uv-icon> | |||
| </view> | |||
| <text class="function-text">活动签到</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'MyPage', | |||
| data() { | |||
| return { | |||
| userInfo: { | |||
| name: '小精灵', | |||
| headImage: '/static/默认头像.png', | |||
| collectionNum: 5, | |||
| score: 41 | |||
| } | |||
| } | |||
| }, | |||
| methods: { | |||
| navigateTo(page) { | |||
| console.log('导航到:', page) | |||
| // 根据不同页面进行导航 | |||
| switch(page) { | |||
| case 'profile': | |||
| uni.navigateTo({ | |||
| url: '/subPages/my/myProfile' | |||
| }) | |||
| break | |||
| case 'reports': | |||
| uni.navigateTo({ | |||
| url: '/subPages/my/myRegistrations' | |||
| }) | |||
| break | |||
| case 'records': | |||
| uni.navigateTo({ | |||
| url: '/subPages/my/exchangeRecord' | |||
| }) | |||
| break | |||
| case 'favorites': | |||
| uni.navigateTo({ | |||
| url: '/subPages/my/productFavorites' | |||
| }) | |||
| break | |||
| case 'favoritesActivity': | |||
| uni.navigateTo({ | |||
| url: '/subPages/my/activityFavorites' | |||
| }) | |||
| break | |||
| case 'about': | |||
| // 关于我们页面 | |||
| uni.navigateTo({ | |||
| url: '/subPages/index/organizationIntroduction' | |||
| }) | |||
| break | |||
| case 'checkin': | |||
| uni.navigateTo({ | |||
| url: '/subPages/my/activityCheckin' | |||
| }) | |||
| break | |||
| case 'points': | |||
| uni.navigateTo({ | |||
| url: '/subPages/shop/pointsDetail' | |||
| }) | |||
| break | |||
| default: | |||
| break | |||
| } | |||
| }, | |||
| logout() { | |||
| uni.showModal({ | |||
| title: '提示', | |||
| content: '确定要退出登录吗?', | |||
| success: (res) => { | |||
| if (res.confirm) { | |||
| // 清除用户信息 | |||
| uni.removeStorageSync('token') | |||
| uni.showToast({ | |||
| title: '已退出登录', | |||
| icon: 'success' | |||
| }) | |||
| // 跳转到登录页面 | |||
| setTimeout(() => { | |||
| uni.reLaunch({ | |||
| url: '/subPages/login/login' | |||
| }) | |||
| }, 1000) | |||
| } | |||
| } | |||
| }) | |||
| }, | |||
| async getUser() { | |||
| if (uni.getStorageSync('token')) { | |||
| const res = await this.$api.user.queryUser() | |||
| this.userInfo = res?.result | |||
| }else{ | |||
| this.userInfo = { | |||
| nickName: '未登录', | |||
| headImage: '/static/默认头像.png', | |||
| collectionNum: '0', | |||
| score: '0' | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| onShow() { | |||
| this.getUser() | |||
| }, | |||
| async onPullDownRefresh() { | |||
| await this.getUser() | |||
| uni.stopPullDownRefresh() | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .my-page { | |||
| min-height: 100vh; | |||
| background-color: #FFFFFF; | |||
| } | |||
| // 顶部渐变背景区域 | |||
| .header-section { | |||
| // background: linear-gradient(180deg, #1488db, #98b5f1); | |||
| background-image: url('/static/我的_背景.png'); | |||
| box-sizing: border-box; | |||
| // background-color: #FFFFFF; | |||
| background-size: 100% 100%; | |||
| padding-top: 260rpx; | |||
| height: 400rpx; | |||
| margin-bottom: 50px; | |||
| position: relative; | |||
| } | |||
| .header-icons { | |||
| display: flex; | |||
| justify-content: flex-end; | |||
| gap: 32rpx; | |||
| margin-bottom: 40rpx; | |||
| } | |||
| // 用户信息区域 | |||
| .user-info { | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| } | |||
| .avatar-container { | |||
| width: 120rpx; | |||
| height: 120rpx; | |||
| border-radius: 50%; | |||
| overflow: hidden; | |||
| margin-bottom: 20rpx; | |||
| border: 4rpx solid rgba(255, 255, 255, 0.3); | |||
| } | |||
| .avatar { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| .username { | |||
| font-size: 30rpx; | |||
| color: #000000; | |||
| // font-weight: bold; | |||
| } | |||
| // 积分信息区域 | |||
| .points-section { | |||
| display: flex; | |||
| justify-content: space-between; | |||
| margin: 30rpx 30rpx; | |||
| gap: 24rpx; | |||
| } | |||
| .points-item { | |||
| border-radius: 12rpx; | |||
| padding: 0rpx 24rpx; | |||
| flex: 1; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| position: relative; | |||
| box-shadow: 0px 1.5px 3px 0px rgba(0,0,0,0.16); | |||
| &.yellow { | |||
| background: #FEF4D1; | |||
| } | |||
| &.blue { | |||
| background: #C7E6FF; | |||
| } | |||
| } | |||
| .points-content { | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: flex-start; | |||
| } | |||
| .points-number { | |||
| font-size: 32rpx; | |||
| color: #000000; | |||
| // font-weight: bold; | |||
| line-height: 1; | |||
| } | |||
| .points-label { | |||
| font-size: 24rpx; | |||
| color: #000000; | |||
| font-size: 24rpx; | |||
| // color: white; | |||
| margin-top: 8rpx; | |||
| &.yellow{ | |||
| color: #DEB31B; | |||
| } | |||
| &.blue{ | |||
| color: #1488db; | |||
| } | |||
| } | |||
| .points-icon { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| &-img{ | |||
| width: 140rpx; | |||
| height: 140rpx; | |||
| } | |||
| } | |||
| // 常用功能区域 | |||
| .functions-container { | |||
| height: 319px; | |||
| background: #ffffff; | |||
| border-radius: 8px; | |||
| box-shadow: 0px 1.5px 3px 0px rgba(0,0,0,0.16); | |||
| margin: 20rpx 30rpx; | |||
| padding: 40rpx; | |||
| } | |||
| .section-title { | |||
| font-size: 32rpx; | |||
| color: #333; | |||
| font-weight: bold; | |||
| margin-bottom: 40rpx; | |||
| display: block; | |||
| } | |||
| .functions-grid { | |||
| display: grid; | |||
| grid-template-columns: repeat(4, 1fr); | |||
| gap: 40rpx 10rpx; | |||
| } | |||
| .function-item { | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| gap: 16rpx; | |||
| padding: 20rpx; | |||
| border-radius: 12rpx; | |||
| transition: all 0.3s ease; | |||
| &:active { | |||
| background-color: #F0F8FF; | |||
| transform: scale(0.95); | |||
| } | |||
| } | |||
| .function-icon { | |||
| width: 48rpx; | |||
| height: 48rpx; | |||
| } | |||
| .function-icon-wrapper { | |||
| width: 48rpx; | |||
| height: 48rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| .function-text { | |||
| font-size: 24rpx; | |||
| color: #000000; | |||
| text-align: center; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,66 @@ | |||
| <template> | |||
| <view class="shop-page"> | |||
| <!-- 导航栏 --> | |||
| <HomePageNav /> | |||
| <!-- 可用积分卡片 --> | |||
| <PointsCard :points="userPoints"/> | |||
| <!-- 商城内容 --> | |||
| <ShopContent ref="shopContentRef" /> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import HomePageNav from '@/pages/components/HomePageNav.vue' | |||
| import PointsCard from '@/pages/components/shop/PointsCard.vue' | |||
| import ShopContent from '@/pages/components/shop/ShopContent.vue' | |||
| export default { | |||
| name: 'Shop', | |||
| components: { | |||
| HomePageNav, | |||
| PointsCard, | |||
| ShopContent | |||
| }, | |||
| data() { | |||
| return { | |||
| userPoints: 1385 | |||
| } | |||
| }, | |||
| methods: { | |||
| async getUserPoints() { | |||
| if (!uni.getStorageSync('token')) { | |||
| // 还没登陆 | |||
| this.userPoints = '未登录' | |||
| }else{ | |||
| // 实际项目中这里应该调用API | |||
| const res = await this.$api.user.queryUser() | |||
| this.userPoints = res?.result?.score || 0 | |||
| } | |||
| } | |||
| }, | |||
| async onShow() { | |||
| // 获取用户积分 | |||
| await this.getUserPoints() | |||
| this.$refs.shopContentRef.initData() | |||
| await this.$refs.shopContentRef.getGoodsList({isRefresh : true}) | |||
| }, | |||
| async onPullDownRefresh() { | |||
| // 下拉刷新时调用 | |||
| this.$refs.shopContentRef.initData() | |||
| await this.$refs.shopContentRef.getGoodsList({isRefresh : true}) | |||
| uni.stopPullDownRefresh() | |||
| }, | |||
| onReachBottom() { | |||
| this.$refs.shopContentRef.getGoodsList() | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .shop-page { | |||
| min-height: 100vh; | |||
| background: #f8f8f8; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,111 @@ | |||
| import Vue from 'vue' | |||
| import Vuex from 'vuex' | |||
| import * as api from '@/api' | |||
| Vue.use(Vuex) | |||
| const store = new Vuex.Store({ | |||
| state: { | |||
| // 存放状态 | |||
| configList: [], | |||
| careerList: [], | |||
| qualificationList: [], | |||
| categoryGoodsList: [], | |||
| categoryActivityList: [] | |||
| }, | |||
| mutations: { | |||
| setConfigList(state, data) { | |||
| state.configList = data | |||
| }, | |||
| setCareerList(state, data) { | |||
| state.careerList = data | |||
| }, | |||
| setQualificationList(state, data) { | |||
| state.qualificationList = data | |||
| }, | |||
| setCategoryGoodsList(state, data) { | |||
| state.categoryGoodsList = data | |||
| }, | |||
| setCategoryActivityList(state, data) { | |||
| state.categoryActivityList = data | |||
| } | |||
| }, | |||
| actions: { | |||
| // 查询配置列表 | |||
| async getConfig({ commit }) { | |||
| const res = await api.config.queryConfigList() | |||
| // 要求变成键值对的样子 | |||
| const config = res.result.records.reduce((acc, item) => { | |||
| if (!item.paramCode) { | |||
| console.log('paramCode为空', item); | |||
| return acc | |||
| } | |||
| acc[item.paramCode] = item | |||
| return acc | |||
| }, {}) | |||
| commit('setConfigList', config) | |||
| }, | |||
| // 查询职业列表 | |||
| async getCareer({ commit }) { | |||
| const res = await api.config.queryCareerList() | |||
| // if (res.code === 0) { | |||
| commit('setCareerList', res.result.records) | |||
| // } else { | |||
| // uni.showToast({ title: res.msg, icon: 'error' }) | |||
| // } | |||
| }, | |||
| // 查询学历列表 | |||
| async getQualification({ commit }) { | |||
| const res = await api.config.queryQualificationList() | |||
| // if (res.code === 0) { | |||
| commit('setQualificationList', res.result.records) | |||
| // } else { | |||
| // uni.showToast({ title: res.msg, icon: 'error' }) | |||
| // } | |||
| }, | |||
| // 查询商品分类列表 | |||
| async getCategoryGoodsList({ commit }) { | |||
| const res = await api.config.queryCategoryGoodsList() | |||
| commit('setCategoryGoodsList', res.result.records) | |||
| }, | |||
| // 查询活动分类列表 | |||
| async getCategoryActivityList({ commit }) { | |||
| const res = await api.config.queryCategoryActivityList() | |||
| commit('setCategoryActivityList', res.result.records) | |||
| }, | |||
| // 初始化数据 | |||
| async initData({ dispatch, state }) { | |||
| // 检查是否已初始化 | |||
| if (state.configList.length > 0 && state.careerList.length > 0 && state.qualificationList.length > 0 && state.categoryGoodsList.length > 0 && state.categoryActivityList.length > 0) { | |||
| console.log('配置数据已初始化,无需重复初始化') | |||
| return | |||
| } | |||
| try { | |||
| await Promise.all([ | |||
| dispatch('getConfig'), | |||
| dispatch('getCareer'), | |||
| dispatch('getQualification'), | |||
| dispatch('getCategoryGoodsList'), | |||
| dispatch('getCategoryActivityList') | |||
| ]) | |||
| console.log('所有配置数据初始化完成') | |||
| } catch (error) { | |||
| console.error('配置数据初始化失败:', error) | |||
| } | |||
| }, | |||
| } | |||
| }) | |||
| export default store | |||
| @ -0,0 +1,351 @@ | |||
| <template> | |||
| <view class="publish-page"> | |||
| <!-- 顶部提示容器 --> | |||
| <view class="tip-container" :class="isPhoto ? 'red' : 'blue'" > | |||
| <uv-icon name="info-circle-fill" size="16" :color="isPhoto ? '#FF4757' : '#007AFF'"></uv-icon> | |||
| <text class="tip-text" :class="isPhoto ? 'red' : 'blue'">{{ configParamText('photo_and_word_introduce') }}</text> | |||
| </view> | |||
| <!-- 主要内容容器 --> | |||
| <view class="main-container"> | |||
| <!-- 木邻说标题 --> | |||
| <view class="title-section"> | |||
| <!-- 加一个小竖条 --> | |||
| <view class="vertical-line" :class="isPhoto ? 'red' : 'blue'"></view> | |||
| <text class="title-text"> {{ isPhoto ? '木邻见' : '木邻说' }} </text> | |||
| </view> | |||
| <!-- 留言板输入区域 --> | |||
| <view class="message-section"> | |||
| <text class="section-label"> {{ isPhoto ? configParamText('photo_title') : configParamText('word_title') }}</text> | |||
| <view class="textarea-container"> | |||
| <textarea | |||
| class="message-textarea" | |||
| v-model="content" | |||
| :placeholder="isPhoto ? configParamText('photo_tip') : configParamText('word_tip')" | |||
| maxlength="500" | |||
| :show-confirm-bar="false" | |||
| ></textarea> | |||
| <view class="char-count"> | |||
| <text class="count-text">{{ content.length }}/500</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 添加图片区域 --> | |||
| <view class="image-section"> | |||
| <view class="image-grid"> | |||
| <!-- 已选择的图片 --> | |||
| <view | |||
| class="image-item" | |||
| v-for="(image, index) in image" | |||
| :key="index" | |||
| > | |||
| <image class="preview-image" :src="image" mode="aspectFill"></image> | |||
| <view class="delete-btn" @click="removeImage(index)"> | |||
| <uv-icon name="close" size="12" color="white"></uv-icon> | |||
| </view> | |||
| </view> | |||
| <!-- 添加图片按钮 --> | |||
| <view | |||
| class="add-image-btn" | |||
| v-if="image.length < 9" | |||
| @click="chooseImage" | |||
| > | |||
| <uv-icon name="plus" size="24" color="#999"></uv-icon> | |||
| <text class="add-text">添加图片</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 提交按钮容器 --> | |||
| <view class="submit-container"> | |||
| <uv-button | |||
| class="submit-btn" | |||
| :type=" isPhoto ? 'error' : 'primary'" | |||
| shape="circle" | |||
| :disabled="!content.trim()" | |||
| @click="submitPost" | |||
| > | |||
| 提交审核 | |||
| </uv-button> | |||
| </view> | |||
| <GlobalPopup ref="globalPopupRef"></GlobalPopup> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'PublishPost', | |||
| data() { | |||
| return { | |||
| content: '', | |||
| image: [], | |||
| isPhoto: false | |||
| } | |||
| }, | |||
| methods: { | |||
| chooseImage() { | |||
| const remainingCount = 9 - this.image.length | |||
| uni.chooseImage({ | |||
| count: 1, | |||
| sourceType: ['album', 'camera'], | |||
| success: async (res) => { | |||
| // const tempFiles = res.tempFiles.map(file => file.tempFilePath) | |||
| // this.image = [...this.image, ...tempFiles] | |||
| // console.log(...res.tempFilePaths); | |||
| const file = { | |||
| path: res.tempFilePaths[0] | |||
| } | |||
| const uploadRes = await this.$utils.uploadImage(file) | |||
| this.image.push(uploadRes.url) | |||
| uni.showToast({ | |||
| title: '图片上传成功', | |||
| icon: 'success' | |||
| }) | |||
| }, | |||
| fail: (err) => { | |||
| console.error('选择图片失败:', err) | |||
| } | |||
| }) | |||
| }, | |||
| removeImage(index) { | |||
| this.image.splice(index, 1) | |||
| }, | |||
| async submitPost() { | |||
| if (!this.content.trim()) { | |||
| uni.showToast({ | |||
| title: '请输入留言内容', | |||
| icon: 'none' | |||
| }) | |||
| return | |||
| } | |||
| const res = await this.$api.community.addPost({ | |||
| content: this.content, | |||
| image: this.image.toString(), | |||
| type: this.isPhoto ? 1 : 0 | |||
| }) | |||
| if (res.code === 200) { | |||
| this.$refs.globalPopupRef.open({ | |||
| content: '您的随手拍内容已提交审核!', | |||
| subContent: '审核通过后会自动展示在随手拍上!', | |||
| titleType: 'submit', | |||
| popupType: 'success', | |||
| closefn: () => { | |||
| setTimeout(() => { | |||
| uni.navigateBack() | |||
| }, 300) | |||
| } | |||
| }) | |||
| }else { | |||
| uni.showToast({ | |||
| title: `${res.message}`, | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| } | |||
| }, | |||
| onLoad(options) { | |||
| if (options.page === 'photo') { | |||
| this.isPhoto = true | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .publish-page { | |||
| min-height: 100vh; | |||
| background-color: #F3F7F8; | |||
| // display: flex; | |||
| // flex-direction: column; | |||
| } | |||
| // 顶部提示容器 | |||
| .tip-container { | |||
| background-color: #E3F2FD; | |||
| padding: 24rpx 32rpx; | |||
| margin: 20rpx; | |||
| border-radius: 12rpx; | |||
| display: flex; | |||
| align-items: flex-start; | |||
| gap: 16rpx; | |||
| border-left: 6rpx solid #007AFF; | |||
| &.red{ | |||
| border-left-color: #FF4757; | |||
| background-color: rgba(255, 71, 87, 0.1); | |||
| } | |||
| &.blue{ | |||
| border-left-color: #007AFF; | |||
| } | |||
| } | |||
| .tip-text { | |||
| font-size: 26rpx; | |||
| color: #1976D2; | |||
| &.red{ | |||
| color: #FF4757; | |||
| } | |||
| line-height: 1.5; | |||
| flex: 1; | |||
| } | |||
| // 主要内容容器 | |||
| .main-container { | |||
| flex: 1; | |||
| margin: 0 20rpx; | |||
| background-color: white; | |||
| border-radius: 16rpx; | |||
| padding: 32rpx; | |||
| box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); | |||
| } | |||
| .title-section { | |||
| margin-bottom: 32rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 16rpx; | |||
| } | |||
| .vertical-line { | |||
| width: 8rpx; | |||
| height: 40rpx; | |||
| border-radius: 4rpx; | |||
| &.red { | |||
| background-color: #FF4757; | |||
| } | |||
| &.blue { | |||
| background-color: #007AFF; | |||
| } | |||
| } | |||
| .title-text { | |||
| font-size: 36rpx; | |||
| font-weight: bold; | |||
| color: #333; | |||
| } | |||
| // 留言板区域 | |||
| .message-section { | |||
| margin-bottom: 40rpx; | |||
| } | |||
| .section-label { | |||
| font-size: 28rpx; | |||
| color: #666; | |||
| display: block; | |||
| margin-bottom: 20rpx; | |||
| } | |||
| .textarea-container { | |||
| position: relative; | |||
| background-color: #f5f5f5; | |||
| border-radius: 12rpx; | |||
| padding: 24rpx; | |||
| } | |||
| .message-textarea { | |||
| width: 100%; | |||
| min-height: 300rpx; | |||
| font-size: 30rpx; | |||
| color: #333; | |||
| background-color: transparent; | |||
| border: none; | |||
| outline: none; | |||
| resize: none; | |||
| line-height: 1.6; | |||
| } | |||
| .char-count { | |||
| position: absolute; | |||
| bottom: 16rpx; | |||
| right: 16rpx; | |||
| } | |||
| .count-text { | |||
| font-size: 24rpx; | |||
| color: #999; | |||
| } | |||
| // 图片区域 | |||
| .image-section { | |||
| margin-bottom: 40rpx; | |||
| } | |||
| .image-grid { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| gap: 16rpx; | |||
| } | |||
| .image-item { | |||
| position: relative; | |||
| width: 200rpx; | |||
| height: 200rpx; | |||
| border-radius: 12rpx; | |||
| overflow: hidden; | |||
| } | |||
| .preview-image { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| .delete-btn { | |||
| position: absolute; | |||
| top: 8rpx; | |||
| right: 8rpx; | |||
| width: 40rpx; | |||
| height: 40rpx; | |||
| background-color: rgba(0, 0, 0, 0.6); | |||
| border-radius: 50%; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| .add-image-btn { | |||
| width: 200rpx; | |||
| height: 200rpx; | |||
| border: 2rpx dashed #ddd; | |||
| border-radius: 12rpx; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| justify-content: center; | |||
| gap: 12rpx; | |||
| background-color: #fafafa; | |||
| transition: all 0.3s ease; | |||
| &:active { | |||
| background-color: #f0f0f0; | |||
| border-color: #007AFF; | |||
| } | |||
| } | |||
| .add-text { | |||
| font-size: 24rpx; | |||
| color: #999; | |||
| } | |||
| // 提交按钮容器 | |||
| .submit-container { | |||
| padding: 32rpx 40rpx; | |||
| // background-color: white; | |||
| margin-top: 60rpx; | |||
| border-top: 1rpx solid #f0f0f0; | |||
| } | |||
| .submit-btn { | |||
| width: 100%; | |||
| height: 88rpx; | |||
| border-radius: 44rpx; | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,309 @@ | |||
| <template> | |||
| <view class="activity-calendar"> | |||
| <!-- 活动列表 --> | |||
| <view class="calendar-content"> | |||
| <view | |||
| v-for="(dayData, index) in activityData" | |||
| :key="index" | |||
| class="day-section" | |||
| > | |||
| <!-- 日期和日历图标 (在容器外部) --> | |||
| <view class="date-header"> | |||
| <image | |||
| src="/subPages/static/活动日历_图标@2x.png" | |||
| class="calendar-icon" | |||
| ></image> | |||
| <text class="date-text">{{ dayData.activityTime }} {{ dayData.dayOfWeek }}</text> | |||
| </view> | |||
| <!-- 活动列表容器 --> | |||
| <view class="activities-container"> | |||
| <view | |||
| v-for="(activity, actIndex) in dayData.activities" | |||
| :key="actIndex" | |||
| class="activity-item" | |||
| @click="viewActivityDetail(activity)" | |||
| > | |||
| <!-- 活动图片 --> | |||
| <image class="activity-image" :src="activity.image" mode="aspectFill"></image> | |||
| <!-- 活动信息 --> | |||
| <view class="activity-info"> | |||
| <view class="title-row"> | |||
| <view class="activity-badge"> | |||
| <text class="badge-text">{{ activity.score }}分</text> | |||
| </view> | |||
| <text class="activity-title">{{ activity.title }}</text> | |||
| </view> | |||
| <view class="activity-location"> | |||
| <uv-icon name="map-fill" size="14" color="#999"></uv-icon> | |||
| <text class="location-text">{{ activity.address }}</text> | |||
| </view> | |||
| <view class="activity-time"> | |||
| <uv-icon name="calendar" size="14" color="#999"></uv-icon> | |||
| <text class="time-text">{{ activity.activityTime }}</text> | |||
| </view> | |||
| <view class="activity-participants"> | |||
| <uv-icon name="account-fill" size="14" color="#999"></uv-icon> | |||
| <text class="participants-text">{{ activity.numActivity }}/{{ activity.numLimit }}人已报名</text> | |||
| </view> | |||
| </view> | |||
| <!-- 查看详情按钮 --> | |||
| <view class="activity-action"> | |||
| <view class="detail-btn" @click.stop="viewActivityDetail(activity)"> | |||
| <text class="detail-btn-text">查看详情</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'ActivityCalendar', | |||
| data() { | |||
| return { | |||
| activityData: [], | |||
| pageNo: 1, | |||
| pageSize: 10, | |||
| hasMore: true | |||
| } | |||
| }, | |||
| methods: { | |||
| viewActivityDetail(activity) { | |||
| // 跳转到活动详情页面 | |||
| uni.navigateTo({ | |||
| url: `/subPages/index/activityDetail?id=${activity.id}` | |||
| }); | |||
| }, | |||
| // 处理后端返回的时间格式 | |||
| formatTime(timeString) { | |||
| // 只截取年月日 中间有空格区分年月日和具体时间 | |||
| const [datePart, timePart] = timeString.split(' '); | |||
| return datePart; | |||
| }, | |||
| // 转化为时间格式 | |||
| changeData(arr, isRefresh) { | |||
| if (isRefresh) { | |||
| this.activityData = [] | |||
| } | |||
| arr.forEach(item => { | |||
| // 先查找是否存在相同日期的数据 | |||
| const existingDay = this.activityData.find(day => | |||
| day.dayOfWeek === item.dayOfWeek && this.formatTime(day.activityTime) === this.formatTime(item.activityTime) | |||
| ); | |||
| if (existingDay) { | |||
| // 如果找到了,添加到现有的activities数组中 | |||
| existingDay.activities.push(item); | |||
| } else { | |||
| // 如果没找到,创建新的日期条目 | |||
| this.activityData.push({ | |||
| activityTime: this.formatTime(item.activityTime), | |||
| dayOfWeek: item.dayOfWeek, | |||
| activities: [item] | |||
| }); | |||
| } | |||
| }); | |||
| }, | |||
| async getActivityData(isRefresh = false) { | |||
| if (!this.hasMore) return | |||
| const res = await this.$api.activity.queryActivityList({ | |||
| pageNo: this.pageNo, | |||
| pageSize: this.pageSize | |||
| }) | |||
| if (res.result.records.length){ | |||
| this.changeData(res.result.records, isRefresh) | |||
| this.pageNo++ | |||
| }else { | |||
| uni.showToast({ | |||
| title: '暂无数据', | |||
| icon: 'none' | |||
| }) | |||
| this.hasMore = false | |||
| } | |||
| }, | |||
| initData() { | |||
| this.hasMore = true | |||
| // this.activityData = [] | |||
| this.pageNo = 1 | |||
| } | |||
| }, | |||
| async onShow() { | |||
| this.initData() | |||
| await this.getActivityData(true); | |||
| }, | |||
| onReachBottom() { | |||
| this.getActivityData(); | |||
| }, | |||
| async onPullDownRefresh() { | |||
| this.initData() | |||
| await this.getActivityData(true); | |||
| uni.stopPullDownRefresh() | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| // @import '@/uni.scss'; | |||
| .activity-calendar { | |||
| background-color: #f5f5f5; | |||
| min-height: 100vh; | |||
| .calendar-content { | |||
| padding: 40rpx 30rpx; | |||
| .day-section { | |||
| margin-bottom: 60rpx; | |||
| &:last-child { | |||
| margin-bottom: 0; | |||
| } | |||
| .date-header { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 30rpx; | |||
| .calendar-icon { | |||
| width: 48rpx; | |||
| height: 48rpx; | |||
| margin-right: 20rpx; | |||
| } | |||
| .date-text { | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| color: #000000; | |||
| } | |||
| } | |||
| .activities-container { | |||
| .activity-item { | |||
| background: #ffffff; | |||
| // border-radius: 16rpx; | |||
| padding: 24rpx; | |||
| margin-bottom: 20rpx; | |||
| // box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); | |||
| display: flex; | |||
| align-items: flex-start; | |||
| transition: all 0.3s ease; | |||
| position: relative; | |||
| &:last-child { | |||
| margin-bottom: 0; | |||
| } | |||
| &:active { | |||
| transform: scale(0.98); | |||
| box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12); | |||
| } | |||
| .activity-image { | |||
| width: 190rpx; | |||
| height: 190rpx; | |||
| border-radius: 8rpx; | |||
| margin-right: 20rpx; | |||
| } | |||
| .activity-info { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| .title-row { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 10rpx; | |||
| .activity-badge { | |||
| width: 31px; | |||
| height: 20px; | |||
| background: #218cdd; | |||
| border-radius: 3.5px; | |||
| margin-right: 7rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| .badge-text { | |||
| font-size: 18rpx; | |||
| color: #fff; | |||
| } | |||
| } | |||
| } | |||
| .activity-title { | |||
| font-size: 28rpx; | |||
| font-weight: bold; | |||
| color: $uni-text-color; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| white-space: nowrap; | |||
| } | |||
| .activity-location, | |||
| .activity-time, | |||
| .activity-participants { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 6rpx; | |||
| .location-text, | |||
| .time-text, | |||
| .participants-text { | |||
| font-size: 24rpx; | |||
| color: $uni-text-color-grey; | |||
| margin-left: 6rpx; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| white-space: nowrap; | |||
| flex: 1; | |||
| } | |||
| } | |||
| } | |||
| .activity-action { | |||
| position: absolute; | |||
| bottom: 20rpx; | |||
| right: 20rpx; | |||
| .detail-btn { | |||
| background: #218CDD; | |||
| border-radius: 26rpx; | |||
| width: 140rpx; | |||
| height: 52rpx; | |||
| text-align: center; | |||
| line-height: 44rpx; | |||
| .detail-btn-text { | |||
| font-size: 26rpx; | |||
| color: #ffffff; | |||
| font-weight: 500; | |||
| } | |||
| } | |||
| .detail-btn:active { | |||
| background: #1976C7; | |||
| transform: scale(0.95); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,487 @@ | |||
| <template> | |||
| <view class="activity-detail"> | |||
| <!-- 轮播图 --> | |||
| <view class="banner-container"> | |||
| <swiper class="banner-swiper" height="450rpx" :indicator-dots="true" :autoplay="true" :interval="3000" :duration="500"> | |||
| <!-- 如果没有,分割 --> | |||
| <swiper-item v-for="(image, index) in imageList" :key="index"> | |||
| <image class="banner-image" :src="image" mode="aspectFill"></image> | |||
| </swiper-item> | |||
| </swiper> | |||
| </view> | |||
| <!-- 活动信息 --> | |||
| <view class="activity-info"> | |||
| <!-- 活动标题和标签 --> | |||
| <view class="title-section"> | |||
| <view class="activity-badge"> | |||
| <text class="badge-text">{{ activityData.score }}积分</text> | |||
| </view> | |||
| <text class="activity-title">{{ activityData.title }}</text> | |||
| </view> | |||
| <!-- 活动详细信息 --> | |||
| <view class="info-section"> | |||
| <view class="info-item"> | |||
| <uv-icon name="calendar" size="16" color="#666"></uv-icon> | |||
| <text class="info-label">活动时间:</text> | |||
| <text class="info-value">{{ activityData.activityTime }}</text> | |||
| </view> | |||
| <view class="info-item"> | |||
| <uv-icon name="clock" size="16" color="#666"></uv-icon> | |||
| <text class="info-label">报名时间:</text> | |||
| <text class="info-value">{{ activityData.startTime }}</text> | |||
| </view> | |||
| <view class="info-item"> | |||
| <uv-icon name="account-fill" size="16" color="#666"></uv-icon> | |||
| <text class="info-label">联系人:</text> | |||
| <text class="info-value">{{ activityData.contact }}</text> | |||
| </view> | |||
| <view class="info-item"> | |||
| <uv-icon name="phone" size="16" color="#666"></uv-icon> | |||
| <text class="info-label">取消规则:</text> | |||
| <text class="info-value">{{ activityData.rule }}</text> | |||
| </view> | |||
| <view class="info-item"> | |||
| <uv-icon name="map-fill" size="16" color="#666"></uv-icon> | |||
| <text class="info-label">活动地点:</text> | |||
| <text class="info-value">{{ activityData.address }}</text> | |||
| </view> | |||
| </view> | |||
| <!-- 活动详情 --> | |||
| <view class="detail-section"> | |||
| <view class="section-title"> | |||
| <text class="title-text">活动详情</text> | |||
| </view> | |||
| <view class="detail-content"> | |||
| <!-- <text class="detail-text"> --> | |||
| <rich-text :nodes="activityData.details"></rich-text> | |||
| <!-- </text> --> | |||
| </view> | |||
| </view> | |||
| <!-- 活动图集 --> | |||
| <view class="gallery-section"> | |||
| <view class="section-title"> | |||
| <text class="title-text">活动图集</text> | |||
| </view> | |||
| <view class="gallery-grid"> | |||
| <image | |||
| v-for="(image, index) in atlas" | |||
| :key="index" | |||
| class="gallery-image" | |||
| :src="image" | |||
| mode="aspectFill" | |||
| @click="previewImage(image, atlas)" | |||
| ></image> | |||
| <!-- <uv-album :urls="activityData.gallery"></uv-album> --> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 固定底部操作栏 --> | |||
| <view class="bottom-action"> | |||
| <view class="action-left"> | |||
| <button open-type="share"> | |||
| <view class="action-item"> | |||
| <uv-icon name="share" size="24" color="#000"></uv-icon> | |||
| <text class="action-text">分享</text> | |||
| </view> | |||
| </button> | |||
| <view class="action-item" @click="collectActivity"> | |||
| <uv-icon name="heart-fill" class="collection-icon" size="24" :color="activityData.isCollection === 1 ? '#ff4757' : '#999'"></uv-icon> | |||
| <text class="action-text">收藏</text> | |||
| </view> | |||
| <view class="action-item"> | |||
| <text class="participants-count"> | |||
| <text :style="{'color': activityData.numActivity >= activityData.numLimit ? '#999' : '#1488DB'}">{{ activityData.numActivity }}</text> | |||
| /{{ activityData.numLimit }}</text> | |||
| <text class="action-text">已报名</text> | |||
| </view> | |||
| </view> | |||
| <view class="action-right"> | |||
| <uv-button | |||
| v-if="activityData.status === '1'" | |||
| type="primary" | |||
| size="normal" | |||
| text="已结束" | |||
| shape="circle" | |||
| @click="signUpActivity" | |||
| :disabled="true" | |||
| ></uv-button> | |||
| <uv-button | |||
| v-else-if="activityData.isApply === 1" | |||
| type="primary" | |||
| size="normal" | |||
| text="您已报名" | |||
| shape="circle" | |||
| @click="signUpActivity" | |||
| :disabled="true" | |||
| ></uv-button> | |||
| <uv-button | |||
| v-else | |||
| type="primary" | |||
| size="normal" | |||
| text="我要报名" | |||
| shape="circle" | |||
| @click="signUpActivity" | |||
| :disabled="activityData.numActivity >= activityData.numLimit " | |||
| ></uv-button> | |||
| </view> | |||
| </view> | |||
| <SignUpForm | |||
| ref="signUpFormRef" | |||
| @close="onSignUpFormClose" | |||
| @submit="onSignUpFormSubmit" | |||
| /> | |||
| <GlobalPopup ref="globalPopupRef"></GlobalPopup> | |||
| </view> | |||
| <!-- 报名表单弹窗 --> | |||
| </template> | |||
| <script> | |||
| import SignUpForm from '@/subPages/index/components/SignUpForm.vue' | |||
| export default { | |||
| components: { | |||
| SignUpForm | |||
| }, | |||
| data() { | |||
| return { | |||
| // isCollected: false, | |||
| showSignUpForm: false, | |||
| activityData: { | |||
| title: '关爱自闭症儿童活动', | |||
| duration: '30积分', | |||
| time: '2025-06-12 14:30', | |||
| registrationTime: '2025-06-01 14:30——2025-09-01 14:30', | |||
| contact: '柳老师 (13256484512)', | |||
| cancelRule: '报名随时可取消', | |||
| location: '长沙市雨花区时代阳光大夏国际大厅2145', | |||
| registeredCount: 9, | |||
| maxCount: 30, | |||
| details: [ | |||
| '身体健康,热爱志愿服务工作,富有责任感和奉献精神', | |||
| '遵纪守法,思想上进,作风正派,服从安排', | |||
| '年龄在60岁以下,具备广告宣传理能力' | |||
| ], | |||
| gallery: [ | |||
| '/static/bannerImage.png', | |||
| '/static/bannerImage.png', | |||
| '/static/bannerImage.png', | |||
| '/static/bannerImage.png' | |||
| ] | |||
| }, | |||
| activityId: null | |||
| } | |||
| }, | |||
| computed:{ | |||
| imageList(){ | |||
| if (this.activityData.image) { | |||
| return this.activityData.image.split(',') | |||
| } | |||
| return [] | |||
| }, | |||
| atlas(){ | |||
| if (this.activityData.atlas) { | |||
| return this.activityData.atlas.split(',') | |||
| } | |||
| return [] | |||
| } | |||
| }, | |||
| onLoad(options) { | |||
| if (options.id) { | |||
| this.activityId = options.id | |||
| this.loadActivityDetail(options.id) | |||
| }else { | |||
| uni.showToast({ | |||
| title: '没有给活动id', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| }, | |||
| methods: { | |||
| // 自定义分享内容 | |||
| mixinCustomShare() { | |||
| return { | |||
| desc: '', | |||
| title: `邀请您参加${this.activityData.title || ''}`, | |||
| imageUrl: this.imageList[0], | |||
| path: '/subPages/index/activityDetail?id=' + this.activityId | |||
| } | |||
| }, | |||
| async loadActivityDetail(id) { | |||
| let params = {} | |||
| if (uni.getStorageSync('token')) { | |||
| params.token = uni.getStorageSync('token') | |||
| } | |||
| // 根据ID加载活动详情 | |||
| const res = await this.$api.activity.queryActivityById({ | |||
| activityId: id, | |||
| ...params | |||
| }) | |||
| this.activityData = res.result | |||
| }, | |||
| previewImage(current, urls) { | |||
| uni.previewImage({ | |||
| current: current, | |||
| urls: urls | |||
| }) | |||
| }, | |||
| async collectActivity() { | |||
| const res = await this.$api.activity.collectionActivity({ | |||
| activityId: this.activityId | |||
| }) | |||
| await this.loadActivityDetail(this.activityId) | |||
| uni.showToast({ | |||
| title: `${res.message}`, | |||
| icon: 'none' | |||
| }) | |||
| }, | |||
| signUpActivity() { | |||
| if (this.activityData.numActivity >= this.activityData.numLimit) { | |||
| uni.showToast({ | |||
| title: '报名人数已满', | |||
| icon: 'none' | |||
| }) | |||
| return | |||
| } | |||
| this.$refs.signUpFormRef.open() | |||
| }, | |||
| onSignUpFormClose() { | |||
| this.$refs.signUpFormRef.close() | |||
| }, | |||
| async onSignUpFormSubmit(formData) { | |||
| console.log('报名表单数据:', formData) | |||
| // 这里可以调用API提交报名数据 | |||
| const res = await this.$api.activity.applyActivity({ | |||
| activityId: this.activityId, | |||
| ...formData | |||
| }) | |||
| if (res.code === 200) { | |||
| this.$refs.globalPopupRef.open({ | |||
| content: '恭喜您报名成功', | |||
| subContent: '别忘了准时参与活动哦!', | |||
| titleType: 'submit', | |||
| popupType: 'success', | |||
| closefn: () => { | |||
| setTimeout(() => { | |||
| uni.navigateBack() | |||
| }, 300); | |||
| } | |||
| }) | |||
| // 更新状态 | |||
| // this.loadActivityDetail(this.activityId) | |||
| }else { | |||
| uni.showToast({ | |||
| title: `${res.message}`, | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| } | |||
| }, | |||
| async onPullDownRefresh() { | |||
| await this.loadActivityDetail(this.activityId) | |||
| uni.stopPullDownRefresh() | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .activity-detail { | |||
| min-height: 100vh; | |||
| background: #f8f8f8; | |||
| padding-bottom: 120rpx; | |||
| .banner-container { | |||
| width: 100%; | |||
| height: 450rpx; | |||
| .banner-swiper { | |||
| width: 100%; | |||
| height: 100%; | |||
| .banner-image { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| } | |||
| } | |||
| .activity-info { | |||
| background: #ffffff; | |||
| // border: 1rpx dashed #F3F7F8; | |||
| margin: 20rpx; | |||
| border-radius: 16rpx; | |||
| padding: 30rpx; | |||
| .title-section { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 30rpx; | |||
| .activity-badge { | |||
| background: #218CDD; | |||
| border-radius: 8rpx; | |||
| height: 40rpx; | |||
| padding: 0 5rpx 10rpx; | |||
| line-height: 40rpx; | |||
| text-align: center; | |||
| margin-right: 16rpx; | |||
| .badge-text { | |||
| color: #ffffff; | |||
| font-size: 24rpx; | |||
| font-weight: 500; | |||
| } | |||
| } | |||
| .activity-title { | |||
| font-size: 36rpx; | |||
| font-weight: bold; | |||
| color: #333333; | |||
| flex: 1; | |||
| } | |||
| } | |||
| .info-section { | |||
| background: #F3F7F8; | |||
| margin-bottom: 40rpx; | |||
| // 虚线属性 | |||
| border: 2rpx dashed #F3F7F8; | |||
| .info-item { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 20rpx; | |||
| &:last-child { | |||
| margin-bottom: 0; | |||
| } | |||
| .info-label { | |||
| font-size: 28rpx; | |||
| color: #999999; | |||
| margin-left: 12rpx; | |||
| margin-right: 8rpx; | |||
| } | |||
| .info-value { | |||
| font-size: 28rpx; | |||
| color: #999999; | |||
| flex: 1; | |||
| } | |||
| } | |||
| } | |||
| .detail-section { | |||
| margin-bottom: 40rpx; | |||
| .section-title { | |||
| margin-bottom: 20rpx; | |||
| .title-text { | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| color: #333333; | |||
| } | |||
| } | |||
| .detail-content { | |||
| .detail-text { | |||
| display: block; | |||
| font-size: 28rpx; | |||
| color: #666666; | |||
| line-height: 1.6; | |||
| margin-bottom: 16rpx; | |||
| &:last-child { | |||
| margin-bottom: 0; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .gallery-section { | |||
| .section-title { | |||
| margin-bottom: 20rpx; | |||
| .title-text { | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| color: #333333; | |||
| } | |||
| } | |||
| .gallery-grid { | |||
| display: grid; | |||
| grid-template-columns: repeat(2, 1fr); | |||
| gap: 16rpx; | |||
| .gallery-image { | |||
| width: 100%; | |||
| height: 200rpx; | |||
| border-radius: 12rpx; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .bottom-action { | |||
| position: fixed; | |||
| bottom: 0; | |||
| left: 0; | |||
| right: 0; | |||
| background: #ffffff; | |||
| padding: 20rpx 30rpx; | |||
| border-top: 1rpx solid #eeeeee; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| z-index: 100; | |||
| .action-left { | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 100rpx; | |||
| .action-item { | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| gap: 8rpx; | |||
| .action-text { | |||
| font-size: 22rpx; | |||
| color: #000; | |||
| } | |||
| .participants-count { | |||
| font-size: 24rpx; | |||
| color: #333333; | |||
| // font-weight: bold; | |||
| } | |||
| } | |||
| } | |||
| .action-right { | |||
| flex-shrink: 0; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,115 @@ | |||
| <template> | |||
| <view class="announcement-page"> | |||
| <!-- 公告列表 --> | |||
| <view class="announcement-list"> | |||
| <view | |||
| class="announcement-item" | |||
| v-for="(item, index) in list" | |||
| :key="index" | |||
| @click="goToDetail(item)" | |||
| > | |||
| <view class="item-content"> | |||
| <view class="text-content"> | |||
| <view class="title">{{ item.title }}</view> | |||
| <view class="description" >{{ item.shortTitle }}</view> | |||
| <!-- <rich-text class="description" :nodes="item.details"></rich-text> --> | |||
| <view class="time">{{ item.createTime }}</view> | |||
| </view> | |||
| <view class="image-content" v-if="item.image"> | |||
| <image :src="item.image" class="announcement-image" mode="aspectFill"></image> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import ListMixin from '@/mixins/list.js' | |||
| export default { | |||
| name: 'Announcement', | |||
| mixins: [ListMixin], | |||
| data() { | |||
| return { | |||
| mixinListApi: 'home.queryNoticeList' | |||
| } | |||
| }, | |||
| methods: { | |||
| goToDetail(item) { | |||
| uni.navigateTo({ | |||
| url: '/subPages/index/announcementDetail?id=' + item.id | |||
| }) | |||
| }, | |||
| // mixinSetParams() { | |||
| // return { | |||
| // type: 1 | |||
| // } | |||
| // } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .announcement-page { | |||
| background-color: $uni-bg-color-grey; | |||
| min-height: 100vh; | |||
| padding: 20rpx; | |||
| } | |||
| .announcement-list { | |||
| .announcement-item { | |||
| background-color: $uni-bg-color; | |||
| border-radius: 16rpx; | |||
| margin-bottom: 20rpx; | |||
| padding: 30rpx; | |||
| box-shadow: 0rpx 3rpx 6rpx 0rpx rgba(0,0,0,0.16); | |||
| .item-content { | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: flex-start; | |||
| .text-content { | |||
| flex: 1; | |||
| margin-right: 20rpx; | |||
| .title { | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| color: #000000; | |||
| margin-bottom: 16rpx; | |||
| line-height: 1.4; | |||
| } | |||
| .description { | |||
| font-size: 28rpx; | |||
| color: #0F2248; | |||
| line-height: 1.5; | |||
| margin-bottom: 20rpx; | |||
| display: -webkit-box; | |||
| -webkit-box-orient: vertical; | |||
| -webkit-line-clamp: 2; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| .time { | |||
| font-size: 24rpx; | |||
| color: $uni-text-color-grey; | |||
| } | |||
| } | |||
| .image-content { | |||
| flex-shrink: 0; | |||
| .announcement-image { | |||
| width: 225rpx; | |||
| height: 200rpx; | |||
| border-radius: 9rpx; | |||
| background-color: $uni-bg-color-grey; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,97 @@ | |||
| <template> | |||
| <view class="announcement-detail"> | |||
| <!-- 标题 --> | |||
| <view class="title">{{ announcementData.title }}</view> | |||
| <!-- 时间 --> | |||
| <view class="time">{{ announcementData.createTime }}</view> | |||
| <!-- 图片 --> | |||
| <view class="image-container" v-if="announcementData.image"> | |||
| <image :src="announcementData.image" class="detail-image" mode="aspectFill"></image> | |||
| </view> | |||
| <!-- 正文内容 --> | |||
| <view class="content"> | |||
| <!-- <text class="content-text">{{ announcementData.content }}</text> --> | |||
| <rich-text :nodes="announcementData.details"></rich-text> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'AnnouncementDetail', | |||
| data() { | |||
| return { | |||
| announcementData: { | |||
| id: '', | |||
| title: '活动公告标题标题内容', | |||
| time: '2025/06/14 16:47:21', | |||
| image: '/static/bannerImage.png', | |||
| content: '正文内容 文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明文字说明。' | |||
| } | |||
| } | |||
| }, | |||
| onLoad(options) { | |||
| // 接收传递的参数 | |||
| if (options.id) { | |||
| this.announcementData.id = options.id; | |||
| // 这里可以根据id获取具体的公告详情 | |||
| this.getAnnouncementDetail(options.id); | |||
| } | |||
| }, | |||
| methods: { | |||
| async getAnnouncementDetail(id) { | |||
| // 模拟获取公告详情的方法 | |||
| // 实际项目中这里应该调用API获取数据 | |||
| const res = await this.$api.home.queryNoticeById({noticeId:id }) | |||
| this.announcementData = res.result | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| // @import '@/uni.scss'; | |||
| .announcement-detail { | |||
| padding: 40rpx; | |||
| background-color: #fff; | |||
| min-height: 100vh; | |||
| .title { | |||
| font-size: 34rpx; | |||
| font-weight: bold; | |||
| color: #000000; | |||
| line-height: 1.5; | |||
| margin-bottom: 20rpx; | |||
| } | |||
| .time { | |||
| font-size: 24rpx; | |||
| color: #999999; | |||
| margin-bottom: 30rpx; | |||
| } | |||
| .image-container { | |||
| margin-bottom: 30rpx; | |||
| .detail-image { | |||
| width: 100%; | |||
| height: 250rpx; | |||
| border-radius: 16rpx; | |||
| // object-fit: cover; | |||
| } | |||
| } | |||
| .content { | |||
| .content-text { | |||
| font-size: 28rpx; | |||
| color: #000000; | |||
| line-height: 1.8; | |||
| text-align: justify; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,357 @@ | |||
| <template> | |||
| <view> | |||
| <uv-popup | |||
| ref="popup" | |||
| mode="bottom" | |||
| border-radius="20" | |||
| @close="handleClose" | |||
| > | |||
| <view class="signup-form"> | |||
| <!-- 表单标题 --> | |||
| <view class="form-header"> | |||
| <text class="form-title">我要报名</text> | |||
| </view> | |||
| <!-- 表单内容 --> | |||
| <view class="form-content"> | |||
| <!-- 姓名 --> | |||
| <view class="form-item"> | |||
| <view class="input-container"> | |||
| <text class="input-label required">姓名</text> | |||
| <uv-input | |||
| v-model="formData.name" | |||
| placeholder="请输入您的姓名" | |||
| border="none" | |||
| :custom-style="inputStyle" | |||
| ></uv-input> | |||
| </view> | |||
| </view> | |||
| <!-- 性别 --> | |||
| <view class="form-item"> | |||
| <view class="input-container" @click="showGenderPicker"> | |||
| <text class="input-label required">性别</text> | |||
| <view class="picker-input"> | |||
| <text class="picker-text" :class="{ placeholder: !formData.sex }">{{ formData.sex || '请选择' }}</text> | |||
| <uv-icon name="arrow-right" size="14" color="#999"></uv-icon> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 年龄 --> | |||
| <view class="form-item"> | |||
| <view class="input-container"> | |||
| <text class="input-label required">年龄</text> | |||
| <uv-input | |||
| v-model="formData.age" | |||
| placeholder="请输入您的年龄" | |||
| type="number" | |||
| border="none" | |||
| :custom-style="inputStyle" | |||
| ></uv-input> | |||
| </view> | |||
| </view> | |||
| <!-- 手机号 --> | |||
| <view class="form-item"> | |||
| <view class="input-container"> | |||
| <text class="input-label required">手机号</text> | |||
| <uv-input | |||
| v-model="formData.phone" | |||
| placeholder="请输入您的手机号" | |||
| type="number" | |||
| border="none" | |||
| :custom-style="inputStyle" | |||
| ></uv-input> | |||
| </view> | |||
| </view> | |||
| <!-- 所在地区 --> | |||
| <view class="form-item"> | |||
| <view class="input-container" @click="chooseLocation"> | |||
| <text class="input-label">所在地区</text> | |||
| <view class="picker-input"> | |||
| <text class="picker-text" :class="{ placeholder: !formData.area }">{{ formData.area || '请选择地址' }}</text> | |||
| <uv-icon name="arrow-right" size="14" color="#999"></uv-icon> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 详细地址 --> | |||
| <view class="form-item"> | |||
| <view class="input-container"> | |||
| <text class="input-label">详细地址</text> | |||
| <uv-input | |||
| v-model="formData.address" | |||
| placeholder="请输入详细地址" | |||
| border="none" | |||
| :custom-style="inputStyle" | |||
| ></uv-input> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 提交按钮 --> | |||
| <view class="form-footer"> | |||
| <uv-button | |||
| type="primary" | |||
| text="提交报名" | |||
| :custom-style="buttonStyle" | |||
| @click="submitForm" | |||
| ></uv-button> | |||
| </view> | |||
| </view> | |||
| </uv-popup> | |||
| <!-- 性别选择器 --> | |||
| <uv-picker | |||
| ref="genderPicker" | |||
| :columns="genderOptions" | |||
| @confirm="onGenderConfirm" | |||
| @cancel="onGenderCancel" | |||
| @close="onGenderCancel" | |||
| ></uv-picker> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'SignUpForm', | |||
| data() { | |||
| return { | |||
| show: false, | |||
| formData: { | |||
| name: '', | |||
| sex: '', | |||
| age: '', | |||
| phone: '', | |||
| area: '', | |||
| address: '' | |||
| }, | |||
| genderOptions: [['男', '女']], | |||
| inputStyle: { | |||
| backgroundColor: 'transparent', | |||
| fontSize: '28rpx' | |||
| }, | |||
| buttonStyle: { | |||
| width: '100%', | |||
| height: '88rpx', | |||
| borderRadius: '44rpx' | |||
| } | |||
| } | |||
| }, | |||
| methods: { | |||
| handleClose() { | |||
| this.$emit('close') | |||
| this.$refs.popup.close() | |||
| }, | |||
| showGenderPicker() { | |||
| this.$refs.genderPicker.open() | |||
| }, | |||
| chooseLocation() { | |||
| uni.chooseLocation({ | |||
| success: (res) => { | |||
| console.log('位置名称:' + res.name) | |||
| console.log('详细地址:' + res.address) | |||
| console.log('纬度:' + res.latitude) | |||
| console.log('经度:' + res.longitude) | |||
| // 设置选中的地址 | |||
| this.formData.area = res.address || res.name | |||
| // 如果详细地址为空,也可以自动填充 | |||
| if (!this.formData.address && res.name) { | |||
| this.formData.address = res.name | |||
| } | |||
| }, | |||
| fail: (err) => { | |||
| console.log('选择位置失败:', err) | |||
| uni.showToast({ | |||
| title: '获取位置失败', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| }) | |||
| }, | |||
| onGenderConfirm(value) { | |||
| this.formData.sex = value.value[0] | |||
| }, | |||
| onGenderCancel() { | |||
| // 性别选择器取消时的处理 | |||
| }, | |||
| validateForm() { | |||
| if (!this.formData.name.trim()) { | |||
| uni.showToast({ | |||
| title: '请输入姓名', | |||
| icon: 'none' | |||
| }) | |||
| return false | |||
| } | |||
| if (!this.formData.sex) { | |||
| uni.showToast({ | |||
| title: '请选择性别', | |||
| icon: 'none' | |||
| }) | |||
| return false | |||
| } | |||
| if (!this.formData.age.trim()) { | |||
| uni.showToast({ | |||
| title: '请输入年龄', | |||
| icon: 'none' | |||
| }) | |||
| return false | |||
| } | |||
| if (!this.formData.phone.trim()) { | |||
| uni.showToast({ | |||
| title: '请输入手机号', | |||
| icon: 'none' | |||
| }) | |||
| return false | |||
| } | |||
| if (!/^1[3-9]\d{9}$/.test(this.formData.phone)) { | |||
| uni.showToast({ | |||
| title: '请输入正确的手机号', | |||
| icon: 'none' | |||
| }) | |||
| return false | |||
| } | |||
| return true | |||
| }, | |||
| submitForm() { | |||
| if (!this.validateForm()) { | |||
| return | |||
| } | |||
| this.formData.sex = this.formData.sex === '男' ? 0 : 1 | |||
| // 提交表单数据 | |||
| // delete this.formData.age | |||
| this.$emit('submit', this.formData) | |||
| // 重置表单 | |||
| this.resetForm() | |||
| // 关闭弹窗 | |||
| this.handleClose() | |||
| }, | |||
| resetForm() { | |||
| this.formData = { | |||
| name: '', | |||
| sex: '', | |||
| age: '', | |||
| phone: '', | |||
| area: '', | |||
| address: '' | |||
| } | |||
| }, | |||
| open() { | |||
| this.$refs.popup.open() | |||
| }, | |||
| close() { | |||
| this.$refs.popup.close() | |||
| } | |||
| }, | |||
| expose: ['open', 'close'] | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .signup-form { | |||
| max-height: 1000rpx; | |||
| display: flex; | |||
| flex-direction: column; | |||
| background: #ffffff; | |||
| .form-header { | |||
| padding: 40rpx 30rpx 20rpx; | |||
| text-align: center; | |||
| border-bottom: 1rpx solid #f0f0f0; | |||
| .form-title { | |||
| font-size: 36rpx; | |||
| font-weight: bold; | |||
| color: #333333; | |||
| } | |||
| } | |||
| .form-content { | |||
| flex: 1; | |||
| padding: 30rpx; | |||
| overflow-y: auto; | |||
| .form-item { | |||
| margin-bottom: 30rpx; | |||
| &:last-child { | |||
| margin-bottom: 0; | |||
| } | |||
| .input-container { | |||
| background: #f3f7f8; | |||
| border-radius: 16rpx; | |||
| padding: 24rpx 30rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| .input-label { | |||
| font-size: 28rpx; | |||
| color: #333333; | |||
| margin-right: 20rpx; | |||
| min-width: 120rpx; | |||
| &.required::before { | |||
| content: '*'; | |||
| color: #ff4757; | |||
| margin-right: 4rpx; | |||
| } | |||
| } | |||
| .picker-input { | |||
| flex: 1; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| .picker-text { | |||
| font-size: 28rpx; | |||
| color: #333333; | |||
| &.placeholder { | |||
| color: #999999; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .form-footer { | |||
| padding: 30rpx; | |||
| border-top: 1rpx solid #f0f0f0; | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,93 @@ | |||
| <template> | |||
| <view class="announcement-detail"> | |||
| <!-- 标题 --> | |||
| <view class="title">{{ introduce.paramDesc }}</view> | |||
| <!-- 时间 --> | |||
| <view class="time">{{ introduce.createTime }}</view> | |||
| <!-- 图片 --> | |||
| <!-- <view class="image-container" v-if="introduce.images && introduce.images.length > 0"> | |||
| <image | |||
| v-for="(image, index) in introduce.images" | |||
| :key="index" | |||
| :src="image" | |||
| class="detail-image" | |||
| mode="aspectFill" | |||
| ></image> | |||
| </view> --> | |||
| <!-- 正文内容 --> | |||
| <view class="content"> | |||
| <!-- <text class="content-text">{{ introduce.content }}</text> --> | |||
| <rich-text :nodes="introduce.paramTextarea"></rich-text> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'organizationIntroduction', | |||
| data() { | |||
| return { | |||
| } | |||
| }, | |||
| computed: { | |||
| introduce() { | |||
| return this.$store.state.configList['config_organization_desc']; | |||
| } | |||
| }, | |||
| methods: { | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| // @import '@/uni.scss'; | |||
| .announcement-detail { | |||
| padding: 40rpx; | |||
| background-color: #fff; | |||
| min-height: 100vh; | |||
| .title { | |||
| font-size: 34rpx; | |||
| font-weight: bold; | |||
| color: #000000; | |||
| line-height: 1.5; | |||
| margin-bottom: 20rpx; | |||
| } | |||
| .time { | |||
| font-size: 24rpx; | |||
| color: #999999; | |||
| margin-bottom: 30rpx; | |||
| } | |||
| .image-container { | |||
| margin-bottom: 30rpx; | |||
| .detail-image { | |||
| width: 100%; | |||
| height: 250rpx; | |||
| border-radius: 16rpx; | |||
| margin-bottom: 20rpx; | |||
| // object-fit: cover; | |||
| &:last-child { | |||
| margin-bottom: 0; | |||
| } | |||
| } | |||
| } | |||
| .content { | |||
| .content-text { | |||
| font-size: 28rpx; | |||
| color: #000000; | |||
| line-height: 1.8; | |||
| text-align: justify; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,330 @@ | |||
| <template> | |||
| <view class="ranking-page"> | |||
| <view class="back-button" @click="goBack"> | |||
| <uv-icon name="arrow-left" color="#ffffff" size="20"></uv-icon> | |||
| </view> | |||
| <!-- 排行榜背景 --> | |||
| <view class="ranking-background"> | |||
| <image src="/subPages/static/rank_bg.png" class="bg-image" mode="aspectFill"></image> | |||
| <!-- 返回按钮 --> | |||
| <!-- 前三名定位布局 --> | |||
| <view class="top-three"> | |||
| <!-- 第二名 --> | |||
| <view class="rank-item rank-second"> | |||
| <image src="/subPages/static/second.png" class="rank-badge"></image> | |||
| <image :src="topThree[1].headImage || '/static/默认头像.png'" class="avatar"></image> | |||
| <view class="name">{{ topThree[1].nickName }}</view> | |||
| <view class="score">{{ topThree[1].score }}积分</view> | |||
| </view> | |||
| <!-- 第一名 --> | |||
| <view class="rank-item rank-first"> | |||
| <image src="/subPages/static/first.png" class="rank-badge"></image> | |||
| <image :src="topThree[0].headImage || '/static/默认头像.png'" class="avatar"></image> | |||
| <view class="name">{{ topThree[0].nickName }}</view> | |||
| <view class="score">{{ topThree[0].score }}积分</view> | |||
| </view> | |||
| <!-- 第三名 --> | |||
| <view class="rank-item rank-third"> | |||
| <image src="/subPages/static/third.png" class="rank-badge"></image> | |||
| <image :src="topThree[2].headImage || '/static/默认头像.png'" class="avatar"></image> | |||
| <view class="name">{{ topThree[2].nickName }}</view> | |||
| <view class="score">{{ topThree[2].score }}积分</view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 我的排名 --> | |||
| <view class="my-ranking" v-if=" myRanking && (myRanking.rank !== null || myRanking.rank !== undefined) "> | |||
| <view class="my-rank-number">{{ myRanking.rank }}</view> | |||
| <view class="my-rank-label">我的排名</view> | |||
| <image :src="myRanking.headImage" class="my-avatar"></image> | |||
| <view class="my-name">{{ myRanking.nickName }}</view> | |||
| <view class="my-score">{{ myRanking.score }}积分</view> | |||
| </view> | |||
| <!-- 排行榜列表 --> | |||
| <view class="ranking-list"> | |||
| <view class="list-item" v-for="(item, index) in rankingList" :key="index"> | |||
| <view class="rank-number">{{ item.rank }}</view> | |||
| <image :src="item.headImage || '/static/默认头像.png'" class="list-avatar"></image> | |||
| <view class="user-info"> | |||
| <view class="user-name">{{ item.nickName }}</view> | |||
| </view> | |||
| <view class="user-score">{{ item.score }}积分</view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import MixinList from '@/mixins/list.js' | |||
| export default { | |||
| mixins: [MixinList], | |||
| name: 'Ranking', | |||
| data() { | |||
| return { | |||
| pageSize: 12, | |||
| mixinListApi: 'score.queryScoreRank', | |||
| mixinListConfig: { | |||
| extraDataPath: 'result.myScore', | |||
| responsePath: 'result.scoreList.records' | |||
| } | |||
| } | |||
| }, | |||
| computed: { | |||
| myRanking() { | |||
| return this.extraData | |||
| }, | |||
| rankingList() { | |||
| return this.list.slice(3) | |||
| }, | |||
| topThree() { | |||
| return this.list.slice(0, 3) | |||
| }, | |||
| }, | |||
| methods: { | |||
| mixinSetParams() { | |||
| const params = { | |||
| pageNo: this.pageNo, | |||
| pageSize: this.pageSize | |||
| } | |||
| if (uni.getStorageSync('token')){ | |||
| params['token'] = uni.getStorageSync('token') | |||
| } | |||
| return params | |||
| }, | |||
| goBack() { | |||
| uni.navigateBack(); | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| // @import '@/uni.scss'; | |||
| .ranking-page { | |||
| min-height: 100vh; | |||
| background-color: #f5f5f5; | |||
| } | |||
| .back-button { | |||
| position: fixed; | |||
| top: 70rpx; | |||
| left: 40rpx; | |||
| width: 70rpx; | |||
| height: 70rpx; | |||
| // background-color: rgba(0, 0, 0, 0.3); | |||
| // border-radius: 50%; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| z-index: 10; | |||
| &:active { | |||
| background-color: rgba(0, 0, 0, 0.5); | |||
| } | |||
| } | |||
| .ranking-background { | |||
| position: relative; | |||
| width: 100%; | |||
| height: 600rpx; | |||
| .bg-image { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| .back-button { | |||
| position: absolute; | |||
| top: 60rpx; | |||
| left: 30rpx; | |||
| width: 60rpx; | |||
| height: 60rpx; | |||
| // background-color: rgba(0, 0, 0, 0.3); | |||
| // border-radius: 50%; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| z-index: 10; | |||
| &:active { | |||
| background-color: rgba(0, 0, 0, 0.5); | |||
| } | |||
| } | |||
| .top-three { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 100%; | |||
| display: flex; | |||
| align-items: flex-end; | |||
| justify-content: center; | |||
| padding-bottom: 80rpx; | |||
| .rank-item { | |||
| position: relative; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| margin: 0 40rpx; | |||
| .rank-badge { | |||
| width: 60rpx; | |||
| height: 60rpx; | |||
| margin-bottom: 10rpx; | |||
| } | |||
| .avatar { | |||
| width: 80rpx; | |||
| height: 80rpx; | |||
| border-radius: 50%; | |||
| margin-bottom: 10rpx; | |||
| border: 4rpx solid #fff; | |||
| } | |||
| .name { | |||
| font-size: 24rpx; | |||
| color: #fff; | |||
| margin-bottom: 5rpx; | |||
| font-weight: bold; | |||
| } | |||
| .score { | |||
| font-size: 20rpx; | |||
| color: #1488DB; | |||
| opacity: 0.9; | |||
| } | |||
| } | |||
| .rank-first { | |||
| transform: translateY(-80rpx); | |||
| .avatar { | |||
| width: 100rpx; | |||
| height: 100rpx; | |||
| } | |||
| .rank-badge { | |||
| width: 80rpx; | |||
| height: 80rpx; | |||
| } | |||
| } | |||
| .rank-second { | |||
| order: -1; | |||
| transform: translateY(-40rpx); | |||
| } | |||
| .rank-third { | |||
| order: 1; | |||
| transform: translateY(-40rpx); | |||
| } | |||
| } | |||
| } | |||
| .my-ranking { | |||
| background: #e1f2ff; | |||
| border-radius: 12rpx; | |||
| margin: 20rpx 30rpx 20rpx 30rpx; | |||
| padding: 30rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| // box-shadow: 0 4rpx 12rpx rgba(20, 136, 219, 0.3); | |||
| position: relative; | |||
| z-index: 5; | |||
| .my-rank-number { | |||
| font-size: 48rpx; | |||
| font-weight: bold; | |||
| color: #000; | |||
| margin-right: 15rpx; | |||
| } | |||
| .my-rank-label { | |||
| font-size: 24rpx; | |||
| color: #1488DB; | |||
| opacity: 0.9; | |||
| margin-right: 20rpx; | |||
| } | |||
| .my-avatar { | |||
| width: 60rpx; | |||
| height: 60rpx; | |||
| border-radius: 50%; | |||
| // border: 3rpx solid #fff; | |||
| margin-right: 15rpx; | |||
| } | |||
| .my-name { | |||
| font-size: 28rpx; | |||
| color: #000; | |||
| font-weight: 500; | |||
| margin-right: 20rpx; | |||
| } | |||
| .my-score { | |||
| font-size: 28rpx; | |||
| color: #1488DB; | |||
| // font-weight: bold; | |||
| } | |||
| } | |||
| .ranking-list { | |||
| padding: 40rpx 30rpx 140rpx; | |||
| background-color: #fff; | |||
| margin-top: -40rpx; | |||
| border-radius: 40rpx 40rpx 0 0; | |||
| .list-item { | |||
| display: flex; | |||
| align-items: center; | |||
| padding: 30rpx 0; | |||
| border-bottom: 1rpx solid #f0f0f0; | |||
| &:last-child { | |||
| border-bottom: none; | |||
| } | |||
| .rank-number { | |||
| width: 60rpx; | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| color: #333; | |||
| text-align: center; | |||
| } | |||
| .list-avatar { | |||
| width: 80rpx; | |||
| height: 80rpx; | |||
| border-radius: 50%; | |||
| margin: 0 30rpx; | |||
| } | |||
| .user-info { | |||
| flex: 1; | |||
| .user-name { | |||
| font-size: 28rpx; | |||
| color: #333; | |||
| font-weight: 500; | |||
| } | |||
| } | |||
| .user-score { | |||
| font-size: 28rpx; | |||
| color: $uni-color-primary; | |||
| // font-weight: bold; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,617 @@ | |||
| <template> | |||
| <view class="volunteer-apply-page"> | |||
| <!-- 头部背景图 --> | |||
| <view class="header-section"> | |||
| <image src="../static/volunteer_bg@2x.png" class="bg-image" mode="aspectFit"></image> | |||
| </view> | |||
| <!-- 提示信息 --> | |||
| <view class="tip-section"> | |||
| <uv-icon name="info-circle-fill" color="#1488DB" size="32"></uv-icon> | |||
| <text class="tip-text">以下内容均为必填项,请根据您的实际情况认真填写!</text> | |||
| </view> | |||
| <!-- 表单内容 --> | |||
| <!-- 如何一键禁用整个表单元素 可不可以设置一个大容器 直接禁止内容的任何点击? --> | |||
| <view class="form-section"> | |||
| <view v-if="status === '0' || status === '1'" class="overlay"></view> | |||
| <uv-form ref="form" :model="formData" :rules="rules" labelPosition="left" labelWidth="120"> | |||
| <!-- 姓名 --> | |||
| <uv-form-item label="姓名" prop="name" borderBottom> | |||
| <uv-input | |||
| v-model="formData.name" | |||
| placeholder="请输入您的姓名" | |||
| border="none" | |||
| clearable | |||
| ></uv-input> | |||
| </uv-form-item> | |||
| <!-- 手机号 --> | |||
| <uv-form-item label="手机号" prop="phone" borderBottom> | |||
| <uv-input | |||
| v-model="formData.phone" | |||
| placeholder="请输入您的手机号" | |||
| border="none" | |||
| clearable | |||
| type="number" | |||
| ></uv-input> | |||
| </uv-form-item> | |||
| <!-- 性别 --> | |||
| <uv-form-item label="性别" prop="sex" borderBottom @click="openGenderPicker"> | |||
| <uv-input | |||
| v-model="formData.sex" | |||
| placeholder="请选择" | |||
| border="none" | |||
| readonly | |||
| suffixIcon="arrow-right" | |||
| ></uv-input> | |||
| </uv-form-item> | |||
| <!-- 所在地区 --> | |||
| <uv-form-item label="所在地区" prop="area" borderBottom @click="openRegionPicker"> | |||
| <uv-input | |||
| v-model="formData.area" | |||
| placeholder="请选择" | |||
| border="none" | |||
| readonly | |||
| suffixIcon="arrow-right" | |||
| ></uv-input> | |||
| </uv-form-item> | |||
| <!-- 详细地址 --> | |||
| <uv-form-item label="详细地址" prop="address" borderBottom> | |||
| <uv-input | |||
| v-model="formData.address" | |||
| placeholder="请输入详细地址" | |||
| border="none" | |||
| clearable | |||
| ></uv-input> | |||
| </uv-form-item> | |||
| <!-- 职业类型 --> | |||
| <uv-form-item label="职业类型" prop="career" borderBottom @click="openProfessionPicker"> | |||
| <uv-input | |||
| v-model="formData.career" | |||
| placeholder="请选择" | |||
| border="none" | |||
| readonly | |||
| suffixIcon="arrow-right" | |||
| ></uv-input> | |||
| </uv-form-item> | |||
| <!-- 最高学历 --> | |||
| <uv-form-item label="最高学历" prop="qualifications" borderBottom @click="openEducationPicker"> | |||
| <uv-input | |||
| v-model="formData.qualifications" | |||
| placeholder="请选择" | |||
| border="none" | |||
| readonly | |||
| suffixIcon="arrow-right" | |||
| ></uv-input> | |||
| </uv-form-item> | |||
| <!-- 技能特长 --> | |||
| <uv-form-item label="技能特长" prop="skill"> | |||
| <uv-textarea | |||
| v-model="formData.skill" | |||
| placeholder="请输入您的技能特长" | |||
| border="none" | |||
| :maxlength="200" | |||
| count | |||
| height="120" | |||
| ></uv-textarea> | |||
| </uv-form-item> | |||
| </uv-form> | |||
| <!-- 紧急联系人信息 --> | |||
| <view class="emergency-section"> | |||
| <view class="section-title">紧急联系人信息</view> | |||
| <uv-form ref="emergencyForm" :model="emergencyData" labelPosition="left" labelWidth="120"> | |||
| <!-- 联系人姓名 --> | |||
| <uv-form-item label="姓名" prop="emergencyName" borderBottom> | |||
| <uv-input | |||
| v-model="emergencyData.emergencyName" | |||
| placeholder="请输入您的紧急联系人姓名" | |||
| border="none" | |||
| clearable | |||
| ></uv-input> | |||
| </uv-form-item> | |||
| <!-- 联系人手机号 --> | |||
| <uv-form-item label="手机号" prop="emergencyPhone" borderBottom> | |||
| <uv-input | |||
| v-model="emergencyData.emergencyPhone" | |||
| placeholder="请输入您的紧急联系人手机号" | |||
| border="none" | |||
| clearable | |||
| type="number" | |||
| ></uv-input> | |||
| </uv-form-item> | |||
| </uv-form> | |||
| </view> | |||
| </view> | |||
| <!-- 提交按钮 --> | |||
| <view class="submit-section"> | |||
| <!-- 圆角的 --> | |||
| <!-- 如果还在审核中 按钮无法点击 --> | |||
| <uv-button | |||
| v-if="status !== '1'" | |||
| type="primary" | |||
| shape="circle" | |||
| size="large" | |||
| :loading="submitting" | |||
| :disabled="status === '0'" | |||
| @click="submitApplication" | |||
| > | |||
| {{statusTips}} | |||
| </uv-button> | |||
| </view> | |||
| <!-- 性别选择器 --> | |||
| <uv-picker | |||
| ref="genderPicker" | |||
| :columns="genderOptions" | |||
| @confirm="onGenderConfirm" | |||
| ></uv-picker> | |||
| <!-- 地区选择器 --> | |||
| <uv-picker | |||
| ref="regionPicker" | |||
| :columns="addressList" | |||
| @confirm="onAddressConfirm" | |||
| @change="onAddressChange" | |||
| keyName="name" | |||
| ></uv-picker> | |||
| <!-- 职业选择器 --> | |||
| <uv-picker | |||
| ref="professionPicker" | |||
| :columns="professionOptions" | |||
| @confirm="onProfessionConfirm" | |||
| ></uv-picker> | |||
| <!-- 学历选择器 --> | |||
| <uv-picker | |||
| ref="educationPicker" | |||
| :columns="educationOptions" | |||
| @confirm="onEducationConfirm" | |||
| ></uv-picker> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import chinaRegions from '@/static/china-regions.json' | |||
| export default { | |||
| name: 'VolunteerApply', | |||
| data() { | |||
| return { | |||
| submitting: false, | |||
| formData: { | |||
| name: '', | |||
| phone: '', | |||
| sex: '', | |||
| // sexText: '', | |||
| area: '', | |||
| address: '', | |||
| career: '', | |||
| // careerText: '', | |||
| qualifications: '', | |||
| // qualificationsText: '', | |||
| skill: '' | |||
| }, | |||
| emergencyData: { | |||
| emergencyName: '', | |||
| emergencyPhone: '' | |||
| }, | |||
| rules: { | |||
| name: [{ required: true, message: '请输入姓名', trigger: 'blur' }], | |||
| phone: [ | |||
| { required: true, message: '请输入手机号', trigger: 'blur' }, | |||
| { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' } | |||
| ], | |||
| sex: [{ required: true, message: '请选择性别', trigger: 'change' }], | |||
| area: [{ required: true, message: '请选择所在地区', trigger: 'change' }], | |||
| address: [{ required: true, message: '请输入详细地址', trigger: 'blur' }], | |||
| career: [{ required: true, message: '请选择职业类型', trigger: 'change' }], | |||
| qualifications: [{ required: true, message: '请选择最高学历', trigger: 'change' }] | |||
| }, | |||
| // 页面的志愿者状态 0-审核中 1-已通过 2-未通过 3-未申请 | |||
| status: '3', | |||
| genderOptions: [['男', '女']], | |||
| provinces: [], //省 | |||
| citys: [], //市 | |||
| areas: [], //区 | |||
| pickerValue: [0, 0, 0], | |||
| defaultValue: [3442, 1, 2], | |||
| } | |||
| }, | |||
| computed: { | |||
| addressList() { | |||
| return [this.provinces, this.citys, this.areas]; | |||
| }, | |||
| professionOptions() { | |||
| return [this.$store.state.careerList.map(item => item.title)] | |||
| }, | |||
| educationOptions() { | |||
| return [this.$store.state.qualificationList.map(item => item.title)] | |||
| }, | |||
| // 不同审核状态对应的提示语句 | |||
| statusTips() { | |||
| if (this.status === '0') { | |||
| return '您的申请正在审核中,请耐心等待。' | |||
| } else if (this.status === '1') { | |||
| return '恭喜您,志愿者申请已通过!' | |||
| } else if (this.status === '2') { | |||
| return '申请未通过,重新提交申请' | |||
| } else if (this.status === '3') { | |||
| return '申请志愿者' | |||
| } | |||
| } | |||
| }, | |||
| async onLoad(options) { | |||
| // 如果是编辑模式,加载已有数据 | |||
| if (options.edit && options.data) { | |||
| this.loadExistingData(JSON.parse(decodeURIComponent(options.data))); | |||
| } | |||
| // 查询志愿者信息情况 | |||
| const {result:info} = await this.$api.home.queryVolunteer() | |||
| // 审核状态为0或者1时 禁用整个表单元素 | |||
| // 审核状态为2时 只禁用提交按钮 | |||
| // 审核状态为3时 不禁用 | |||
| if (info !== {} && info.status !== null && info.status !== undefined ){ | |||
| // 如果有值了 说明申请过志愿者 | |||
| this.status = info.status | |||
| // 回显数据 | |||
| this.formData = { | |||
| name: info.name, | |||
| phone: info.phone, | |||
| sex: info.sex, | |||
| // sexText: info.sex, | |||
| area: info.area, | |||
| address: info.address, | |||
| career: info.careerName, | |||
| // careerText: info.career, | |||
| qualifications: info.qualificationsName, | |||
| // qualificationsText: info.qualifications, | |||
| skill: info.skill | |||
| } | |||
| // 回显紧急联系人 | |||
| this.emergencyData = { | |||
| emergencyName: info.emergencyName, | |||
| emergencyPhone: info.emergencyPhone | |||
| } | |||
| }else{ | |||
| this.status = '3' | |||
| } | |||
| }, | |||
| created() { | |||
| this.getAddressData() | |||
| // 调试store数据 | |||
| // console.log('Store careerList:', this.$store.state.careerList) | |||
| // console.log('Store qualificationList:', this.$store.state.qualificationList) | |||
| }, | |||
| methods: { | |||
| // 加载已有数据(编辑模式) | |||
| loadExistingData(data) { | |||
| this.formData = { | |||
| name: data.name || '李双欢', | |||
| phone: data.phone || '15478451233', | |||
| sex: data.sex || '男', | |||
| // sexText: data.sex || '男', | |||
| area: data.area || '湖南省长沙市区', | |||
| areaText: data.area || '湖南省长沙市区', | |||
| address: data.address || '阳光小区45栋二单元1203', | |||
| career: data.career || '专业技术人员', | |||
| // careerText: data.career || '专业技术人员', | |||
| qualifications: data.qualifications || '本科', | |||
| // qualificationsText: data.qualifications || '本科', | |||
| skill: data.skill || '计算机、跑步' | |||
| }; | |||
| this.emergencyData = { | |||
| emergencyName: data.emergencyName || '李四', | |||
| emergencyPhone: data.emergencyPhone || '14563236320' | |||
| }; | |||
| }, | |||
| // 初始化地区数据 | |||
| getAddressData() { | |||
| console.log('开始加载地区数据'); | |||
| try { | |||
| // 直接使用导入的地区数据(已简化,只包含name字段) | |||
| this.provinces = chinaRegions; | |||
| // console.log('成功加载地区数据,省份数量:', this.provinces.length); | |||
| this.handlePickValueDefault(); | |||
| // uni.showToast({ | |||
| // title: '地区数据加载成功', | |||
| // icon: 'success' | |||
| // }); | |||
| } catch (error) { | |||
| // console.error('加载地区数据失败:', error); | |||
| // uni.showToast({ | |||
| // title: '地区数据加载失败', | |||
| // icon: 'error' | |||
| // }); | |||
| } | |||
| }, | |||
| handlePickValueDefault() { | |||
| if (this.provinces.length > 0) { | |||
| // 设置省(默认选择第一个省份) | |||
| this.pickerValue[0] = 0; | |||
| // 设置市(默认选择第一个市) | |||
| this.citys = this.provinces[0]?.children || []; | |||
| this.pickerValue[1] = 0; | |||
| // 设置区(默认选择第一个区) | |||
| this.areas = this.citys[0]?.children || []; | |||
| this.pickerValue[2] = 0; | |||
| console.log('初始化地区数据:', { | |||
| provinces: this.provinces.length, | |||
| citys: this.citys.length, | |||
| areas: this.areas.length | |||
| }); | |||
| } | |||
| }, | |||
| // 打开性别选择器 | |||
| openGenderPicker() { | |||
| // console.log('我点击了性别选择去'); | |||
| this.$refs.genderPicker.open(); | |||
| }, | |||
| // 打开地区选择器 | |||
| openRegionPicker() { | |||
| this.$refs.regionPicker.open(); | |||
| }, | |||
| // 打开职业选择器 | |||
| openProfessionPicker() { | |||
| this.$refs.professionPicker.open(); | |||
| }, | |||
| // 打开学历选择器 | |||
| openEducationPicker() { | |||
| this.$refs.educationPicker.open(); | |||
| }, | |||
| // 性别选择确认 | |||
| onGenderConfirm(value) { | |||
| this.formData.sex = value.value[0]; | |||
| }, | |||
| // 地区选择变化(三级联动) | |||
| onAddressChange(e) { | |||
| console.log('地区选择变化:', e); | |||
| const { columnIndex, index, value } = e; | |||
| if (columnIndex === 0) { | |||
| // 选择省份时,更新市级数据 | |||
| this.citys = this.provinces[index]?.children || []; | |||
| this.areas = this.citys[0]?.children || []; | |||
| this.pickerValue = [index, 0, 0]; | |||
| } else if (columnIndex === 1) { | |||
| // 选择市时,更新区级数据 | |||
| this.areas = this.citys[index]?.children || []; | |||
| this.pickerValue[1] = index; | |||
| this.pickerValue[2] = 0; | |||
| } else if (columnIndex === 2) { | |||
| // 选择区 | |||
| this.pickerValue[2] = index; | |||
| } | |||
| }, | |||
| // 地区选择确认 | |||
| onAddressConfirm(e) { | |||
| console.log('确认选择的地区:', e); | |||
| if (e.value && e.value.length >= 3) { | |||
| const selectedArea = `${e.value[0].name}/${e.value[1].name}/${e.value[2].name}`; | |||
| this.formData.area = selectedArea; // 给area字段赋值用于表单验证 | |||
| uni.showToast({ | |||
| icon: 'success', | |||
| title: '地区选择成功' | |||
| }); | |||
| } else { | |||
| uni.showToast({ | |||
| icon: 'none', | |||
| title: '请选择完整的省市区信息' | |||
| }); | |||
| } | |||
| }, | |||
| // 职业选择确认 | |||
| onProfessionConfirm(value) { | |||
| console.log('职业选择确认:', value.value[0]); | |||
| this.formData.career = value.value[0]; | |||
| }, | |||
| // 学历选择确认 | |||
| onEducationConfirm(value) { | |||
| console.log('学历选择确认:', value.value[0]); | |||
| this.formData.qualifications = value.value[0]; | |||
| }, | |||
| // 提交申请 | |||
| async submitApplication() { | |||
| try { | |||
| // 验证主表单 | |||
| const valid = await this.$refs.form.validate(); | |||
| if (!valid) return; | |||
| // 验证紧急联系人信息 | |||
| if (!this.emergencyData.emergencyName || !this.emergencyData.emergencyPhone) { | |||
| uni.showToast({ | |||
| title: '请填写紧急联系人信息', | |||
| icon: 'none' | |||
| }); | |||
| return; | |||
| } | |||
| // 电话格式的校验 | |||
| if (!this.$utils.checkPhone(this.formData.phone)) { | |||
| uni.showToast({ | |||
| title: '请输入正确的电话号码', | |||
| icon: 'none' | |||
| }); | |||
| return; | |||
| } | |||
| // 电话格式的校验 | |||
| if (!this.$utils.checkPhone(this.emergencyData.emergencyPhone)) { | |||
| uni.showToast({ | |||
| title: '请输入正确的紧急联系人电话', | |||
| icon: 'none' | |||
| }); | |||
| return; | |||
| } | |||
| this.submitting = true; | |||
| // 模拟提交 | |||
| await this.submitVolunteerApplication(); | |||
| } catch (error) { | |||
| console.error('提交失败:', error); | |||
| uni.showToast({ | |||
| title: '提交失败,请重试', | |||
| icon: 'none' | |||
| }); | |||
| } finally { | |||
| this.submitting = false; | |||
| } | |||
| }, | |||
| // 提交志愿者申请API | |||
| async submitVolunteerApplication() { | |||
| // 要求通过title获取id | |||
| const career = this.$store.state.careerList.find(item => item.title === this.formData.career).id | |||
| const qualifications = this.$store.state.qualificationList.find(item => item.title === this.formData.qualifications).id | |||
| const res = await this.$api.home.applyVolunteer({...this.formData, ...this.emergencyData, career, qualifications}) | |||
| if (res.code === 200){ | |||
| uni.showToast({ | |||
| title: `${res.message}`, | |||
| icon: 'none' | |||
| }) | |||
| setTimeout(() => { | |||
| uni.navigateBack(); | |||
| }, 1000); | |||
| }else { | |||
| uni.showToast({ | |||
| title: res.msg, | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| // @import '@/uni.scss'; | |||
| .overlay { | |||
| position: absolute; | |||
| inset: 0; | |||
| width: 100%; | |||
| height: 100%; | |||
| background-color: rgba(255, 255, 255, 0.1); | |||
| z-index: 10; | |||
| } | |||
| .volunteer-apply-page { | |||
| min-height: 100vh; | |||
| background-color: #f5f5f5; | |||
| } | |||
| .header-section { | |||
| position: relative; | |||
| width: 96%; | |||
| height: 290rpx; | |||
| margin: 25rpx auto; | |||
| .bg-image { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| } | |||
| .tip-section { | |||
| display: flex; | |||
| align-items: center; | |||
| padding: 30rpx; | |||
| background-color: #fff; | |||
| margin: 20rpx 30rpx; | |||
| border-radius: 16rpx; | |||
| box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); | |||
| .tip-text { | |||
| margin-left: 16rpx; | |||
| font-size: 28rpx; | |||
| color: #666; | |||
| line-height: 1.5; | |||
| } | |||
| } | |||
| .form-section { | |||
| background-color: #fff; | |||
| margin: 20rpx 30rpx; | |||
| border-radius: 16rpx; | |||
| padding: 40rpx 30rpx; | |||
| box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); | |||
| position: relative; | |||
| } | |||
| .emergency-section { | |||
| margin-top: 40rpx; | |||
| padding-top: 40rpx; | |||
| border-top: 1rpx solid #f0f0f0; | |||
| .section-title { | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| color: #333; | |||
| margin-bottom: 30rpx; | |||
| } | |||
| } | |||
| .submit-section { | |||
| padding: 40rpx 30rpx; | |||
| padding-bottom: 60rpx; | |||
| } | |||
| // 覆盖uvUI样式 | |||
| :deep(.uv-form-item__body__right__content__input) { | |||
| font-size: 28rpx !important; | |||
| } | |||
| :deep(.uv-form-item__body__left__text) { | |||
| font-size: 28rpx !important; | |||
| color: #333 !important; | |||
| } | |||
| :deep(.uv-input__content__field-wrapper__field) { | |||
| font-size: 28rpx !important; | |||
| } | |||
| :deep(.uv-textarea__content__field) { | |||
| font-size: 28rpx !important; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,291 @@ | |||
| <template> | |||
| <view class="login-container"> | |||
| <!-- 背景图 --> | |||
| <image class="bg-image" :src="configParamImage('config_login_bg')" mode="aspectFill"></image> | |||
| <!-- 内容区域 --> | |||
| <view class="content"> | |||
| <!-- 标题图片 --> | |||
| <view class="title-section"> | |||
| <!-- <image class="title-image" src="/subPages/static/登录_标题.png" mode="widthFix"></image> --> | |||
| <view class="login-title">{{ configParamText('config_login_title') }}</view> | |||
| </view> | |||
| <!-- 按钮区域 --> | |||
| <view class="button-section"> | |||
| <!-- 授权手机号登录按钮 --> | |||
| <view class="login-btn primary" @click="phoneLogin"> | |||
| <text class="btn-text">授权手机号登录</text> | |||
| </view> | |||
| <!-- 取消登录按钮 --> | |||
| <view class="login-btn secondary" @click="cancelLogin"> | |||
| <text class="btn-text">取消登录</text> | |||
| </view> | |||
| <!-- 协议文字 --> | |||
| <view class="agreement-text-container"> | |||
| <view class="agreement-checkbox-row"> | |||
| <view class="custom-checkbox" @click="toggleAgreement"> | |||
| <uv-icon | |||
| v-if="!isAgreed" | |||
| name="checkmark-circle" | |||
| size="20" | |||
| color="#cccccc"> | |||
| </uv-icon> | |||
| <uv-icon | |||
| v-else | |||
| name="checkmark-circle-fill" | |||
| size="20" | |||
| color="#1488DB"> | |||
| </uv-icon> | |||
| </view> | |||
| <view class="agreement-text-content"> | |||
| <text class="agreement-text">阅读并同意我们的 </text> | |||
| <text class="agreement-link" @click="showPolicy">《服务协议与隐私条款》</text> | |||
| <text class="agreement-text"> 以及 </text> | |||
| <text class="agreement-link" @click="showPrivacy">《个人信息保护指引》</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <uv-modal ref="modalPolicy" title="服务协议与隐私条款"> | |||
| <view class="slot-content"> | |||
| <rich-text :nodes="configParamTextarea('config_login_policy')"></rich-text> | |||
| </view> | |||
| </uv-modal> | |||
| <uv-modal ref="modalPrivacy" title="个人信息保护指引"> | |||
| <view class="slot-content"> | |||
| <rich-text :nodes="configParamTextarea('config_login_privacy')"></rich-text> | |||
| </view> | |||
| </uv-modal> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'Login', | |||
| data() { | |||
| return { | |||
| isAgreed: false | |||
| } | |||
| }, | |||
| methods: { | |||
| // 切换协议同意状态 | |||
| toggleAgreement() { | |||
| this.isAgreed = !this.isAgreed; | |||
| console.log('协议同意状态:', this.isAgreed); | |||
| }, | |||
| // 手机号授权登录 | |||
| phoneLogin() { | |||
| if (!this.isAgreed) { | |||
| uni.showToast({ | |||
| title: '请先同意协议条款', | |||
| icon: 'none' | |||
| }); | |||
| return; | |||
| } | |||
| uni.login({ | |||
| provider: 'weixin', | |||
| success: async (loginRes) => { | |||
| const res = await this.$api.login.login({ | |||
| code: loginRes.code | |||
| }) | |||
| uni.setStorageSync('token', res.result.token) | |||
| const userInfo = res.result.userInfo | |||
| if (!userInfo.headImage || !userInfo.nickName || !userInfo.phone) { | |||
| uni.showToast({ | |||
| title: '请先完善个人信息', | |||
| icon: 'none' | |||
| }) | |||
| setTimeout(() => { | |||
| uni.navigateTo({ | |||
| url: '/subPages/login/userInfo' | |||
| }) | |||
| }, 500) | |||
| }else { | |||
| uni.showToast({ | |||
| title: '登录成功', | |||
| icon: 'success' | |||
| }) | |||
| setTimeout(() => { | |||
| uni.switchTab({ | |||
| url: '/pages/index/index' | |||
| }) | |||
| }, 500) | |||
| } | |||
| }, | |||
| fail: (error) => { | |||
| uni.showToast({ | |||
| title: `${error.errMsg}`, | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| }) | |||
| }, | |||
| // 取消登录 | |||
| cancelLogin() { | |||
| console.log('取消登录'); | |||
| // 重定向到首页 | |||
| uni.switchTab({ | |||
| url: '/pages/index/index' | |||
| }) | |||
| }, | |||
| // 显示服务协议 | |||
| showPolicy() { | |||
| this.$refs.modalPolicy.open() | |||
| }, | |||
| // 显示隐私条款 | |||
| showPrivacy() { | |||
| this.$refs.modalPrivacy.open() | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .login-container { | |||
| position: relative; | |||
| width: 100vw; | |||
| height: 100vh; | |||
| overflow: hidden; | |||
| } | |||
| .bg-image { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 40%; | |||
| z-index: 1; | |||
| } | |||
| .content { | |||
| position: relative; | |||
| z-index: 2; | |||
| height: 100%; | |||
| display: flex; | |||
| flex-direction: column; | |||
| padding: 0 40rpx; | |||
| } | |||
| .title-section { | |||
| flex: 1; | |||
| display: flex; | |||
| align-items: flex-end; | |||
| justify-content: center; | |||
| padding: 120rpx; | |||
| .login-title{ | |||
| color: #1488db; | |||
| font-size: 48rpx; | |||
| font-weight: bold; | |||
| position: relative; | |||
| } | |||
| .login-title::after{ | |||
| content: ''; | |||
| position: absolute; | |||
| left: 20rpx; | |||
| bottom: -8rpx; | |||
| width: 100%; | |||
| height: 24rpx; | |||
| border-radius: 50%; | |||
| background: linear-gradient(to bottom, #0085e4, transparent); | |||
| } | |||
| } | |||
| // .welcome-section { | |||
| // display: flex; | |||
| // justify-content: center; | |||
| // margin-bottom: 100rpx; | |||
| // .welcome-box { | |||
| // border: 2rpx dashed #1488DB; | |||
| // border-radius: 10rpx; | |||
| // padding: 20rpx 40rpx; | |||
| // background: rgba(255, 255, 255, 0.9); | |||
| // .welcome-text { | |||
| // font-size: 28rpx; | |||
| // color: #1488DB; | |||
| // font-weight: 500; | |||
| // } | |||
| // } | |||
| // } | |||
| .button-section { | |||
| flex: 1; | |||
| margin-bottom: 60rpx; | |||
| align-items: flex-start; | |||
| .login-btn { | |||
| width: 100%; | |||
| height: 88rpx; | |||
| border-radius: 44rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| margin-bottom: 30rpx; | |||
| &.primary { | |||
| background: #1488DB; | |||
| .btn-text { | |||
| color: #ffffff; | |||
| font-size: 32rpx; | |||
| font-weight: 500; | |||
| } | |||
| } | |||
| &.secondary { | |||
| background: rgba(255, 255, 255, 0.9); | |||
| border: 2rpx solid #cccccc; | |||
| .btn-text { | |||
| color: #666666; | |||
| font-size: 32rpx; | |||
| } | |||
| } | |||
| } | |||
| .agreement-text-container { | |||
| margin-top: 40rpx; | |||
| .agreement-checkbox-row { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| .custom-checkbox { | |||
| margin-right: 10rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| } | |||
| .agreement-text-content { | |||
| flex: 1; | |||
| text-align: left; | |||
| .agreement-text { | |||
| font-size: 24rpx; | |||
| color: #666666; | |||
| } | |||
| .agreement-link { | |||
| font-size: 24rpx; | |||
| color: #1488DB; | |||
| text-decoration: underline; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,416 @@ | |||
| <template> | |||
| <view class="user-info-container"> | |||
| <!-- 内容区域 --> | |||
| <view class="content"> | |||
| <!-- 头像区域 --> | |||
| <view class="avatar-section"> | |||
| <view class="app-info"> | |||
| <image | |||
| class="app-logo" | |||
| :src="configParamImage('config_logo')" | |||
| mode="aspectFit" | |||
| ></image> | |||
| <text class="app-name">{{ configParamText('config_app_name') }}</text> | |||
| </view> | |||
| </view> | |||
| <!-- 表单区域 --> | |||
| <view class="form-section"> | |||
| <!-- 头像 --> | |||
| <!-- 在模板中修改头像区域 --> | |||
| <view class="form-item"> | |||
| <text class="form-label">头像</text> | |||
| <view class="avatar-upload"> | |||
| <button | |||
| class="avatar-button" | |||
| open-type="chooseAvatar" | |||
| @chooseavatar="onChooseAvatar" | |||
| > | |||
| <image | |||
| class="avatar-image" | |||
| :src="userInfo.headImage || '/static/待上传头像.png'" | |||
| mode="aspectFill" | |||
| ></image> | |||
| </button> | |||
| </view> | |||
| </view> | |||
| <!-- 昵称 --> | |||
| <view class="form-item"> | |||
| <text class="form-label">昵称</text> | |||
| <input | |||
| class="form-input" | |||
| v-model="userInfo.nickName" | |||
| placeholder="请输入昵称" | |||
| type="nickname" | |||
| @blur="onNicknameBlur" | |||
| /> | |||
| </view> | |||
| <!-- 手机号 --> | |||
| <view class="form-item"> | |||
| <text class="form-label">手机号</text> | |||
| <view class="phone-input-container"> | |||
| <input | |||
| class="form-input phone-input" | |||
| v-model="userInfo.phone" | |||
| placeholder="请输入手机号" | |||
| type="number" | |||
| maxlength="11" | |||
| /> | |||
| <button | |||
| class="get-phone-btn" | |||
| open-type="getPhoneNumber" | |||
| @getphonenumber="getPhoneNumber" | |||
| > | |||
| <text class="btn-text">获取手机号</text> | |||
| </button> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 确定按钮 --> | |||
| <view class="submit-section"> | |||
| <view class="submit-btn" @click="submitUserInfo"> | |||
| <text class="submit-text">确定</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'UserInfo', | |||
| data() { | |||
| return { | |||
| userInfo: { | |||
| headImage: '', | |||
| nickName: '', | |||
| phone: '' | |||
| } | |||
| } | |||
| }, | |||
| onLoad() { | |||
| // 获取微信用户信息 | |||
| this.getWechatUserInfo(); | |||
| }, | |||
| methods: { | |||
| // 获取微信用户信息 | |||
| async getWechatUserInfo() { | |||
| const { result } = await this.$api.user.queryUser() | |||
| this.userInfo.nickName = result.nickName | |||
| this.userInfo.headImage = result.headImage | |||
| this.userInfo.phone = result.phone | |||
| }, | |||
| // 提交表单 | |||
| // 选择头像并上传到OSS | |||
| async onChooseAvatar(e) { | |||
| console.log('选择头像回调', e); | |||
| if (e.detail.avatarUrl) { | |||
| try { | |||
| // 显示上传中提示 | |||
| uni.showLoading({ title: '上传头像中...' }); | |||
| // 构造文件对象 | |||
| const file = { | |||
| path: e.detail.avatarUrl, | |||
| tempFilePath: e.detail.avatarUrl | |||
| }; | |||
| // 上传到OSS | |||
| const uploadResult = await this.$utils.uploadImage(file); | |||
| uni.hideLoading(); | |||
| if (uploadResult.success) { | |||
| // 上传成功,更新头像URL | |||
| this.userInfo.headImage = uploadResult.url; | |||
| console.log('头像上传成功', uploadResult.url); | |||
| uni.showToast({ | |||
| title: '头像上传成功', | |||
| icon: 'success' | |||
| }); | |||
| } else { | |||
| // 上传失败,使用本地头像 | |||
| // this.userInfo.headImage = e.detail.avatarUrl; | |||
| uni.showToast({ | |||
| title: '头像上传失败!请稍后重试!', | |||
| icon: 'none' | |||
| }); | |||
| } | |||
| } catch (error) { | |||
| uni.hideLoading(); | |||
| console.error('头像上传异常:', error); | |||
| // 异常情况下使用本地头像 | |||
| this.userInfo.headImage = e.detail.avatarUrl; | |||
| uni.showToast({ | |||
| title: '头像处理异常,使用本地头像', | |||
| icon: 'none' | |||
| }); | |||
| } | |||
| } else { | |||
| uni.showToast({ | |||
| title: '头像选择失败', | |||
| icon: 'none' | |||
| }); | |||
| } | |||
| }, | |||
| // 昵称输入失焦 | |||
| onNicknameBlur() { | |||
| if (!this.userInfo.nickname.trim()) { | |||
| uni.showToast({ | |||
| title: '请输入昵称', | |||
| icon: 'none' | |||
| }); | |||
| } | |||
| }, | |||
| // 获取手机号 | |||
| async getPhoneNumber(e) { | |||
| console.log('获取手机号回调', e); | |||
| if (e.detail.errMsg === 'getPhoneNumber:ok') { | |||
| // 获取成功,可以通过e.detail.code发送到后端换取手机号 | |||
| console.log('获取手机号成功', e.detail); | |||
| const res = await this.$api.login.bindPhone({ | |||
| phoneCode: e.detail.code | |||
| }) | |||
| const str = JSON.parse(res.result); | |||
| this.userInfo.phone = str.phone_info.phoneNumber | |||
| uni.showToast({ | |||
| title: '手机号获取成功', | |||
| icon: 'success' | |||
| }); | |||
| // 这里需要将e.detail.code发送到后端解密获取真实手机号 | |||
| // 暂时模拟设置手机号 | |||
| // this.userInfo.phone = '138****8888'; | |||
| } else { | |||
| // 如果失败了就申请开启权限 | |||
| uni.showToast({ | |||
| title: '手机号获取失败', | |||
| icon: 'error' | |||
| }) | |||
| } | |||
| }, | |||
| // 提交用户信息 | |||
| async submitUserInfo() { | |||
| if (!this.userInfo.nickName.trim()) { | |||
| uni.showToast({ | |||
| title: '请输入昵称', | |||
| icon: 'none' | |||
| }); | |||
| return; | |||
| } | |||
| if (!this.userInfo.phone.trim()) { | |||
| uni.showToast({ | |||
| title: '请输入手机号', | |||
| icon: 'none' | |||
| }); | |||
| return; | |||
| } | |||
| if (!/^1[3-9]\d{9}$/.test(this.userInfo.phone)) { | |||
| uni.showToast({ | |||
| title: '请输入正确的手机号', | |||
| icon: 'none' | |||
| }); | |||
| return; | |||
| } | |||
| console.log('提交用户信息', this.userInfo); | |||
| // 提交用户信息 | |||
| await this.$api.user.updateUser({ | |||
| nickName: this.userInfo.nickName, | |||
| phone: this.userInfo.phone, | |||
| headImage: this.userInfo.headImage, | |||
| address: '' | |||
| }) | |||
| // 这里可以调用API保存用户信息 | |||
| uni.showToast({ | |||
| title: '信息保存成功', | |||
| icon: 'success' | |||
| }); | |||
| // 跳转到首页或其他页面 | |||
| setTimeout(() => { | |||
| uni.switchTab({ | |||
| url: '/pages/index/index' | |||
| }); | |||
| }, 1000); | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .user-info-container { | |||
| min-height: 100vh; | |||
| background-color: #f5f5f5; | |||
| } | |||
| .custom-navbar { | |||
| position: fixed; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| z-index: 1000; | |||
| background-color: #1488DB; | |||
| .navbar-content { | |||
| height: 88rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| padding-top: var(--status-bar-height, 44rpx); | |||
| .navbar-title { | |||
| font-size: 36rpx; | |||
| font-weight: 500; | |||
| color: #ffffff; | |||
| } | |||
| } | |||
| } | |||
| .content { | |||
| padding-top: calc(88rpx + var(--status-bar-height, 44rpx)); | |||
| padding: calc(88rpx + var(--status-bar-height, 44rpx)) 40rpx 40rpx; | |||
| } | |||
| .avatar-section { | |||
| display: flex; | |||
| justify-content: center; | |||
| margin-bottom: 80rpx; | |||
| .app-info { | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| .app-logo { | |||
| width: 160rpx; | |||
| height: 160rpx; | |||
| border-radius: 20rpx; | |||
| border: 4rpx dashed #cccccc; | |||
| margin-bottom: 20rpx; | |||
| } | |||
| .app-name { | |||
| font-size: 32rpx; | |||
| font-weight: 500; | |||
| color: #333333; | |||
| } | |||
| } | |||
| } | |||
| .form-section { | |||
| background-color: #ffffff; | |||
| border-radius: 20rpx; | |||
| padding: 40rpx; | |||
| margin-bottom: 60rpx; | |||
| } | |||
| .form-item { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 40rpx; | |||
| &:last-child { | |||
| margin-bottom: 0; | |||
| } | |||
| .form-label { | |||
| width: 120rpx; | |||
| font-size: 32rpx; | |||
| color: #333333; | |||
| font-weight: 500; | |||
| } | |||
| .form-input { | |||
| flex: 3; | |||
| height: 80rpx; | |||
| padding: 0 20rpx; | |||
| // border: 2rpx solid #e0e0e0; | |||
| border-radius: 10rpx; | |||
| font-size: 30rpx; | |||
| color: #333333; | |||
| &.phone-input { | |||
| margin-right: 20rpx; | |||
| } | |||
| } | |||
| .avatar-upload { | |||
| .avatar-image { | |||
| width: 120rpx; | |||
| height: 120rpx; | |||
| border-radius: 10rpx; | |||
| border: 2rpx dashed #cccccc; | |||
| } | |||
| } | |||
| .phone-input-container { | |||
| flex: 1; | |||
| display: flex; | |||
| align-items: center; | |||
| .get-phone-btn { | |||
| flex: 2; | |||
| // padding: 0rpx 0rpx; | |||
| background-color: #1488DB; | |||
| border-radius: 40rpx; | |||
| border: none; | |||
| outline: none; | |||
| &::after { | |||
| border: none; | |||
| } | |||
| .btn-text { | |||
| font-size: 26rpx; | |||
| color: #ffffff; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .submit-section { | |||
| padding: 0 40rpx; | |||
| .submit-btn { | |||
| width: 100%; | |||
| height: 88rpx; | |||
| background-color: #1488DB; | |||
| border-radius: 44rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| .submit-text { | |||
| font-size: 32rpx; | |||
| font-weight: 500; | |||
| color: #ffffff; | |||
| } | |||
| } | |||
| } | |||
| // 添加按钮样式 | |||
| .avatar-button { | |||
| padding: 0; | |||
| margin: 0; | |||
| background: transparent; | |||
| border: none; | |||
| outline: none; | |||
| &::after { | |||
| border: none; | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,163 @@ | |||
| <template> | |||
| <view class="page"> | |||
| <!-- 导航栏 --> | |||
| <!-- 签到列表 --> | |||
| <view class="content"> | |||
| <view v-if="list.length > 0" class="list"> | |||
| <view v-for="item in list" :key="item.id" class="activity-item" @click="checkinActivity(item)"> | |||
| <image class="activity-image" :src="item.image" mode="aspectFill"></image> | |||
| <view class="activity-info"> | |||
| <view class="title-row"> | |||
| <view class="activity-badge"> | |||
| <text class="badge-text">{{ item.score }}分</text> | |||
| </view> | |||
| <text class="activity-title">{{ item.title }}</text> | |||
| </view> | |||
| <view class="activity-location"> | |||
| <uv-icon name="map-fill" size="14" color="#999"></uv-icon> | |||
| <text class="location-text">{{ item.address }}</text> | |||
| </view> | |||
| <view class="activity-time"> | |||
| <uv-icon name="calendar" size="14" color="#999"></uv-icon> | |||
| <text class="time-text">{{ item.activityTime }}</text> | |||
| </view> | |||
| <view class="activity-participants"> | |||
| <uv-icon name="account-fill" size="14" color="#999"></uv-icon> | |||
| <text class="participants-text">{{ item.numActivity }}人已报名</text> | |||
| </view> | |||
| </view> | |||
| <view class="activity-action"> | |||
| <uv-button | |||
| type="primary" | |||
| size="mini" | |||
| text="签到码" | |||
| ></uv-button> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <view v-else class="empty"> | |||
| <uv-empty mode="data" text="暂无可签到活动"></uv-empty> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import MixinList from '@/mixins/list' | |||
| export default { | |||
| mixins: [MixinList], | |||
| data() { | |||
| return { | |||
| list: [], | |||
| mixinListApi: 'activity.queryActivityList', | |||
| } | |||
| }, | |||
| methods: { | |||
| // 跳转到签到码界面 | |||
| checkinActivity(item) { | |||
| uni.navigateTo({ | |||
| url: `/subPages/my/checkinCode?id=${item.id}` | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .page { | |||
| background-color: #f5f5f5; | |||
| min-height: 100vh; | |||
| } | |||
| .content { | |||
| padding: 20rpx; | |||
| } | |||
| .list { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 20rpx; | |||
| } | |||
| .activity-item { | |||
| display: flex; | |||
| margin-bottom: 30rpx; | |||
| background: #fff; | |||
| border-radius: 12rpx; | |||
| padding: 20rpx; | |||
| box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); | |||
| } | |||
| .activity-image { | |||
| width: 180rpx; | |||
| height: 180rpx; | |||
| border-radius: 8rpx; | |||
| margin-right: 20rpx; | |||
| } | |||
| .activity-info { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| } | |||
| .title-row { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 10rpx; | |||
| } | |||
| .activity-badge { | |||
| width: 62rpx; | |||
| height: 40rpx; | |||
| background: #218cdd; | |||
| border-radius: 7rpx; | |||
| margin-right: 14rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| .badge-text { | |||
| font-size: 18rpx; | |||
| color: #fff; | |||
| } | |||
| .activity-title { | |||
| font-size: 28rpx; | |||
| font-weight: bold; | |||
| color: #333; | |||
| line-height: 1.4; | |||
| display: -webkit-box; | |||
| -webkit-box-orient: vertical; | |||
| -webkit-line-clamp: 2; | |||
| overflow: hidden; | |||
| } | |||
| .activity-location, .activity-time, .activity-participants { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 6rpx; | |||
| } | |||
| .location-text, .time-text, .participants-text { | |||
| font-size: 24rpx; | |||
| color: #999; | |||
| margin-left: 6rpx; | |||
| } | |||
| .activity-action { | |||
| display: flex; | |||
| align-items: flex-end; | |||
| padding-bottom: 10rpx; | |||
| } | |||
| .empty { | |||
| margin-top: 200rpx; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,145 @@ | |||
| <template> | |||
| <view class="page"> | |||
| <!-- 收藏列表 --> | |||
| <view class="content"> | |||
| <view v-if="list.length > 0" class="activity-list"> | |||
| <view class="activity-item" v-for="item in list" :key="item.id" @click="viewActivityDetail(item)"> | |||
| <image class="activity-image" :src="item.communityActivity.image" mode="aspectFill"></image> | |||
| <view class="activity-info"> | |||
| <view class="title-row"> | |||
| <view class="activity-badge"> | |||
| <text class="badge-text">{{ item.communityActivity.score }}分</text> | |||
| </view> | |||
| <text class="activity-title">{{ item.communityActivity.title }}</text> | |||
| </view> | |||
| <view class="activity-location"> | |||
| <uv-icon name="map-fill" size="14" color="#999"></uv-icon> | |||
| <text class="location-text">{{ item.communityActivity.address || '线上活动' }}</text> | |||
| </view> | |||
| <view class="activity-time"> | |||
| <uv-icon name="calendar" size="14" color="#999"></uv-icon> | |||
| <text class="time-text">{{ item.communityActivity.activityTime }}</text> | |||
| </view> | |||
| <view class="activity-participants"> | |||
| <uv-icon name="account-fill" size="14" ></uv-icon> | |||
| <text class="participants-text">报名人数:{{ item.communityActivity.numActivity + '/' + item.communityActivity.numLimit }}</text> | |||
| </view> | |||
| </view> | |||
| <view class="activity-action"> | |||
| <uv-button type="primary" size="mini" text="查看详情" @click.stop="viewActivityDetail(item)"></uv-button> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <view v-else class="empty"> | |||
| <uv-empty mode="data" text="暂无收藏活动"></uv-empty> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import MixinList from '@/mixins/list.js' | |||
| export default { | |||
| mixins: [MixinList], | |||
| data() { | |||
| return { | |||
| mixinListApi: 'activity.queryActivityCollectionList', | |||
| } | |||
| }, | |||
| methods: { | |||
| // 查看活动详情 | |||
| viewActivityDetail(item) { | |||
| uni.navigateTo({ | |||
| url: `/subPages/index/activityDetail?id=${item.activityId}` | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .page { | |||
| background-color: #f5f5f5; | |||
| min-height: 100vh; | |||
| } | |||
| .content { | |||
| padding: 20rpx; | |||
| } | |||
| .activity-list { | |||
| .activity-item { | |||
| display: flex; | |||
| margin-bottom: 30rpx; | |||
| background: #fff; | |||
| border-radius: 12rpx; | |||
| padding: 20rpx; | |||
| .activity-image { | |||
| width: 180rpx; | |||
| height: 180rpx; | |||
| border-radius: 8rpx; | |||
| margin-right: 20rpx; | |||
| } | |||
| .activity-info { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| .title-row { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 10rpx; | |||
| .activity-badge { | |||
| width: 31px; | |||
| height: 20px; | |||
| background: #218cdd; | |||
| border-radius: 3.5px; | |||
| margin-right: 7rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| .badge-text { | |||
| font-size: 18rpx; | |||
| color: #fff; | |||
| } | |||
| } | |||
| } | |||
| .activity-title { | |||
| font-size: 28rpx; | |||
| font-weight: bold; | |||
| color: #333; | |||
| } | |||
| .activity-location, .activity-time, .activity-participants { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 6rpx; | |||
| .location-text, .time-text, .participants-text { | |||
| font-size: 24rpx; | |||
| color: #999; | |||
| margin-left: 6rpx; | |||
| } | |||
| } | |||
| } | |||
| .activity-action { | |||
| display: flex; | |||
| align-items: flex-end; | |||
| padding-bottom: 10rpx; | |||
| } | |||
| } | |||
| } | |||
| .empty { | |||
| margin-top: 200rpx; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,246 @@ | |||
| <template> | |||
| <view class="page"> | |||
| <!-- 活动信息卡片 --> | |||
| <view class="activity-card"> | |||
| <image class="activity-image" :src="activityInfo.image" mode="aspectFill"></image> | |||
| <view class="activity-info"> | |||
| <view class="title-row"> | |||
| <view class="activity-badge"> | |||
| <text class="badge-text">{{ activityInfo.score }}分</text> | |||
| </view> | |||
| <text class="activity-title">{{ activityInfo.title }}</text> | |||
| </view> | |||
| <view class="activity-location"> | |||
| <uv-icon name="map-fill" size="14" color="#999"></uv-icon> | |||
| <text class="location-text">{{ activityInfo.address }}</text> | |||
| </view> | |||
| <view class="activity-time"> | |||
| <uv-icon name="calendar" size="14" color="#999"></uv-icon> | |||
| <text class="time-text">{{ activityInfo.activityTime }}</text> | |||
| </view> | |||
| <view class="activity-participants"> | |||
| <uv-icon name="account-fill" size="14" color="#999"></uv-icon> | |||
| <text class="participants-text">{{ activityInfo.numActivity }}/{{ activityInfo.numLimit }}人已报名</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <!-- 二维码区域 --> | |||
| <view class="qrcode-container"> | |||
| <view class="qrcode-wrapper"> | |||
| <!-- <uv-qrcode ref="qrcode" size="300px" value="https://h5.uvui.cn"></uv-qrcode> --> | |||
| <uv-qrcode | |||
| ref="qrcode" | |||
| :value="qrcodeValue" | |||
| size="500rpx" | |||
| @complete="onQRCodeComplete" | |||
| ></uv-qrcode> | |||
| </view> | |||
| </view> | |||
| <!-- 保存按钮 --> | |||
| <view class="save-container"> | |||
| <uv-button | |||
| type="primary" | |||
| text="保存到手机" | |||
| size="large" | |||
| shape="circle" | |||
| @click="saveQRCode" | |||
| ></uv-button> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import utils from '@/utils/index' | |||
| export default { | |||
| data() { | |||
| return { | |||
| activityInfo: { | |||
| }, | |||
| qrcodeValue: 'https://h5.uvui.cn', | |||
| qrCodeReady: false | |||
| } | |||
| }, | |||
| async onLoad(options) { | |||
| // 获取传递的参数 | |||
| if (options.id) { | |||
| await this.getActiviyDetail(options.id) | |||
| } | |||
| // 生成签到码 | |||
| this.generateQRCode() | |||
| }, | |||
| methods: { | |||
| async getActiviyDetail(id) { | |||
| const res = await this.$api.activity.queryActivityById({ | |||
| activityId: id | |||
| }) | |||
| this.activityInfo = res.result | |||
| }, | |||
| // 生成二维码内容 | |||
| generateQRCode() { | |||
| // 生成包含活动信息的二维码内容 | |||
| const qrData = { | |||
| activityId: this.activityInfo.id, | |||
| activityTitle: this.activityInfo.title, | |||
| type: 'checkin', | |||
| timestamp: Date.now() | |||
| } | |||
| this.qrcodeValue = JSON.stringify(qrData) | |||
| }, | |||
| // 二维码生成完成事件 | |||
| onQRCodeComplete(result) { | |||
| if (result.success) { | |||
| this.qrCodeReady = true | |||
| console.log('二维码生成成功') | |||
| } else { | |||
| console.log('二维码生成失败') | |||
| } | |||
| }, | |||
| // 保存二维码到手机 | |||
| saveQRCode() { | |||
| if (!this.qrCodeReady) { | |||
| uni.showToast({ | |||
| title: '二维码还未生成完成', | |||
| icon: 'none' | |||
| }) | |||
| return | |||
| } | |||
| this.$refs.qrcode.save({ | |||
| success: (res) => { | |||
| uni.showToast({ | |||
| title: '保存成功', | |||
| icon: 'success' | |||
| }) | |||
| }, | |||
| fail: (err) => { | |||
| console.log('保存失败:', err) | |||
| // 用uniapp获取保存权限 | |||
| utils.authoriza({ | |||
| scope: 'writePhotosAlbum', | |||
| successfn: () => { | |||
| // 保存二维码 | |||
| this.$refs.qrcode.save({ | |||
| success: (res) => { | |||
| uni.showToast({ | |||
| title: '保存成功', | |||
| icon: 'success' | |||
| }) | |||
| }, | |||
| fail: (err) => { | |||
| console.log('保存失败:', err) | |||
| uni.showToast({ | |||
| title: '保存失败', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| }) | |||
| }, | |||
| failfn: () => { | |||
| uni.showToast({ | |||
| title: '保存失败', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| }) | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .page { | |||
| background-color: #f5f5f5; | |||
| min-height: 100vh; | |||
| padding: 20rpx; | |||
| } | |||
| .activity-card { | |||
| display: flex; | |||
| background: #fff; | |||
| border-radius: 12rpx; | |||
| padding: 20rpx; | |||
| margin-bottom: 40rpx; | |||
| box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); | |||
| } | |||
| .activity-image { | |||
| width: 180rpx; | |||
| height: 180rpx; | |||
| border-radius: 8rpx; | |||
| margin-right: 20rpx; | |||
| } | |||
| .activity-info { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| } | |||
| .title-row { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 10rpx; | |||
| } | |||
| .activity-badge { | |||
| width: 31px; | |||
| height: 20px; | |||
| background: #218cdd; | |||
| border-radius: 3.5px; | |||
| margin-right: 7rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| .badge-text { | |||
| font-size: 18rpx; | |||
| color: #fff; | |||
| } | |||
| .activity-title { | |||
| font-size: 28rpx; | |||
| font-weight: bold; | |||
| color: #333; | |||
| line-height: 1.4; | |||
| } | |||
| .activity-location, .activity-time, .activity-participants { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 6rpx; | |||
| } | |||
| .location-text, .time-text, .participants-text { | |||
| font-size: 24rpx; | |||
| color: #999; | |||
| margin-left: 6rpx; | |||
| } | |||
| .qrcode-container { | |||
| display: flex; | |||
| justify-content: center; | |||
| margin-bottom: 60rpx; | |||
| } | |||
| .qrcode-wrapper { | |||
| background: #fff; | |||
| border-radius: 12rpx; | |||
| padding: 40rpx; | |||
| box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); | |||
| } | |||
| .save-container { | |||
| padding: 0 40rpx; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,215 @@ | |||
| <template> | |||
| <view class="goods-detail"> | |||
| <!-- 轮播图 --> | |||
| <view class="banner-container"> | |||
| <uv-swiper | |||
| indicator | |||
| indicatorMode="dot" | |||
| indicatorActiveColor="blue" | |||
| height="700rpx" | |||
| :list="goodsData.image ? goodsData.image.split(',') : []"></uv-swiper> | |||
| </view> | |||
| <!-- 商品信息 --> | |||
| <view class="goods-info"> | |||
| <!-- 积分信息 --> | |||
| <view class="points-section"> | |||
| <image src="/static/积分图标.png" class="points-icon" mode="aspectFit"></image> | |||
| <text class="points-text">{{ goodsData.price }}积分</text> | |||
| </view> | |||
| <!-- 商品标题 --> | |||
| <view class="title-section"> | |||
| <text class="goods-title">{{ goodsData.title }}</text> | |||
| </view> | |||
| </view> | |||
| <!-- 商品详情独立容器 --> | |||
| <view class="detail-container"> | |||
| <!-- 商品详情标题 --> | |||
| <view class="detail-title-section"> | |||
| <rich-text :nodes="goodsData.details"></rich-text> | |||
| </view> | |||
| </view> | |||
| <!-- 底部操作栏 - 只在待领取状态显示 --> | |||
| <view v-if="status === 'pending'" class="bottom-bar"> | |||
| <view class="exchange-btn" @click="confirmReceive"> | |||
| <text class="exchange-text">确认取货</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'ExchangeDetail', | |||
| data() { | |||
| return { | |||
| goodsId: '', | |||
| status: 'pending', // pending: 待领取, received: 已领取, cancelled: 系统取消 | |||
| goodsData: { | |||
| // id: 1, | |||
| // title: '哪吒之魔童闹海新款首套装哪吒校内艺术手办树脂摆件学生小礼品', | |||
| // price: 100, | |||
| // category: '积分兑换', | |||
| // exchangeCount: 120, | |||
| // sales: 0, | |||
| // inventory: 50, | |||
| // details: '这是一款美味的薄脆小饼干,口感酥脆,营养丰富。采用优质原料制作,无添加剂,适合全家人享用。每一口都能感受到浓郁的香味和酥脆的口感,是您休闲时光的最佳选择。', | |||
| // image: '/static/商城_商品1.png,/static/商城_商品2.png,/static/bannerImage.png' | |||
| } | |||
| } | |||
| }, | |||
| onLoad(options) { | |||
| if (options.id) { | |||
| this.goodsId = options.id; | |||
| } | |||
| if (options.status) { | |||
| this.status = options.status; | |||
| } | |||
| this.getGoodsDetail(this.goodsId); | |||
| }, | |||
| methods: { | |||
| async getGoodsDetail(id) { | |||
| const res = await this.$api.user.queryOrderById({ orderId: id }) | |||
| this.goodsData = res.result | |||
| }, | |||
| previewImage(current, urls) { | |||
| uni.previewImage({ | |||
| current: current, | |||
| urls: urls | |||
| }); | |||
| }, | |||
| confirmReceive() { | |||
| uni.showModal({ | |||
| title: '确认取货', | |||
| content: `确定已取货${this.goodsData.title}吗?`, | |||
| success: async (res) => { | |||
| if (res.confirm) { | |||
| const res2 = await this.$api.user.finishOrder({ orderId: this.goodsId }) | |||
| if (res2.code === 200) { | |||
| uni.showToast({ | |||
| title: `已成功取货${this.goodsData.title}`, | |||
| icon: 'success' | |||
| }); | |||
| // 延迟返回上一页 | |||
| setTimeout(() => { | |||
| uni.navigateBack(); | |||
| }, 1500); | |||
| }else { | |||
| uni.showToast({ | |||
| title: res2.msg, | |||
| icon: 'error' | |||
| }); | |||
| } | |||
| } | |||
| } | |||
| }); | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .goods-detail { | |||
| background: #f8f8f8; | |||
| min-height: 100vh; | |||
| padding-bottom: 120rpx; // 为底部固定栏留出空间 | |||
| } | |||
| .banner-container { | |||
| height: 700rpx; | |||
| .banner-swiper { | |||
| width: 100%; | |||
| .banner-image { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| } | |||
| } | |||
| .goods-info { | |||
| background: #ffffff; | |||
| margin-top: 20rpx; | |||
| padding: 30rpx; | |||
| } | |||
| .points-section { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 20rpx; | |||
| .points-icon { | |||
| width: 32rpx; | |||
| height: 32rpx; | |||
| margin-right: 8rpx; | |||
| } | |||
| .points-text { | |||
| font-size: 32rpx; | |||
| font-weight: bold; | |||
| color: #1488DB; | |||
| } | |||
| } | |||
| .title-section { | |||
| margin-bottom: 40rpx; | |||
| .goods-title { | |||
| font-size: 28rpx; | |||
| color: #333333; | |||
| line-height: 1.5; | |||
| display: block; | |||
| } | |||
| } | |||
| .detail-container { | |||
| background: #ffffff; | |||
| margin-top: 20rpx; | |||
| padding: 30rpx; | |||
| margin-bottom: 120rpx; | |||
| } | |||
| .detail-title-section { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| margin-bottom: 30rpx; | |||
| } | |||
| .bottom-bar { | |||
| position: fixed; | |||
| bottom: 0; | |||
| left: 0; | |||
| right: 0; | |||
| background: #ffffff; | |||
| padding: 20rpx 30rpx; | |||
| border-top: 1rpx solid #f0f0f0; | |||
| z-index: 999; | |||
| box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1); | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| .exchange-btn { | |||
| width: 100%; | |||
| height: 80rpx; | |||
| border-radius: 40rpx; | |||
| background-color: #218cdd; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| .exchange-text { | |||
| color: #ffffff; | |||
| font-size: 28rpx; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||