一 公告
1.1 微信小程序端
const api = require("../../config/settings.js")
Page({
data: {
noticeList: [
{
title: '公告标题1',
create_time: '2024-04-25',
content: '公告内容描述1,公告内容描述1,公告内容描述1。', // 可以根据实际情况添加更多内容
igm: '/images/notice/notice1.jpg' // 图片路径,根据实际情况修改
},
{
title: '公告标题2',
create_time: '2024-04-26',
content: '公告内容描述2,公告内容描述2,公告内容描述2。', // 可以根据实际情况添加更多内容
igm: '/images/notice/notice2.jpg' // 图片路径,根据实际情况修改
},
// 可以添加更多社区公告数据
]
},
onLoad: function () {
// 页面加载时执行的逻辑
this.refresh()
},
refresh(){
wx.showLoading({
mask: true
})
wx.request({
url: api.notice,
method: "GET",
success: (res) => {
this.setData({
noticeList: res.data
})
},
complete() {
wx.hideLoading()
}
})
}
})
<!-- community_notice.wxml -->
<view class="container">
<!-- 使用wx:for循环遍历社区公告列表 -->
<view wx:for="{{noticeList}}" wx:key="index" class="notice-item">
<!-- 左侧图片 -->
<image class="notice-image" src="{{item.igm}}" mode="aspectFill"></image>
<!-- 右侧内容 -->
<view class="notice-content">
<view class="notice-title">{{item.title}}</view>
<view class="notice-time">{{item.create_time}}</view>
<view class="notice-details">{{item.content}}</view>
</view>
</view>
</view>
/* community_notice.wxss */
.container {
padding: 20rpx;
}
.notice-item {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx; /* 添加间距 */
border-bottom: 1px solid
padding-bottom: 20rpx; /* 增加底部内边距 */
}
.notice-image {
width: 150rpx;
height: 120rpx;
border-radius: 6rpx;
margin-right: 20rpx;
}
.notice-content {
flex: 1;
}
.notice-title {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.notice-time {
font-size: 24rpx;
color:
margin-bottom: 10rpx;
}
.notice-details {
font-size: 24rpx;
color:
}
1.2 后端接口
from .models import Notice
from .serializer import NoticeSerializer
class NoticeView(GenericViewSet,ListModelMixin):
queryset =Notice.objects.all().order_by('create_time')
serializer_class = NoticeSerializer
class NoticeSerializer(serializers.ModelSerializer):
class Meta:
model = Notice
fields = ['id', 'title','igm','create_time','content']
extra_kwargs={
'create_time':{'format':"%Y-%m-%d"}
}
二 活动列表
2.1 小程序端
var app = getApp();
var api = require("../../config/settings.js")
Page({
data: {
activityList: [
]
},
onLoad: function () {
// 页面加载时执行的逻辑
this.refresh()
},
refresh(){
wx.showLoading({
mask: true
})
wx.request({
url: api.activity,
method: "GET",
success: (res) => {
this.setData({
activityList: res.data
})
},
complete() {
wx.hideLoading()
}
})
},
handleSignup: function (event) {
// 处理报名按钮点击事件
var index = event.currentTarget.dataset.index; // 获取当前点击的活动索引
console.log('点击了报名按钮,索引为:', index);
}
})
<!-- activity_signup.wxml -->
<view class="container">
<!-- 使用wx:for循环遍历活动报名列表 -->
<view wx:for="{{activityList}}" wx:key="index" class="activity-item">
<!-- 活动内容 -->
<view class="activity-content">
<view class="activity-title">{{item.title}}</view>
<view class="activity-enrollment">报名人数:{{item.count}} | 总人数:{{item.total_count}}</view>
<view class="activity-time">获得积分:{{item.score}}</view>
<view class="activity-time">{{item.date}}</view>
<view class="activity-description">{{item.text}}</view>
</view>
<!-- 报名按钮 -->
<button class="signup-btn" bindtap="handleSignup">报名</button>
</view>
</view>
/* activity_signup.wxss */
.container {
padding: 20rpx;
}
.activity-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 20rpx;
border-bottom: 1px solid
padding-bottom: 20rpx;
}
.activity-content {
flex: 1;
}
.activity-title {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.activity-time {
font-size: 24rpx;
color:
margin-bottom: 10rpx;
}
.activity-enrollment {
font-size: 24rpx;
color:
margin-bottom: 10rpx;
}
.activity-description {
font-size: 24rpx;
color:
margin-top: 10rpx;
white-space: pre-wrap; /* 自动换行 */
}
.signup-btn {
background-color:
color:
border: none;
border-radius: 4rpx;
padding: 10rpx 20rpx;
font-size: 24rpx;
}
2.2 后端接口
from .models import Activity
from .serializer import ActivitySerializer
class ActivityView(GenericViewSet,ListModelMixin):
queryset =Activity.objects.all().order_by('date')
serializer_class = ActivitySerializer
class ActivitySerializer(serializers.ModelSerializer):
class Meta:
model = Activity
fields = ['id', 'title','text','date','count','score','total_count']
extra_kwargs={
'date':{'format':"%Y-%m-%d"}
}
class UserInfo(models.Model):
name = models.CharField(verbose_name="姓名", max_length=32)
avatar = models.FileField(verbose_name="头像", max_length=128, upload_to='avatar')
create_date = models.DateField(verbose_name="日期", auto_now_add=True)
score = models.IntegerField(verbose_name="积分", default=0)
class Meta:
verbose_name_plural = '用户表'
def __str__(self):
return self.name
class Activity(models.Model):
title = models.CharField(verbose_name="活动标题", max_length=128)
text = models.TextField(verbose_name="活动描述", null=True, blank=True)
date = models.DateField(verbose_name="举办活动日期")
count = models.IntegerField(verbose_name='报名人数', default=0)
total_count = models.IntegerField(verbose_name='总人数', default=0)
score = models.IntegerField(verbose_name="积分", default=0)
join_record = models.ManyToManyField(verbose_name="参与者",
through="JoinRecord",
through_fields=("activity", "user"),
to="UserInfo")
class Meta:
verbose_name_plural = '活动表'
def __str__(self):
return self.title
class JoinRecord(models.Model):
user = models.ForeignKey(verbose_name='用户', to="UserInfo", on_delete=models.CASCADE)
activity = models.ForeignKey(verbose_name="活动", to="Activity", on_delete=models.CASCADE, related_name='ac')
exchange = models.BooleanField(verbose_name="是否已兑换", default=False)
class Meta:
verbose_name_plural = '活动报名记录'
三 登录功能
3.1 小程序端 my页面
<block wx:if="{{userInfo==null}}">
<!-- login.wxml -->
<view class="container1">
<view class="main">
<view class="icon-view">
<!-- 应用图标 -->
<image src="/images/icon/icon.png" class="app-icon"></image>
<text class="title">智慧社区</text>
</view>
</view>
<van-cell-group>
<van-cell>
<button type="warn" open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">手机号快捷登录</button>
</van-cell>
</van-cell-group>
<!-- 其他手机号登录 -->
<van-cell-group>
<van-cell>
<button type="primary" plain bindtap="handleOtherLogin">其他手机号登录</button>
</van-cell>
</van-cell-group>
<!-- 用户协议同意 -->
<view class="agreement-container">
<checkbox class="checkbox" value="{{agreed}}" bindchange="handleAgreeChange"></checkbox>
<text class="agreement-text">我已阅读并同意</text>
<navigator url="" class="agreement-link">《用户协议》</navigator>
</view>
</view>
</block>
<block wx:else>
<view class="container">
<view class="top-view">
<view class="user">
<view class="row">
<image class="avatar" src="{{userInfo.avatar}}"></image>
<view class="name">
<view bindtap="logout">{{userInfo.name}}</view>
</view>
</view>
</view>
<view class="numbers">
<view class="row">
<text>{{userInfo.score}}</text>
<text>积分</text>
</view>
<view class="row">
<text>55</text>
<text>其他</text>
</view>
<view class="row">
<text>77</text>
<text>其他</text>
</view>
<view class="row">
<text>56</text>
<text>其他</text>
</view>
</view>
</view>
<van-list>
<van-cell title="积分兑换记录" is-link />
<van-cell title="我参加的活动" is-link />
<van-cell title="分享应用" is-link />
<van-cell title="联系客服" is-link />
<van-cell title="退出登录" is-link bind:tap="handleLogout"/>
</van-list>
</view>
</block>
var app = getApp();
var api = require("../../config/settings.js")
Page({
data: {
userInfo: null,
},
getPhoneNumber(event) {
console.log(event)
// 通过获取手机号返回的code--传递给后端--后端调用:POST https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=ACCESS_TOKEN -->获取手机号--》后端签发token给前端
wx.request({
url: api.quick_login,
method: 'POST',
data: {
code: event.detail.code
},
success: (res) => {
console.log(res)
//在此返回登录信息,用户登录
var data = res.data;
console.log(data)
if (data.code == 100) {
console.log('---', data)
var token = data.token
var name = data.name
var score = data.score
var avatar = data.avatar
app.initUserInfo(name, score, avatar, token)
var info = app.globalData.userInfo
console.log('globalData.userInfo', info)
if (info) {
this.setData({
userInfo: info
})
}
} else {
wx.showToast({
title: '登录失败',
})
}
}
})
},
handleOtherLogin(e) {
wx.navigateTo({
url: '/pages/otherlogin/otherlogin'
})
},
onShow() {
var info = app.globalData.userInfo
console.log('globalData.userInfo', info)
if (info) {
this.setData({
userInfo: info
})
}
},
handleLogout() {
app.logoutUserInfo()
this.setData({
userInfo: null
})
}
})
page{
height: 100%;
}
.login-area{
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.login-area .btn{
width: 200rpx;
height: 200rpx;
border-radius: 500%;
background-color:
color: white;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.user-area{
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.user-area image{
width: 200rpx;
height: 200rpx;
border-radius: 500%;
}
.user-area .name{
font-size: 30rpx;
padding: 30rpx 0;
}
.user-area .logout{
color:
}
.top-view{
background-color:
color: white;
padding: 40rpx;
}
.top-view .user{
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.top-view .user .row{
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.top-view .user .avatar{
width: 100rpx;
height: 100rpx;
border-radius: 50%;
}
.top-view .user .name{
display: flex;
flex-direction: row;
justify-content: flex-start;
padding-left: 20rpx;
}
.top-view .user .name navigator{
padding: 0 5rpx;
}
.top-view .site{
background-color: rgba(0, 0, 0, 0.16);
padding: 20rpx;
border-top-left-radius: 32rpx;
border-bottom-left-radius: 32rpx;
}
.top-view .numbers{
display: flex;
flex-direction: row;
justify-content: space-between;
font-size: 28rpx;
padding: 40rpx;
padding-bottom: 0rpx;
}
.top-view .numbers .row{
display: flex;
flex-direction: column;
align-items: center;
}
/* login.wxss */
.container1 {
padding: 20rpx;
}
.main{
display: flex;
justify-content: center;
align-items: center;
}
.icon-view{
display: flex;
flex-direction: column;
margin-bottom: 50rpx;
}
.app-icon {
width: 100rpx;
height: 100rpx;
margin: 40rpx auto 20rpx; /* 上边距为40rpx,下边距为20rpx,左右居中 */
}
.quick-login-header {
display: flex;
align-items: center;
}
.icon {
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
}
.title {
font-size: 28rpx;
font-weight: bold;
color:
}
.divider {
height: 20rpx;
}
.login-option {
font-size: 28rpx;
color:
}
.login-option .van-cell__icon {
color:
}
.agreement-container {
display: flex;
align-items: center;
margin-top: 20rpx;
}
.checkbox {
margin-right: 10rpx;
}
.agreement-text {
font-size: 24rpx;
color:
}
.agreement-link {
font-size: 24rpx;
color:
}
3.2 小程序端-app.js
App({
globalData: {
userInfo: null
},
initUserInfo: function(name,score,avatar,token) {
var info = {
name: name,
score: score,
avatar: avatar,
token:token
};
this.globalData.userInfo = info
wx.setStorageSync('userInfo', info);
},
logoutUserInfo:function(){
wx.removeStorageSync('userInfo');
this.globalData.userInfo=null;
},
onLaunch(){
var info =wx.getStorageSync('userInfo')
console.log(info)
this.globalData.userInfo = info
}
})
####app.json###
"usingComponents": {
"van-field": "@vant/weapp/field/index",
"van-button": "@vant/weapp/button/index",
"van-cell-group": "@vant/weapp/cell-group/index",
"van-nav-bar": "@vant/weapp/nav-bar/index",
"van-grid": "@vant/weapp/grid/index",
"van-grid-item": "@vant/weapp/grid-item/index",
"van-cell": "@vant/weapp/cell/index",
"van-notice-bar": "@vant/weapp/notice-bar/index",
"van-image": "@vant/weapp/image/index",
"van-divider": "@vant/weapp/divider/index",
"van-tab": "@vant/weapp/tab/index",
"van-tabs": "@vant/weapp/tabs/index",
"van-dropdown-menu": "@vant/weapp/dropdown-menu/index",
"van-dropdown-item": "@vant/weapp/dropdown-item/index"
},
3.3 小程序端-settings.js
const rootUrl = 'http://192.168.71.100:8000/app01';
module.exports = {
welcome:rootUrl+'/welcome/',
banner:rootUrl+'/banner/',
collection:rootUrl+'/collection/',
statistics:rootUrl+'/statistics/',
face:rootUrl+'/face/',
voice:rootUrl+'/voice/',
notice:rootUrl+'/notice/',
activity:rootUrl+'/activity/',
quick_login:rootUrl+'/user/quick_login/',
send_sms:rootUrl+'/user/send_sms/',
login:rootUrl+'/user/login/',
}
3.4 小程序端-login
<view class="container">
<view class="main">
<view class="icon-view">
<!-- 应用图标 -->
<image src="/images/icon/icon.png" class="app-icon"></image>
<text class="title">智慧社区</text>
</view>
</view>
<van-field value="{{ phone }}" bind:input="onPhoneInput" label="手机号" type="tel" placeholder="请输入手机号" clearable="{{ true }}" />
<van-field value="{{code}}" bind:input="onCodeInput" center clearable label="验证码" placeholder="请输入验证码" use-button-slot>
<van-button slot="button" size="small" type="primary" bind:tap="sendCode" disabled='{{sendCodeDisabled}}'>
{{buttonText}}
</van-button>
</van-field>
<van-button type="info" block="{{ true }}" bind:tap="login">登录</van-button>
</view>
.container {
padding: 20rpx;
}
.main{
display: flex;
justify-content: center;
align-items: center;
}
.icon-view{
display: flex;
flex-direction: column;
margin-bottom: 50rpx;
}
.title {
font-size: 28rpx;
font-weight: bold;
color:
}
.app-icon {
width: 100rpx;
height: 100rpx;
margin: 40rpx auto 20rpx; /* 上边距为40rpx,下边距为20rpx,左右居中 */
}
const api = require("../../config/settings.js")
var app = getApp()
Page({
data: {
phone: '',
code: '',
agreed: false,
sendCodeDisabled: false,
buttonText: '发送验证码',
loading: false,
timer: null,
countDown: 60
},
// 监听手机号输入
onPhoneInput(event) {
this.setData({
phone: event.detail
});
},
// 监听验证码输入
onCodeInput(event) {
this.setData({
code: event.detail
});
},
// 发送验证码
sendCode() {
// 在这里编写发送验证码的逻辑,此处仅做示例
console.log('发送验证码',this.data.phone,this.data.code);
if(this.data.phone){
wx.request({
url: api.send_sms+'?mobile='+this.data.phone,
method:'GET',
success:(res)=>{
wx.showToast({
title: res.data.msg,
})
}
})
this.setData({
sendCodeDisabled: true,
timer: setInterval(this.countDown, 1000)
});
}else{
wx.showToast({
title: '请输入手机号',
})
}
},
// 登录
login() {
// 在这里编写登录逻辑,此处仅做示例
console.log('登录');
if(this.data.phone&&this.data.code){
wx.request({
url: api.login,
method:'POST',
data:{mobile:this.data.phone,code:this.data.code},
success:(res)=>{
var data = res.data;
console.log(data)
if (data.code == 100) {
console.log('---', data)
var token = data.token
var name = data.name
var score = data.score
var avatar = data.avatar
app.initUserInfo(name, score, avatar, token)
var info = app.globalData.userInfo
console.log('globalData.userInfo', info)
wx.navigateBack()
} else {
wx.showToast({
title: '登录失败',
})
}
}
})
this.setData({
sendCodeDisabled: true,
timer: setInterval(this.countDown, 1000)
});
}else{
wx.showToast({
title: '请输入手机号和验证码',
})
}
},
// 倒计时
countDown() {
let countDown = this.data.countDown;
if (countDown === 0) {
clearInterval(this.data.timer);
this.setData({
buttonText: '发送验证码',
sendCodeDisabled: false,
countDown: 60
});
return;
}
this.setData({
buttonText: countDown + 's',
countDown: countDown - 1
});
},
onUnload() {
clearInterval(this.data.timer);
}
});
3.3 后端接口
router.register('user', LoginView, 'user')
from libs.send_tx_sms import get_code, send_sms_by_phone
from django.core.cache import cache
from rest_framework.decorators import action
from .models import UserInfo
from rest_framework_simplejwt.tokens import RefreshToken
from faker import Faker
class LoginView(GenericViewSet):
@action(methods=['GET'], detail=False)
def send_sms(self, request, *args, **kwargs):
mobile = request.query_params.get('mobile')
code = get_code()
cache.set(f'sms_{mobile}', code)
res = send_sms_by_phone(mobile, code)
if res:
return Response({'code': 100, 'msg': '短信发送成功'})
else:
return Response({'code': 101, 'msg': '短信发送失败,请稍后再试'})
@action(methods=['POST'], detail=False)
def login(self, request, *args, **kwargs):
mobile = request.data.get('mobile')
code = request.data.get('code')
old_code = cache.get(f'sms_{mobile}')
if old_code == code:
user = UserInfo.objects.filter(mobile=mobile).first()
if not user:
fake = Faker('zh_CN')
username = fake.name()
user = UserInfo.objects.create(mobile=mobile, name=username)
refresh = RefreshToken.for_user(user)
return Response(
{'code': 100, 'msg': '登录成功', 'token': str(refresh.access_token), 'name': user.name,
'score': user.score,'avatar':'http://127.0.0.1:8000/media/'+str(user.avatar)})
else:
return Response({'code':101,'msg':'验证码错误'})
@action(methods=['POST'], detail=False)
def quick_login(self, request, *args, **kwargs):
code = request.data.get('code')
user=UserInfo.objects.filter(pk=1).first()
refresh = RefreshToken.for_user(user)
return Response({'code': 100, 'msg': '登录成功', 'token': str(refresh.access_token), 'name': user.name,'score': user.score,'avatar':'http://127.0.0.1:8000/media/'+str(user.avatar)})
四 活动报名
4.1 小程序端
handleSignup: function (event) {
// 1 校验用户是否登录
var info = app.globalData.userInfo
if (info) {
//2 处理报名按钮点击事件
var index = event.mark.id; // 获取当前点击的活动索引
console.log('点击了报名按钮,索引为:', index);
wx.request({
url: api.join,
method:'POST',
data:{'id':index},
header:{token:info.token},
success:(res)=>{
wx.showToast({
title: res.data.msg,
})
}
})
} else {
wx.showToast({
title: '请先登录',
})
}
}
4.2 后端接口
class ActivityJoinView(GenericViewSet):
authentication_classes = [MyJSONWebTokenAuthentication]
@action(methods=['POST'], detail=False)
def join(self, request, *args, **kwargs):
activity_id = request.data.get('id')
user = request.user
activity = Activity.objects.filter(pk=activity_id).first()
join_record=JoinRecord.objects.filter(activity_id=activity_id,user=user).first()
if join_record:
return Response({'code': 101, 'msg': "已经报名过,不用重复报名"})
else:
activity.count = activity.count + 1
activity.save()
JoinRecord.objects.create(activity=activity,user=user)
return Response({'code': 100, 'msg': "报名成功"})
from .models import UserInfo
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_simplejwt.authentication import JWTAuthentication
class MyJSONWebTokenAuthentication(JWTAuthentication):
def authenticate(self, request):
jwt_value = request.META.get("HTTP_TOKEN")
if not jwt_value:
raise AuthenticationFailed('token 字段是必须的')
validated_token = self.get_validated_token(jwt_value)
print(validated_token['user_id'])
user = UserInfo.objects.filter(pk=validated_token['user_id']).first()
return user, jwt_value
五 积分商城
5.1 小程序端
<van-dropdown-menu active-color="#1989fa">
<van-dropdown-item value="{{ value1 }}" options="{{ option1 }}" />
<van-dropdown-item value="{{ value2 }}" options="{{ option2 }}" />
</van-dropdown-menu>
<van-grid column-num="3" border="{{ true }}">
<van-grid-item use-slot wx:for="{{ 8 }}" wx:for-item="index" border>
<image style="width: 100%; height: 90px;" src="https://img.yzcdn.cn/vant/apple-{{ index + 1 }}.jpg" />
<view class="desc">
<view class="title">{{item.title}}</view>
<view class="exchange">
<view>{{item.price}}积分</view>
<van-button color="linear-gradient(to right, #4bb0ff, #6149f6)" bindtap="doExchange" data-gid="{{item.id}}" size="mini">兑换</van-button>
</view>
</view>
</van-grid-item>
</van-grid>
const api = require("../../config/settings")
var app = getApp()
Page({
data: {
option1: [
{ text: '全部商品', value: 0 },
{ text: '最新上架', value: 1 },
{ text: '活动商品', value: 2 },
],
option2: [
{ text: '默认排序', value: 'a' },
{ text: '好评排序', value: 'b' },
{ text: '销量排序', value: 'c' },
],
value1: 0,
value2: 'a',
},
})
{
"usingComponents": {},
"navigationBarTitleText": "积分商城"
}