微信小程序——个人相册(前端)

微信小程序——个人相册(前端)

项目效果图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.项目功能

1)用户管理,信息包括:头像、昵称,功能包括:获取微信用户信息、验证用户是否存在、修改头像、修改昵称
2)上传相片:上传图片
3)照册列表:封面图(轮播图)、照片列表、照片选择、删除照片
4)照片信息:照片信息包括 显示照片、大小(字节)、上传时间

2.项目结构

在这里插入图片描述

3.数据库

此项目使用MySQL8.0,创建数据库myphoto,SQL文件我已放在文章顶部。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.创建项目

  1. 小程序项目名:myphoto
  2. 使用2.30.0版本的基础库以及不启用远程校验
  3. 准备好appid和appsecret,后端需要用到
    在这里插入图片描述

5.代码

5.1 项目准备
  1. app.js

// app.js
App({
  async onLaunch(){
    let res=await wx.login();
    let code=res.code;

    wx.request({
      url: this.globalData.rootPath+'/account/getUserInfo',
      data:{code},
      success:(result)=>{
        let userInfo=result.data;
        if (userInfo) {
          this.globalData.userInfo = userInfo;
          wx.redirectTo({
            url: '/pages/index/index',
          })
        } else {
          wx.redirectTo({
            url: '/pages/mine/mine'
          });
        }
      }
    })
  },
  globalData: {
    rootPath:"http://localhost:8089/myhome_war_exploded",//请修改为自己的服务根路径
    userInfo: null,
  },
})

  1. app.json

{
  "entryPagePath": "pages/index/index",
  "pages": [
    "pages/index/index",
    "pages/File/File",
    "pages/mine/mine",
    "pages/detail/datail"
  ],
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "Weixin",
    "navigationBarTextStyle": "black"
  },
  "tabBar": {
    "selectedColor": "#33a3dc",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "./pages/images/1.png",
        "selectedIconPath": "./pages/images/1-1.png"
      },
      {
        "pagePath": "pages/File/File",
        "text": "文件",
        "iconPath": "./pages/images/2.png",
        "selectedIconPath": "./pages/images/2-2.png"
      },
      {
        "pagePath": "pages/mine/mine",
        "text": "我的",
        "iconPath": "./pages/images/3.png",
        "selectedIconPath": "./pages/images/3-3.png"
      }
    ]
  },
  "style": "v2",
  "sitemapLocation": "sitemap.json"
}

5.2 mine(用户登录界面)

页面需求:
① 打开小程序时,先从服务端根据用户的code获取用户信息,如果用户存在,打开index,不存在,跳转到mine
② 把从服务端获取的用户信息保存到全局对象中

获取用户信息
① 进入mine.js时判断全局对象中是否有用户信息的缓存,如果有,显示用户信息,如果没有,显示“获取用户信息”按钮。
② 点击按钮,获取微信用户信息,获取成功后隐藏“获取用户信息按钮”,并显示用户头像和昵称
③ 保存用户信息到全局对象中
保存用户信息
① 如果是获取的微信用户信息,把用户信息保存到服务端
② 在服务端保存用户信息前需通过小程序传入的code来获取openId(用于唯一标识 每一个微信用户)。
③ 数据库的主键使用UUID生成,长度为32位(去掉“-”字符)
服务端返回生成的用户ID,在小程序端,把返回的用户id添加到缓存的用户信息中
修改昵称
① 点击my视图中“修改昵称”按钮,昵称从显示状态切换为可编辑状态,同时按钮的文字修改为“确定修改”
② 修改昵称后,点击“确定修改”按钮,把修改后的昵称提交到服务端保存,并修改小程序中缓存用户信息的昵称
修改头像
① 点击头像,从本地相册中选择一张照片,确定后先替换本地头像
② 把新头像上传到服务端,并保存存放路径和访问路径保存
服务端返回访问路径到小程序,更新缓存中用户头像的路径和my中头像的路径

  1. mine.wxml

<!--pages/mine/mine.wxml-->
<view class="container">
<block wx:if="{{!hasUserInfo}}">
<button bindtap="getUserInfo">获取用户信息</button>
</block>
<block wx:else>
<image src="{{userInfo.avatarUrl}}" style="border-radius: 50%; height:400rpx;width:400rpx;" bindtap="modifyAvatar"/>
<text style="padding-top: 100rpx; font-size: 50rpx;" hidden="{{isModify}}">{{userInfo.nickName}}</text>
<form bindsubmit="modifynickname">
  <input type="text" name="nickname" style="margin-top: 100rpx; padding: 10rpx;font-size: 50rpx; border:2rpx solid #ddd;" hidden="{{!isModify}}" value="{{userInfo.nickName}}"/>
<button form-type="submit" type="primary" style="margin-top: 100rpx;">
{{btnTitle}}
</button>
</form>
</block>
</view>

  1. mine.js

const app=getApp();
const {req} =require("../../utils/repuest")
Page({
  data:{
    hasUserInfo:false,
    isModify:false,
    btnTitle:'修改昵称',
     userInfo:{
      avatarUrl:'http://localhost:8089/myhome/photo/1.png',
      nickName:'当前用户昵称'
     }
  },
  onLoad(){
    let userInfo=app.globalData.userInfo;
    if(userInfo){
      if(!userInfo.avatarUrl.startsWith("http")){
        userInfo.avatarUrl=app.globalData.rootPath+userInfo.avatarUrl;
      }
      this.setData({
        userInfo:userInfo,
        hasUserInfo:true
      })
    }
  },
getUserInfo(){
   wx.getUserProfile({
     desc: 'desc',
     success:(res)=>{
       let userinfo=res.userInfo;
      //  userinfo.accId='';
       this.setData({
         userinfo:userinfo,
         hasUserInfo:true,
         nickName:this.data.nickName
       })
       this.saveUserInfo();
     }
   })
  },
  async saveUserInfo(){
    let userInfo=this.data.userinfo;
    let code=(await(await wx.login())).code;
    req({
      url:"/account/save",
      method:"POST",
      header:{'content-type':'application/x-www-form-urlencoded'},
      data:{...userInfo,code},
    }).then((res)=>{
      let accId=res.data;
      if(accId){
        userInfo.accId=accId;
        app.globalData.getUserInfo=userInfo;
        this.setData({userinfo:userInfo})
      }
    });
  },
  modifynickname(e){
    let isModify=this.data.isModify;
    if(!isModify){
      this.setData({
        isModify: true,
      inputPlaceholder: "请输入新昵称",
      btnTitle: "确定修改"
      })
    }else{
      this.setData({
        isModify: false,
      inputPlaceholder: "当前昵称",
      btnTitle: "修改昵称"
      })
      this.updateNickName;
    }
  },
  updateNickName(e){
    let userInfo=this.data.userInfo;
    let nickName = e.detail.value.nickName; // 假设昵称输入框的值为nickName
  let accId = userInfo.accId; // 假设用户的accid存在于userInfo中

  // 调用req方法访问服务器实现昵称修改
  req({
    url: 'http://localhost:8089/myhome/account/modifyNickName',//请修改为自己的实际端口号
    method: 'POST',
    data: {
      nickName: nickName,
      accId:accId
    },
    success: function(response) {
      // 根据服务器返回的结果进行相应的处理
      console.log('昵称修改成功', response);
      // 可以根据需要更新页面的数据或进行其他操作
    },
    fail: function(error) {
      console.log('昵称修改失败', error);
      // 可以根据需要进行错误处理或提示用户
    }
  });
  },
  uploadAvatar(filePath){
    wx.uploadFile({
      
      filePath: filePath,
      name: 'avatar',
      url:"http://localhost:8089/myhome/account/uploadAvatar",
      formData:{accId:this.data.userinfo.accId},
      success(res){
        let avatarUrl=res.data;
        if(avatarUrl){
          this.setData({
            "userinfo.avatarUrl":app.globalData.rootPath+avatarUrl
          })
        }
      }
    })
  },
  modifyAvatar(){
    wx.chooseMedia({
      count:1,
      mediaType:'image',
      sourceType:'album'
    }).then(res=>{
      let avatarUrl=res.tempFiles[0].tempFilePath;
      this.setData({
        'userinfo.avatarUrl':avatarUrl
      })
      this.uploadAvatar(avatarUrl);
    }).catch(res=>{})
  }
})

5.3 File(文件上传界面)

页面需求
① 在upload视图中“上传”按钮默认为禁用,点击按钮上方的大图标,可在本地相册中选择要上传的照片(一次只能选择一张)
② 确定后,用选中的照片替换大图标,“上传”按钮设为可用
③ 点击“上传”,把照片上传到服务端
④ 上传成功后,照片区恢复显示大图标,“上传”按钮为禁用

  1. File.wxml

<!--pages/File/File.wxml-->
<view style="padding:40rpx;box-sizing: border-box;display: block;">
<view style="text-align: center;color: #f00;">点击以下图片选择相片</view>

<image src="{{filePath}}" style="width: 100%;" mode="{{mode}}" bindtap="selectPhoto"></image>

<button bindtap="uploadPhoto" disabled="{{!isSelectImg}}" type="primary">上传</button>
</view>

  1. File.js

//获取应用实例  
var app = getApp();
Page({
  data: {
    filePath:"/pages/images/2-2.png",
    mode:"center",
    isSelectImg:false
  },
  selectPhoto(){
    wx.chooseMedia({
      count:1,
      mediaType:'image',
      sourceType:'album'
    }).then(res=>{
      let filePath = res.tempFiles[0].tempFilePath;
      this.setData({
        filePath:filePath,
        isSelectImg:true
      })
    }).catch(res=>{})
},
uploadPhoto(){
  let _this = this;
  wx.uploadFile({
    filePath: this.data.filePath,
    name: 'file',
    url: getApp().globalData.rootPath+'/photo/upload',
    formData:{accId: getApp().globalData.userInfo.accId},
    success:res=>{
      console.log(res);
      if(res.data.replaceAll("\"","") =="1"){
        wx.showToast({
          title: '上传成功',
        })
      }else{
        wx.showToast({
          title: '上传失败',
          icon:"error"
        })
      }
      this.setData({
        filePath:"/pages/images/2-2.png",
        mode:"center",
        isSelectImg:false
      })
    }
  })
},
});

5.4 index(首页)

页面需求:
① 在进入index页面时首先判断缓存中是否有用户信息
② 如果不没有,页面跳转到my,并每隔1秒轮询一次,直到用户信息存在后停止轮询
③ 如果用户存在,从服务端此用户的照片(一次最多10张,最后上传的照片显示在最前面)
在页面中显示(一排显示两张)
触底分页
① 当照片滚动到视图最下方时,从服务端加载新的照片(和上一轮的照片数量相同)
② 新加载的照片追加到已显示照片的后方
开始加载前显示加载动画,加载完成后结束加载动画
下拉刷新
① 开启index页面的下拉刷新功能
在页面的下拉刷新事件回调中清空已显示的所有照片,重新从服务端加载照片
长按显示复选框和操作栏
① 在照片上长按,在每个照片的左上角显示复选框,并在视图的底部显示操作栏
② 复选框默认时都为未选中
③ 操作栏中的操作包括“取消”、“全选”、“删除”三项
取消操作
① 点击操作栏中的“取消”完成以下操作
② 清空所有选中
③ 隐藏复选框
④ 隐藏操作栏
勾选和取消勾选、全选操作
① 点击照片上的复选框,如果勾选,记录选中的照片ID
② 如果是取消勾选,从记录中删除对应的照片ID
③ 点击“全选”,勾选所有复选框,并记录所有被选中的照片ID
删除
① 点击操作栏中“删除”,删除所有被勾选的照片
② 如果未选择要删除的照片,提示“请勾选要删除的照片”
③ 删除前需询问“确定要删除选中的照片吗?”,确定后方能删除
删除成功后刷新照片

  1. index.wxml

<swiper indicator-dots="{{true}}" current="{{currentIndex}}" autoplay>
<swiper-item wx:for="{{covers}}" wx:key="*this">
<image src="{{item.accessUrl}}" style="width: 100%;" mode="widthFix"></image>
</swiper-item>
</swiper>
<scroll-view style="padding: 60rpx 40rpx;box-sizing: border-box;">
<view style="display: flex;flex-wrap: wrap;">
<view wx:for="{{photoList}}" wx:key="*this" wx:for-item="photo" style="flex:none;width:50%;height:300rpx;box-sizing:border-box;padding:6rpx;position:relative;">
<checkbox mark:photoId="{{photo.photoId}}" mark:index="{{index}}" style="position: absolute;" hidden="{{!delSelect}}" bindtap="checkedPhoto" checked="{{selectList[index]}}"/>
<image src="{{photo.photoAccessUrl}}" mode="" style="width: 100%;height: 100%;" bindlongpress="enabelDelSelect" bindtap="showImage" mark:index="{{index}}"/>
</view>
</view>
</scroll-view>

<button loading="{{loadding}}" style="background: #fff;"></button>
<view style="position: fixed;bottom: 0;width: 100%;display: flex;justify-content: space-between;box-sizing: border-box;padding: 10rpx 20rpx;" wx:if="{{delSelect}}">
<text bindtap="cancelSelect" style="color: #00f;text-decoration: underline;">取消</text>
<text bindtap="selectAll" style="color: #00f;text-decoration: underline;">全选</text>
<text bindtap="deletePhoto" style="color: #00f;text-decoration: underline;">删除</text>

</view>
  1. index.js

// index.js
// 获取应用实例
const app = getApp()
const {req}=require("../../utils/repuest")

Page({
  data: {
    covers:[],
    photoList:[],
    delSelect:false,
    selectList:[],
    pageNum:1,
    loadding:true,
    currentIndex:0
  },
  selectPhotoList(){
    this.setData({loadding:true});
    req({
      url:"/photo/list",
      data:{
        accId:app.globalData.userInfo.accId,
        pageNum:this.data.pageNum
      }
    }).then(res=>{
      let photoList = res.data;
      if(photoList.length > 0){
        for(let photo of photoList){
          photo.photoAccessUrl = app.globalData.rootPath+photo.photoAccessUrl;
        }
        this.setData({
          photoList:this.data.photoList.concat(photoList),
          pageNum:this.data.pageNum+1
        })
      }else{
        // wx.showToast({
        //   title: '图片以全部加载',
        // })
      }
      this.setData({
        loadding:false
      })
    })
  },

  onLoad() {
    let time = setInterval(()=>{
      let userInfo = app.globalData.userInfo;
      if(userInfo){
        clearInterval(time);
        this.selectCover();
        this.selectPhotoList();
      }
    },1000);
  },
  onPullDownRefresh(){
    this.setData({
      covers:[],
      photoList:[],
      pageNum:1,
      currentIndex:0
    });
    this.selectCover();
    this.selectPhotoList();
  },
  //在照片上长按,在每个照片的左上角显示复选框,并在视图的底部显示操作栏
  enabelDelSelect(){
    this.setData({
      delSelect:true
    });
  },
  /*
① 点击操作栏中的“取消”完成以下操作
② 清空所有选中
③ 隐藏复选框
隐藏操作栏
*/
  cancelSelect(){
    this.setData({
      selectList:[],
      delSelect:false
    });
  },
  onReachBottom(){
    console.log(this.data.pageNum);
    this.setData({loadding:true});
    this.selectPhotoList();
  },
  checkedPhoto(e){
    let index = e.mark.index;
    let photoId = e.mark.photoId;
    let selectList = this.data.selectList;
    if(selectList[index]){
      selectList[index]=undefined;
    }else{
      selectList[index]=photoId;
    }
    this.setData({
      selectList:selectList
    })
  },
  selectAll(){
    let selectList = [];
    let photoList = this.data.photoList;
    for(let photo of photoList){
      selectList.push(photo.photoId);
    }
    this.setData({
      selectList:selectList
    });
  },
  deletePhoto(){
    let selectList = this.data.selectList;
    let deleteList = '';
    for(let id of selectList){
      if(id){
        deleteList+='&photoIds='+id;
      }
    }
    if(deleteList == ''){
      wx.showToast({
        title: '请勾选要删除的照片',
      })
    }else{
      wx.showModal({
        title: '确定要删除选中的照片吗?',
        content: '',
        complete: (res) => {
          if (res.cancel) {
              
          }
          if (res.confirm) {
            req({
              url:"/photo/delete?photoIds=''"+deleteList,
              method:"GET"
            }).then(res=>{
              this.setData({
                photoList:[],
                pageNum:1
              });
              this.cancelSelect();
              this.selectPhotoList();
            })
          }
        }
      })
    }
  },
  showImage(e){
    let index = e.mark.index;
    let photoList = this.data.photoList;
    wx.navigateTo({
      url: '/pages/detail/datail?photoList='+JSON.stringify(photoList)+"&index="+index+"&coverLength="+this.data.covers.length,
    })
  },
  selectCover(){
    req({
      url:'/cover/list?accId='+app.globalData.userInfo.accId,
      method:'GET',
    }).then(res=>{
      let cover = res.data;
      console.log(cover);
      this.setData({
        covers:cover,
        currentIndex:0
      })
    })
  },
  onShow(){
        // this.selectCover();
        // this.selectPhotoList();
  }
})

5.5 detail(照片详情界面)

页面需求:
① 在index中点击照片,跳转到照片详情页
② 显示点击的照片和照片的大小和上传时间
③ 可通过在照片上左、右滑动切换照片(以及照片的信息),可切换的照片范围为index中所显示的所有照片
设置封面
① 在detail视图中,如果上方显示的照片是封面,默认勾选“设为封面”复选框;
② 取消复选框的勾选,把状态传递到服务端,删除对应的封面设置
③ 点击复选框勾选,把状态传递到服务端,添加此照片为封面
④ 最多只能添加四个封面,如果勾选后此照片为第五个封面,提示“最多只能设置四个封面”,自动取消选中
如果设置和取消成功提示“封面设置成功”
查询封面
① 进入index页面时查询此用户的封面(轮播图),并显示,封面最多四张,自动播放。
② 在detail页面中设置封面时,更新此页面的封面列表

  1. detail.wxml

<view class="container">
<image src="{{photo.photoAccessUrl}}" style="padding: 60rpx;width: 100%;height: 400rpx;" bindtouchstart="start" bindtouchmove="end"/>
</view>
<view style="text-align: left;padding-top: 10rpx;">
<checkbox bindtap="fm" checked="{{isFm}}">设置封面</checkbox>
<text style="text-align: left;">\n图片大小:{{photo.photoSize}}</text>
<text>\n上传时间:{{uploadTime}}</text>
</view>
  1. detail.js

const app = getApp()
const {req}=require("../../utils/repuest")
Page({
  /**
   * 页面的初始数据
   */
  data: {
      photoList:[],
      index:0,
      photo:{},
      isFm:false,
      uploadTime:'',
      startPoint:0,
      startFlag:false,
      cover:{},
      coverLength:0
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad:function(res) {
      let photoList = JSON.parse(res.photoList);
      let index = res.index;
      let coverLength = res.coverLength;
      let photo = photoList[index];
      let uploadTime = new Date(Number.parseInt(photo.uploadTimestamp));
      let s = uploadTime.toLocaleString();
      this.setData({
        photoList:photoList,
        photo:photo,
        uploadTime:s,
        index:index,
        coverLength:coverLength
      });
      this.selectCover();
  },
  selectCover(){
    req({
      url:'/cover/getOne?accId='+app.globalData.userInfo.accId+"&photoId="+this.data.photo.photoId,
      method:'GET'
    }).then(res=>{
      if(res.data){
        this.setData({
          cover:res.data,
          isFm:true
        });
      }else{
        this.setData({
          isFm:false
        });
      }
    })
  },
  fm(){
    console.log(this.data.isFm);
    if(!this.data.isFm){
      console.log(this.data.coverLength);
      if(this.data.coverLength<4){
        req({
          url:'/cover/save',
          method:'POST',
          header:{
            "Content-Type":"application/x-www-form-urlencoded"
        },
          data:{accId:app.globalData.userInfo.accId,
                photoId:this.data.photo.photoId,
                accessUrl:this.data.photo.photoAccessUrl
          }
        }).then(res=>{
          this.setData({
            coverLength:Number.parseInt(this.data.coverLength)+1
          });
          this.selectCover();
        })
      }else{
        wx.showM({
          title: '封面只能设置四个',
        })
      }
    }else{
      req({
        url:'/cover/delete?coverId='+this.data.cover.coverId,
        method:'GET'
      }).then(res=>{
        this.setData({
          coverLength:Number.parseInt(this.data.coverLength)-1
        });
        this.selectCover();
      })
    }
  },
  start(e){
    this.setData({
      startPoint:e.touches[0].clientX,
      startFlag:true
    })
  },
  end(e){
    let point = this.data.startPoint;
    if(((point-e.touches[e.touches.length-1].clientX)>50) && this.data.startFlag){
      let index = this.data.index;
      console.log(index);
      if(index-1<0){
        index = this.data.photoList.length-1;
      }else{
        index = index-1;
      }
      let photo = this.data.photoList[index];
      let uploadTime = new Date(Number.parseInt(photo.uploadTimestamp));
      let s = uploadTime.toLocaleString();
      this.setData({
        photo:photo,
        index:index,
        uploadTime:s,
        startFlag:false,
        startPoint:0
      });
    }else if(((point-e.touches[e.touches.length-1].clientX)<-50) && this.data.startFlag) {
      let index = this.data.index;
      console.log(index);
      if(index+1>this.data.photoList.length-1){
        index = 0;
      }else{
        index = index+1;
      }
      let photo = this.data.photoList[index];
      let uploadTime = new Date(Number.parseInt(photo.uploadTimestamp));
      let s = uploadTime.toLocaleString();
      this.setData({
        photo:photo,
        index:index,
        uploadTime:s,
        startFlag:false,
        startPoint:0
      });
    }
    this.selectCover();
  },

  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady() {

  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow() {

  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide() {

  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload() {

  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh() {

  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom() {

  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage() {

  }
})

总结

作为一个微信小程序个人相册的开发者,我对这个项目有一些自我总结和反思。

首先,我认为成功的部分在于我能够充分理解用户的需求,并且提供了一个简单易用的相册功能。用户可以方便地上传照片、创建相册、查看照片信息等等。

其次,我采用了适当的设计和布局,使得小程序界面美观而且用户友好。我尽量减少了复杂的操作流程,让用户能够轻松地浏览和管理他们的照片。同时,我也在界面上保持了一定的一致性,使得用户可以很快熟悉和掌握小程序的使用方式。

然而,也存在一些需要改进的地方。首先,我认识到在小程序的功能方面还有一些局限性。例如,目前我只实现了基本的照片管理功能,但是还有很多其他可能的功能可以加入,比如照片编辑、滤镜效果、批量操作、分享照片、下载照片等等。这些功能可以提升用户的体验,并增加小程序的吸引力。

其次,我需要进一步改进小程序的性能和加载速度。尽管我尽力优化了代码和资源,但是随着用户上传的照片数量增加,小程序的加载速度可能会变慢。我需要继续寻找更好的方法来提高性能,以确保用户能够流畅地使用小程序。

最后,用户反馈对于改进小程序也非常重要。我会积极收集用户的意见和建议,并根据他们的反馈来改进小程序的功能和界面设计。通过不断改进和迭代,我相信我可以打造出一个更好的微信小程序个人相册。

总而言之,开发微信小程序个人相册是一次很有意义的经历。我学到了很多关于用户需求、设计和性能优化的知识,并且锻炼了我的开发技能。通过不断改进和学习,我希望能够为用户提供更好的相册体验,并且不断完善和扩展这个小程序。

后端介绍
最后献上整体项目源码:个人相册(前端)

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值