Declaration: This project is only for learning, welcome to communicate ~ PS: new xiao Bai yi mei, if there is a mistake, welcome to point out ~

Writing in the front

Chinese New Year has a lot of time, why has been at home not to go out of the house to look at the computer, this is the moral decay or the disappearance of humanity… And it all started with a bat…

Ahem, there is no skin. Let’s get to the point. Wechat launched small programs can be described as lightweight and powerful, so RECENTLY I also began to learn small programs, learn a lot and read a lot of documents, but always feel that they did not learn anything, feel very confused. Is the so-called practice out of true knowledge, so I chose from the high imitation of others small program, choose to choose to finally choose jingdong preferred this small program (absolutely not because of its clean interface!) .

The development tools

  • Wechat developer tools
  • VS Code
  • Icon: iconfont

Effect of quick reference

Without further ado, let’s start with a wave of pictures. Click here to see more pictures










The project structure

For this project, I used normal development and simulated all the data in jSON-server.

It may seem strange to many people, but THIS is because I find it very inconvenient that the easy Mock website often fails to open requests. Therefore, I have not chosen mock data for the time being, and I will move the data to Easy Mock when I have time later.

| - | | - API jd_recommend project a simulated data interface - db. Json simulation data file | | - assets resources - the ICONS icon resources | | - components - images image resources component module | - navigationBar custom custom navigation | - toast toast | | - stepper has great vant stepping apparatus component -... Other small program required components | | - pages project page - the about on page | | - account my orders page - afterMarket service type page | | - appointment my reservation page - buy to fill in the order information page | | - commentDetail comments details page - discount coupons page | | - explore found page | | - fix - feed back page after page - goodsDetail worth buying preferential page | - the index page for details | | - jd jingdong goods details page - login login page | - | | - seller customer service page named orderDetail order details page - service return / | | - shopCart shopping cart page after page - the user personal center page | - style Common style | - comment. WXSS comments area style. | - goodsCard WXSS commodity card style | - nav. WXSS navigation bar style. | - orderCard WXSS order card style. | - popright WXSS filter box style | - popup. WXSS drop-down menu style on | - | utils public module - util. Js promise encapsulated interface app. Js global js app. Json global json configuration app. WXSS global WXSSCopy the code

Custom Components

NavigationBar has a built-in title that can be configured directly in app.json. However, the built-in navigationBar looks fixed, and you’ve definitely seen navigationbars that look something like this:


navigationBar

First, let’s structure the page:

<! -- components/navigationBar/index.wxml --><view class='nav-wrap' style='height: {{height*2 + 20}}px; '>
    <! -- Title in the middle of navigation bar -->
    <view class='nav-title' style='line-height: {{height*2 + 44}}px; '>{{navbarData.title}}</view>
    <view style='display: flex; justify-content: space-around; flex-direction: column'>
        <! -- The back button and home button in the upper left corner of the navigation bar -->
        <! Wx :if='{{navbardata. showCapsule}}'
        <view class='nav-capsule' style='height: {{height*2 + 44}}px; ' wx:if='{{navbarData.showCapsule}}'>
            <! -- Top left back button, wx:if='{{! Share}}' null return button display -->
            <view bindtap='_navback'>
                <image src='.. /.. /assets/icons/back.png' mode='aspectFill' class='back-pre'></image>
            </view>
            <view class='navbar-v-line' wx:if='{{! share}}'></view>
            <view bindtap='_backhome'>
                <image src='.. /.. /assets/icons/back_home.png' mode='aspectFill' class='back-home'></image>
            </view>
        </view>
    </view>
</view>
Copy the code

This is a very common page structure, but it is worth noting that the height is determined by the height of the device being fetched. Then it’s time for Chettuzai to come on line:

/* components/navigationBar/index.wxss */
/* The top should be fixed and the title should be centered. The custom button and title should be aligned up and down with the right wechat native capsule */
.nav-wrap {
    position: fixed;
    width: 100%;
    top: 0;
    background: #fff;
    color: # 000;
    z-index: 9999999;
    border-bottom: 1rpx solid #EFEFF4;
}
/* The title should be centered */
.nav-title {
    position: absolute;
    text-align: center;
    max-width: 400rpx;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    margin: auto;
    font-size: 36rpx;
    color: #2c2b2b;
    /* font-weight: 600; * /
}
.nav-capsule {
    display: flex;
    align-items: center;
    margin-left: 30rpx;
    width: 140rpx;
    justify-content: space-between;
    height: 100%;
}
.navbar-v-line {
    width: 1px;
    height: 32rpx;
    background-color: #e5e5e5;
}
.back-pre..back-home {
    width: 32rpx;
    height: 36rpx;
    margin-top: 4rpx;
    padding: 10rpx;
}
.nav-capsule .back-home {
    width: 36rpx;
    height: 40rpx;
    margin-top: 3rpx;
}
Copy the code
// components/navigationBar/index.js
const app = getApp()
Component({
  properties: {
    navbarData: {   //navbarData Data passed by the parent page, variable name is self - named
      type: Object.value: {},
      // observer: function (newVal, oldVal) { }}},data: {
    height: ' '.// Default value Displays the upper left corner by default
    navbarData: {
      showCapsule: 1}},attached: function () {
    // Define the height of the navigation bar for easy alignment
    this.setData({
      height: app.globalData.height
    })
  },
  methods: {
    // return to the previous page
    _navback() {
      wx.navigateBack()
    },
    // Return to the home page
    _backhome() {
      wx.switchTab({
        url: '/pages/index/index',})}}})Copy the code

The two buttons here are to return to the home page, I feel wrong when developing, so I changed.

Here is to get the global variables defined, namely for the height of the equipment at the top of the window (the window height is different, different equipment according to this to set a custom navigation bar height), in the app. To define it in js:

app.js
App({
  onLaunch: function () {... wx.getSystemInfo({success: (res) = > {
        this.globalData.height = res.statusBarHeight
      }
    })
  },
  globalData: {... height:0}})Copy the code

Remember to write custom components in JSON

// components/navigationBar/index.json
{
  "component": true
}
Copy the code

The next step is to invoke this component

<navigationBar navbar-data='{{navbarData}}'></navigationBar>
Copy the code

Don’t forget to include this component in the JSON that references the page (since I only use the custom navigation bar for the product details page, I only configure the custom navigation bar in the JSON of the page)

"usingComponents": {
    "navigationBar": ".. /.. /components/navigationBar/index"
 }
Copy the code

Note that because it’s a custom navigationBar, the page extends across the screen, but navigationBar uses the Fix layout to get out of the document flow, and the content added below will run up and overlap with the navigationBar. Style =’margin-top: {{height}}px’

Toast

Toast is also ready for you to use as an applet. Although it can replace ICONS, you will find that if you want to display two lines of text, you cannot do so. I also searched related implementation methods during the development process, and found that most of them can be implemented by adding \r\n at the back of the text to be wrapped, but MY own test did not work, so I couldn’t help but make one myself.

<! -- components/toast/index.wxml -->
<! -- The height from the top is dynamically determined by business requirements -->
<view class='mask' hidden="{{hide}}" style='top: {{toastData.top}}'>
    <image class="image" src='.. /.. /assets/icons/{{toastData.icon}}.png' mode='aspectFit'></image>
    <view class="info">
        <view class='info1' wx:if="{{toastData.info1 ! = ' '}}">{{toastData.info1}}</view>
        <view class="info2" wx:if="{{toastData.info2 ! = ' '}}">{{toastData.info2}}</view>
    </view>
</view>
Copy the code
/* components/toast/index.wxss */
.mask {
    width: 440rpx;
    height: auto;
    border-radius: 20rpx;
    position: fixed;
    left: 155rpx;
    z-index: 1000;
    background: rgba(0, 0, 0, 0.6);
    text-align: center;
    padding-bottom: 30rpx;
}
.image {
    z-index: 1000;
    width: 80rpx;
    height: 80rpx;
    padding-top: 30rpx;
    padding-bottom: 20rpx;
}
.info1..info2 {
    color: #ffffff;
    font-size: 32rpx;
}
.info {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}
Copy the code
// components/toast/index.js
Component({
  properties: {              // Define component properties
    toastData: {           // Display prompt information
      type: Object.// Type (mandatory), currently accepted types include: String, Number, Boolean, Object, Array, null (for any type)
      value: {
        icon: 'success'
      }     // The initial value of the property (optional), if not specified one will be selected based on the type}},data: {
    hide: true
  },
  methods: {
    showToast: function () {
      let that = this;
      that.setData({
        hide: false
      });
    },
    hideToast: function (e) {
      let that = this;
      setTimeout(function () {
        that.setData({
          hide: true
        });
      }, 2000); }}})Copy the code

There are two methods defined for the component to show and hide the Toast. Note here that calling the custom component definition method first fetches the component on the page

<toast id="toast" toast-data="{{toastData}}"></toast>
Copy the code
Page({
   data: {
    toastData: { // Toast requires parameters
      icon: "success".info1: "Added to shopping cart successfully".top: "50%"
    }
  },
  onReady() {
    this.toast = this.selectComponent("#toast"); }})Copy the code

Then add these two sentences to the event that needs to trigger Toast:

this.toast.showToast()
this.toast.hideToast()
Copy the code

Function implementation

navigation

The so-called navigation, is also very common, is according to the selection of different columns, display different categories of content. Such as:


  1. Click the navigation column to display the corresponding column data.
  2. If there is nothing in the column, display the corresponding prompt message.

It’s not that hard to implement. Personally, JINGdong preferred has a disadvantage here is that if you click the navigation column on the right, the navigation bar will not move to the right to show the following project, maybe its developer has a different idea.

<view class="navigator">
  <scroll-view scroll-x="true" class="nav" scroll-left="{{navScrollLeft}}" scroll-with-animation="{{true}}">
    <block wx:for="{{navData}}" wx:for-index="id" wx:for-item="navItem" wx:key="id">
      <view class="nav-item {{currentTab == id? 'active':''}}" data-name="{{navItem.name}}" data-current="{{id}}" bindtap="switchNav">
        {{navItem.name}}
      </view>
    </block>
  </scroll-view>
</view>
Copy the code

Dynamic data filling can be realized through JS. The current set here is the current selected column, and the style can be changed according to this.

switchNav(e) {
        const cur = e.currentTarget.dataset.current; // Number
        let currData = []
        // console.log(cur.toString());
        if (cur === 0) {
            currData = this.data.goods
        } else {
            this.data.goods.forEach(val= > {
                if (val.category === cur.toString()) {
                    currData.push(val)
                }
            })
        }
        this.setData({
            currentTab: cur,
            category: cur,
            currData
        });
}
Copy the code

If you want to automatically slide to the click method to show more content, you can do that by dynamically changing the navScrollLeft, and I’m not going to go into details here, but I had a hard time implementing it, and it wasn’t very good so I didn’t put it in the code, If you want to make this effect in the future of the navigation bar recommended to go to the Internet to search the demo after understanding the borrowed use, after all, the legend of the program ape’s highest realm is copy and paste, dog head (mistake)

Pull-up menus and filter boxes

The two are similar, but the positions are different. Here’s an example of a filter box. Let’s see what it looks like:


<! -- Click filter to pop up the selection menu -->
<view class="float {{isRuleTrue? 'isRuleShow':'isRuleHide'}}">
    <view class="animation-element" animation="{{animation}}">. In the middle of their own specific content...<! -- Two buttons at the bottom -->
        <view class='bottom'>
            <view class="animation-reset" bindtap="reset">reset</view>
            <view class="animation-button" bindtap="success">determine</view>
        </view>
    </view>
</view>
Copy the code
/* Filter popboxes */
/* Layout of the popbox */
.isRuleShow {
    display: block;
}
.isRuleHide {
    display: none;
}
.float {
    height: 100%;
    width: 100%;
    position: fixed;
    z-index: 999;
    top: 0;
    left: 0;
    /* Popup background color */
    background-color: rgba(0, 0, 0, 0.5);
    padding-left: 30rpx;
    padding-left: 30rpx;
    /* margin-top:80rpx; * /
}
.animation-element {
    width: 600rpx;
    height: 100%;
    padding-left: 30rpx;
    padding-right: 30rpx;
    background-color: #ffffff;
    border: 1px solid #f3f0f0;
    position: absolute;
    right: -550rpx;
    box-sizing: border-box;
}
.bottom {
    width: 600rpx;
    height: 110rpx;
    font-size: 32rpx;
    padding-top: 55rpx;
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
}
.animation-reset {
    width: 50%;
    height: 100%;
    line-height: 50%;
    text-align: center;
    padding-top: 55rpx;
    border-top: 1px solid #EFEFF4;
}
.animation-button {
    width: 50%;
    height: 100%;
    line-height: 50%;
    color: #fff;
    text-align: center;
    background-color: #ED7358;
    padding-top: 55rpx;
}
Copy the code

The key point is that the display and hide events need to use animation. If you are not familiar with animation, you can refer to some materials or official documents. Also, I cut out some of my other business implementations.

showSelect() { // Displays the selection menu
    this.setData({
      isRuleTrue: true
    })
    Step indicates the start of an action
    this.animation.translate(- 245..0).step()
    this.setData({ animation: this.animation.export() })
 },
 success: function () { // Close the selection menu
    this.setData({
      isRuleTrue: false.selected: true
    })
    this.animation.translate(0.0).step()
    this.setData({ animation: this.animation.export() })
 },
Copy the code

search

Having said the filter box, let’s talk about the search box next to it. It looks something like this:


<view class="search">
    <image class="icon" src=".. /.. /assets/icons/search.png"></image>
    <! -- Input box of after-sale application bar -->
    <input type="text" wx:if="{{currentTab === 0}}" placeholder=Name of commodity/commodity no. / Order No. / series No. value="{{inputValue}}" bindinput="gainContent" bindfocus="showCancel" bindblur="hideCancel" />
    <input type="text" wx:else placeholder="Commodity name/Order Number/Service Order Number" value="{{inputValue}}" bindinput="gainContent" bindfocus="showCancel" bindblur="hideCancel" />
    <! Clear the text icon -->
    <image class="delete" src=".. /.. /assets/icons/delete.png" wx:if="{{inputValue ! = ' '}}" bindtap="hideCancel"></image>
    <view class="select" wx:if="{{! cancelShowed}}" bindtap="showSelect">
        <! -- Normal filter icon -->
        <image class="select_icon" wx:if="{{! selected}}" src=".. /.. /assets/icons/select.png"></image>
        <! -- Select the filter icon after filtering -->
        <image class="select_icon" wx:if="{{selected}}" src=".. /.. /assets/icons/select_active.png"></image>
        <text class="select_text {{selected? 'select_active':''}}">screening</text>
    </view>
    <text class="cancel" wx:if="{{cancelShowed}}" bindtap="hideCancel">cancel</text>
</view>
Copy the code
.search {
    /* height: 100%; * /
    width: 100%;
    height: 120rpx;
    padding-left: 30rpx;
    padding-top: 20rpx;
    background-color: #fff;
    /* background-color: red; * /
    position: relative;
    display: flex;
    flex: 1;
    box-sizing: border-box;
}
input {
    width: 593rpx;
    height: 80rpx;
    font-size: 25rpx;
    border-radius: 50rpx;
    background-color: #F5F5F5;
    box-sizing: border-box;
    padding-left: 60rpx;
    padding-right: 60rpx;
}
.icon {
    position: absolute;
    top: 45rpx;
    left: 45rpx;
    background-size: 100%;
    width: 30rpx;
    height: 30rpx;
}
.delete {
    width: 40rpx;
    height: 40rpx;
    position: absolute;
    left: 570rpx;
    top: 40rpx;
    /* Image blocked by text box cannot trigger click event */
    z-index: 990;
}
.cancel {
    font-size: 25rpx;
    color: #BCBCBC;
    position: absolute;
    top: 35%;
    right: 70rpx;
}
Copy the code

One of the things to watch out for here is the cancel icon and you’ll notice that it doesn’t respond when you click on it, and that’s because it’s covered by the input layer so you can’t trigger the event, so you need to raise the level.

searchText(str) {
    let currData = []
    this.data.currData.forEach(val= > {
      if (val.title.toLowerCase().indexOf(str.toLowerCase()) >= 0) {
        currData.push(val)
      }
    });
    this.setData({
      currData
    })
},
gainContent(e) { // Get the contents of the input box
    const text = e.detail.value;
    console.log(typeof text)
    this.setData({
      inputValue: text
    })
    this.searchText(text)
    if (text === ' ') {
      this.fillData(this.data.currentTab)
    }
    console.log(typeof text, 'typeOf text');
},
deleteContent() { // Delete the contents of the input box
    this.setData({
      inputValue: ' '
    })
    this.fillData(this.data.currentTab)
},
showCancel() { // Display is cancelled
    this.setData({
      cancelShowed: true
    })
  },
hideCancel() { // Hide cancel
    this.setData({
      cancelShowed: false.inputValue: ' '
    })
    this.fillData(this.data.currentTab)
},
Copy the code

When realizing the search, pay attention to the unified conversion of the input content and the title of the search to uppercase or lowercase, so that you can do violence to ignore case, can provide a better user experience.

Shopping cart logic


Here I use the small program wx.setStorage() implementation:

<view class='bottom'>
    <view class="animation-reset" bindtap="addCart">Add to shopping cart</view>
    <view class="animation-button" bindtap="buy">Buy now</view>
</view>
Copy the code
addCart() { // Add to cart
    this.setData({
      toastData: { // Toast requires parameters
        icon: "success".info1: "Added to shopping cart successfully".top: "50%"}})this.toast.showToast()
    this.toast.hideToast()
    this.hideModal()
    // Implement the part that actually adds the shopping cart
    let cartData = wx.getStorageSync('cart') | | [];let count = 0
    cartData.map(val= > {
      if (val.title === this.data.currData[0].title && val.type === this.data.choose_value) {
        val.num += this.data.num
        count++ // check if the same item is found}})if (count === 0) { // Not found to add new item information to cart
      let data = {
        id: this.data.currData[0]._id,
        title: this.data.currData[0].title,
        weight: "0.78 kg".type: this.data.choose_value,
        num: this.data.num,
        price: this.data.currData[0].plain_price,
        img: this.data.currData[0].thumb,
        discount: 20.select: true // Check whether to enable subsequent calculation of the total price
      }
      cartData.push(data)
    }
    // Refresh the quantity on the shopping cart icon
    let allNum = 0
    cartData.forEach(val= > {
      allNum += val.num
    });
    this.setData({
      allNum
    })
    wx.setStorage({
      key: 'cart'.data: cartData
    })
 },
Copy the code

It’s up to you to decide on your own development, or if you’re using cloud development, you can choose to store your data in a cloud database.

Back to the top

<! -- Slide some distance to show back to top button -->
<scroll-view class="bigWrap" 
    scroll-y="true" 
    scroll-top="{{scrollTop}}" 
    bindscroll="scroll" 
    style="position: absolute; left: 0; top:0; bottom: 0; right: -999rpx;">
<view class="goTop" bindtap="goTop" wx:if="{{&& floorstatus}}">
    <image class="icon_goTop" src=".. /.. /assets/icons/back_to_top.png"></image>
</view>
</scroll-view>

Copy the code

{{scrollTop}} is used to indicate the position from the top when sliding. Its style is also very simple, use the fixed position to fix it on the screen, here must pay attention to the page hierarchy, otherwise it may be blocked by other components!

/* Return to top */
.goTop {
    position: fixed;
    bottom: 200rpx;
    right: 20rpx;
    width: 65rpx;
    height: 65rpx;
    border: 1px solid #DDDDDD;
    border-radius: 50%;
    background-color: #fff;
    text-align: center;
}
.icon_goTop {
    width: 40rpx;
    height: 40rpx;
    padding-top: 12rpx;
    padding-left: 2rpx;
}
Copy the code
GoTop (e) {// return to top this.setData({scrollTop: 0})}Copy the code

You’ll also notice that it doesn’t show up until you’ve reached a certain distance thanks to the swiper binding:

scroll(e) { // Scroll events
    // When the container rolls, it assigns the scrolling distance to this.data.scrollTop
    let floorstatus = false
    if (e.detail.scrollTop > 300) {
      floorstatus = true
    }
    this.setData({
      floorstatus
    })
}
Copy the code

It may look like some very easy and insignificant functions, but it is still a little difficult for me as a beginner. I hope I won’t be scolded if someone sees the realization of some of my functions.

A few points worth noting

Page jump value rendering problem

Page jump value is a very common operation in the development of small programs, let’s take a look at the value like this:


showOptions(e) { // Jump to the account options screen
    let index = e.currentTarget.dataset.index;
    console.log(index);
    if (this.data.loginFlag === 0) {
      wx.navigateTo({
        url: '.. /login/login? prev=account'})}else if (index === 3) {
      wx.navigateTo({
        url: '.. /service/service'})}else {
      index = index + 1;
      if(index > 3) {
        index = 0
      }
      // Use storage to carry index to the next interface, otherwise the data cannot be rendered at load time
      wx.setStorage({
        key: 'account:index'.data: index
      })
      wx.navigateTo({
        url: '.. /account/account? index=' + index
      })
    }
 }
Copy the code

And then it comes out:

onLoad(options) {
    let that = this
    let index = wx.getStorageSync('account:index'); . Render corresponding subscript data... }Copy the code

I can’t think of a better way, so I will deal with it for the time being. If you encounter the same problem in development, welcome to communicate with you ~

Event bubbling problem

Anyone who has done applets or Vue development has heard the term event bubble: an event of a child that triggers an event of a parent, such as a click. I was the lucky goose, and THIS happened to me when I was developing. Clicking on an item in the shopping cart can jump to the item details, but I originally bound the jump event to each item card, so when CLICKING on the item quantity, I changed the number but also jumped to the item details directly, such as the following…

The solution to this problem is to replace bindtap with catchtap to prevent child element events from bubbling up.

However as it happens, I am the luckiest goose, step by step what I use is some great Vant Weapp component library, I don’t have the search a lot of information to find effective solutions, almost give up on using the component library, but finally found jingdong optimization applet shopping cart binding was a jump in commodity pictures and headlines.

This is important, so bubbling should be considered in your development, which is why I put it at the end.

Write in the back

Finally, I want to say that small program development is really not easy, and developing a good small program needs to consider all aspects of performance and user experience. “Joyful” really jumped up and sang “Joyful” when I thought I was almost done with my first mini program. It’s not easy being a programmer. No wonder they lose their hair. But the good news is that there are a lot of people who are willing to share their technology, and I looked at a lot of sources, communities, and documentation during the development process. I will not stop learning the small program. There are still many bad things in this project. I hope you can share with me. I hope my work can be helpful to those who are beginners in small programs.

Finally, attach my Github project address: github.com/tearill/jd_… If you think this project is good or helpful to you, welcome star, every star you light will be my motivation to move forward!