Browse Source

'初始化'

hfll
hflllll 7 months ago
commit
c1d7b9bba2
649 changed files with 76432 additions and 0 deletions
  1. +1
    -0
      .gitignore
  2. +23
    -0
      App.vue
  3. +56
    -0
      api/http.js
  4. +19
    -0
      api/index.js
  5. +73
    -0
      api/modules/activity.js
  6. +26
    -0
      api/modules/community.js
  7. +47
    -0
      api/modules/config.js
  8. +53
    -0
      api/modules/home.js
  9. +25
    -0
      api/modules/login.js
  10. +25
    -0
      api/modules/score.js
  11. +55
    -0
      api/modules/shop.js
  12. +49
    -0
      api/modules/user.js
  13. +115
    -0
      api/request.js
  14. +77
    -0
      config/index.js
  15. +20
    -0
      index.html
  16. +44
    -0
      main.js
  17. +78
    -0
      manifest.json
  18. +49
    -0
      mixins/config.js
  19. +133
    -0
      mixins/list.js
  20. +242
    -0
      pages.json
  21. +198
    -0
      pages/components/GlobalPopup.vue
  22. +23
    -0
      pages/components/HomePageNav.vue
  23. +226
    -0
      pages/components/Search.vue
  24. +236
    -0
      pages/components/index/RecommendedActivities.vue
  25. +143
    -0
      pages/components/index/VolunteerFeatures.vue
  26. +192
    -0
      pages/components/index/VolunteerHeader.vue
  27. +224
    -0
      pages/components/index/VolunteerRanking.vue
  28. +158
    -0
      pages/components/searchDemo.vue
  29. +146
    -0
      pages/components/shop/PointsCard.vue
  30. +423
    -0
      pages/components/shop/ShopContent.vue
  31. +403
    -0
      pages/index/activity.vue
  32. +425
    -0
      pages/index/community.vue
  33. +67
    -0
      pages/index/index.vue
  34. +375
    -0
      pages/index/my.vue
  35. +66
    -0
      pages/index/shop.vue
  36. BIN
      static/bannerImage.png
  37. +10597
    -0
      static/china-regions.json
  38. BIN
      static/logo.png
  39. BIN
      static/主页.png
  40. BIN
      static/主页_点击.png
  41. BIN
      static/兑换失败.png
  42. BIN
      static/兑换成功.png
  43. BIN
      static/可用积分背景图.png
  44. BIN
      static/商城.png
  45. BIN
      static/商城_商品1.png
  46. BIN
      static/商城_商品2.png
  47. BIN
      static/商城_点击.png
  48. BIN
      static/商城_积分明细框.png
  49. BIN
      static/失败弹窗.png
  50. BIN
      static/待上传头像.png
  51. BIN
      static/志愿者箭头.png
  52. BIN
      static/成为志愿者.png
  53. BIN
      static/成功弹窗.png
  54. BIN
      static/我的.png
  55. BIN
      static/我的_兑换记录.png
  56. BIN
      static/我的_关于我们.png
  57. BIN
      static/我的_商品收藏.png
  58. BIN
      static/我的_我的报名.png
  59. BIN
      static/我的_我的资料.png
  60. BIN
      static/我的_活动收藏.png
  61. BIN
      static/我的_点击.png
  62. BIN
      static/我的_积分.png
  63. BIN
      static/我的_背景.png
  64. BIN
      static/我的_退出登录.png
  65. BIN
      static/报名成功.png
  66. BIN
      static/推荐活动.png
  67. BIN
      static/提交成功.png
  68. BIN
      static/暂无搜索结果.png
  69. BIN
      static/暂无收藏.png
  70. BIN
      static/活动.png
  71. BIN
      static/活动_点击.png
  72. BIN
      static/活动日历.png
  73. BIN
      static/活动箭头.png
  74. BIN
      static/社区.png
  75. BIN
      static/社区_点击.png
  76. BIN
      static/社区_背景.png
  77. BIN
      static/社区_背景2.png
  78. BIN
      static/积分图标.png
  79. BIN
      static/积分排行榜.png
  80. BIN
      static/签到成功.png
  81. BIN
      static/组织介绍.png
  82. BIN
      static/组织箭头.png
  83. BIN
      static/首页_小喇叭.png
  84. BIN
      static/默认头像.png
  85. +111
    -0
      stores/index.js
  86. +351
    -0
      subPages/community/publishPost.vue
  87. +309
    -0
      subPages/index/activityCalendar.vue
  88. +487
    -0
      subPages/index/activityDetail.vue
  89. +115
    -0
      subPages/index/announcement.vue
  90. +97
    -0
      subPages/index/announcementDetail.vue
  91. +357
    -0
      subPages/index/components/SignUpForm.vue
  92. +93
    -0
      subPages/index/organizationIntroduction.vue
  93. +330
    -0
      subPages/index/ranking.vue
  94. +617
    -0
      subPages/index/volunteerApply.vue
  95. +291
    -0
      subPages/login/login.vue
  96. +416
    -0
      subPages/login/userInfo.vue
  97. +163
    -0
      subPages/my/activityCheckin.vue
  98. +145
    -0
      subPages/my/activityFavorites.vue
  99. +246
    -0
      subPages/my/checkinCode.vue
  100. +215
    -0
      subPages/my/exchangeDetail.vue

+ 1
- 0
.gitignore View File

@ -0,0 +1 @@
unpackage/

+ 23
- 0
App.vue View File

@ -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>

+ 56
- 0
api/http.js View File

@ -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)
}

+ 19
- 0
api/index.js View File

@ -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
}

+ 73
- 0
api/modules/activity.js View File

@ -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
})
},
}

+ 26
- 0
api/modules/community.js View File

@ -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
})
}
}

+ 47
- 0
api/modules/config.js View File

@ -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
})
},
}

+ 53
- 0
api/modules/home.js View File

@ -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
})
},
}

+ 25
- 0
api/modules/login.js View File

@ -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
})
}
}

+ 25
- 0
api/modules/score.js View File

@ -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
})
}
}

+ 55
- 0
api/modules/shop.js View File

@ -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
})
}
}

+ 49
- 0
api/modules/user.js View File

@ -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
})
},
}

+ 115
- 0
api/request.js View File

@ -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()
}
}
})
})
}

+ 77
- 0
config/index.js View File

@ -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

+ 20
- 0
index.html View File

@ -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>

+ 44
- 0
main.js View File

@ -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

+ 78
- 0
manifest.json View File

@ -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"
}

+ 49
- 0
mixins/config.js View File

@ -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()
}
}
}

+ 133
- 0
mixins/list.js View File

@ -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()
}
}

+ 242
- 0
pages.json View File

@ -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"
}
]
}
}

+ 198
- 0
pages/components/GlobalPopup.vue View File

@ -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
})
// 11
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>

+ 23
- 0
pages/components/HomePageNav.vue View File

@ -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>

+ 226
- 0
pages/components/Search.vue View File

@ -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>

+ 236
- 0
pages/components/index/RecommendedActivities.vue View File

@ -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>

+ 143
- 0
pages/components/index/VolunteerFeatures.vue View File

@ -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>

+ 192
- 0
pages/components/index/VolunteerHeader.vue View File

@ -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>

+ 224
- 0
pages/components/index/VolunteerRanking.vue View File

@ -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>

+ 158
- 0
pages/components/searchDemo.vue View File

@ -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>

+ 146
- 0
pages/components/shop/PointsCard.vue View File

@ -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>

+ 423
- 0
pages/components/shop/ShopContent.vue View File

@ -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}`
})
},
// isRefreshfalse
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>

+ 403
- 0
pages/index/activity.vue View File

@ -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>

+ 425
- 0
pages/index/community.vue View File

@ -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>

+ 67
- 0
pages/index/index.vue View File

@ -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>

+ 375
- 0
pages/index/my.vue View File

@ -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>

+ 66
- 0
pages/index/shop.vue View File

@ -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>

BIN
static/bannerImage.png View File

Before After
Width: 709  |  Height: 275  |  Size: 158 KiB

+ 10597
- 0
static/china-regions.json
File diff suppressed because it is too large
View File


BIN
static/logo.png View File

Before After
Width: 237  |  Height: 237  |  Size: 16 KiB

BIN
static/主页.png View File

Before After
Width: 48  |  Height: 48  |  Size: 1.2 KiB

BIN
static/主页_点击.png View File

Before After
Width: 48  |  Height: 48  |  Size: 1.2 KiB

BIN
static/兑换失败.png View File

Before After
Width: 254  |  Height: 101  |  Size: 8.8 KiB

BIN
static/兑换成功.png View File

Before After
Width: 254  |  Height: 101  |  Size: 8.8 KiB

BIN
static/可用积分背景图.png View File

Before After
Width: 724  |  Height: 290  |  Size: 27 KiB

BIN
static/商城.png View File

Before After
Width: 48  |  Height: 48  |  Size: 1.6 KiB

BIN
static/商城_商品1.png View File

Before After
Width: 227  |  Height: 227  |  Size: 99 KiB

BIN
static/商城_商品2.png View File

Before After
Width: 229  |  Height: 229  |  Size: 68 KiB

BIN
static/商城_点击.png View File

Before After
Width: 48  |  Height: 48  |  Size: 1.9 KiB

BIN
static/商城_积分明细框.png View File

Before After
Width: 149  |  Height: 65  |  Size: 2.9 KiB

BIN
static/失败弹窗.png View File

Before After
Width: 632  |  Height: 835  |  Size: 33 KiB

BIN
static/待上传头像.png View File

Before After
Width: 99  |  Height: 99  |  Size: 2.1 KiB

BIN
static/志愿者箭头.png View File

Before After
Width: 56  |  Height: 29  |  Size: 1.1 KiB

BIN
static/成为志愿者.png View File

Before After
Width: 334  |  Height: 451  |  Size: 20 KiB

BIN
static/成功弹窗.png View File

Before After
Width: 632  |  Height: 835  |  Size: 34 KiB

BIN
static/我的.png View File

Before After
Width: 48  |  Height: 48  |  Size: 1.4 KiB

BIN
static/我的_兑换记录.png View File

Before After
Width: 65  |  Height: 65  |  Size: 2.4 KiB

BIN
static/我的_关于我们.png View File

Before After
Width: 65  |  Height: 65  |  Size: 3.4 KiB

BIN
static/我的_商品收藏.png View File

Before After
Width: 65  |  Height: 65  |  Size: 2.7 KiB

BIN
static/我的_我的报名.png View File

Before After
Width: 65  |  Height: 65  |  Size: 1.9 KiB

BIN
static/我的_我的资料.png View File

Before After
Width: 65  |  Height: 65  |  Size: 2.9 KiB

BIN
static/我的_活动收藏.png View File

Before After
Width: 140  |  Height: 140  |  Size: 8.4 KiB

BIN
static/我的_点击.png View File

Before After
Width: 48  |  Height: 48  |  Size: 1.2 KiB

BIN
static/我的_积分.png View File

Before After
Width: 140  |  Height: 140  |  Size: 10 KiB

BIN
static/我的_背景.png View File

Before After
Width: 750  |  Height: 394  |  Size: 12 KiB

BIN
static/我的_退出登录.png View File

Before After
Width: 65  |  Height: 65  |  Size: 2.7 KiB

BIN
static/报名成功.png View File

Before After
Width: 254  |  Height: 101  |  Size: 11 KiB

BIN
static/推荐活动.png View File

Before After
Width: 158  |  Height: 50  |  Size: 3.6 KiB

BIN
static/提交成功.png View File

Before After
Width: 254  |  Height: 101  |  Size: 11 KiB

BIN
static/暂无搜索结果.png View File

Before After
Width: 464  |  Height: 484  |  Size: 36 KiB

BIN
static/暂无收藏.png View File

Before After
Width: 391  |  Height: 471  |  Size: 31 KiB

BIN
static/活动.png View File

Before After
Width: 48  |  Height: 48  |  Size: 803 B

BIN
static/活动_点击.png View File

Before After
Width: 48  |  Height: 48  |  Size: 736 B

BIN
static/活动日历.png View File

Before After
Width: 322  |  Height: 213  |  Size: 8.6 KiB

BIN
static/活动箭头.png View File

Before After
Width: 45  |  Height: 24  |  Size: 694 B

BIN
static/社区.png View File

Before After
Width: 48  |  Height: 48  |  Size: 1.6 KiB

BIN
static/社区_点击.png View File

Before After
Width: 48  |  Height: 48  |  Size: 1.5 KiB

BIN
static/社区_背景.png View File

Before After
Width: 375  |  Height: 188  |  Size: 59 KiB

BIN
static/社区_背景2.png View File

Before After
Width: 375  |  Height: 188  |  Size: 66 KiB

BIN
static/积分图标.png View File

Before After
Width: 18  |  Height: 18  |  Size: 651 B

BIN
static/积分排行榜.png View File

Before After
Width: 193  |  Height: 52  |  Size: 4.4 KiB

BIN
static/签到成功.png View File

Before After
Width: 512  |  Height: 175  |  Size: 24 KiB

BIN
static/组织介绍.png View File

Before After
Width: 321  |  Height: 213  |  Size: 10 KiB

BIN
static/组织箭头.png View File

Before After
Width: 45  |  Height: 24  |  Size: 743 B

BIN
static/首页_小喇叭.png View File

Before After
Width: 40  |  Height: 40  |  Size: 1.4 KiB

BIN
static/默认头像.png View File

Before After
Width: 113  |  Height: 113  |  Size: 24 KiB

+ 111
- 0
stores/index.js View File

@ -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

+ 351
- 0
subPages/community/publishPost.vue View File

@ -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>

+ 309
- 0
subPages/index/activityCalendar.vue View File

@ -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>

+ 487
- 0
subPages/index/activityDetail.vue View File

@ -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>

+ 115
- 0
subPages/index/announcement.vue View File

@ -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>

+ 97
- 0
subPages/index/announcementDetail.vue View File

@ -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>

+ 357
- 0
subPages/index/components/SignUpForm.vue View File

@ -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>

+ 93
- 0
subPages/index/organizationIntroduction.vue View File

@ -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>

+ 330
- 0
subPages/index/ranking.vue View File

@ -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>

+ 617
- 0
subPages/index/volunteerApply.vue View File

@ -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()
// 01
// 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() {
// titleid
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>

+ 291
- 0
subPages/login/login.vue View File

@ -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>

+ 416
- 0
subPages/login/userInfo.vue View File

@ -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>

+ 163
- 0
subPages/my/activityCheckin.vue View File

@ -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>

+ 145
- 0
subPages/my/activityFavorites.vue View File

@ -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>

+ 246
- 0
subPages/my/checkinCode.vue View File

@ -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>

+ 215
- 0
subPages/my/exchangeDetail.vue View File

@ -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>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save