小程序笔记过程


框架扩展

一:mobx-miniprogram介绍

为了方便进行页面、组件之间数据的传递,小程序官方提供了一个扩展工具库: `mobx-miniprogram`

mobx-miniprogram` 是针对微信小程序开发的一个简单、高效、轻量级状态管理库,它基于Mobx状态管理框架实现。

使用 `mobx-miniprogram` 定义管理的状态是响应式的,当状态一旦它改变,所有关联组件都会自动更新相对应的数据通过该扩展工具库,开发者可以很方便地在小程序中全局共享的状态,并自动更新视图组件,从而提升小程序的开发效率

需要注意:在使用 `mobx-miniprogram` 需要安装两个包:`mobx-miniprogram` 和 `mobx-miniprogram-bindings

1.  mobx-miniprogram` 的作用:创建 `Store` 对象,用于存储应用的数据

2.  mobx-miniprogram-bindings` 的作用:将状态和组件、页面进行绑定关联,从而在组件和页面中操作数据


二:创建Store对象

如果需要创建 Store 对象需要使用 `mobx-miniprogram` ,因此需要先熟悉 `mobx-miniprogram` 三个核心概念:

1. `observable`:用于创建一个被监测的对象,对象的属性就是应用的状态(state),这些状态会被转换成响应式数据。

2. `action`:用于修改状态(state)的方法,需要使用 action 函数显式的声明创建。

3. `computed`:根据已有状态(state)生成的新值。计算属性是一个方法,在方法前面必须加上 `get` 修饰符

mobx-miniprogram` 详细的使用步骤如下:

1. 在项目的根目录下创建 `store` 文件夹,然后在该文件夹下新建 `index.js`

2. 在 `/store/index.js` 导入 `observable ` 、`action` 方法

3. 使用 `observable ` 方法需要接受一个 `store` 对象,存储应用的状态

import { observable, action } from 'mobx-miniprogram'
   
   // 使用 observable 创建一个被监测的对象
   export const numStore = observable({
   
     // 创建应用状态
     numA: 1,
     numB: 2,
       
     // 使用 action 更新 numA 以及 numB
     update: action(function () {
       this.numA+=1
       this.numB+=1
     }),
   
     // 计算属性,使用 get 修饰符,
     get sum() {
       return this.numA + this.numB;
     }
   
   })

在组件中使用数据

如果需要 `Page` 或者`Component`中对共享的数据进行读取、更新操作,需要使用 `mobx-miniprogram-bindings`

mobx-miniprogram-bindings` 的作用就是将 `Store` 和 页面或组件进行绑定关联

如果需要在组件中使用状态,需要 `mobx-miniprogram-bindings` 库中导入 `ComponentWithStore` 方法

在使用时:**<font color="red">需要将 `Component` 方法替换成 `ComponentWithStore` 方法 </font>**,原本组件配置项也需要写到该方法中

在替换以后,就会新增一个 `storeBindings` 配置项,配置项常用的属性有以下三个:

1. `store`: 指定要绑定的 `Store` 对象

2. `fields`: 指定需要绑定的 `data` 字段

3. `actions`: 指定需要映射的 `actions` 方法

 在页面中使用数据-方式1

Component 方法用于创建自定义组件。

小程序的页面也可以视为自定义组件,因此页面也可以使用 Component 方法进行构建,从而实现复杂的页面逻辑开发。

如果我们使用了 Component 方法来构建页面,那么页面中如果想使用 `Store` 中的数据,使用方式和组件的使用方式是一样的

1. 从 `mobx-miniprogram-bindings` 库中导入 `ComponentWithStore` 方法

2. 将 `Component` 方法替换成 `ComponentWithStore` 方法

3. 然后配置 `storeBindings` 从 `Store` 中映射数据和方法即可

// index/index.js
import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'

ComponentWithStore({
  data: {
    someData: '...'
  },
  storeBindings: {
    store: numStore,
    fields: ['numA', 'numB', 'sum'],
    actions: ['update']
  }
})

绑定多个 store 以及命名空间


在实际开发中,一个页面或者组件可能会绑定多个 `Store` ,这时候我们可以将 `storeBindings` 改造成数组。数组每一项就是一个个要绑定的 `Store`。


如果多个 `Store` 中存在相同的数据,显示会出现异常。还可以通过 `namespace` 属性给当前 `Store` 开启命名空间,在开启命名空间以后,访问数据的时候,需要加上 `namespace ` 的名字才可以。

import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
import { numStore } from '../../stores/numstore'

export const indexBehavior = BehaviorWithStore({
  storeBindings: [
    {
      namespace: 'numStore',
      store: numStore,
      fields: ['numA', 'numB', 'sum'],
      actions: ['update'],
    }
  ]
})

用户管理

什么是token

Token`是服务器生成的一串字符串,用作客户端发起请求的一个身份令牌。当第一次登录成功后,服务器生成一个 `Token` 便将此 `Token` 返回给客户端,客户端在接收到 `Token` 以后,会使用某种方式将 `Token` 保存到本地。以后客户端发起请求,只需要在请求头上带上这个 `Token` ,服务器通过验证 `Token` 来确认用户的身份,而无需再次带上用户名和密码。

登入流程介绍

1. 用户访问小程序,点击 [登录] ,调用 `wx.login()` 方法获取 **临时登录凭证code**

 临时登录凭证 code,就像是一个会过期的临时身份证一样,有效时间仅为 5分钟

2. 使用 `wx.request()` 方法将 **临时登录凭证code** 传递给开发者服务器,方便后续可以换取微信用户身份 id

3. 开发者的后台接收 **临时登录凭证code**,同时在微信公众后台拿到 `AppId` 和 `AppSecret` ,向微信服务器发送请求,

   请求参数合法的话,微信服务器会给开发者后台返回 openid(微信用户的唯一标识) 以及 session_key(会话密钥) 等

实现小程序登录功能

在 `/api/user.js`  文件中根据接口文档,创建登录的 `API` 函数 `login`

2. 给登录按钮绑定点击事件,对应 `login` 回调函数

3. 在 `login` 回调函数中调用 `wx.login()` 方法,获取**临时登录凭证code**

4. 在 `/pages/login/login.js` 中导入封装好的 `API` 函数,传入 **临时登录凭证code** 然后调用

5. 在登录成功以后将 `token` 存储到本地

```js
import http from '../utils/http'

/**
 * @description 授权登录
 * @param {*} code 临时登录凭证code
 * @returns Promise
 */
export const reqLogin = (code) => {
  return http.get(`/mall-api/weixin/wxLogin/${code}`)
}

import { reqLogin } from '../../api/user'
import { toast } from '../../utils/extendApi'

Page({
  // 点击登录
  login() {
    // 调用 wx.login 获取用户信息
    wx.login({
      success: async ({ code }) => {
        if (code) {
          // 调用接口 API,传入 code 进行登录
          const res = await reqLogin(code)

          // 登录成功以后将 token 存储到本地
          wx.setStorageSync('token', res.data.token)

          // 返回之前的页面
          wx.navigateBack()
        } else {
          // 登录失败后给用户进行提示
          toast({ title: '授权失败,请稍后再试~~~' })
        }
      }
    })
  }
})


用户信息存储到 Store

1. 安装`Mobx`两个包,在安装好包以后,对包进行构建,点击 `构建 npm`

2. 在项目的根目录下创建 `store` 文件夹,然后在该文件夹下新建 `userstore.js`

3. 导入核心的`observable ` 、`action` 方法,创建`Store`,同时声明数据和方法

4. 在登录页面,导入`ComponentWithStore` 方法,并配置 `storeBindings` 方法让页面和 `Store` 对象关联

// 导入 observable 函数用于创建可观察对象
// 导入 action 修改 store 中的可观察状态
import { observable, action } from 'mobx-miniprogram'
import { getStorage } from '../utils/storage'

// 创建 store 对象,存储应用的状态
export const userStore = observable({
  // 创建可观察状态 token
  token: getStorage('token') || '',

  // 对 token 进行修改
  setToken: action(function (token) {
    this.token = token
  })
})
import { reqLogin } from '../../api/user'
+ import { userStore } from '../../api/userstore'

+ import { ComponentWithStore } from 'mobx-miniprogram-bindings'

+ ComponentWithStore({
    
+   storeBindings: {
+     store: userStore,
+     fields: ['token'],
+     actions: ['setToken']
+   }

+   methods: {
    // 授权登录
    login() {
      // 使用 wx.login 获取用户的临时登录凭证 code
      wx.login({
        success: async ({ code }) => {
          if (code) {
            // 在获取到临时登录凭证 code 以后,需要传递给开发者服务器
            const { data } = await reqLogin(code)

            // 登录成功以后,需要将服务器响应的自定义登录态存储到本地
            setStorage('token', data.token)
              
+             // 将数据存储到 store 对象中
+             this.setToken(data.token)
          } else {
            toast({ title: '授权失败,请重新授权' })
          }
        }
      })
    }
+   }
})

获取用户信息并存储到 Store

1. 在 `store/userstore.js` 中新增`userInfo`字段,同时创建修改的`action`方法

2. 在 `login.js` 中使用映射 `userInfo` 数据和 `setUserInfo` 方法

3. 在 `/api/user.js`  文件中根据接口文档,创建获取用户信息的 `API` 函数 `reqUserInfo`

4. 在 `/pages/login/login.js` 中导入封装好的获取商品列表的 `API` 函数

5. 创建 `getUserInfo` 方法,在 `getUserInfo` 方法中调用接口 `API` 函数 `reqUserInfo`

6. 在登录成功以后,调用`getUserInfo` 方法获取用户,然后将用户信息存到本地以及 `Store`

/**
 * @description 获取用户信息
 * @returns Promise
 */
export const reqUserInfo = () => {
  return http.get(`/mall-api/weixin/getuserInfo`)
}

// 导入 observable 函数用于创建可观察对象
// 导入 action 修改 store 中的可观察状态
import { observable, action } from 'mobx-miniprogram'
import { getStorage } from '../utils/storage'

// 创建 store 对象,存储应用的状态
export const userStore = observable({
  // 创建可观察状态 token
  // token,登录令牌
  token: getStorage('token') || '',
+   // 用户信息
+   userInfo: wx.getStorageSync('userInfo') || {},

  // 对 token 进行修改
  setToken: action(function (token) {
    this.token = token
  }),
    
+   // 设置用户信息
+   setUserInfo: action(function (userInfo) {
+     this.userInfo = userInfo
+   })
})


使用数据渲染用户信息

import { ComponentWithStore } from 'mobx-miniprogram-bindings'

+ ComponentWithStore({
    
+   storeBindings: {
+     store: userStore,
+     fields: ['token', 'userInfo']
+   }

})


<!--pages/info/info.wxml-->
<view class="container bg">
  <!-- 顶部展示图 -->
  <view class="top-show">
    <image mode="widthFix" class="top-show-img" src="/static/images/banner.jpg"></image>
  </view>
  <view class="wrap">

    <!-- 未登录面板 -->
+     <view class="user-container section" wx:if="{{ !token }}" bindtap="toLoginPage">
      <view class="avatar-container">
        <image src="/static/images/avatar.png"></image>
        <view class="no-login">
          <text class="ellipsis">未登录</text>
          <text>点击授权登录</text>
        </view>
      </view>
    </view>

+     <!-- 登录以后得面包 -->
+     <view wx:else class="user-container section">
+       <view class="avatar-container">
+         <image src="{{ userInfo.headimgurl }}"></image>
+         <view class="no-login">
+           <text class="ellipsis">{{ userInfo.nickname }}</text>
+         </view>
+       </view>
+       <view class="setting">
+         设置
+       </view>
+     </view>

    <!-- 订单面板 -->
    <view class="order section">
      <view class="order-title-wrap">
        <text class="title">我的订单</text>
        <text class="more">查看更多></text>
      </view>
      <view class="order-content-wrap">
        <view class="order-content-item">
+           <navigator wx:if="{{ !token }}" url="/pages/login/login">
+             <view class="iconfont icon-dingdan"></view>
+             <text>商品订单</text>
+           </navigator>
+           <navigator wx:else url="/pages/order/list/index">
+             <view class="iconfont icon-dingdan"></view>
+             <text>商品订单</text>
+           </navigator>
        </view>
        <view class="order-content-item">
          <view class="iconfont icon-lipinka"></view>
          <text>礼品卡订单</text>
        </view>
        <view class="order-content-item">
          <view class="iconfont icon-tuikuan"></view>
          <text>退款/售后</text>
        </view>

      </view>
    </view>

    <!-- 关于售前售后服务面板 -->
    <view class="after-scale section">
      <!-- coding... -->
    </view>

    <!-- 底部面板 -->
    <view class="info-footer">
    111
    </view>
  </view>
</view>

用户信息

更新用户信息-渲染用户信息

1. 新建 `behavior.js` 文件,从 `mobx-miniprogram-bindings` 库中导入 `BehaviorWithStore` 方法

2. 在 `BehaviorWithStore` 方法中配置 `storeBindings` 配置项从 `Store` 中映射数据和方法

3. 在 `Page` 方法中导入创建的 `behavior` ,然后配置 `behavior` 属性,并使用导入的 `behavior`

// behavior.js

import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
// 导入 store 对象
import { userStore } from '../../stores/userstore'

export const userBehavior = BehaviorWithStore({
  storeBindings: {
    store: userStore,
    fields: ['userInfo']
  }
})
import { userBehavior } from './behavior'

Page({
    
  behaviors: [userBehavior],
    
  // 页面的初始数据
  data: {
    isShowPopup: false // 控制更新用户昵称的弹框显示与否
  },

  // 其他代码略...
})

更新用户信息-获取头像临时路径

1. 给 `button` 按钮绑定 `open-type` 属性,值为 `chooseAvatar`

2. 用户点击了头像后,在 `bindchooseavatar` 事件回调获取到头像信息的临时路径

<view class="avatar">
  <button
    class="avatar-btn"
    hover-class="none"
+     open-type="chooseAvatar"
+     bindchooseavatar="chooseAvatar"
  >
    <image src="{{ userInfo.headimgurl || '/assets/images/avatar.png' }}" />
  </button>
</view>

 更新用户信息-头像上传到服务器

1. 在获取到用户的临时头像路径以后,调用 `wx.uploadFile()` 方法,同时设置好基本的参数,

2. 在上传成功后,获取到服务器返回的永久地址

3. 将地址赋值给 `data` 中的数据

地址管理

1. 在新增收货地址页面 `data` 中声明所需要的字段

2. 定义收货地址所需要的全部接口 `API` 函数

Page{{
  
  // 页面的初始数据
  data: {
	  name: '', // 收货人
      phone: '', // 手机号
      provinceName: '', // 省
      provinceCode: '', // 省 编码
      cityName: '', // 市
      cityCode: '', // 市 编码
      districtName: '', // 区
      districtCode: '', // 区 编码
      address: '',  // 详细地址
      fullAddress: '', // 完整地址 (省 + 市 + 区 + 详细地址)
      isDefault: 0 // 设置默认地址,是否默认地址 → 0:否  1:是
  }
}}
import http from '../utils/http'

/**
 * @description 实现新增收货地址
 * @param {*} data
 * @returns Promise
 */
export const reqAddAddress = (data) => {
  return http.post('/userAddress/save', data)
}

/**
 * @description 获取收货地址列表
 * @returns Promise
 */
export const reqAddressList = () => {
  return http.get('/userAddress/findUserAddress')
}

/**
 * @description 获取收货地址详情
 * @param {*} id 收货地址id
 * @returns Promise
 */
export const reqAddressInfo = (id) => {
  return http.get(`/userAddress/${id}`)
}

/**
 * @description 编辑收货地址
 * @param {*} data
 * @returns Promise
 */
export const reqUpdateAddress = (data) => {
  return http.post('/userAddress/update', data)
}

/**
 * @description 删除收货地址
 * @param {*} id 收货地址 id
 * @returns Promise
 */
export const reqDelAddress = (id) => {
  return instance.get(`/userAddress/delete/${id}`)
}

```

收集省市区数据

1. 给 `picker` 选择组件添加`change` 事件来监听属性值的改变,获取选中的省市区

2. 将获取到省市区标识和编码赋值给 `data`中的字段

// 省市区选择
  onAddressChange(event) {
    const [provinceCode, cityCode, districtCode] = event.detail.code
    const [provinceName, cityName, districtName] = event.detail.value

    // 存储省市区对应的编码
    this.setData({
      provinceCode,
      provinceName,
      cityCode,
      cityName,
      districtName,
      districtCode
    })
  }
    
  // coding...
})

商品管理

01. 配置商品管理分包

1. 在 `modules` 目录下创建 `goodModule` 文件夹,用来存放商品管理分包

2. 在 `app.json` 的 `subpackages` 进行商品管理分包配置

5. 在 `app.json` 的 `preloadRule` 进行商品管理分包配置

{
  "subPackages": [
    {
      "root": "modules/settingModule",
      "name": "settingModule",
      "pages": [
        "pages/address/add/index",
        "pages/address/list/index",
        "pages/profile/profile"
      ]
    },
+     {
+       "root": "modules/goodModule",
+       "name": "goodModule",
+       "pages": ["pages/goods/list/list", "pages/goods/detail/detail"]
    }
  ],
  "preloadRule": {
    "pages/settings/settings": {
      "network": "all",
      "packages": ["settingModule"]
    },
+     "pages/category/category": {
+       "network": "all",
+       "packages": ["goodModule"]
+     }
  }
}

02. 封装商品模块接口 API

为了方便后续进行商品管理模块的开发,我们在这一节将商品管理所有的接口封装成接口 API 函数

import http from '../utils/http'

/**
 * @description 获取商品列表
 * @return Promise
 */
export const reqGoodsList = ({ limit, page, ...reset }) => {
  return http.get(`/mall-api/goods/list/${page}/${limit}`, reset)
}

/**
 * @description 获取商品详情
 * @param {*} goodsId 商品Id
 * @returns Promise
 */
export const reqGoodsInfo = (goodsId) => {
  return http.get(`/mall-api/goods/${goodsId}`)
}

03. 商品列表-准备列表请求参数

1. 在商品列表的 `data` 字段中,根据接口文档,定义商品列表接口需要使用的字段

2. 在商品列表的 `onLoad` 钩子函数中接收请求的参数,并将请求参数进行合并

Page({
  // 页面的初始数据
  data: {
    goodsList: [], // 商品列表数据
    isFinish: false, // 判断数据是否加载完毕

+     // 接口请求参数
+     requestData: {
+       page: 1, // 页码
+       limit: 10, // 每页请求多少条数据
+       category1Id: '', // 一级分类 id
+       category2Id: '' // 二级分类 id
+     }
  },

+     // 生命周期函数--监听页面加载
+     onLoad(options) {
+       // 接收传递的参数
+       Object.assign(this.data.requestData, options)
+     }
})

04. 商品列表-获取商品列表数据并渲染

1. 在 `/pages/goods/list/list.js` 中导入封装好的获取商品列表的 `API` 函数

2. 页面数据在页面加载的时候进行调用,在 `onLoad` 钩子函数中调用  `reqGoodsList` 方法

3. 在获取到数据以后,使用后端返回的数据对页面进行渲染

Page({
  // 页面的初始数据
  data: {
    goodsList: [], // 商品列表数据
+     total: 0, // 数据总条数
    isFinish: false, // 判断数据是否加载完毕
    // 接口请求参数
    requestData: {
      page: 1, // 页码
      limit: 10, // 每页请求多少条数据
      category1Id: '', // 一级分类 id
      category2Id: '' // 二级分类 id
    }
  },

+   // 获取商品列表的数据
+   async getGoodsList() {
+     // 调用 API 获取数据
+     const { data } = await reqGoodsList(this.data.requestData)
+ 
+     // 将返回的数据赋值给 data 中的变量
+     this.setData({
+       goodsList: data.records,
+       total: data.total
+     })
+   },

  // 生命周期函数--监听页面加载
  onLoad(options) {
    // 接收传递的参数
    Object.assign(this.data.requestData, options)

+    // 获取商品列表的数据
+    this.getGoodsList()
  }
})

06. 商品列表-判断数据是否加载完毕

1. 在数据返回以后,将数据中的 `total` 赋值给 `data` 中的变量 `total`

2. 在 `onReachBottom` 中进行 `total` 和 `goodsList` 进行对比

3. 模板中使用 `total` 和 `goodsList` 进行对比

import { reqGoodsList } from '../../../api/goods'

Page({
    
  // coding...
    
  // 监听页面的上拉操作
  onReachBottom() {
+     // 从 data 中解构数据
+     const { total, goodsList, requestData } = this.data
+     let { page } = requestData
+ 
+     // 判断数据是否加载完毕
+     if (total === goodsList.length) {
+       // 如果相等,数据数据加载完毕
+       // 如果数据加载完毕,需要给用户提示,同时不继续加载下一个数据
+       this.setData({
+         isFinish: true
+       })
+ 
+       return
+     }

    // 页码 + 1
    this.setData({
      requestData: { ...this.data.requestData, page: (page += 1) }
    })

    // 重新发送请求
    this.getGoodsList()
  }
})

08. 商品列表-实现下拉刷新功能

1. 在`页面.json` 中开启允许下拉,同时可以配置 窗口、loading 样式等

2. 在`页面.js` 中定义 `onPullDownRefresh` 事件监听用户下拉刷新

{
  "usingComponents": {
    "goods-card": "/components/goods-card/goods-card"
  },

  "navigationBarTitleText": "商品列表",
  "enablePullDownRefresh": true,
  "backgroundColor": "#f7f4f8",
  "backgroundTextStyle": "dark"
}

购物车

01. 购物车-封装购物车接口 API

为了方便后续进行购物车模块的开发,我们在这一节将购物车所有的接口封装成接口 API 函数

import http from '../utils/http'

/**
 * @description 获取购物车列表数据
 * @returns Promise
 */
export const reqCartList = () => {
  return http.get('/mall-api/cart/getCartList')
}

/**
 * @description 加入购物车
 * @param {*} data
 * @returns Promise
 */
export const reqAddCart = (data) => {
  return http.get(`/cart/addToCart/${data.goodsId}/${data.count}`, data)
}

/**
 * @description 更新商品的选中状态
 * @param {*} goodsId 商品 id
 * @param {*} isChecked 商品的选中状态
 * @returns Promise
 */
export const reqUpdateChecked = (goodsId, isChecked) => {
  return http.get(`/cart/checkCart/${goodsId}/${isChecked}`)
}

/**
 * @description 全选和全不选
 * @param {*} isChecked 商品的选中状态
 * @returns Promise
 */
export const reqCheckAllCart = (isChecked) => {
  return http.get(`/cart/checkAllCart/${isChecked}`)
}

/**
 * @description 删除购物车商品
 * @param {*} goodsId 商品 id
 * @returns Promise
 */
export const reqDelCart = (goodsId) => {
  return http.get(`/cart/delete/${goodsId}`)
}

02. 加入购物车-模板分析和渲染

1. 如果点击的是加入购物车,需要将当前商品加入到购物车

2. 如果点击的是立即购买,需要跳转到结算支付页面,立即购买该商品

3. 如果是立即购买,不支持购买多个商品

<!-- 商品的底部商品导航 -->
<van-goods-action>
  <!-- coding... -->
+   <van-goods-action-button text="加入购物车" type="warning" bindtap="handleAddcart" />
+   <van-goods-action-button text="立即购买" bindtap="handeGotoBuy" />
</van-goods-action>

<!-- 加入购物车、立即购买弹框 -->
<!-- show 控制弹框的隐藏和展示 -->
<!-- bind:close 点击关闭弹框时触发的回调 -->
<van-action-sheet show="{{ show }}" bind:close="onClose">
  <view class="sheet-wrapper">
      
    <!-- 代码略... -->
      
    <!-- 购买数量弹框 -->
+     <view class="buy-btn" wx:if="{{ buyNow === 0 }}">
      <!-- Stepper 步进器,由增加按钮、减少按钮和输入框组成,控制购买数量 -->
      <van-stepper value="{{ count }}" bind:change="onChangeGoodsCount" />
    </view>
      
    <!-- 代码略... -->
  </view>

点击立即购买和加入购物车的时候,通过 buyNow 属性,来区分是进行的某种操作

Page({
 
  // 页面的初始数据
  data: {
    goodsInfo: {}, // 商品详情
    show: false, // 加入购物车和立即购买时显示的弹框
    count: 1, // 商品购买数量,默认是 1
    blessing: '', // 祝福语
+     buyNow: '' // 是否立即购买
  },
  

  // 加入购物车
  handleAddcart() {
    this.setData({
      show: true,
+       buyNow: 0
    })
  },

  // 立即购买
  handeGotoBuy() {
    this.setData({
      show: true,
+       buyNow: 1
    })
  },

03. 加入购物车-关联 Store 对象

当用户点击加入购物车 或者 立即购买时,需要判断用户是否进行了登录。我们需要使用 `Token` 进行判断,因此需要让页面和 `Store` 对象建立关联。这时候可以使用 `BehaviorWithStore` 让页面 和 `Store` 对象建立关联

// 导入 BehaviorWithStore 让页面和 Store 对象建立关联
import { BehaviorWithStore } from 'mobx-miniprogram-bindings'
// 导入用户 Store
import { userStore } from '@/stores/userstore'

export const userBehavior = BehaviorWithStore({
  storeBindings: {
    store: userStore,
    fields: ['token']
  }
})

04. 加入购物车和立即购买区分处理

1. 给 `Stepper` 步进器组件,通过`value`设置输入值,同时绑定`change`事件,并将值同步到 `data` 中

2. 根据接口文档,导入封装的购物车的接口 API

3. 点击弹框按钮的时候,判断点击的加入购物车还是立即购买,执行不同的操作

<van-stepper
  value="{{ count }}"
+  integer
+  min="1"
+  max="200"
  bind:change="onChangeGoodsCount"
/>
// 监听是否更改了购买数量
onChangeGoodsCount(event) {
  // 将最新的购买数量同步到 data
  this.setData({
    count: Number(event.detail)
  })
},

// 弹框的确定按钮
async handleSubmit() {
 // 解构获取数据
 const { token, count, blessing, buyNow } = this.data
 const goodsId = this.goodsId
  
  // 如果没有 token ,让用户新登录
  if (!this.data.token) {
    wx.navigateTo({
      url: '/pages/login/login'
    })
      
    return
  }

  // 将用户输入的值转成 Number 类型
  const count = Number(event.detail)
  // 验证购买数量的正则
  const reg = /^([1-9]|[1-9]\d|1\d{2}|200)$/
  // 使用正则验证
  const res = reg.test(count)

  // 如果验证没有通过,直接返回,不执行后续的逻辑
  if (!res) return
 

  // 加入购物车
  if (buyNow === 0) {
    // 加入购物车
    const res = await reqAddGood({ goodsId, count, blessing })

    if (res.code === 200) {
        
      wx.showToast({
        title: '加入购物车成功'
      })
      
      this.setData({
        show: false
      })
        
    }
  } else {
    // 立即购买
    wx.navigateTo({
      url: `/pages/order/detail/index?goodsId=${goodsId}&blessing=${blessing}`
    })
  }
}

05. 加入购物车-展示购物车购买数量

1. 进入商品详情,调用方法,在方法中判断`token`是否存在

2. 如何存在,则获取购物车列表数据,通过累加计算得出商品购买数量,展示购买的数量

3. 不存在,不执行任何逻辑,

Page({
    
  data: {
    // coding...
+     allCount: '' // 购物车商品总数量
  },
    
      // 弹框的确定按钮
  async handleSubmit() {
    // 如果没有 token ,让用户新登录
    if (!this.data.token) {
      wx.navigateTo({
        url: '/pages/login/login'
      })

      return
    }

    // 解构获取数据
    const { count, blessing, allCount } = this.data
    const goodsId = this.goodsId

    // 加入购物车
    if (this.data.buyNow === 0) {
      // 加入购物车
      const res = await reqAddCart({ goodsId, count, blessing })

      if (res.code === 200) {
        wx.toast({
          title: '加入购物车成功',
          icon: 'success',
          mask: false
        })

+         // 购物车购买数量合计
+         this.getCartCount()

        this.setData({
          show: false
        })
      }
    } else {
      // 立即购买
      wx.navigateTo({
        url: `/pages/order/detail/detail?goodsId=${goodsId}&blessing=${blessing}`
      })
    }
  },
  
+   // 计算购买数量
+   async getCartCount() {
+     // 如果没有 token ,说明用户是第一次访问小程序,没有进行登录过
+     if (!this.data.token) return
+ 
+     // 获取购物的商品
+     const res = await reqCartList()
+ 
+     if (res.data.length !== 0) {
+       // 购物车商品累加
+       let allCount = 0
+ 
+       // 获取购物车商品数量
+       res.data.forEach((item) => {
+         allCount += item.count
+       })
+ 
+       // 将购物车购买数量赋值
+       this.setData({
+         // 展示的数据要求是字符串
+         allCount: (allCount > 99 ? '99+' : allCount) + ''
+       })
+     }
+   },

  onLoad(options) {
    // 接收传递的商品 ID,并且将 商品 ID 挂载到 this 上面
    this.goodsId = options.goodsId

    // 调用获取商品详情数据的方法
    this.getGoodsInfo()

+     // 计算购买数量
+     this.getCartCount()
  }
    
  // coding...
})

06. 购物车-购物车关联 Store 对象

当用户进入购物车页面时时,需要判断用户是否进行了登录来控制页面的展示效果这时候我们就需要使用 `Token` 进行判断,因此需要让页面和 `Store` 对象建立关联。因为购物车页面采用的 `Component` 方法进行构建这时候可以使用 `ComponentWithStore` 让页面 和 `Store` 对象建立关联。

import { ComponentWithStore } from 'mobx-miniprogram-bindings'
+ import { userStore } from '@/stores/userstore'
+ import { reqCartList } from '@/api/cart'

+ ComponentWithStore({
+   storeBindings: {
+     store: userStore,
+     fields: ['token']
+   },

  // 组件的初始数据
  data: {
    cartList: [],
    emptyDes: '还没有添加商品,快去添加吧~'
  },

+   // 组件的方法列表
+   methods: {
+     // 处理页面的展示
+     async showTipList() {
+       // 将 token 进行解构
+       const { token } = this.data
+
+ 		console.log(token)
+     },

    onShow() {
+       this.showTipList()
    }
  }
})

07. 购物车-获取并渲染购物车列表

1. 导入封装好的获取列表数据的 `API` 函数

4. 在 `onShow` 钩子中,根据产品的需求,处理页面的提示

5. 在获取到数据以后,使用后端返回的数据对页面进行渲染

import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { userStore } from '@/stores/userstore'
import { reqCartList } from '@/api/cart'

ComponentWithStore({
  storeBindings: {
    store: userStore,
    fields: ['token']
  },

  // 组件的初始数据
  data: {
    cartList: [],
    emptyDes: '还没有添加商品,快去添加吧~'
  },

  // 组件的方法列表
  methods: {
+     // 获取购物车列表数据 + 处理页面的展示
+     async showTipGetList() {
+       // 将 token 进行解构
+       const { token } = this.data
+ 
+       // 1. 如果没有登录,购物车列表,展示文案:您尚未登录,点击登录获取更多权益
+       if (!token) {
+         this.setData({
+           emptyDes: '您尚未登录,点击登录获取更多权益',
+           cartList: []
+         })
+ 
+         return
+       }
+ 
+       // 获取商品列表数据
+       const { data: cartList, code } = await reqCartList()
+ 
+       if (code === 200) {
+         // 2. 如果用户登录,购物车列表为空,展示文案: 还没有添加商品,快去添加吧~
+         this.setData({
+           cartList,
+           emptyDes: cartList === 0 && '还没有添加商品,快去添加吧~'
+         })
+       }
+     },

    // 页面展示时触发
    onShow() {
+       this.showTipGetList()
    }
  }
})
<view>
  <view
    wx:if="{{ token && cartList.length }}"
    class="container goods-wrap"
    bindtap="onSwipeCellPageTap"
  >
    <view class="cart-wrap">
+       <view class="goods-item" wx:for="{{ cartList }}" wx:key="id">
        <van-swipe-cell class="goods-swipe" right-width="{{ 65 }}">
          <view class="goods-info">
            <view class="left">
              <van-checkbox
                checked-color="#FA4126"
+                 value="{{ item.checked }}"
              ></van-checkbox>
            </view>
            <view class="mid">
+               <image class="img" src="{{ item.imageUrl }}" />
            </view>
            <view class="right">
+               <view class="title"> {{ item.name }} </view>
              <view class="buy">
                <view class="price">
                  <view class="symbol">¥</view>
+                   <view class="num">{{ item.price }}</view>
                </view>
                <view class="buy-btn">
+                   <van-stepper value="{{ item.count }}" />
                </view>
              </view>
            </view>
          </view>
          <view slot="right" class="van-swipe-cell__right">删除</view>
        </van-swipe-cell>
      </view>
    </view>

    <!-- 底部工具栏 -->
    <van-submit-bar price="{{ 3050 }}" button-text="去结算" tip="{{ true }}">
      <van-checkbox value="{{ true }}" checked-color="#FA4126"> 全选 </van-checkbox>
    </van-submit-bar>
  </view>

  <van-empty wx:else description="{{ emptyDes }}">
+     <navigator url="/pages/index/index" wx:if="{{ token }}">
+       <van-button round type="danger" class="bottom-button">去购物</van-button>
+     </navigator>
+ 
+     <navigator url="/pages/login/login" wx:else>
+       <van-button round type="danger" class="bottom-button">去登录</van-button>
+     </navigator>
  </van-empty>
</view>

08. 购物车-更新商品的购买状态

1. 导入封装好的获取列表数据的 `API` 函数

3. 当点击切换切换商品状态的时候,调用 `reqUpdateGoodStatus`,并传参

4. 在更新成功,将本地的数据一并改变。

<van-checkbox
  checked-color="#FA4126"
+   value="{{ item.isChecked }}"
+   bind:change="updateChecked"
+   data-id="{{ item.goodsId }}"
+   data-index="{{ index }}"
></van-checkbox>

import { ComponentWithStore } from 'mobx-miniprogram-bindings'
import { userStore } from '@/stores/userstore'

+ import { reqCartList, reqUpdateChecked } from '@/api/cart'

Component({

  // coding...

  // 组件的方法列表
  methods: {
    // 切换商品的选中状态
    async updateChecked(event) {
      // 获取最新的选中状态
      const { detail } = event
      // 获取商品的索引和 id
      const { id, index } = event.target.dataset
      // 将最新的状态格式化成后端所需要的数据格式
      const isChecked = detail ? 1 : 0

      // 调用接口,传入参数,更新商品的状态
      const res = await reqUpdateChecked(id, isChecked)

      // 如果数据更新成功,需要将本地的数据一同改变
      if (res.code === 200) {
        this.setData({
          [`cartList[${index}].isChecked`]: isChecked
        })
      }
    },

    // 获取购物车列表数据
    async getCartList() {
      // coding...
    }
  }
})

结算支付

01. 配置分包并跳转到结算页面

随着项目功能的增加,项目体积也随着增大,从而影响小程序的加载速度,影响用户的体验。

因此我们需要将 `结算支付` 功能配置成一个分包,当用户在访问设置页面时,还预先加载 `结算支付` 所在的分包

subPackages": [
  {
    "root": "modules/settingModule",
    "name": "settingModule",
    "pages": [
      "pages/address/add/index",
      "pages/address/list/index",
      "pages/profile/profile"
    ]
  },
  {
    "root": "modules/goodModule",
    "name": "goodModule",
    "pages": ["pages/goods/list/list", "pages/goods/detail/detail"]
  },
+   {
+     "root": "modules/orderPayModule",
+     "name": "orderPayModule",
+     "pages": [
+       "pages/order/detail/detail",
+       "pages/order/list/list"
+     ]
+   }
],
"preloadRule": {
  "pages/settings/settings": {
    "network": "all",
    "packages": ["settingModule"]
  },
  "pages/category/category": {
    "network": "all",
    "packages": ["goodModule"]
  },
+   "pages/cart/cart": {
+     "network": "all",
+     "packages": ["orderPayModule"]
+   }
}
// 跳转到订单结算页面
toOrder() {
  if (this.data.totalPrice === 0) {
    wx.toast({
      title: '请选择需要购买的商品'
    })

    return
  }

  // 跳转到订单的结算页面
  wx.navigateTo({
    url: '/modules/orderPayModule/pages/order/detail/detail'
  })
}
<van-submit-bar
  wx:if="{{ cartList.length }}"
  price="{{ totalPrice * 100 }}"
  button-text="去结算"
  tip="{{ true }}"
+  bindsubmit="toOrder"
>
  <van-checkbox
    value="{{ selectAllStatus }}"
    checked-color="#FA4126"
    bindchange="selectAllStatus"
  >
    全选
  </van-checkbox>
</van-submit-bar>

02. 封装结算支付的接口 API

为了方便后续进行结算支付模块的开发,我们在这一节将结算支付所有的接口封装成接口 API 函数

import http from '@/utils/http'

/**
 * @description 获取订单详情
 * @returns Promise
 */
export const reqOrderInfo = () => {
  return http.get('/order/trade')
}

/**
 * @description 获取订单列表
 * @param {*} page 页码
 * @param {*} limit 每页展示的条数
 * @returns Promise
 */
export const reqOrderList = (page, limit) => {
  return http.get(`/order/order/${page}/${limit}`)
}

/**
 * @description 获取订单收货地址
 * @returns Promise
 */
export const reqOrderAddress = () => {
  return http.get('/userAddress/getOrderAddress')
}

/**
 * @description 获取立即购买商品的详情信息
 * @param { Object } params { goodsId: 商品 Id,  blessing:祝福语 }
 * @returns Promise
 */
export const reqBuyNowGoods = ({ goodsId, ...data }) => {
  return http.get(`/order/buy/${goodsId}`, data)
}

/**
 * @description 提交订单
 * @returns Promise
 */
export const reqSubmitOrder = () => {
  return http.post('/order/submitOrder')
}

/**
 * @description 获取微信预支付信息
 * @param {*} orderNo 订单 ID
 * @returns Promise
 */
export const reqPreBuyInfo = (orderNo) => {
  return http.get(`/webChat/createJsapi/${orderNo}`)
}

/**
 * @description 微信支付状态查询
 * @param {*} orderNo
 * @returns Promise
 */
export const reqPayStatus = (orderNo) => {
  return http.get(`/webChat/queryPayStatus/${orderNo}`)
}

03. 商品结算-获取收货地址

1. 在进入结算页面的时候,调用接口 `API` 函数,获取数据

2. 然后根据数据并渲染结构

import { getTradeAddress } from '../../../api/order'

Page({
    
  data: {
    // coding...
+     orderAddress: {} // 收货地址
  },

+   // 获取收货地址
+   async getAddress() {
+     const { data: orderAddress } = await reqOrderAddress()
+ 
+     this.setData({
+       orderAddress
+     })
+   },

+   // 页面展示时触发的钩子函数
+   onShow() {
+     this.getAddress()
+   }
})
<!--pages/order/index.wxml-->
<view class="container order">
  <view class="address-card">
    <!-- 添加收货地址 -->
    <view wx:if="{{ !tradeAddress.id }}" class="add-address"  bindtap="toAddress">
      <van-icon size="22px" name="add" />
      <view>添加收货地址</view>
    </view>

    <view wx:else class="order-address flex">
      <view class="address-content">
        <view class="title">{{ tradeAddress.fullAddress }}</view>
        <view class="info flex">
          <text>{{ tradeAddress.name }}</text>
          <text>{{ tradeAddress.phone }}</text>
        </view>
      </view>

      <view class="select-address">
        <navigator class="navigator" url="/modules/settingModule/pages/address/list/index">
          <van-icon color="#bbb" name="arrow" size="22px" />
        </navigator>
      </view>
    </view>

    <view class="top-line"></view>
  </view>

  <view class="order-info">
    <!-- coding... -->
  </view>

</view>

 04. 商品结算-更新收货地址功能

1. 在 `app.js` 中定义全局共享的数据 `globalData.address`

2. 点击箭头,携带参数跳转到收货地址页面,标识是从订单结算页面进入

3. 在选择收货地址成功以后,将数据存储到 `globalData.address`中,然后返回到订单结算页面。

4. 在订单结算页面判断 `globalData.address` 是否存在收货地址数据,如果存在则渲染

App({
    
+   // 定义全局共享的数据
+   globalData: {
+    address: {}
+  }
    
  // coding...
})
<view
  class="info"
+   bindtap="changeAddress"
+   data-id="{{ item.id }}"
>
  <view class="user-info">
    <text>{{ item.name }}</text>
    <text>{{ item.phone }}</text>
    <text wx:if="{{ item.isDefault === 1 }}" class="default-tag">默认</text>
  </view>

  <view class="address-info"> {{ item.fullAddress }} </view>
</view>
// 导入接口 API 函数
import { reqAddressList, reqDelAddress } from '@/api/address'
import { swipeCellBehavior } from '@/behaviors/swipeCell'

+ // 获取全局的应用实例
+ const app = getApp()

Page({

  // coding...

+   // 切换收货地址
+   changeAddress(event) {
+     // 判断是否是从订单结算页面进入
+     if (this.flag !== '1') return
+ 
+     // 获取到点击的收货地址 id
+     const addressId = event.currentTarget.dataset.id
+     // 从收货地址列表中获取到获取到点击的收货地址详细信息
+     const address = this.data.addressList.find((item) => item.id === addressId)
+ 
+     // 如果获取成功,将数据存储到 globalData 中
+     if (address) {
+       app.globalData.address = address
+       wx.navigateBack()
+     }
+   },

+   onLoad(options) {
+     this.flag = options.flag
+   }
})
<view class="select-address">
  <navigator
    class="navigator"
+     url="/modules/settingModule/pages/address/list/index?flag=1"
  >
    <van-icon color="#bbb" name="arrow" size="22px" />
  </navigator>
</view>
// 获取订单页面的收货地址
  async getAddress() {
      
+     // 如果 globalData 存在收货地址,取出收货地址
+     if (app.globalData.address.id) {
+       this.setData({
+         orderAddress: app.globalData.address
+       })
+ 
+       // 在赋值以后需要将收货地址清空
+       app.globalData.address = {}
+ 
+       return
+     }

    // 如果 globalData 中不存在收货地址,获取收货地址渲染即可
    const { data: orderAddress } = await reqOrderAddress()

    this.setData({
      orderAddress
    })
  },

订单列表

1.获取订单页面的收货地址

为了方便后续进行商品管理模块的开发,我们在这一节将商品管理所有的接口封装成接口 API 函数

/**
 * @description 获取订单列表
 * @returns Promise
 */
export const reqOrderList = (page, limit) => {
  return http.get(`/order/order/${page}/${limit}`)
}

02. 获取订单列表数据并渲染

当用户从个人中心页面点击进入订单中心的时候,就需要获取到订单中心的数据。

在页面调用 `API` 函数获取订单列表的数据,

在获取到数据以后,使用后端返回的数据对页面进行渲染

/ 导入封装的接口 API 函数
+ import { reqOrderList } from '@/api/orderpay'

Page({
  // 页面的初始数据
  data: {
    orderList: [1, 2, 3], // 订单列表
+     page: 1, // 页码
+     limit: 10, // 每页展示的条数
+     total: 0 // 订单列表总条数
  },

+   // 获取订单列表
+   async getOrderList() {
+     // 解构获取数据
+     const { page, limit } = this.data
+     // 调用接口获取订单列表数据
+     const res = await reqOrderList(page, limit)
+ 
+     if (res.code === 200) {
+       this.setData({
+         orderList: res.data.records,
+         total: res.data.total
+       })
+     }
+   },

+   // 生命周期函数--监听页面加载
+   onLoad() {
+     this.getOrderList()
+   }

})

<!--pages/order/list/index.wxml-->
<view class="order-container container">
+   <view class="order-list" wx:if="{{ orderList.length > 0 }}">
+     <view class="order-item" wx:for="{{ orderList }}" wx:key="index">
+       <view class="order-item-header list-flex">
        <view class="orderno">订单号<text class="no">{{ orderList.orderNo }}</text></view>
+         <view class="order-status {{ item.orderStatus === 1 ? 'order-active' : '' }}">
+           {{ item.orderStatus === 1 ? '已支付' : '未支付'}}
+         </view>
      </view>
      <view
        class="goods-item list-flex"
+         wx:for="{{ item.orderDetailList }}"
+         wx:key="id"
+         wx:for-item="goods"
+         wx:for-index="goodsIndex"
      >
        <view class="left">
+           <image src="{{ goods.imageUrl }}" mode="widthFix" class="img" />
        </view>
        <view class="mid">
+           <view class="goods-name">{{ goods.name }}</view>
+           <view class="goods-blessing">{{ goods.blessing }}</view>
        </view>
        <view class="right">
+           <view class="goods-price">¥{{ goods.price }}</view>
+           <view class="goods-count">x{{ goods.count }}</view>
        </view>
      </view>
      <view class="order-item-footer">
        <view class="total-amount list-flex">
          <text class="text">实付</text>
+           <text class="price"><text>¥</text>{{ item.totalAmount }}</text>
        </view>
      </view>
    </view>
  </view>
  <van-empty wx:else description="还没有购买商品,快去购买吧~" />
</view>

03. 订单列表上拉加载更多

当用户进行了上拉操作时,需要在 `.js` 文件中声明 `onReachBottom` 方法,用来监听页面的上拉触底行为当用户上拉时,需要对 `page` 参数进行加 1 即可,当参数发生改变后,需要重新发送请求,拿最新的 `page` 向服务器要数据在下一页的商品数据返回以后,需要将下一页的数据和之前的数据进行合并

// 导入封装的接口 API 函数
import { reqOrderList } from '@/api/orderpay'

Page({
  // 页面的初始数据
  data: {
    orderList: [1, 2, 3], // 订单列表
    page: 1, // 页码
    limit: 10, // 每页展示的条数
    total: 0 // 订单列表总条数
  },

  // 获取订单列表
  async getOrderList() {
    // 解构获取数据
    const { page, limit } = this.data
    // 调用接口获取订单列表数据
    const res = await reqOrderList(page, limit)

    if (res.code === 200) {
      this.setData({
+         orderList: [...this.data.orderList, ...res.data.records],
        total: res.data.total
      })
    }
  },

+   // 页面上拉触底事件的处理函数
+   onReachBottom() {
+     // 解构数据
+     const { page } = this.data
+ 
+     // 更新 page
+     this.setData({
+       page: page + 1
+     })
+ 
+     // 重新发送请求
+     this.getOrderList()
+   },

  // 生命周期函数--监听页面加载
  onLoad() {
    this.getOrderList()
  }
})

 04. 判断数据是否加载完毕

如何判断数据是否加载完成 ❓

可以使用后端返回的 `total` 和 `goodsList` 进行对比,如果 total 大于 `goodsList` ,说明订单中心数据没有加载完,可以继续上拉加载更多。

目前还没有接收 `total`,需要先将后台返回的 total 进行赋值到 data 中,然后使用 `onReachBottom` 中进行判断

// 页面上拉触底事件的处理函数
onReachBottom() {
+   // 解构数据
+   const { page, total, orderList } = this.data
+ 
+   // 数据总条数 和 订单列表长度进行对比
+   if (total === orderList.length) {
+     return wx.toast({ title: '数据加载完毕' })
+   }

  // 更新 page
  this.setData({
    page: page + 1
  })

  // 重新发送请求
  this.getOrderList()
}

代码优化

1. 分享功能

目前小程序页面都没有配置分享功能,需要给小程序页面设置分享功能。

但是并不是所有页面都需要设置分享功能,

具体哪些页面需要设置分享功能,可以和产品经理进行协商。

// 转发功能
onShareAppMessage() {
  return {
    title: '所有的怦然心动,都是你',
    path: '/pages/index/index',
    imageUrl: '../../assets/images/love.jpg'
  }
},

// 转发到朋友圈功能
onShareTimeline() {}

2. 优化-分包调整

1. 将 [设置页面] 配置到 [设置模块分包],在访问个人中心页面时,提前预下载 [设置模块分包]

2. 进入订单结算页面时,提前预下载 [设置模块分包]

{
    "subPackages": [
    {
      "root": "modules/settingModule",
      "name": "settingModule",
      "pages": [
+         "pages/settings/settings",
        "pages/address/add/index",
        "pages/address/list/index",
        "pages/profile/profile"
      ]
    },
    {
      "root": "modules/goodModule",
      "name": "goodModule",
      "pages": ["pages/goods/list/list", "pages/goods/detail/detail"]
    },
    {
      "root": "modules/orderPayModule",
      "name": "orderPayModule",
      "pages": ["pages/order/detail/detail", "pages/order/list/list"]
    }
  ],
  "preloadRule": {
+     "pages/my/my": {
      "network": "all",
      "packages": ["settingModule"]
    },
+     "modules/orderPayModule/pages/order/detail/detail": {
+       "network": "all",
+       "packages": ["settingModule"]
+     },
    "pages/category/category": {
      "network": "all",
      "packages": ["goodModule"]
    },
    "pages/cart/cart": {
      "network": "all",
      "packages": ["orderPayModule"]
    }
  },
}

3. 优化-关键按钮添加防抖函数

为了防止用户频繁点击按钮而导致的重复提交或者多次请求的问题,

我们需要给关键按钮添加防抖函数,这里可以使用 `licia` 提供的防抖函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值